From 58399baa2a551717448279d6384ffc7be269af7d Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 2 Feb 2023 18:10:05 +0100 Subject: [PATCH] Menu design upgrades 1. Icons are always on the left. If there is one icon in a menu item, every other menu item will have an indent to match the space of the icon as seen in the Storybook menu. 2. If there are no icons, the item label is not indented. 3. There is a node on the right of each menu item that can be populated with keyboard shortcuts, illustrations, etc. 4. Active items (denoted with a checkmark) will be both bold and blue. 5. There will be an optional `description` text node that will be below the item label. 6. Keyboard shortcuts will list all commands in one container. 7. The decorative menu tooltip arrows are no longer shown. --- .../src/components/ToolbarMenuListItem.tsx | 40 +--- code/ui/components/src/index.ts | 2 + code/ui/components/src/tabs/tabs.hooks.tsx | 1 - code/ui/components/src/tooltip/ListItem.tsx | 105 ++++++----- code/ui/components/src/tooltip/Tooltip.tsx | 2 +- .../src/tooltip/TooltipLinkList.stories.tsx | 178 +++++++++++++++--- .../src/tooltip/TooltipLinkList.tsx | 27 ++- .../components/src/tooltip/assets/ellipse.png | Bin 0 -> 1489 bytes code/ui/components/src/typings.d.ts | 1 + .../src/components/sidebar/Menu.stories.tsx | 26 +-- .../manager/src/components/sidebar/Menu.tsx | 4 + .../src/components/sidebar/RefIndicator.tsx | 3 +- .../manager/src/containers/Menu.stories.tsx | 93 +++++++++ code/ui/manager/src/containers/menu.tsx | 73 +++---- code/ui/manager/src/globals/exports.ts | 2 + 15 files changed, 381 insertions(+), 176 deletions(-) create mode 100644 code/ui/components/src/tooltip/assets/ellipse.png create mode 100644 code/ui/manager/src/containers/Menu.stories.tsx diff --git a/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx b/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx index 35979e0b05dd..63e383d41446 100644 --- a/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx +++ b/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx @@ -1,18 +1,10 @@ -import type { ReactNode } from 'react'; +import type { ComponentProps } from 'react'; import React from 'react'; +import type { TooltipLinkList } from '@storybook/components'; import { Icons } from '@storybook/components'; import type { ToolbarItem } from '../types'; -interface ListItem { - id: string; - left?: ReactNode; - title?: ReactNode; - right?: ReactNode; - active?: boolean; - onClick?: () => void; -} - -type ToolbarMenuListItemProps = { +export type ToolbarMenuListItemProps = { currentValue: string; onClick: () => void; } & ToolbarItem; @@ -28,34 +20,18 @@ export const ToolbarMenuListItem = ({ currentValue, }: ToolbarMenuListItemProps) => { const Icon = icon && ; - const hasContent = left || right || title; - const Item: ListItem = { + const Item: ComponentProps['links'][0] = { id: value || currentValue, active: currentValue === value, + right, + title, + left, onClick, }; - if (left) { - Item.left = left; - } - - if (right) { - Item.right = right; - } - - if (title) { - Item.title = title; - } - if (icon && !hideIcon) { - if (hasContent && !right) { - Item.right = Icon; - } else if (hasContent && !left) { - Item.left = Icon; - } else if (!hasContent) { - Item.right = Icon; - } + Item.left = Icon; } return Item; diff --git a/code/ui/components/src/index.ts b/code/ui/components/src/index.ts index e2b6ef984165..8690955e4a75 100644 --- a/code/ui/components/src/index.ts +++ b/code/ui/components/src/index.ts @@ -57,6 +57,7 @@ export { WithTooltip, WithTooltipPure } from './tooltip/lazy-WithTooltip'; export { TooltipMessage } from './tooltip/TooltipMessage'; export { TooltipNote } from './tooltip/TooltipNote'; export { TooltipLinkList } from './tooltip/TooltipLinkList'; +export { default as ListItem } from './tooltip/ListItem'; // Toolbar and subcomponents export { Tabs, TabsState, TabBar, TabWrapper } from './tabs/tabs'; @@ -68,6 +69,7 @@ export { AddonPanel } from './addon-panel/addon-panel'; // Graphics export type { IconsProps } from './icon/icon'; export { Icons, Symbols } from './icon/icon'; +export { icons } from './icon/icons'; export { StorybookLogo } from './brand/StorybookLogo'; export { StorybookIcon } from './brand/StorybookIcon'; diff --git a/code/ui/components/src/tabs/tabs.hooks.tsx b/code/ui/components/src/tabs/tabs.hooks.tsx index d6aeb270a672..a3d506137937 100644 --- a/code/ui/components/src/tabs/tabs.hooks.tsx +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -60,7 +60,6 @@ export function useList(list: ChildrenList) { <> ( - { - '& svg': { - transition: 'all 200ms ease-out', - opacity: 0, - height: 12, - width: 12, - margin: '3px 0', - verticalAlign: 'top', - }, - '& path': { - fill: 'inherit', - }, +const Right = styled.span({ + display: 'flex', + '& svg': { + height: 12, + width: 12, + margin: '3px 0', + verticalAlign: 'top', }, - ({ active, theme }) => - active - ? { - '& svg': { - opacity: 1, - }, - '& path': { - fill: theme.color.secondary, - }, - } - : {} -); - -const Center = styled.span({ - flex: 1, - textAlign: 'left', - display: 'inline-flex', - - '& > * + *': { - paddingLeft: 10, + '& path': { + fill: 'inherit', }, }); +const Center = styled.span<{ isIndented: boolean }>( + { + flex: 1, + textAlign: 'left', + display: 'flex', + flexDirection: 'column', + }, + ({ isIndented }) => (isIndented ? { marginLeft: 24 } : {}) +); + export interface CenterTextProps { active?: boolean; disabled?: boolean; } const CenterText = styled.span( - { - flex: 1, - textAlign: 'center', - }, ({ active, theme }) => active ? { @@ -112,17 +96,22 @@ export interface LeftProps { active?: boolean; } -const Left = styled.span(({ active, theme }) => - active - ? { - '& svg': { - opacity: 1, - }, - '& path': { - fill: theme.color.primary, - }, - } - : {} +const Left = styled.span( + ({ active, theme }) => + active + ? { + '& svg': { + opacity: 1, + }, + '& svg path': { + fill: theme.color.secondary, + }, + } + : {}, + () => ({ + display: 'flex', + maxWidth: 14, + }) ); export interface ItemProps { @@ -188,14 +177,20 @@ export type LinkWrapperType = FC; export interface ListItemProps extends Omit, 'href' | 'title'> { loading?: boolean; + /** + * @deprecated ReactNode elements on the right side of the list item are deprecated. + * Use `icon` property instead. + */ left?: ReactNode; title?: ReactNode; center?: ReactNode; right?: ReactNode; + icon?: keyof typeof icons | ReactElement; active?: boolean; disabled?: boolean; href?: string; LinkWrapper?: LinkWrapperType; + isIndented?: boolean; } const ListItem: FC = ({ @@ -204,8 +199,10 @@ const ListItem: FC = ({ title, center, right, + icon, active, disabled, + isIndented, href, onClick, LinkWrapper, @@ -214,11 +211,17 @@ const ListItem: FC = ({ const itemProps = getItemProps(onClick, href, LinkWrapper); const commonProps = { active, disabled }; + const isStorybookIcon = typeof icon === 'string' && icons[icon]; + return ( - {left && {left}} + {icon ? ( + {isStorybookIcon ? : icon} + ) : ( + left && {left} + )} {title || center ? ( -
+
{title && ( {title} diff --git a/code/ui/components/src/tooltip/Tooltip.tsx b/code/ui/components/src/tooltip/Tooltip.tsx index 98e8dce76a20..7c748773b6ea 100644 --- a/code/ui/components/src/tooltip/Tooltip.tsx +++ b/code/ui/components/src/tooltip/Tooltip.tsx @@ -126,7 +126,7 @@ export interface TooltipProps { export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>( ( - { placement, hasChrome, children, arrowProps, tooltipRef, color, withArrows = true, ...props }, + { placement, hasChrome, children, arrowProps, tooltipRef, color, withArrows, ...props }, ref ) => { return ( diff --git a/code/ui/components/src/tooltip/TooltipLinkList.stories.tsx b/code/ui/components/src/tooltip/TooltipLinkList.stories.tsx index 6aed9627a331..e3006e96766b 100644 --- a/code/ui/components/src/tooltip/TooltipLinkList.stories.tsx +++ b/code/ui/components/src/tooltip/TooltipLinkList.stories.tsx @@ -1,9 +1,11 @@ import type { FunctionComponent, MouseEvent, ReactElement } from 'react'; import React, { Children, cloneElement } from 'react'; import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react'; import { WithTooltip } from './WithTooltip'; import { TooltipLinkList } from './TooltipLinkList'; +import { Icons } from '../icon/icon'; +import ellipseUrl from './assets/ellipse.png'; const onLinkClick = action('onLinkClick'); @@ -27,24 +29,6 @@ const StoryLinkWrapper: FunctionComponent<StoryLinkWrapperProps> = ({ }); }; -export const links = [ - { - id: '1', - title: 'Link', - href: 'http://google.com', - }, - { - id: '2', - title: 'Link', - href: 'http://google.com', - }, - { - id: '3', - title: 'callback', - onClick: action('onClick'), - }, -]; - export default { component: TooltipLinkList, decorators: [ @@ -61,18 +45,160 @@ export default { ), ], excludeStories: ['links'], -} as Meta; +} satisfies Meta<typeof TooltipLinkList>; -export const Links = { +type Story = StoryObj<typeof TooltipLinkList>; + +export const WithoutIcons = { args: { - links: links.slice(0, 2), + links: [ + { + id: '1', + title: 'Link 1', + center: 'This is an addition description', + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + href: 'http://google.com', + }, + ], LinkWrapper: StoryLinkWrapper, }, -}; +} satisfies Story; -export const LinksAndCallback = { +export const WithOneIcon = { args: { - links, + links: [ + { + id: '1', + title: 'Link 1', + center: 'This is an addition description', + icon: 'link', + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + href: 'http://google.com', + }, + ], LinkWrapper: StoryLinkWrapper, }, -}; +} satisfies Story; + +export const ActiveWithoutAnyIcons = { + args: { + links: [ + { + id: '1', + title: 'Link 1', + active: true, + center: 'This is an addition description', + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + href: 'http://google.com', + }, + ], + LinkWrapper: StoryLinkWrapper, + }, +} satisfies Story; + +export const ActiveWithSeparateIcon = { + args: { + links: [ + { + id: '1', + title: 'Link 1', + icon: 'link', + center: 'This is an addition description', + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + active: true, + center: 'This is an addition description', + href: 'http://google.com', + }, + ], + LinkWrapper: StoryLinkWrapper, + }, +} satisfies Story; + +export const ActiveAndIcon = { + args: { + links: [ + { + id: '1', + title: 'Link 1', + active: true, + icon: 'link', + center: 'This is an addition description', + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + href: 'http://google.com', + }, + ], + LinkWrapper: StoryLinkWrapper, + }, +} satisfies Story; + +export const WithIllustration = { + args: { + links: [ + { + id: '1', + title: 'Link 1', + active: true, + icon: 'link', + right: <img src={ellipseUrl} width="16" height="16" alt="ellipse" />, + center: 'This is an addition description', + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + right: <img src={ellipseUrl} width="16" height="16" alt="ellipse" />, + href: 'http://google.com', + }, + ], + LinkWrapper: StoryLinkWrapper, + }, +} satisfies Story; + +export const WithCustomIcon = { + args: { + links: [ + { + id: '1', + title: 'Link 1', + active: true, + icon: <Icons icon="linux" />, + right: <img src={ellipseUrl} width="16" height="16" alt="ellipse" />, + center: 'This is an addition description', + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + right: <img src={ellipseUrl} width="16" height="16" alt="ellipse" />, + href: 'http://google.com', + }, + ], + LinkWrapper: StoryLinkWrapper, + }, +} satisfies Story; diff --git a/code/ui/components/src/tooltip/TooltipLinkList.tsx b/code/ui/components/src/tooltip/TooltipLinkList.tsx index b3bc5e697409..8e3008eb6b64 100644 --- a/code/ui/components/src/tooltip/TooltipLinkList.tsx +++ b/code/ui/components/src/tooltip/TooltipLinkList.tsx @@ -28,8 +28,8 @@ export interface TooltipLinkListProps { LinkWrapper?: LinkWrapperType; } -const Item: FC<TooltipLinkListProps['links'][number]> = (props) => { - const { LinkWrapper, onClick: onClickFromProps, id, ...rest } = props; +const Item: FC<TooltipLinkListProps['links'][number] & { isIndented?: boolean }> = (props) => { + const { LinkWrapper, onClick: onClickFromProps, id, isIndented, ...rest } = props; const { title, href, active } = rest; const onClick = useCallback( (event: SyntheticEvent) => { @@ -47,19 +47,28 @@ const Item: FC<TooltipLinkListProps['links'][number]> = (props) => { href={href} id={`list-item-${id}`} LinkWrapper={LinkWrapper} + isIndented={isIndented} {...rest} {...(hasOnClick ? { onClick } : {})} /> ); }; -export const TooltipLinkList: FC<TooltipLinkListProps> = ({ links, LinkWrapper }) => ( - <List> - {links.map(({ isGatsby, ...p }) => ( - <Item key={p.id} LinkWrapper={isGatsby ? LinkWrapper : null} {...p} /> - ))} - </List> -); +export const TooltipLinkList: FC<TooltipLinkListProps> = ({ links, LinkWrapper }) => { + const hasOneLeftElement = links.some((link) => link.left || link.icon); + return ( + <List> + {links.map(({ isGatsby, ...p }) => ( + <Item + key={p.id} + LinkWrapper={isGatsby ? LinkWrapper : null} + isIndented={hasOneLeftElement} + {...p} + /> + ))} + </List> + ); +}; TooltipLinkList.defaultProps = { LinkWrapper: ListItem.defaultProps.LinkWrapper, diff --git a/code/ui/components/src/tooltip/assets/ellipse.png b/code/ui/components/src/tooltip/assets/ellipse.png new file mode 100644 index 0000000000000000000000000000000000000000..b17235199494c80bc530b2e71d11eb1ec9e43d38 GIT binary patch literal 1489 zcmV;?1upuDP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH1!hS^K~#7Fl~=J+ z97hoCo;wgH3V9W{N-kVxoyn1pz%h_n8B{s2v!lv6kh$yt6xijA(-$B`R;J6O3=uhS zl|hk}v1E#LYg`D}W4Ap!-90mVAcO|)_GWf=`t_UF-LnWcY;@R7pft<n%+$j1MZ239 znC7EaGAs<_Ba^=^TG{caeUP7jy7%Al7lZ!FR(_Lp@q>n+-%DY7at&rWJ-Bl_p_vX@ z!)T4xhN%WHJg@sL7#3~&`!oBQg@23L$8XPOAHMkEL3Z8x=Dmh@l7ya4Z$o);XuE3` zYhvM&{qs}Y7a{sC>KH&-L9Ze_{ATCXc6aA6`6vLiJ(WUnybl$vN4D$*k)SqM+qA!J zA7c(37@dKFrM33%{LbI^w}<JC02IF_T`FPMt`j|gyq+`3AFAD~jmZ}%BmPQ457oSU z*(pZ5i$5QBuLW@W)4jA+;4=!w1LY(PRG^+S=fuRj4&RA!Q}pcIM2XM7U;ih)5&*5I zN$VVUt${g^^&p5;_rUQRY>@lYO#udj?mvE7TSY0H&~C4{pIibkI)zQ9y<WwU93CY0 z4#Z`anK-FKyhn`H65+fjl_l_YuterM@2brPfYaXnw7S4`Q4qV%k5b@3{6tm0_uF&9 zM7d!?5;O>nIrXhpS*~w=`)3*$C}5LqFn848jcA-o6h{#Q6&O9H?h6|JA$rj58C9b- z2Vl7DGF-2AynZSYG%rAEPR1R>BuoH{5f#IjWI)~GEsza!{Io~Z+g{vF%tQ&x`Upjw zlHbtCK@`PBMp%~0L@+$AzYt8q2+$|h$=R~O$}G6G$w8ExsEGs{FeK4LL{r!ecb7sY zh*1WKB5$-K+f=Y%HGo~6Lz=!K#v?u&i`U!lQc^TS#L21Wl5jlB3ZG0xU1Qft8%iae zD@IjIQ54jzqDX^204Ts4bHY*YHO8wO8<KStB#O{YK-GPeWHp+d0j3>A4~UUrk|1@< zp!ZxJ#ReVhZDKs79Hf#1bZ2AtK<bP~gG3Q*pE!}l5l4a)5H!%lWqb9|P`1<aOJ4Cn zo(bt|==n@ZS_2%*9P1S|1c1W49T&$XhXW3c)#H&w5@`NK2%6?a1w<%9*wmP3LwY<U zyCwVY(sO;`$g#c3KxXXFfr=?6U2tk5@>>yADT*tG14e9B4Tj1nwL#es_!XL0h79ca z%s-wzph;w-$;rTX2{2AKObRH4gdxp<I#U_iGxjJqu($FcO#Q+T0ghnu1ZgTpjfkKv z@Wt8#;$sv^dEiWrZUHlWHZXSw2RXFp2}XIG^%&Qgu?^R=pBSU2<^eTI6$6GaRP|gK z^LXRsO75y5R@B#><JAd#XfY$GY$Ak6h*?&;oKC2j^`hG5mbSvfa-w`#k2nCGr>`?; z<0Fcql!zkdH4}Pn*=s)*?ol*TTn_Oe$-c0*et)r)c>s00)v31ZC4=}Vq`?vDfIAga zWHgo)gwhNlVMSTdNfEQod235ecmUYX@{(xD&OPGgLs)AqA8&d>j%%$KTAas;Beb$P z=j=8v^#}9$B>;8n^xtIFEm*O0vuHRt?G1|vxn^LTDNrOVVaRe|u8zv~#LA%mRlN4) z_kzh^@&jyvWx|3$?-d_P8-`t`QLRZhDhz;daCEn_(_0fuFMs>|VBFr7Z$`Lrmce8- zZ{^w#Jh1MgcvDE6C3pzpt>UqZelc}C|9azYc9pF*yr<!+O-^vKLC<F%L90HrQqLbb zgc;PMI>z2RdU9LqpBOyt|Ft@w!8NT7A87dCO=d5vw4ZK3HN9+Kj25klukBcB;Uwa> r1&f(6a9}%olLZa_`0mFWy`KLMIWH<k6{wmd00000NkvXXu0mjflh(3K literal 0 HcmV?d00001 diff --git a/code/ui/components/src/typings.d.ts b/code/ui/components/src/typings.d.ts index b7d6bd46cf5b..388e1a012e23 100644 --- a/code/ui/components/src/typings.d.ts +++ b/code/ui/components/src/typings.d.ts @@ -1,2 +1,3 @@ declare module '*.md'; declare module '*.mdx'; +declare module '*.png'; diff --git a/code/ui/manager/src/components/sidebar/Menu.stories.tsx b/code/ui/manager/src/components/sidebar/Menu.stories.tsx index cb57d6d6e003..8d22f6aecd8d 100644 --- a/code/ui/manager/src/components/sidebar/Menu.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Menu.stories.tsx @@ -1,33 +1,21 @@ import { expect } from '@storybook/jest'; -import type { FunctionComponent } from 'react'; -import React, { Fragment } from 'react'; +import type { ComponentProps } from 'react'; +import React from 'react'; import { TooltipLinkList } from '@storybook/components'; import { styled } from '@storybook/theming'; import { within, userEvent, screen } from '@storybook/testing-library'; -import { MenuItemIcon, SidebarMenu, ToolbarMenu } from './Menu'; +import { SidebarMenu, ToolbarMenu } from './Menu'; import { useMenu } from '../../containers/menu'; export default { - component: MenuItemIcon, + component: SidebarMenu, title: 'Sidebar/Menu', - decorators: [ - (StoryFn: FunctionComponent) => ( - <Fragment> - <StoryFn /> - </Fragment> - ), - ], }; -const fakemenu = [ - { title: 'has icon', left: <MenuItemIcon icon="check" />, id: 'icon' }, - { - title: 'has imgSrc', - left: <MenuItemIcon imgSrc="https://storybook.js.org/images/placeholders/20x20.png" />, - id: 'img', - }, - { title: 'has neither', left: <MenuItemIcon />, id: 'non' }, +const fakemenu: ComponentProps<typeof TooltipLinkList>['links'] = [ + { title: 'has icon', icon: 'link', id: 'icon' }, + { title: 'has no icon', id: 'non' }, ]; export const Items = () => <TooltipLinkList links={fakemenu} />; diff --git a/code/ui/manager/src/components/sidebar/Menu.tsx b/code/ui/manager/src/components/sidebar/Menu.tsx index 64c0508e4c67..48a876c05436 100644 --- a/code/ui/manager/src/components/sidebar/Menu.tsx +++ b/code/ui/manager/src/components/sidebar/Menu.tsx @@ -66,6 +66,10 @@ export interface ListItemIconProps { imgSrc?: string; } +/** + * @deprecated Please use `Icons` from `@storybook/components` instead + * Component will be removed in SB 8.0 + */ export const MenuItemIcon = ({ icon, imgSrc }: ListItemIconProps) => { if (icon) { return <Icon icon={icon} />; diff --git a/code/ui/manager/src/components/sidebar/RefIndicator.tsx b/code/ui/manager/src/components/sidebar/RefIndicator.tsx index 0d116a63f5c0..3c8cd8b5a375 100644 --- a/code/ui/manager/src/components/sidebar/RefIndicator.tsx +++ b/code/ui/manager/src/components/sidebar/RefIndicator.tsx @@ -7,7 +7,6 @@ import { styled } from '@storybook/theming'; import { transparentize } from 'polished'; import { useStorybookApi } from '@storybook/manager-api'; -import { MenuItemIcon } from './Menu'; import type { RefType } from './types'; import type { getStateType } from './utils'; @@ -222,7 +221,7 @@ export const RefIndicator = React.memo( tooltip={ <TooltipLinkList links={Object.entries(ref.versions).map(([id, href]) => ({ - left: href === ref.url ? <MenuItemIcon icon="check" /> : <span />, + icon: href === ref.url ? 'check' : undefined, id, title: id, href, diff --git a/code/ui/manager/src/containers/Menu.stories.tsx b/code/ui/manager/src/containers/Menu.stories.tsx new file mode 100644 index 000000000000..0865f8709748 --- /dev/null +++ b/code/ui/manager/src/containers/Menu.stories.tsx @@ -0,0 +1,93 @@ +import type { FunctionComponent, MouseEvent, ReactElement } from 'react'; +import React, { Children, cloneElement } from 'react'; +import { action } from '@storybook/addon-actions'; +import type { Meta, StoryObj } from '@storybook/react'; +import { TooltipLinkList, WithTooltip } from '@storybook/components'; +import { Shortcut } from './menu'; + +const onLinkClick = action('onLinkClick'); + +interface StoryLinkWrapperProps { + href: string; + passHref?: boolean; +} + +const StoryLinkWrapper: FunctionComponent<StoryLinkWrapperProps> = ({ + href, + passHref = false, + children, +}) => { + const child = Children.only(children) as ReactElement; + return cloneElement(child, { + href: passHref && href, + onClick: (e: MouseEvent) => { + e.preventDefault(); + onLinkClick(href); + }, + }); +}; + +export default { + component: TooltipLinkList, + decorators: [ + (storyFn) => ( + <div + style={{ + height: '300px', + }} + > + <WithTooltip placement="top" trigger="click" startOpen tooltip={storyFn()}> + <div>Tooltip</div> + </WithTooltip> + </div> + ), + ], + excludeStories: ['links'], +} satisfies Meta<typeof TooltipLinkList>; + +type Story = StoryObj<typeof TooltipLinkList>; + +export const WithShortcuts = { + args: { + links: [ + { + id: '1', + title: 'Link 1', + center: 'This is an addition description', + right: <Shortcut keys={['⌘']} />, + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + right: <Shortcut keys={['⌘', 'K']} />, + href: 'http://google.com', + }, + ], + LinkWrapper: StoryLinkWrapper, + }, +} satisfies Story; + +export const WithShortcutsActive = { + args: { + links: [ + { + id: '1', + title: 'Link 1', + center: 'This is an addition description', + active: true, + right: <Shortcut keys={['⌘']} />, + href: 'http://google.com', + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + right: <Shortcut keys={['⌘', 'K']} />, + href: 'http://google.com', + }, + ], + LinkWrapper: StoryLinkWrapper, + }, +} satisfies Story; diff --git a/code/ui/manager/src/containers/menu.tsx b/code/ui/manager/src/containers/menu.tsx index 9e7bc34e04ef..23408c86f77a 100644 --- a/code/ui/manager/src/containers/menu.tsx +++ b/code/ui/manager/src/containers/menu.tsx @@ -1,12 +1,11 @@ import type { FC } from 'react'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; -import { Badge } from '@storybook/components'; +import { Badge, Icons } from '@storybook/components'; import type { API } from '@storybook/manager-api'; import { styled, useTheme } from '@storybook/theming'; import { shortcutToHumanString } from '@storybook/manager-api'; -import { MenuItemIcon } from '../components/sidebar/Menu'; const focusableUIElements = { storySearchField: 'storybook-explorer-searchfield', @@ -14,8 +13,8 @@ const focusableUIElements = { storyPanelRoot: 'storybook-panel-root', }; -const Key = styled.code(({ theme }) => ({ - width: 16, +const Key = styled.span(({ theme }) => ({ + display: 'inline-block', height: 16, lineHeight: '16px', textAlign: 'center', @@ -25,17 +24,26 @@ const Key = styled.code(({ theme }) => ({ borderRadius: 2, userSelect: 'none', pointerEvents: 'none', - '& + &': { - marginLeft: 2, - }, + padding: '0 6px', })); -const Shortcut: FC<{ keys: string[] }> = ({ keys }) => ( +const KeyChild = styled.code( + ({ theme }) => ` + padding: 0; + + & + & { + margin-left: 6px; + } +` +); + +export const Shortcut: FC<{ keys: string[] }> = ({ keys }) => ( <> - {keys.map((key, index) => ( - // eslint-disable-next-line react/no-array-index-key - <Key key={index}>{shortcutToHumanString([key])}</Key> - ))} + <Key> + {keys.map((key, index) => ( + <KeyChild key={key}>{shortcutToHumanString([key])}</KeyChild> + ))} + </Key> </> ); @@ -56,9 +64,8 @@ export const useMenu = ( title: 'About your Storybook', onClick: () => api.navigateToSettingsPage('/settings/about'), right: api.versionUpdateAvailable() && <Badge status="positive">Update</Badge>, - left: <MenuItemIcon />, }), - [api, enableShortcuts, shortcutKeys] + [api] ); const releaseNotes = useMemo( @@ -66,9 +73,8 @@ export const useMenu = ( id: 'release-notes', title: 'Release notes', onClick: () => api.navigateToSettingsPage('/settings/release-notes'), - left: <MenuItemIcon />, }), - [api, enableShortcuts, shortcutKeys] + [api] ); const shortcuts = useMemo( @@ -77,12 +83,11 @@ export const useMenu = ( title: 'Keyboard shortcuts', onClick: () => api.navigateToSettingsPage('/settings/shortcuts'), right: enableShortcuts ? <Shortcut keys={shortcutKeys.shortcutsPage} /> : null, - left: <MenuItemIcon />, style: { borderBottom: `4px solid ${theme.appBorderColor}`, }, }), - [api, enableShortcuts, shortcutKeys] + [api, enableShortcuts, shortcutKeys.shortcutsPage, theme.appBorderColor] ); const sidebarToggle = useMemo( @@ -90,8 +95,9 @@ export const useMenu = ( id: 'S', title: 'Show sidebar', onClick: () => api.toggleNav(), + active: showNav, right: enableShortcuts ? <Shortcut keys={shortcutKeys.toggleNav} /> : null, - left: showNav ? <MenuItemIcon icon="check" /> : <MenuItemIcon />, + left: showNav ? <Icons icon="check" /> : null, }), [api, enableShortcuts, shortcutKeys, showNav] ); @@ -101,8 +107,9 @@ export const useMenu = ( id: 'T', title: 'Show toolbar', onClick: () => api.toggleToolbar(), + active: showToolbar, right: enableShortcuts ? <Shortcut keys={shortcutKeys.toolbar} /> : null, - left: showToolbar ? <MenuItemIcon icon="check" /> : <MenuItemIcon />, + left: showToolbar ? <Icons icon="check" /> : null, }), [api, enableShortcuts, shortcutKeys, showToolbar] ); @@ -112,8 +119,9 @@ export const useMenu = ( id: 'A', title: 'Show addons', onClick: () => api.togglePanel(), + active: showPanel, right: enableShortcuts ? <Shortcut keys={shortcutKeys.togglePanel} /> : null, - left: showPanel ? <MenuItemIcon icon="check" /> : <MenuItemIcon />, + left: showPanel ? <Icons icon="check" /> : null, }), [api, enableShortcuts, shortcutKeys, showPanel] ); @@ -124,7 +132,6 @@ export const useMenu = ( title: 'Change addons orientation', onClick: () => api.togglePanelPosition(), right: enableShortcuts ? <Shortcut keys={shortcutKeys.panelPosition} /> : null, - left: <MenuItemIcon />, }), [api, enableShortcuts, shortcutKeys] ); @@ -134,8 +141,9 @@ export const useMenu = ( id: 'F', title: 'Go full screen', onClick: () => api.toggleFullscreen(), + active: isFullscreen, right: enableShortcuts ? <Shortcut keys={shortcutKeys.fullScreen} /> : null, - left: isFullscreen ? 'check' : <MenuItemIcon />, + left: isFullscreen ? <Icons icon="check" /> : null, }), [api, enableShortcuts, shortcutKeys, isFullscreen] ); @@ -146,7 +154,6 @@ export const useMenu = ( title: 'Search', onClick: () => api.focusOnUIElement(focusableUIElements.storySearchField), right: enableShortcuts ? <Shortcut keys={shortcutKeys.search} /> : null, - left: <MenuItemIcon />, }), [api, enableShortcuts, shortcutKeys] ); @@ -157,7 +164,6 @@ export const useMenu = ( title: 'Previous component', onClick: () => api.jumpToComponent(-1), right: enableShortcuts ? <Shortcut keys={shortcutKeys.prevComponent} /> : null, - left: <MenuItemIcon />, }), [api, enableShortcuts, shortcutKeys] ); @@ -168,7 +174,6 @@ export const useMenu = ( title: 'Next component', onClick: () => api.jumpToComponent(1), right: enableShortcuts ? <Shortcut keys={shortcutKeys.nextComponent} /> : null, - left: <MenuItemIcon />, }), [api, enableShortcuts, shortcutKeys] ); @@ -179,7 +184,6 @@ export const useMenu = ( title: 'Previous story', onClick: () => api.jumpToStory(-1), right: enableShortcuts ? <Shortcut keys={shortcutKeys.prevStory} /> : null, - left: <MenuItemIcon />, }), [api, enableShortcuts, shortcutKeys] ); @@ -190,7 +194,6 @@ export const useMenu = ( title: 'Next story', onClick: () => api.jumpToStory(1), right: enableShortcuts ? <Shortcut keys={shortcutKeys.nextStory} /> : null, - left: <MenuItemIcon />, }), [api, enableShortcuts, shortcutKeys] ); @@ -201,24 +204,22 @@ export const useMenu = ( title: 'Collapse all', onClick: () => api.collapseAll(), right: enableShortcuts ? <Shortcut keys={shortcutKeys.collapseAll} /> : null, - left: <MenuItemIcon />, }), [api, enableShortcuts, shortcutKeys] ); - const getAddonsShortcuts = (): any[] => { + const getAddonsShortcuts = useCallback(() => { const addonsShortcuts = api.getAddonsShortcuts(); const keys = shortcutKeys as any; return Object.entries(addonsShortcuts) - .filter(([actionName, { showInMenu }]) => showInMenu) + .filter(([_, { showInMenu }]) => showInMenu) .map(([actionName, { label, action }]) => ({ id: actionName, title: label, onClick: () => action(), right: enableShortcuts ? <Shortcut keys={keys[actionName]} /> : null, - left: <MenuItemIcon />, })); - }; + }, [api, enableShortcuts, shortcutKeys]); return useMemo( () => [ @@ -240,7 +241,8 @@ export const useMenu = ( ], [ about, - ...(api.releaseNotesVersion() ? [releaseNotes] : []), + api, + releaseNotes, shortcuts, sidebarToggle, toolbarToogle, @@ -253,6 +255,7 @@ export const useMenu = ( prev, next, collapse, + getAddonsShortcuts, ] ); }; diff --git a/code/ui/manager/src/globals/exports.ts b/code/ui/manager/src/globals/exports.ts index bcc126d07c7d..ca4dfa361bfb 100644 --- a/code/ui/manager/src/globals/exports.ts +++ b/code/ui/manager/src/globals/exports.ts @@ -73,6 +73,7 @@ export default { 'Img', 'LI', 'Link', + 'ListItem', 'Loader', 'OL', 'P', @@ -105,6 +106,7 @@ export default { 'components', 'createCopyToClipboardFunction', 'getStoryHref', + 'icons', 'interleaveSeparators', 'nameSpaceClassNames', 'resetComponents',