diff --git a/code/addons/a11y/src/components/VisionSimulator.tsx b/code/addons/a11y/src/components/VisionSimulator.tsx index 4b528802dd76..b66f8eef297d 100644 --- a/code/addons/a11y/src/components/VisionSimulator.tsx +++ b/code/addons/a11y/src/components/VisionSimulator.tsx @@ -133,7 +133,6 @@ export const VisionSimulator = () => { )} { const colorList = getColorList(filter, (i) => { setFilter(i); diff --git a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx index 71b33e7821f4..2d531964812d 100644 --- a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx +++ b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx @@ -116,7 +116,6 @@ export const BackgroundSelector: FC = memo(function BackgroundSelector() { { return ( diff --git a/code/addons/toolbars/src/components/ToolbarMenuList.tsx b/code/addons/toolbars/src/components/ToolbarMenuList.tsx index b3db9112af82..3b7056a217c0 100644 --- a/code/addons/toolbars/src/components/ToolbarMenuList.tsx +++ b/code/addons/toolbars/src/components/ToolbarMenuList.tsx @@ -57,7 +57,6 @@ export const ToolbarMenuList: FC = withKeyboardCycle( return ( { const links = items // Special case handling for various "type" variants diff --git a/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx b/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx index 35979e0b05dd..c07a885562dc 100644 --- a/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx +++ b/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx @@ -1,18 +1,9 @@ -import type { ReactNode } from 'react'; import React from 'react'; +import type { TooltipLinkListLink } 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 +19,18 @@ export const ToolbarMenuListItem = ({ currentValue, }: ToolbarMenuListItemProps) => { const Icon = icon && ; - const hasContent = left || right || title; - const Item: ListItem = { + const Item: TooltipLinkListLink = { 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/addons/viewport/src/Tool.tsx b/code/addons/viewport/src/Tool.tsx index 75eec71d656e..dd321b637cf9 100644 --- a/code/addons/viewport/src/Tool.tsx +++ b/code/addons/viewport/src/Tool.tsx @@ -180,7 +180,6 @@ export const ViewportTool: FC = memo( ( )} diff --git a/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx b/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx index e2961973c9fa..131aab285a1b 100644 --- a/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx +++ b/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx @@ -161,7 +161,6 @@ const ArgSummary: FC = ({ value, initialExpandedArgs }) => { return ( { diff --git a/code/ui/blocks/src/controls/Color.tsx b/code/ui/blocks/src/controls/Color.tsx index 0ced85cb271e..b126c80e3b2d 100644 --- a/code/ui/blocks/src/controls/Color.tsx +++ b/code/ui/blocks/src/controls/Color.tsx @@ -320,7 +320,6 @@ export const ColorControl: FC = ({ return ( addPreset(color)} diff --git a/code/ui/components/src/index.ts b/code/ui/components/src/index.ts index e2b6ef984165..6f8a9ab78ca0 100644 --- a/code/ui/components/src/index.ts +++ b/code/ui/components/src/index.ts @@ -56,7 +56,8 @@ export { Form } from './form/index'; export { WithTooltip, WithTooltipPure } from './tooltip/lazy-WithTooltip'; export { TooltipMessage } from './tooltip/TooltipMessage'; export { TooltipNote } from './tooltip/TooltipNote'; -export { TooltipLinkList } from './tooltip/TooltipLinkList'; +export { TooltipLinkList, type Link as TooltipLinkListLink } 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) { <> index).map((i) => @@ -220,10 +227,18 @@ export const StatefulDynamicWithOpenTooltip = { const addonsTab = await canvas.findByRole('tab', { name: /Addons/ }); await waitFor(async () => { - await fireEvent(addonsTab, new MouseEvent('mouseenter', { bubbles: true })); - const tooltip = await screen.getByTestId('tooltip'); - await expect(tooltip).toBeInTheDocument(); + const tooltip = await screen.queryByTestId('tooltip'); + + if (!tooltip) { + await userEvent.click(addonsTab); + } + + if (!tooltip) { + throw new Error('Tooltip not found'); + } }); + + expect(screen.queryByTestId('tooltip')).toBeInTheDocument(); }, render: (args) => ( diff --git a/code/ui/components/src/tooltip/ListItem.tsx b/code/ui/components/src/tooltip/ListItem.tsx index c6d1a442154a..64394dcc4070 100644 --- a/code/ui/components/src/tooltip/ListItem.tsx +++ b/code/ui/components/src/tooltip/ListItem.tsx @@ -1,8 +1,10 @@ -import type { FC, ReactNode, ComponentProps } from 'react'; +import type { FC, ReactNode, ComponentProps, ReactElement } from 'react'; import React from 'react'; import { styled } from '@storybook/theming'; import memoize from 'memoizerific'; import { transparentize } from 'polished'; +import { Icons } from '../icon/icon'; +import { icons } from '../icon/icons'; export interface TitleProps { children?: ReactNode; @@ -47,53 +49,39 @@ export interface RightProps { active?: boolean; } -const Right = styled.span( - { - '& 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', - }, + ({ theme }) => ({ + fontSize: '11px', + lineHeight: '14px', + }), ({ active, theme }) => active ? { @@ -112,17 +100,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 { @@ -139,7 +132,7 @@ const Item = styled.a( justifyContent: 'space-between', lineHeight: '18px', - padding: '7px 15px', + padding: '7px 10px', display: 'flex', alignItems: 'center', @@ -188,14 +181,20 @@ export type LinkWrapperType = FC; export interface ListItemProps extends Omit, 'href' | 'title'> { loading?: boolean; + /** + * @deprecated This property will be removed in SB 8.0 + * 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 +203,10 @@ const ListItem: FC = ({ title, center, right, + icon, active, disabled, + isIndented, href, onClick, LinkWrapper, @@ -214,11 +215,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..d4ccde5e41df 100644 --- a/code/ui/components/src/tooltip/Tooltip.tsx +++ b/code/ui/components/src/tooltip/Tooltip.tsx @@ -108,7 +108,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 * 2, + borderRadius: theme.appBorderRadius, fontSize: theme.typography.size.s1, } : {} @@ -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..3691e91239af 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: [ @@ -54,25 +38,167 @@ export default { height: '300px', }} > - <WithTooltip placement="top" trigger="click" startOpen tooltip={storyFn()}> + <WithTooltip placement="top" startOpen tooltip={storyFn()}> <div>Tooltip</div> </WithTooltip> </div> ), ], 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..2311ca458a5c 100644 --- a/code/ui/components/src/tooltip/TooltipLinkList.tsx +++ b/code/ui/components/src/tooltip/TooltipLinkList.tsx @@ -13,7 +13,7 @@ const List = styled.div( maxHeight: 15.5 * 32, // 11.5 items }, ({ theme }) => ({ - borderRadius: theme.appBorderRadius * 2, + borderRadius: theme.appBorderRadius, }) ); @@ -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<Link & { 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/TooltipMessage.stories.tsx b/code/ui/components/src/tooltip/TooltipMessage.stories.tsx index 4b5a4714c5c3..9c210eb90bab 100644 --- a/code/ui/components/src/tooltip/TooltipMessage.stories.tsx +++ b/code/ui/components/src/tooltip/TooltipMessage.stories.tsx @@ -12,7 +12,7 @@ export default { height: '300px', }} > - <WithTooltip placement="top" trigger="click" startOpen tooltip={storyFn()}> + <WithTooltip placement="top" startOpen tooltip={storyFn()}> <div>Tooltip</div> </WithTooltip> </div> diff --git a/code/ui/components/src/tooltip/TooltipNote.stories.tsx b/code/ui/components/src/tooltip/TooltipNote.stories.tsx index f21030c62b75..6c1e533edc77 100644 --- a/code/ui/components/src/tooltip/TooltipNote.stories.tsx +++ b/code/ui/components/src/tooltip/TooltipNote.stories.tsx @@ -11,13 +11,7 @@ export default { height: '300px', }} > - <WithTooltip - hasChrome={false} - placement="top" - trigger="click" - startOpen - tooltip={storyFn()} - > + <WithTooltip hasChrome={false} placement="top" startOpen tooltip={storyFn()}> <div>Tooltip</div> </WithTooltip> </div> diff --git a/code/ui/components/src/tooltip/WithTooltip.stories.tsx b/code/ui/components/src/tooltip/WithTooltip.stories.tsx index f2f7c9e0f6da..76eab8d7c113 100644 --- a/code/ui/components/src/tooltip/WithTooltip.stories.tsx +++ b/code/ui/components/src/tooltip/WithTooltip.stories.tsx @@ -86,7 +86,6 @@ export const SimpleHoverFunctional: StoryObj<ComponentProps<typeof WithTooltip>> export const SimpleClick: StoryObj<ComponentProps<typeof WithTooltip>> = { args: { placement: 'top', - trigger: 'click', }, render: (args) => ( <WithTooltip tooltip={<Tooltip />} {...args}> @@ -98,7 +97,6 @@ export const SimpleClick: StoryObj<ComponentProps<typeof WithTooltip>> = { export const SimpleClickStartOpen: StoryObj<ComponentProps<typeof WithTooltip>> = { args: { placement: 'top', - trigger: 'click', startOpen: true, }, render: (args) => ( @@ -111,7 +109,6 @@ export const SimpleClickStartOpen: StoryObj<ComponentProps<typeof WithTooltip>> export const SimpleClickCloseOnClick: StoryObj<ComponentProps<typeof WithTooltip>> = { args: { placement: 'top', - trigger: 'click', closeOnOutsideClick: true, }, render: (args) => ( @@ -124,7 +121,6 @@ export const SimpleClickCloseOnClick: StoryObj<ComponentProps<typeof WithTooltip export const WithoutChrome: StoryObj<ComponentProps<typeof WithTooltip>> = { args: { placement: 'top', - trigger: 'click', hasChrome: false, }, render: (args) => ( diff --git a/code/ui/components/src/tooltip/WithTooltip.tsx b/code/ui/components/src/tooltip/WithTooltip.tsx index b60443a76124..70dadd67dc16 100644 --- a/code/ui/components/src/tooltip/WithTooltip.tsx +++ b/code/ui/components/src/tooltip/WithTooltip.tsx @@ -139,7 +139,7 @@ const WithTooltipPure: FC<WithTooltipPureProps> = ({ WithTooltipPure.defaultProps = { svg: false, - trigger: 'hover', + trigger: 'click', closeOnOutsideClick: false, placement: 'top', modifiers: [ 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 000000000000..b17235199494 Binary files /dev/null and b/code/ui/components/src/tooltip/assets/ellipse.png differ 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..fb553512efd8 100644 --- a/code/ui/manager/src/components/sidebar/Menu.tsx +++ b/code/ui/manager/src/components/sidebar/Menu.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react'; import { styled } from '@storybook/theming'; import { transparentize } from 'polished'; -import type { Button } from '@storybook/components'; +import type { Button, TooltipLinkListLink } from '@storybook/components'; import { WithTooltip, TooltipLinkList, Icons, IconButton } from '@storybook/components'; export type MenuList = ComponentProps<typeof TooltipLinkList>['links']; @@ -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} />; @@ -76,7 +80,7 @@ export const MenuItemIcon = ({ icon, imgSrc }: ListItemIconProps) => { return <Placeholder />; }; -type ClickHandler = ComponentProps<typeof TooltipLinkList>['links'][number]['onClick']; +type ClickHandler = TooltipLinkListLink['onClick']; const SidebarMenuList: FC<{ menu: MenuList; @@ -103,7 +107,6 @@ export const SidebarMenu: FC<{ return ( <WithTooltip placement="top" - trigger="click" closeOnOutsideClick tooltip={({ onHide }) => <SidebarMenuList onHide={onHide} menu={menu} />} > @@ -120,7 +123,6 @@ export const ToolbarMenu: FC<{ return ( <WithTooltip placement="bottom" - trigger="click" closeOnOutsideClick modifiers={[ { diff --git a/code/ui/manager/src/components/sidebar/RefBlocks.tsx b/code/ui/manager/src/components/sidebar/RefBlocks.tsx index d92d6f26dacc..ddc0c64886d4 100644 --- a/code/ui/manager/src/components/sidebar/RefBlocks.tsx +++ b/code/ui/manager/src/components/sidebar/RefBlocks.tsx @@ -169,7 +169,6 @@ export const ErrorBlock: FC<{ error: Error }> = ({ error }) => ( Oh no! Something went wrong loading this Storybook. <br /> <WithTooltip - trigger="click" tooltip={ <ErrorDisplay> <ErrorFormatter error={error} /> diff --git a/code/ui/manager/src/components/sidebar/RefIndicator.tsx b/code/ui/manager/src/components/sidebar/RefIndicator.tsx index 0d116a63f5c0..6dd822de9ec2 100644 --- a/code/ui/manager/src/components/sidebar/RefIndicator.tsx +++ b/code/ui/manager/src/components/sidebar/RefIndicator.tsx @@ -1,20 +1,20 @@ import { global } from '@storybook/global'; -import type { FC, ComponentProps } from 'react'; +import type { FC } from 'react'; import React, { useMemo, useCallback, forwardRef } from 'react'; +import type { TooltipLinkListLink } from '@storybook/components'; import { Icons, WithTooltip, Spaced, TooltipLinkList } from '@storybook/components'; 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'; const { document, window: globalWindow } = global; -export type ClickHandler = ComponentProps<typeof TooltipLinkList>['links'][number]['onClick']; +export type ClickHandler = TooltipLinkListLink['onClick']; export interface IndicatorIconProps { type: ReturnType<typeof getStateType>; } @@ -190,7 +190,6 @@ export const RefIndicator = React.memo( <IndicatorPlacement ref={forwardedRef}> <WithTooltip placement="bottom-start" - trigger="click" tooltip={ <MessageWrapper> <Spaced row={0}> @@ -218,11 +217,10 @@ export const RefIndicator = React.memo( {ref.versions && Object.keys(ref.versions).length ? ( <WithTooltip placement="bottom-start" - trigger="click" 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..5db85f26f163 --- /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" 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..dba70ddb950c 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,27 @@ 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; + vertical-align: middle; + + & + & { + 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 +65,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 +74,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 +84,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 +96,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 +108,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 +120,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 +133,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 +142,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 +155,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 +165,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 +175,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 +185,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 +195,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 +205,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 +242,8 @@ export const useMenu = ( ], [ about, - ...(api.releaseNotesVersion() ? [releaseNotes] : []), + api, + releaseNotes, shortcuts, sidebarToggle, toolbarToogle, @@ -253,6 +256,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',