Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Add skip to canvas/sidebar links #15740

Merged
merged 22 commits into from Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
af0e6a3
feat(wip): skip to content/sidebar links
darleendenno Jul 25, 2021
6551391
styles: update link text and positioning
darleendenno Jul 26, 2021
9ba5a6b
refactor: respect enter event for skip links within tree nodes
darleendenno Jul 27, 2021
69591b2
fix: allow events to bubble up in sidebar items
yannbf Jul 27, 2021
e873a3e
chore(heading): add story types
yannbf Jul 27, 2021
e9fd619
fix: tree node skip styles and destination links
darleendenno Jul 29, 2021
23e9cd4
styles: hide preview skip to content link on mobile
darleendenno Jul 29, 2021
2b22a3a
refactor: revert useHighlighted updates
darleendenno Aug 3, 2021
349eb16
refactor: semantic naming for stories and components
darleendenno Aug 3, 2021
5004c0f
fix: remove unused imports
darleendenno Aug 3, 2021
bf76162
refactor: style syntax consistency
darleendenno Aug 3, 2021
e5d5955
styles: hide links on mobile
darleendenno Aug 3, 2021
593e1b5
fix: canvas link dupe render
darleendenno Aug 3, 2021
7c0c408
add delay, make interaction stories async
darleendenno Aug 3, 2021
a53ada8
refactor: replace dataTestId with getAllByText
darleendenno Aug 5, 2021
65d5e81
styles: use numeric convention
darleendenno Aug 5, 2021
6f715da
refactor: chromatic theme default, add skipLinkHref prop to Heading
darleendenno Aug 6, 2021
78f3b44
fix: hide skip to sidebar link when sidebar isn't available
darleendenno Aug 6, 2021
a5c93ac
fix: revert single story chromatic theme override, modify interaction…
darleendenno Aug 12, 2021
9b48cfc
fix: initial render doesn't have storyId for href
darleendenno Aug 19, 2021
1980a36
styles: bump heading link down
darleendenno Aug 19, 2021
5455df4
Merge branch 'next' into feat/add-skip-to-content
shilman Aug 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 22 additions & 1 deletion lib/ui/src/components/preview/FramesRenderer.tsx
@@ -1,5 +1,6 @@
import React, { Fragment, FunctionComponent, useMemo, useEffect, useState } from 'react';
import { Global, CSSObject } from '@storybook/theming';
import { Button } from '@storybook/components';
import { Global, CSSObject, styled } from '@storybook/theming';
import { IFrame } from './iframe';
import { FramesRendererProps } from './utils/types';
import { stringifyQueryParams } from './utils/stringifyQueryParams';
Expand All @@ -12,6 +13,23 @@ const getActive = (refId: FramesRendererProps['refId']) => {
return 'storybook-preview-iframe';
};

const SkipToSidebarLink = styled(Button)(({ theme }) => ({
display: 'none',
'@media (min-width: 600px)': {
display: 'block',
position: 'absolute',
top: '10px',
right: '15px',
darleendenno marked this conversation as resolved.
Show resolved Hide resolved
padding: '10px 15px',
fontSize: theme.typography.size.s1,
darleendenno marked this conversation as resolved.
Show resolved Hide resolved
transform: 'translateY(-100px)',
'&:focus': {
transform: 'translateY(0)',
zIndex: 1,
},
},
}));

export const FramesRenderer: FunctionComponent<FramesRendererProps> = ({
refs,
story,
Expand Down Expand Up @@ -74,6 +92,9 @@ export const FramesRenderer: FunctionComponent<FramesRendererProps> = ({
<Global styles={styles} />
{Object.entries(frames).map(([id, src]) => (
<Fragment key={id}>
<SkipToSidebarLink secondary isLink tabIndex={0} href={`#${storyId}`}>
Skip to sidebar
</SkipToSidebarLink>
<IFrame
active={id === active}
key={refs[id] ? refs[id].url : id}
Expand Down
34 changes: 22 additions & 12 deletions lib/ui/src/components/sidebar/Heading.stories.tsx
@@ -1,30 +1,34 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { ThemeProvider, useTheme, Theme } from '@storybook/theming';
import { action } from '@storybook/addon-actions';
import { screen } from '@testing-library/dom';

import { Heading } from './Heading';

type Story = ComponentStory<typeof Heading>;

export default {
component: Heading,
title: 'UI/Sidebar/Heading',
excludeStories: /.*Data$/,
parameters: { layout: 'fullscreen' },
decorators: [
(storyFn: any) => <div style={{ padding: '0 20px', maxWidth: '230px' }}>{storyFn()}</div>,
(storyFn) => <div style={{ padding: '0 20px', maxWidth: '230px' }}>{storyFn()}</div>,
],
};
} as ComponentMeta<typeof Heading>;

const menuItems = [
{ title: 'Menu Item 1', onClick: action('onActivateMenuItem'), id: '1' },
{ title: 'Menu Item 2', onClick: action('onActivateMenuItem'), id: '2' },
{ title: 'Menu Item 3', onClick: action('onActivateMenuItem'), id: '3' },
];

export const menuHighlighted = () => <Heading menuHighlighted menu={menuItems} />;
export const menuHighlighted: Story = () => <Heading menuHighlighted menu={menuItems} />;

export const standardData = { menu: menuItems };

export const standard = () => {
export const standard: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -42,7 +46,7 @@ export const standard = () => {
);
};

export const standardNoLink = () => {
export const standardNoLink: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -60,7 +64,7 @@ export const standardNoLink = () => {
);
};

export const linkAndText = () => {
export const linkAndText: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -78,7 +82,7 @@ export const linkAndText = () => {
);
};

export const onlyText = () => {
export const onlyText: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -96,7 +100,7 @@ export const onlyText = () => {
);
};

export const longText = () => {
export const longText: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -114,7 +118,7 @@ export const longText = () => {
);
};

export const customBrandImage = () => {
export const customBrandImage: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -132,7 +136,7 @@ export const customBrandImage = () => {
);
};

export const customBrandImageTall = () => {
export const customBrandImageTall: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -150,7 +154,7 @@ export const customBrandImageTall = () => {
);
};

export const customBrandImageUnsizedSVG = () => {
export const customBrandImageUnsizedSVG: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -168,7 +172,7 @@ export const customBrandImageUnsizedSVG = () => {
);
};

export const noBrand = () => {
export const noBrand: Story = () => {
const theme = useTheme() as Theme;
return (
<ThemeProvider
Expand All @@ -185,3 +189,9 @@ export const noBrand = () => {
</ThemeProvider>
);
};

export const skipToCanvasLinkFocused: Story = {
args: { menu: menuItems },
parameters: { layout: 'padded' },
play: () => screen.getByTestId('heading--skip').focus(),
};
27 changes: 27 additions & 0 deletions lib/ui/src/components/sidebar/Heading.tsx
@@ -1,5 +1,6 @@
import React, { FunctionComponent, ComponentProps } from 'react';
import { styled } from '@storybook/theming';
import { Button } from '@storybook/components';

import { Brand } from './Brand';
import { SidebarMenu, MenuList } from './Menu';
Expand Down Expand Up @@ -34,13 +35,39 @@ const HeadingWrapper = styled.div({
position: 'relative',
});

const SkipToCanvasLink = styled(Button)(({ theme }) => ({
display: 'none',
'@media (min-width: 600px)': {
display: 'block',
position: 'absolute',
width: '100%',
padding: '10px 15px',
fontSize: theme.typography.size.s1,
zIndex: 1,
transform: 'translate(0,-100px)',
'&:focus': {
transform: 'translate(0,-10px)',
darleendenno marked this conversation as resolved.
Show resolved Hide resolved
},
},
}));

export const Heading: FunctionComponent<HeadingProps & ComponentProps<typeof HeadingWrapper>> = ({
menuHighlighted = false,
menu,
...props
}) => {
return (
<HeadingWrapper {...props}>
<SkipToCanvasLink
secondary
isLink
tabIndex={0}
data-testid="heading--skip"
href="#storybook-preview-wrapper"
>
Skip to canvas
</SkipToCanvasLink>

<BrandArea>
<Brand />
</BrandArea>
Expand Down
19 changes: 19 additions & 0 deletions lib/ui/src/components/sidebar/Tree.stories.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import type { StoriesHash } from '@storybook/api';
import { screen } from '@testing-library/dom';

import { Tree } from './Tree';
import { stories } from './mockdata.large';
Expand Down Expand Up @@ -95,3 +96,21 @@ export const SingleStoryComponents = () => {
/>
);
};

// node must be selected, highlighted, and focused
// in order to tab to 'Skip to canvas' link
export const SkipToCanvasLinkFocused = {
args: {
isBrowsing: true,
isMain: true,
refId,
data: stories,
highlightedRef: { current: { itemId: 'tooltip-tooltipbuildlist--default', refId } },
setHighlightedItemId: log,
selectedStoryId: 'tooltip-tooltipbuildlist--default',
onSelectStoryId: () => {},
},
play: () => {
screen.getByTestId('node--skip').focus();
darleendenno marked this conversation as resolved.
Show resolved Hide resolved
},
};
75 changes: 55 additions & 20 deletions lib/ui/src/components/sidebar/Tree.tsx
@@ -1,7 +1,7 @@
import type { Group, Story, StoriesHash } from '@storybook/api';
import { isRoot, isStory } from '@storybook/api';
import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
import { Button, Icons } from '@storybook/components';
import { transparentize } from 'polished';
import React, { MutableRefObject, useCallback, useMemo, useRef } from 'react';

Expand Down Expand Up @@ -98,6 +98,28 @@ const CollapseButton = styled.button(({ theme }) => ({
},
}));

const LeafNodeStyleWrapper = styled.div(({ theme }) => ({
position: 'relative',
}));

const SkipToContentLink = styled(Button)(({ theme }) => ({
display: 'none',
'@media (min-width: 600px)': {
display: 'block',
zIndex: -1,
position: 'absolute',
top: 1,
right: 20,
height: '20px',
fontSize: '10px',
padding: '5px 10px',
'&:focus': {
background: 'white',
zIndex: 1,
},
},
}));

interface NodeProps {
item: Item;
refId: string;
Expand Down Expand Up @@ -130,25 +152,38 @@ const Node = React.memo<NodeProps>(
if (isStory(item)) {
const LeafNode = item.isComponent ? DocumentNode : StoryNode;
return (
<LeafNode
key={id}
id={id}
className="sidebar-item"
data-ref-id={refId}
data-item-id={item.id}
data-parent-id={item.parent}
data-nodetype={item.isComponent ? 'document' : 'story'}
data-selected={isSelected}
data-highlightable={isDisplayed}
depth={isOrphan ? item.depth : item.depth - 1}
href={getLink(item.id, refId)}
onClick={(event) => {
event.preventDefault();
onSelectStoryId(item.id);
}}
>
{item.renderLabel?.(item) || item.name}
</LeafNode>
<LeafNodeStyleWrapper>
<LeafNode
key={id}
id={id}
className="sidebar-item"
data-ref-id={refId}
data-item-id={item.id}
data-parent-id={item.parent}
data-nodetype={item.isComponent ? 'document' : 'story'}
data-selected={isSelected}
data-highlightable={isDisplayed}
depth={isOrphan ? item.depth : item.depth - 1}
href={getLink(item.id, refId)}
onClick={(event) => {
event.preventDefault();
onSelectStoryId(item.id);
}}
>
{item.renderLabel?.(item) || item.name}
</LeafNode>
{isSelected && (
<SkipToContentLink
secondary
outline
isLink
href="#storybook-preview-wrapper"
data-testid="node--skip"
>
Skip to canvas
</SkipToContentLink>
)}
</LeafNodeStyleWrapper>
);
}

Expand Down
1 change: 1 addition & 0 deletions lib/ui/src/components/sidebar/TreeNode.stories.tsx
Expand Up @@ -6,6 +6,7 @@ import { ComponentNode, DocumentNode, GroupNode, StoryNode } from './TreeNode';
export default {
title: 'UI/Sidebar/TreeNode',
parameters: { layout: 'fullscreen' },
component: StoryNode,
};

export const Types = () => (
Expand Down
3 changes: 2 additions & 1 deletion lib/ui/src/components/sidebar/TreeNode.tsx
Expand Up @@ -57,6 +57,7 @@ const BranchNode = styled.button<{
isExpandable?: boolean;
isExpanded?: boolean;
isComponent?: boolean;
isSelected?: boolean;
}>(({ theme, depth = 0, isExpandable = false }) => ({
width: '100%',
border: 'none',
Expand Down Expand Up @@ -151,7 +152,7 @@ export const GroupNode: FunctionComponent<
));

export const ComponentNode: FunctionComponent<ComponentProps<typeof BranchNode>> = React.memo(
({ theme, children, isExpanded, isExpandable, ...props }) => (
({ theme, children, isExpanded, isExpandable, isSelected, ...props }) => (
<BranchNode isExpandable={isExpandable} tabIndex={-1} {...props}>
{isExpandable && <CollapseIcon isExpanded={isExpanded} />}
<TypeIcon symbol="component" color="secondary" />
Expand Down
2 changes: 0 additions & 2 deletions lib/ui/src/components/sidebar/useExpanded.ts
Expand Up @@ -167,8 +167,6 @@ export const useExpanded = ({
(target as HTMLButtonElement).blur();
}

event.preventDefault();

const type = highlightedElement.getAttribute('data-nodetype');
if ((isEnter || isSpace) && ['component', 'story', 'document'].includes(type)) {
onSelectStoryId(highlightedItemId);
Expand Down