Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
"@radix-ui/react-scroll-area": "1.2.0-rc.7",
"@radix-ui/react-slot": "^1.0.2",
"@react-aria/interactions": "^3.25.5",
"@react-aria/landmark": "^3.0.8",
"@react-aria/overlays": "^3.29.1",
"@react-aria/tabs": "^3.10.7",
"@react-aria/toolbar": "3.0.0-beta.20",
Expand Down
7 changes: 4 additions & 3 deletions code/core/src/components/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { type ComponentProps, forwardRef } from 'react';
import React, { type ComponentProps, type DOMAttributes, forwardRef } from 'react';

import type { CSSObject, color } from 'storybook/theming';
import { keyframes, styled } from 'storybook/theming';
Expand Down Expand Up @@ -92,15 +92,16 @@ const CardOutline = styled.div<{
interface CardProps extends ComponentProps<typeof CardContent> {
outlineAnimation?: 'none' | 'rainbow' | 'spin';
outlineColor?: keyof typeof color;
outlineAttrs?: DOMAttributes<HTMLDivElement>;
}

export const Card = Object.assign(
forwardRef<HTMLDivElement, CardProps>(function Card(
{ outlineAnimation = 'none', outlineColor, ...props },
{ outlineAnimation = 'none', outlineColor, outlineAttrs: outlineAttrs = {}, ...props },
ref
) {
return (
<CardOutline animation={outlineAnimation} color={outlineColor} ref={ref}>
<CardOutline animation={outlineAnimation} color={outlineColor} ref={ref} {...outlineAttrs}>
<CardContent {...props} />
</CardOutline>
);
Expand Down
3 changes: 2 additions & 1 deletion code/core/src/components/components/Form/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ export const styles = (({ theme }: { theme: StorybookTheme }) => ({
},
},

'&[disabled]': {
'&[disabled], &[aria-disabled="true"]': {
background: theme.base === 'light' ? theme.color.lighter : 'transparent',
cursor: 'not-allowed',
},

'&:-webkit-autofill': { WebkitBoxShadow: `0 0 0 3em ${theme.color.lightest} inset` },
Expand Down
9 changes: 9 additions & 0 deletions code/core/src/manager-api/modules/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export interface API_Shortcuts {
openInEditor: API_KeyCollection;
openInIsolation: API_KeyCollection;
copyStoryLink: API_KeyCollection;
goToPreviousLandmark: API_KeyCollection;
goToNextLandmark: API_KeyCollection;
// TODO: bring this back once we want to add shortcuts for this
// copyStoryName: API_KeyCollection;
}
Expand Down Expand Up @@ -157,6 +159,8 @@ export const defaultShortcuts: API_Shortcuts = Object.freeze({
openInEditor: ['alt', 'shift', 'E'],
openInIsolation: ['alt', 'shift', 'I'],
copyStoryLink: ['alt', 'shift', 'L'],
goToPreviousLandmark: ['shift', 'F6'], // hardcoded in react-aria
goToNextLandmark: ['F6'], // hardcoded in react-aria
// TODO: bring this back once we want to add shortcuts for this
// copyStoryName: ['alt', 'shift', 'C'],
});
Expand Down Expand Up @@ -273,6 +277,11 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => {
break;
}

// Handled by @react-aria/interactions and useLandmarkIndicator
case 'goToNextLandmark':
case 'goToPreviousLandmark':
break;

case 'focusNav': {
if (fullAPI.getIsFullscreen()) {
fullAPI.toggleFullscreen(false);
Expand Down
4 changes: 4 additions & 0 deletions code/core/src/manager/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Notifications } from '../../container/Notifications';
import { MobileNavigation } from '../mobile/navigation/MobileNavigation';
import { useLayout } from './LayoutProvider';
import { useDragging } from './useDragging';
import { useLandmarkIndicator } from './useLandmarkIndicator';

interface InternalLayoutState {
isDragging: boolean;
Expand Down Expand Up @@ -158,6 +159,9 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
isDragging,
} = useLayoutSyncingState({ api, managerLayoutState, setManagerLayoutState, isDesktop, hasTab });

// Install landmark navigation listener in parent container of all landmarks.
useLandmarkIndicator();

return (
<LayoutContainer
navSize={navSize}
Expand Down
63 changes: 63 additions & 0 deletions code/core/src/manager/components/layout/useLandmarkIndicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useRef } from 'react';

import { useTheme } from 'storybook/theming';

function findActiveLandmarkElement() {
let currentElement: Element | null = document.activeElement;
let landmarkElement: HTMLElement | null = null;

while (currentElement) {
if (currentElement instanceof HTMLElement && currentElement.hasAttribute('data-sb-landmark')) {
landmarkElement = currentElement;
break;
}
currentElement = currentElement.parentElement;
}

return landmarkElement;
}

// Global keyboard handler for F6/Shift+F6 landmark navigation that
// highlights the landmark containing the current element. This helps
// users who navigate through landmark shortcuts more quickly visualise
// which region of the UI they landed into.
export function useLandmarkIndicator() {
const theme = useTheme();
const currentAnimationRef = useRef<Animation | null>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'F6') {
return;
}

const landmarkElement = findActiveLandmarkElement();
if (!landmarkElement) {
return;
}

// Cancel previous landmark animation if user switches fast.
if (currentAnimationRef.current) {
currentAnimationRef.current.cancel();
currentAnimationRef.current = null;
}

const animation = landmarkElement.animate(
[{ border: `2px solid ${theme.color.primary}` }, { border: `2px solid transparent` }],
{
duration: 1500,
pseudoElement: '::after',
}
);
currentAnimationRef.current = animation;

animation.onfinish = () => {
currentAnimationRef.current = null;
};
};

document.addEventListener('keydown', handleKeyDown, { capture: true });
return () => {
document.removeEventListener('keydown', handleKeyDown, { capture: true });
};
}, [theme.color.primary]);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';
import type { ComponentProps, FC } from 'react';

import { Button } from 'storybook/internal/components';
Expand All @@ -10,6 +10,7 @@ import { useId } from '@react-aria/utils';
import { useStorybookApi, useStorybookState } from 'storybook/manager-api';
import { styled } from 'storybook/theming';

import { useLandmark } from '../../../hooks/useLandmark';
import { useLayout } from '../../layout/LayoutProvider';
import { MobileAddonsDrawer } from './MobileAddonsDrawer';
import { MobileMenuDrawer } from './MobileMenuDrawer';
Expand Down Expand Up @@ -77,6 +78,12 @@ export const MobileNavigation: FC<MobileNavigationProps & ComponentProps<typeof
const fullStoryName = useFullStoryName();
const headingId = useId();

const sectionRef = useRef<HTMLElement>(null);
const { landmarkProps } = useLandmark(
{ 'aria-labelledby': headingId, role: 'banner' },
sectionRef
);

return (
<Container {...props}>
<MobileMenuDrawer
Expand All @@ -96,7 +103,7 @@ export const MobileNavigation: FC<MobileNavigationProps & ComponentProps<typeof
</MobileAddonsDrawer>

{!isMobilePanelOpen && (
<MobileBottomBar className="sb-bar" aria-labelledby={headingId}>
<MobileBottomBar className="sb-bar" {...landmarkProps} ref={sectionRef}>
<h2 id={headingId} className="sb-sr-only">
Navigation controls
</h2>
Expand Down Expand Up @@ -132,7 +139,7 @@ export const MobileNavigation: FC<MobileNavigationProps & ComponentProps<typeof
);
};

const Container = styled.div(({ theme }) => ({
const Container = styled.section(({ theme }) => ({
bottom: 0,
left: 0,
width: '100%',
Expand All @@ -141,7 +148,7 @@ const Container = styled.div(({ theme }) => ({
borderTop: `1px solid ${theme.appBorderColor}`,
}));

const MobileBottomBar = styled.section({
const MobileBottomBar = styled.header({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
Expand Down
15 changes: 11 additions & 4 deletions code/core/src/manager/components/panel/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import React, { Component, useMemo } from 'react';
import React, { Component, useMemo, useRef } from 'react';

import {
Button,
Expand All @@ -17,6 +17,7 @@ import { BottomBarIcon, CloseIcon, DocumentIcon, SidebarAltIcon } from '@storybo
import type { State } from 'storybook/manager-api';
import { styled } from 'storybook/theming';

import { useLandmark } from '../../hooks/useLandmark';
import { useLayout } from '../layout/LayoutProvider';

export interface SafeTabProps {
Expand Down Expand Up @@ -61,7 +62,7 @@ class TabErrorBoundary extends Component<ErrorBoundaryProps, { hasError: boolean
}
}

const Section = styled.section({
const Aside = styled.aside({
height: '100%',
display: 'flex',
flexDirection: 'column',
Expand Down Expand Up @@ -152,8 +153,14 @@ export const AddonPanel = React.memo<{
[actions, isDesktop, panelPosition, setMobilePanelOpen, shortcuts]
);

const asideRef = useRef<HTMLElement>(null);
const { landmarkProps } = useLandmark(
{ 'aria-labelledby': 'storybook-panel-heading', role: 'region' },
asideRef
);

return (
<Section aria-labelledby="storybook-panel-heading">
<Aside ref={asideRef} {...landmarkProps}>
<h2 id="storybook-panel-heading" className="sb-sr-only">
Addon panel
</h2>
Expand All @@ -174,7 +181,7 @@ export const AddonPanel = React.memo<{
</StatelessTabList>
{Object.keys(panels).length ? <PreRenderAddons panels={panels} /> : null}
</StatelessTabsView>
</Section>
</Aside>
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const FramesRenderer: FC<FramesRendererProps> = ({
}
return (
<SkipToSidebarLink ariaLabel={false} asChild>
<a href={`#${selectedStoryId}`} tabIndex={0} title="Skip to sidebar">
<a href={`#${selectedStoryId}`} tabIndex={0}>
Skip to sidebar
</a>
</SkipToSidebarLink>
Expand Down
9 changes: 8 additions & 1 deletion code/core/src/manager/components/preview/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { TabListState } from '@react-stately/tabs';
import { Helmet } from 'react-helmet-async';
import { type Combo, Consumer, addons, merge, types } from 'storybook/manager-api';

import { useLandmark } from '../../hooks/useLandmark';
import { FramesRenderer } from './FramesRenderer';
import { ToolbarComp } from './Toolbar';
import { ApplyWrappers } from './Wrappers';
Expand Down Expand Up @@ -111,6 +112,12 @@ const Preview = React.memo<PreviewProps>(function Preview(props) {
}
}, [entry, viewMode, storyId, api]);

const mainRef = useRef<HTMLElement>(null);
const { landmarkProps } = useLandmark(
{ 'aria-labelledby': 'main-preview-heading', role: 'main' },
mainRef
);

return (
<Fragment>
{previewId === 'main' && (
Expand All @@ -128,7 +135,7 @@ const Preview = React.memo<PreviewProps>(function Preview(props) {
tools={tools}
toolsExtra={toolsExtra}
/>
<S.FrameWrap aria-labelledby="main-preview-heading">
<S.FrameWrap ref={mainRef} {...landmarkProps}>
<h2 id="main-preview-heading" className="sb-sr-only">
Main preview area
</h2>
Expand Down
12 changes: 10 additions & 2 deletions code/core/src/manager/components/preview/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';

import { AbstractToolbar, Button, Separator, TabList } from 'storybook/internal/components';
import { type Addon_BaseType, Addon_TypesEnum } from 'storybook/internal/types';
Expand All @@ -18,6 +18,7 @@ import {
} from 'storybook/manager-api';
import { styled } from 'storybook/theming';

import { useLandmark } from '../../hooks/useLandmark';
import { useLayout } from '../layout/LayoutProvider';
import type { PreviewProps } from './utils/types';

Expand Down Expand Up @@ -83,12 +84,19 @@ export const ToolbarComp = React.memo<ToolData>(function ToolbarComp({
tabs,
tabState,
}) {
const sectionRef = useRef<HTMLElement>(null);
const { landmarkProps } = useLandmark(
{ 'aria-labelledby': 'sb-preview-toolbar-title', role: 'region' },
sectionRef
);

return isShown && (tabs || tools || toolsExtra) ? (
<StyledSection
className="sb-bar"
key="toolbar"
data-testid="sb-preview-toolbar"
aria-labelledby="sb-preview-toolbar-title"
ref={sectionRef}
{...landmarkProps}
>
<h2 id="sb-preview-toolbar-title" className="sb-sr-only">
Toolbar
Expand Down
4 changes: 2 additions & 2 deletions code/core/src/manager/components/preview/utils/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { Link } from 'storybook/internal/router';

import { styled } from 'storybook/theming';

export const PreviewContainer = styled.main({
export const PreviewContainer = styled.div({
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
overflow: 'hidden',
});

export const FrameWrap = styled.section({
export const FrameWrap = styled.main({
overflow: 'auto',
width: '100%',
zIndex: 3,
Expand Down
2 changes: 2 additions & 0 deletions code/core/src/manager/components/sidebar/Explorer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const Simple = () => (
isLoading={false}
isBrowsing
hasEntries={true}
isHidden={false}
/>
);

Expand All @@ -96,5 +97,6 @@ export const WithRefs = () => (
isLoading={false}
isBrowsing
hasEntries={true}
isHidden={false}
/>
);
Loading