Skip to content

Commit

Permalink
Merge pull request #29507 from storybookjs/dropdown-menu-groups
Browse files Browse the repository at this point in the history
Core: Add support for groups to `TooltipLinkList` and use it for main Storybook menu
  • Loading branch information
ghengeveld authored Nov 1, 2024
2 parents 46e37f1 + b02bc0a commit 5a4322c
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 83 deletions.
1 change: 1 addition & 0 deletions code/core/src/components/components/tooltip/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const Item = styled.div<ItemProps>(
({ theme }) => ({
width: '100%',
border: 'none',
borderRadius: theme.appBorderRadius,
background: 'none',
fontSize: theme.typography.size.s1,
transition: 'all 150ms ease-out',
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/components/components/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const Wrapper = styled.div<WrapperProps>(
drop-shadow(0px 5px 5px rgba(0,0,0,0.05))
drop-shadow(0 1px 3px rgba(0,0,0,0.1))
`,
borderRadius: theme.appBorderRadius,
borderRadius: theme.appBorderRadius + 2,
fontSize: theme.typography.size.s1,
}
: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,45 @@ export const WithCustomIcon = {
],
},
} satisfies Story;

export const WithGroups = {
args: {
links: [
[
{
id: '1',
title: 'Link 1',
center: 'This is an addition description',
href: 'http://google.com',
onClick: onLinkClick,
},
],
[
{
id: '1',
title: 'Link 1',
center: 'This is an addition description',
icon: <LinkIcon />,
href: 'http://google.com',
onClick: onLinkClick,
},
{
id: '2',
title: 'Link 2',
center: 'This is an addition description',
href: 'http://google.com',
onClick: onLinkClick,
},
],
[
{
id: '2',
title: 'Link 2',
center: 'This is an addition description',
href: 'http://google.com',
onClick: onLinkClick,
},
],
],
},
} satisfies Story;
30 changes: 23 additions & 7 deletions code/core/src/components/components/tooltip/TooltipLinkList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ const List = styled.div(
minWidth: 180,
overflow: 'hidden',
overflowY: 'auto',
maxHeight: 15.5 * 32, // 11.5 items
maxHeight: 15.5 * 32 + 8, // 15.5 items at 32px each + 8px padding
},
({ theme }) => ({
borderRadius: theme.appBorderRadius,
borderRadius: theme.appBorderRadius + 2,
})
);

const Group = styled.div(({ theme }) => ({
padding: 4,
'& + &': {
borderTop: `1px solid ${theme.appBorderColor}`,
},
}));

export interface Link extends Omit<ListItemProps, 'onClick'> {
id: string;
onClick?: (
Expand All @@ -42,17 +49,26 @@ const Item = ({ id, onClick, ...rest }: ItemProps) => {
};

export interface TooltipLinkListProps extends ComponentProps<typeof List> {
links: Link[];
links: Link[] | Link[][];
LinkWrapper?: LinkWrapperType;
}

export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkListProps) => {
const isIndented = links.some((link) => link.icon);
const groups = Array.isArray(links[0]) ? (links as Link[][]) : [links as Link[]];
const isIndented = groups.some((group) => group.some((link) => link.icon));
return (
<List {...props}>
{links.map((link) => (
<Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />
))}
{groups
.filter((group) => group.length)
.map((group, index) => {
return (
<Group key={group.map((link) => link.id).join(`~${index}~`)}>
{group.map((link) => (
<Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />
))}
</Group>
);
})}
</List>
);
};
29 changes: 17 additions & 12 deletions code/core/src/manager/components/sidebar/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { CloseIcon, CogIcon } from '@storybook/icons';

import { transparentize } from 'polished';

import type { useMenu } from '../../container/Menu';
import { useLayout } from '../layout/LayoutProvider';

export type MenuList = ComponentProps<typeof TooltipLinkList>['links'];
export type MenuList = ReturnType<typeof useMenu>;

export const SidebarIconButton: FC<ComponentProps<typeof Button> & { highlighted: boolean }> =
styled(IconButton)<
Expand Down Expand Up @@ -60,17 +61,21 @@ const SidebarMenuList: FC<{
menu: MenuList;
onHide: () => void;
}> = ({ menu, onHide }) => {
const links = useMemo(() => {
return menu.map(({ onClick, ...rest }) => ({
...rest,
onClick: ((event, item) => {
if (onClick) {
onClick(event, item);
}
onHide();
}) as ClickHandler,
}));
}, [menu, onHide]);
const links = useMemo(
() =>
menu.map((group) =>
group.map(({ onClick, ...rest }) => ({
...rest,
onClick: ((event, item) => {
if (onClick) {
onClick(event, item);
}
onHide();
}) as ClickHandler,
}))
),
[menu, onHide]
);
return <TooltipLinkList links={links} />;
};

Expand Down
84 changes: 45 additions & 39 deletions code/core/src/manager/components/sidebar/TagsFilterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { Tag } from '@storybook/types';

import type { API } from '@storybook/core/manager-api';

import type { Link } from '../../../components/components/tooltip/TooltipLinkList';

const BUILT_IN_TAGS_SHOW = new Set(['play-fn']);

const Wrapper = styled.div({
Expand All @@ -29,56 +31,60 @@ export const TagsFilterPanel = ({
toggleTag,
isDevelopment,
}: TagsFilterPanelProps) => {
const theme = useTheme();
const userTags = allTags.filter((tag) => !BUILT_IN_TAGS_SHOW.has(tag));
const docsUrl = api.getDocsUrl({ subpath: 'writing-stories/tags#filtering-by-custom-tags' });
const items = allTags.map((tag) => {
const checked = selectedTags.includes(tag);
const id = `tag-${tag}`;
return {
id,
title: tag,
right: (
<input
type="checkbox"
id={id}
name={id}
value={tag}
checked={checked}
onChange={() => {
// The onClick handler higher up the tree will handle the toggle
// For controlled inputs, a onClick handler is needed, though
// Accessibility-wise this isn't optimal, but I guess that's a limitation
// of the current design of TooltipLinkList
}}
/>
),
onClick: () => toggleTag(tag),
};
}) as any[];

const groups = [
allTags.map((tag) => {
const checked = selectedTags.includes(tag);
const id = `tag-${tag}`;
return {
id,
title: tag,
right: (
<input
type="checkbox"
id={id}
name={id}
value={tag}
checked={checked}
onChange={() => {
// The onClick handler higher up the tree will handle the toggle
// For controlled inputs, a onClick handler is needed, though
// Accessibility-wise this isn't optimal, but I guess that's a limitation
// of the current design of TooltipLinkList
}}
/>
),
onClick: () => toggleTag(tag),
};
}),
] as Link[][];

if (allTags.length === 0) {
items.push({
id: 'no-tags',
title: 'There are no tags. Use tags to organize and filter your Storybook.',
isIndented: false,
});
groups.push([
{
id: 'no-tags',
title: 'There are no tags. Use tags to organize and filter your Storybook.',
isIndented: false,
},
]);
}

if (userTags.length === 0 && isDevelopment) {
items.push({
id: 'tags-docs',
title: 'Learn how to add tags',
icon: <ShareAltIcon />,
href: docsUrl,
style: {
borderTop: `4px solid ${theme.appBorderColor}`,
groups.push([
{
id: 'tags-docs',
title: 'Learn how to add tags',
icon: <ShareAltIcon />,
href: docsUrl,
},
});
]);
}

return (
<Wrapper>
<TooltipLinkList links={items} />
<TooltipLinkList links={groups} />
</Wrapper>
);
};
51 changes: 27 additions & 24 deletions code/core/src/manager/container/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { STORIES_COLLAPSE_ALL } from '@storybook/core/core-events';
import type { API, State } from '@storybook/core/manager-api';
import { shortcutToHumanString } from '@storybook/core/manager-api';

import type { Link } from '../../components/components/tooltip/TooltipLinkList';

const focusableUIElements = {
storySearchField: 'storybook-explorer-searchfield',
storyListMenu: 'storybook-explorer-menu',
Expand Down Expand Up @@ -58,8 +60,7 @@ export const useMenu = (
isPanelShown: boolean,
isNavShown: boolean,
enableShortcuts: boolean
) => {
const theme = useTheme();
): Link[][] => {
const shortcutKeys = api.getShortcutKeys();

const about = useMemo(
Expand Down Expand Up @@ -105,11 +106,8 @@ export const useMenu = (
title: 'Keyboard shortcuts',
onClick: () => api.changeSettingsTab('shortcuts'),
right: enableShortcuts ? <Shortcut keys={shortcutKeys.shortcutsPage} /> : null,
style: {
borderBottom: `4px solid ${theme.appBorderColor}`,
},
}),
[api, enableShortcuts, shortcutKeys.shortcutsPage, theme.appBorderColor]
[api, enableShortcuts, shortcutKeys.shortcutsPage]
);

const sidebarToggle = useMemo(
Expand Down Expand Up @@ -244,24 +242,29 @@ export const useMenu = (
}, [api, enableShortcuts, shortcutKeys]);

return useMemo(
() => [
about,
...(state.whatsNewData?.status === 'SUCCESS' ? [whatsNew] : []),
documentation,
shortcuts,
sidebarToggle,
toolbarToogle,
addonsToggle,
addonsOrientationToggle,
fullscreenToggle,
searchToggle,
up,
down,
prev,
next,
collapse,
...getAddonsShortcuts(),
],
() =>
[
[
about,
...(state.whatsNewData?.status === 'SUCCESS' ? [whatsNew] : []),
documentation,
shortcuts,
],
[
sidebarToggle,
toolbarToogle,
addonsToggle,
addonsOrientationToggle,
fullscreenToggle,
searchToggle,
up,
down,
prev,
next,
collapse,
],
getAddonsShortcuts(),
] satisfies Link[][],
[
about,
state,
Expand Down

0 comments on commit 5a4322c

Please sign in to comment.