From 8ab49a0f5cbef0cb0da76f2735942e184f8fb092 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 25 Jan 2023 15:47:55 +0100 Subject: [PATCH 01/16] Tab UI Improvements --- .../a11y/src/components/VisionSimulator.tsx | 2 +- .../src/containers/BackgroundSelector.tsx | 2 +- .../src/components/ToolbarMenuList.tsx | 2 +- code/addons/viewport/src/Tool.tsx | 2 +- .../src/components/ArgsTable/ArgValue.tsx | 6 +- code/ui/blocks/src/controls/Color.tsx | 4 +- code/ui/components/src/bar/bar.tsx | 2 +- code/ui/components/src/bar/button.tsx | 21 ++- .../src/hooks/useOnWindowResize.tsx | 9 + code/ui/components/src/tabs/tabs.helpers.tsx | 34 ++++ code/ui/components/src/tabs/tabs.hooks.tsx | 174 ++++++++++++++++++ code/ui/components/src/tabs/tabs.stories.tsx | 115 +++++++++--- code/ui/components/src/tabs/tabs.tsx | 89 +++++---- code/ui/components/src/tooltip/ListItem.tsx | 4 +- code/ui/components/src/tooltip/Tooltip.tsx | 10 +- .../src/tooltip/TooltipLinkList.tsx | 3 +- .../src/tooltip/WithTooltip.stories.tsx | 2 +- .../ui/components/src/tooltip/WithTooltip.tsx | 138 ++++++++------ .../notifications/NotificationItem.tsx | 1 - .../src/components/panel/panel.stories.tsx | 23 ++- .../ui/manager/src/components/panel/panel.tsx | 1 + .../manager/src/components/sidebar/Menu.tsx | 4 +- .../src/components/sidebar/RefBlocks.tsx | 1 - 23 files changed, 491 insertions(+), 158 deletions(-) create mode 100644 code/ui/components/src/hooks/useOnWindowResize.tsx create mode 100644 code/ui/components/src/tabs/tabs.helpers.tsx create mode 100644 code/ui/components/src/tabs/tabs.hooks.tsx diff --git a/code/addons/a11y/src/components/VisionSimulator.tsx b/code/addons/a11y/src/components/VisionSimulator.tsx index 77c8b2bccea0..4b528802dd76 100644 --- a/code/addons/a11y/src/components/VisionSimulator.tsx +++ b/code/addons/a11y/src/components/VisionSimulator.tsx @@ -141,7 +141,7 @@ export const VisionSimulator = () => { }); return ; }} - closeOnClick + closeOnOutsideClick onDoubleClick={() => setFilter(null)} > diff --git a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx index ef1d9c1caed5..71b33e7821f4 100644 --- a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx +++ b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx @@ -117,7 +117,7 @@ export const BackgroundSelector: FC = memo(function BackgroundSelector() { { return ( = withKeyboardCycle( }); return ; }} - closeOnClick + closeOnOutsideClick > ( )} - closeOnClick + closeOnOutsideClick > = ({ value, initialExpandedArgs }) => { return ( { + visible={isOpen} + onVisibleChange={(isVisible) => { setIsOpen(isVisible); }} tooltip={ diff --git a/code/ui/blocks/src/controls/Color.tsx b/code/ui/blocks/src/controls/Color.tsx index 179b8c09e3f7..0ced85cb271e 100644 --- a/code/ui/blocks/src/controls/Color.tsx +++ b/code/ui/blocks/src/controls/Color.tsx @@ -322,8 +322,8 @@ export const ColorControl: FC = ({ addPreset(color)} + closeOnOutsideClick + onVisibleChange={() => addPreset(color)} tooltip={ ( +export const Side = styled.div( { display: 'flex', whiteSpace: 'nowrap', diff --git a/code/ui/components/src/bar/button.tsx b/code/ui/components/src/bar/button.tsx index 34126b1ca54b..4d3102e6a13d 100644 --- a/code/ui/components/src/bar/button.tsx +++ b/code/ui/components/src/bar/button.tsx @@ -10,17 +10,30 @@ interface BarButtonProps } interface BarLinkProps extends DetailedHTMLProps, HTMLAnchorElement> { + disabled?: void; href: string; } -const ButtonOrLink = ({ children, ...restProps }: BarButtonProps | BarLinkProps) => - restProps.href != null ? ( - {children} +const ButtonOrLink = React.forwardRef< + HTMLAnchorElement | HTMLButtonElement, + BarLinkProps | BarButtonProps +>(({ children, ...restProps }, ref) => { + return restProps.href != null ? ( + } {...(restProps as BarLinkProps)}> + {children} + ) : ( - ); +}); + +ButtonOrLink.displayName = 'ButtonOrLink'; export interface TabButtonProps { active?: boolean; diff --git a/code/ui/components/src/hooks/useOnWindowResize.tsx b/code/ui/components/src/hooks/useOnWindowResize.tsx new file mode 100644 index 000000000000..8c2773a6d445 --- /dev/null +++ b/code/ui/components/src/hooks/useOnWindowResize.tsx @@ -0,0 +1,9 @@ +import { useEffect } from 'react'; + +export function useOnWindowResize(cb: (ev: UIEvent) => void) { + useEffect(() => { + window.addEventListener('resize', cb); + + return () => window.removeEventListener('resize', cb); + }, [cb]); +} diff --git a/code/ui/components/src/tabs/tabs.helpers.tsx b/code/ui/components/src/tabs/tabs.helpers.tsx new file mode 100644 index 000000000000..b92d38733077 --- /dev/null +++ b/code/ui/components/src/tabs/tabs.helpers.tsx @@ -0,0 +1,34 @@ +import { styled } from '@storybook/theming'; +import type { ReactElement } from 'react'; +import React, { Children } from 'react'; + +export interface VisuallyHiddenProps { + active?: boolean; +} + +export const VisuallyHidden = styled.div(({ active }) => + active ? { display: 'block' } : { display: 'none' } +); + +export const childrenToList = (children: any, selected: string) => + Children.toArray(children).map( + ({ props: { title, id, color, children: childrenOfChild } }: ReactElement, index) => { + const content = Array.isArray(childrenOfChild) ? childrenOfChild[0] : childrenOfChild; + return { + active: selected ? id === selected : index === 0, + title, + id, + color, + render: + typeof content === 'function' + ? content + : ({ active, key }: any) => ( + + {content} + + ), + }; + } + ); + +export type ChildrenList = ReturnType; diff --git a/code/ui/components/src/tabs/tabs.hooks.tsx b/code/ui/components/src/tabs/tabs.hooks.tsx new file mode 100644 index 000000000000..10ea8a40b553 --- /dev/null +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -0,0 +1,174 @@ +import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { sanitize } from '@storybook/csf'; +import { styled } from '@storybook/theming'; +import { TabButton } from '../bar/button'; +import { useOnWindowResize } from '../hooks/useOnWindowResize'; +import { TooltipLinkList } from '../tooltip/TooltipLinkList'; +import { WithTooltip } from '../tooltip/WithTooltip'; +import type { ChildrenList } from './tabs.helpers'; +import type { Link } from '../tooltip/TooltipLinkList'; + +const CollapseIcon = styled.span<{ isActive: boolean }>(({ theme, isActive }) => ({ + display: 'inline-block', + width: 0, + height: 0, + marginLeft: 8, + color: isActive ? theme.color.secondary : theme.color.mediumdark, + borderRight: '3px solid transparent', + borderLeft: `3px solid transparent`, + borderTop: '3px solid', + transition: 'transform .1s ease-out', +})); + +const AddonButton = styled(TabButton)<{ preActive: boolean }>(({ active, theme, preActive }) => { + return ` + color: ${preActive || active ? theme.color.secondary : theme.color.mediumdark}; + &:hover { + color: ${theme.color.secondary}; + .addon-collapsible-icon { + color: ${theme.color.secondary}; + } + } + `; +}); + +export function useList(list: ChildrenList) { + const tabBarRef = useRef(); + const addonsRef = useRef(); + const tabRefs = useRef(new Map()); + + const [visibleList, setVisibleList] = useState(list); + const [invisibleList, setInvisibleList] = useState([]); + const previousList = useRef(list); + + const AddonTab = useCallback( + ({ + menuName, + actions, + }: { + menuName: string; + actions?: { + onSelect: (id: string) => void; + } & Record; + }) => { + const isAddonsActive = invisibleList.some(({ active }) => active); + const [isTooltipVisible, setTooltipVisible] = useState(false); + return ( + <> + { + const tabTitle = typeof title === 'function' ? title() : title; + return { + id, + title: tabTitle, + color, + active, + onClick: (e) => { + e.preventDefault(); + actions.onSelect(id); + }, + } as Link; + })} + /> + } + > + + {menuName} + + + + {invisibleList.map(({ title, id, color }) => { + const tabTitle = typeof title === 'function' ? title() : title; + return ( + { + tabRefs.current.set(tabTitle, ref); + }} + className="tabbutton" + type="button" + key={id} + textColor={color} + role="tab" + > + {tabTitle} + + ); + })} + + ); + }, + [invisibleList] + ); + + const setTabLists = useCallback(() => { + // get x and width from tabBarRef div + const { x, width } = tabBarRef.current.getBoundingClientRect(); + const { width: widthAddonsTab } = addonsRef.current.getBoundingClientRect(); + const rightBorder = invisibleList.length ? x + width - widthAddonsTab : x + width; + + const newVisibleList: ChildrenList = []; + + let widthSum = 0; + + const newInvisibleList = list.filter((item) => { + const { title } = item; + const tabTitle = typeof title === 'function' ? title() : title; + const tabButton = tabRefs.current.get(tabTitle); + + if (!tabButton) { + return false; + } + const { width: tabWidth } = tabButton.getBoundingClientRect(); + + const crossBorder = x + widthSum + tabWidth > rightBorder; + + if (!crossBorder) { + newVisibleList.push(item); + } + + widthSum += tabWidth; + + return crossBorder; + }); + + if (newVisibleList.length !== visibleList.length || previousList.current !== list) { + setVisibleList(newVisibleList); + setInvisibleList(newInvisibleList); + previousList.current = list; + } + }, [invisibleList.length, list, visibleList]); + + useOnWindowResize(setTabLists); + + useLayoutEffect(setTabLists, [setTabLists]); + + return { + tabRefs, + addonsRef, + tabBarRef, + visibleList, + invisibleList, + AddonTab, + }; +} diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 9aaa4fb3c457..67f5baaf4bc2 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -1,8 +1,9 @@ -import type { ComponentProps, Key } from 'react'; +import type { Key } from 'react'; import React, { Fragment } from 'react'; import { action } from '@storybook/addon-actions'; import { logger } from '@storybook/client-logger'; -import type { Meta } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { within, fireEvent, waitFor, screen, getByText } from '@storybook/testing-library'; import { Tabs, TabsState, TabWrapper } from './tabs'; const colours = Array.from(new Array(15), (val, index) => index).map((i) => @@ -67,7 +68,7 @@ const panels: Panels = { ), }, test3: { - title: 'Tab with scroll!', + title: 'Tab title #3', render: ({ active, key }) => active ? (
@@ -133,10 +134,15 @@ export default {
), ], -} as Meta; + args: { + menuName: 'Addons', + }, +} satisfies Meta; + +type Story = StoryObj; export const StatefulStatic = { - render: (args: ComponentProps) => ( + render: (args) => (
{({ active, selected }: { active: boolean; selected: string }) => @@ -148,10 +154,10 @@ export const StatefulStatic = {
), -}; +} satisfies Story; export const StatefulStaticWithSetButtonTextColors = { - render: (args: ComponentProps) => ( + render: (args) => (
@@ -165,9 +171,10 @@ export const StatefulStaticWithSetButtonTextColors = {
), -}; +} satisfies Story; + export const StatefulStaticWithSetBackgroundColor = { - render: (args: ComponentProps) => ( + render: (args) => (
@@ -181,11 +188,28 @@ export const StatefulStaticWithSetBackgroundColor = {
), -}; +} satisfies Story; -export const StatefulDynamic = { - render: (args: ComponentProps) => ( - +export const StatefulDynamicWithOpenTooltip = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await new Promise((res) => + // The timeout is necessary to wait for Storybook to adjust the viewport + setTimeout(async () => { + const addonsTab = canvas.getByText('Addons'); + fireEvent(addonsTab, new MouseEvent('mouseenter', { bubbles: true })); + await waitFor(() => screen.getByTestId('tooltip')); + res(undefined); + }, 500) + ); + }, + render: (args) => ( + {Object.entries(panels).map(([k, v]) => (
{v.render} @@ -193,16 +217,47 @@ export const StatefulDynamic = { ))} ), -}; +} satisfies Story; + +export const StatefulDynamicWithSelectedAddon = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, + play: async (context) => { + await StatefulDynamicWithOpenTooltip.play(context); + + const popperContainer = screen.getByTestId('tooltip'); + const tab4 = getByText(popperContainer, 'Tab title #4', {}); + fireEvent(tab4, new MouseEvent('click', { bubbles: true })); + await waitFor(() => screen.getByText('CONTENT 4')); + + // reopen the tooltip + await StatefulDynamicWithOpenTooltip.play(context); + }, + render: (args) => ( + + {Object.entries(panels).map(([k, v]) => ( +
+ {v.render} +
+ ))} +
+ ), +} satisfies Story; + export const StatefulNoInitial = { - render: (args: ComponentProps) => {content}, -}; + render: (args) => {content}, +} satisfies Story; + export const StatelessBordered = { - render: (args: ComponentProps) => ( + render: (args) => ( ), -}; +} satisfies Story; + export const StatelessWithTools = { - render: (args: ComponentProps) => ( + render: (args) => ( ), -}; +} satisfies Story; + export const StatelessAbsolute = { - render: (args: ComponentProps) => ( + render: (args) => ( ), -}; +} satisfies Story; + export const StatelessAbsoluteBordered = { - render: (args: ComponentProps) => ( + render: (args) => ( ), -}; +} satisfies Story; + export const StatelessEmpty = { - render: (args: ComponentProps) => ( + render: (args) => ( ), -}; +} satisfies Story; diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index 276be1cb737b..c7e7e212570d 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -1,11 +1,14 @@ -import type { FC, MouseEvent, ReactElement, ReactNode } from 'react'; -import React, { Children, Component, Fragment, memo } from 'react'; +import type { FC, MouseEvent, ReactNode } from 'react'; +import React, { useMemo, Component, Fragment, memo } from 'react'; import { styled } from '@storybook/theming'; import { sanitize } from '@storybook/csf'; import { Placeholder } from '../placeholder/placeholder'; -import { FlexBar } from '../bar/bar'; import { TabButton } from '../bar/button'; +import { Side } from '../bar/bar'; +import type { ChildrenList } from './tabs.helpers'; +import { childrenToList, VisuallyHidden } from './tabs.helpers'; +import { useList } from './tabs.hooks'; export interface WrapperProps { bordered?: boolean; @@ -37,8 +40,19 @@ const Wrapper = styled.div( } ); +const WrapperChildren = styled.div<{ backgroundColor: string }>(({ theme, backgroundColor }) => ({ + color: theme.barTextColor, + display: 'flex', + width: '100%', + height: 40, + boxShadow: `${theme.appBorderColor} 0 -1px 0 0 inset`, + background: backgroundColor ?? theme.barBg, +})); + export const TabBar = styled.div({ overflow: 'hidden', + whiteSpace: 'nowrap', + flexGrow: 1, '&:first-of-type': { marginLeft: -3, @@ -89,14 +103,6 @@ const Content = styled.div( : {} ); -export interface VisuallyHiddenProps { - active?: boolean; -} - -const VisuallyHidden = styled.div(({ active }) => - active ? { display: 'block' } : { display: 'none' } -); - export interface TabWrapperProps { active: boolean; render?: () => JSX.Element; @@ -109,27 +115,6 @@ export const TabWrapper: FC = ({ active, render, children }) => export const panelProps = {}; -const childrenToList = (children: any, selected: string) => - Children.toArray(children).map( - ({ props: { title, id, color, children: childrenOfChild } }: ReactElement, index) => { - const content = Array.isArray(childrenOfChild) ? childrenOfChild[0] : childrenOfChild; - return { - active: selected ? id === selected : index === 0, - title, - id, - color, - render: - typeof content === 'function' - ? content - : ({ active, key }: any) => ( - - {content} - - ), - }; - } - ); - export interface TabsProps { children?: FuncChildren[] | ReactNode; id?: string; @@ -141,21 +126,41 @@ export interface TabsProps { backgroundColor?: string; absolute?: boolean; bordered?: boolean; + menuName: string; } export const Tabs: FC = memo( - ({ children, selected, actions, absolute, bordered, tools, backgroundColor, id: htmlId }) => { - const list = childrenToList(children, selected); + ({ + children, + selected, + actions, + absolute, + bordered, + tools, + backgroundColor, + id: htmlId, + menuName, + }) => { + const list = useMemo( + () => childrenToList(children, selected), + [children, selected] + ); + + const { visibleList, tabBarRef, tabRefs, AddonTab } = useList(list); return list.length ? ( - - - {list.map(({ title, id, active, color }) => { + + + {visibleList.map(({ title, id, active, color }, index) => { const tabTitle = typeof title === 'function' ? title() : title; + return ( { + tabRefs.current.set(tabTitle, ref); + }} className={`tabbutton ${active ? 'tabbutton-active' : ''}`} type="button" key={id} @@ -171,9 +176,10 @@ export const Tabs: FC = memo( ); })} + - {tools ? {tools} : null} - + {tools ? {tools} : null} + {list.map(({ id, active, render }) => render({ key: id, active }))} @@ -203,6 +209,7 @@ export interface TabsStateProps { absolute: boolean; bordered: boolean; backgroundColor: string; + menuName: string; } export interface TabsStateState { @@ -216,6 +223,7 @@ export class TabsState extends Component { absolute: false, bordered: false, backgroundColor: '', + menuName: undefined, }; constructor(props: TabsStateProps) { @@ -231,7 +239,7 @@ export class TabsState extends Component { }; render() { - const { bordered = false, absolute = false, children, backgroundColor } = this.props; + const { bordered = false, absolute = false, children, backgroundColor, menuName } = this.props; const { selected } = this.state; return ( { absolute={absolute} selected={selected} backgroundColor={backgroundColor} + menuName={menuName} actions={this.handlers} > {children} diff --git a/code/ui/components/src/tooltip/ListItem.tsx b/code/ui/components/src/tooltip/ListItem.tsx index cbb45bce396a..0e2b13b4672f 100644 --- a/code/ui/components/src/tooltip/ListItem.tsx +++ b/code/ui/components/src/tooltip/ListItem.tsx @@ -23,7 +23,7 @@ const Title = styled(({ active, loading, disabled, ...rest }: TitleProps) => active ? { - color: theme.color.primary, + color: theme.color.secondary, fontWeight: theme.typography.weight.bold, } : {}, @@ -97,7 +97,7 @@ const CenterText = styled.span( ({ active, theme }) => active ? { - color: theme.color.primary, + color: theme.color.secondary, } : {}, ({ theme, disabled }) => diff --git a/code/ui/components/src/tooltip/Tooltip.tsx b/code/ui/components/src/tooltip/Tooltip.tsx index 942550e88202..98e8dce76a20 100644 --- a/code/ui/components/src/tooltip/Tooltip.tsx +++ b/code/ui/components/src/tooltip/Tooltip.tsx @@ -121,13 +121,17 @@ export interface TooltipProps { arrowProps?: any; placement?: string; color?: keyof Color; + withArrows?: boolean; } export const Tooltip = React.forwardRef( - ({ placement, hasChrome, children, arrowProps, tooltipRef, color, ...props }, ref) => { + ( + { placement, hasChrome, children, arrowProps, tooltipRef, color, withArrows = true, ...props }, + ref + ) => { return ( - - {hasChrome && } + + {hasChrome && withArrows && } {children} ); diff --git a/code/ui/components/src/tooltip/TooltipLinkList.tsx b/code/ui/components/src/tooltip/TooltipLinkList.tsx index 947656b785c4..b3bc5e697409 100644 --- a/code/ui/components/src/tooltip/TooltipLinkList.tsx +++ b/code/ui/components/src/tooltip/TooltipLinkList.tsx @@ -29,7 +29,7 @@ export interface TooltipLinkListProps { } const Item: FC = (props) => { - const { LinkWrapper, onClick: onClickFromProps, ...rest } = props; + const { LinkWrapper, onClick: onClickFromProps, id, ...rest } = props; const { title, href, active } = rest; const onClick = useCallback( (event: SyntheticEvent) => { @@ -45,6 +45,7 @@ const Item: FC = (props) => { title={title} active={active} href={href} + id={`list-item-${id}`} LinkWrapper={LinkWrapper} {...rest} {...(hasOnClick ? { onClick } : {})} diff --git a/code/ui/components/src/tooltip/WithTooltip.stories.tsx b/code/ui/components/src/tooltip/WithTooltip.stories.tsx index 6792be11b086..f2f7c9e0f6da 100644 --- a/code/ui/components/src/tooltip/WithTooltip.stories.tsx +++ b/code/ui/components/src/tooltip/WithTooltip.stories.tsx @@ -112,7 +112,7 @@ export const SimpleClickCloseOnClick: StoryObj ( } {...args}> diff --git a/code/ui/components/src/tooltip/WithTooltip.tsx b/code/ui/components/src/tooltip/WithTooltip.tsx index b29801488162..b0a295f330a4 100644 --- a/code/ui/components/src/tooltip/WithTooltip.tsx +++ b/code/ui/components/src/tooltip/WithTooltip.tsx @@ -4,90 +4,117 @@ import ReactDOM from 'react-dom'; import { styled } from '@storybook/theming'; import { global } from '@storybook/global'; -import type { TriggerType } from 'react-popper-tooltip'; +import type { Config as ReactPopperTooltipConfig, PopperOptions } from 'react-popper-tooltip'; import { usePopperTooltip } from 'react-popper-tooltip'; -import type { Modifier, Placement } from '@popperjs/core'; import { Tooltip } from './Tooltip'; const { document } = global; // A target that doesn't speak popper -const TargetContainer = styled.div<{ mode: string }>` +const TargetContainer = styled.div<{ trigger: ReactPopperTooltipConfig['trigger'] }>` display: inline-block; - cursor: ${(props) => (props.mode === 'hover' ? 'default' : 'pointer')}; + cursor: ${(props) => + props.trigger === 'hover' || props.trigger.includes('hover') ? 'default' : 'pointer'}; `; -const TargetSvgContainer = styled.g<{ mode: string }>` - cursor: ${(props) => (props.mode === 'hover' ? 'default' : 'pointer')}; +const TargetSvgContainer = styled.g<{ trigger: ReactPopperTooltipConfig['trigger'] }>` + cursor: ${(props) => + props.trigger === 'hover' || props.trigger.includes('hover') ? 'default' : 'pointer'}; `; interface WithHideFn { onHide: () => void; } -export interface WithTooltipPureProps { +export interface WithTooltipPureProps + extends Omit, + PopperOptions { svg?: boolean; - trigger?: TriggerType; - closeOnClick?: boolean; - placement?: Placement; - modifiers?: Array>>; + withArrows?: boolean; hasChrome?: boolean; tooltip: ReactNode | ((p: WithHideFn) => ReactNode); children: ReactNode; - tooltipShown?: boolean; - onVisibilityChange?: (visibility: boolean) => void | boolean; onDoubleClick?: () => void; + /** + * If `true`, a click outside the trigger element closes the tooltip + * @default false + */ + closeOnOutsideClick?: boolean; } // Pure, does not bind to the body const WithTooltipPure: FC = ({ svg, trigger, - closeOnClick, + closeOnOutsideClick, placement, - modifiers, hasChrome, + withArrows, + offset, tooltip, children, - tooltipShown, - onVisibilityChange, + closeOnTriggerHidden, + mutationObserverOptions, + defaultVisible, + delayHide, + visible, + interactive, + delayShow, + modifiers, + strategy, + followCursor, + onVisibleChange, ...props }) => { const Container = svg ? TargetSvgContainer : TargetContainer; - const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible, state } = - usePopperTooltip( - { - trigger, - placement, - defaultVisible: tooltipShown, - closeOnOutsideClick: closeOnClick, - onVisibleChange: onVisibilityChange, - }, - { - modifiers, - } - ); + const { + getArrowProps, + getTooltipProps, + setTooltipRef, + setTriggerRef, + visible: isVisible, + state, + } = usePopperTooltip( + { + trigger, + placement, + defaultVisible, + delayHide, + interactive, + closeOnOutsideClick, + closeOnTriggerHidden, + onVisibleChange, + delayShow, + followCursor, + mutationObserverOptions, + visible, + offset, + }, + { + modifiers, + strategy, + } + ); + + const tooltipComponent = ( + + {typeof tooltip === 'function' ? tooltip({ onHide: () => onVisibleChange(false) }) : tooltip} + + ); return ( <> - + {children} - {visible && - ReactDOM.createPortal( - - {typeof tooltip === 'function' - ? tooltip({ onHide: () => onVisibilityChange(false) }) - : tooltip} - , - document.body - )} + {isVisible && ReactDOM.createPortal(tooltipComponent, document.body)} ); }; @@ -95,7 +122,7 @@ const WithTooltipPure: FC = ({ WithTooltipPure.defaultProps = { svg: false, trigger: 'hover', - closeOnClick: false, + closeOnOutsideClick: false, placement: 'top', modifiers: [ { @@ -118,17 +145,18 @@ WithTooltipPure.defaultProps = { }, ], hasChrome: true, - tooltipShown: false, + defaultVisible: false, }; const WithToolTipState: FC< - WithTooltipPureProps & { + Omit & { startOpen?: boolean; + onVisibleChange?: (visible: boolean) => void | boolean; } -> = ({ startOpen = false, onVisibilityChange: onChange, ...rest }) => { +> = ({ startOpen = false, onVisibleChange: onChange, ...rest }) => { const [tooltipShown, setTooltipShown] = useState(startOpen); - const onVisibilityChange: (visibility: boolean) => void = useCallback( - (visibility) => { + const onVisibilityChange = useCallback( + (visibility: boolean) => { if (onChange && onChange(visibility) === false) return; setTooltipShown(visibility); }, @@ -176,11 +204,7 @@ const WithToolTipState: FC< }); return ( - + ); }; diff --git a/code/ui/manager/src/components/notifications/NotificationItem.tsx b/code/ui/manager/src/components/notifications/NotificationItem.tsx index a595012de53e..48bb989b985d 100644 --- a/code/ui/manager/src/components/notifications/NotificationItem.tsx +++ b/code/ui/manager/src/components/notifications/NotificationItem.tsx @@ -106,7 +106,6 @@ const DismissButtonWrapper = styled(IconButton)(({ theme }) => ({ const DismissNotificationItem: FC<{ onDismiss: () => void; }> = ({ onDismiss }) => ( - // @ts-expect-error (we need to improve the types of IconButton) { diff --git a/code/ui/manager/src/components/panel/panel.stories.tsx b/code/ui/manager/src/components/panel/panel.stories.tsx index 388305193d74..a8a48426de7e 100644 --- a/code/ui/manager/src/components/panel/panel.stories.tsx +++ b/code/ui/manager/src/components/panel/panel.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; import Panel from './panel'; import { panels, shortcuts } from '../layout/app.mockdata'; @@ -12,15 +12,18 @@ export default { component: Panel, }; -export const Default = () => ( - -); +export const Default = () => { + const [selectedPanel, setSelectedPanel] = useState('test10'); + return ( + + ); +}; export const NoPanels = () => ( diff --git a/code/ui/manager/src/components/sidebar/Menu.tsx b/code/ui/manager/src/components/sidebar/Menu.tsx index c1e72f64dd8d..64c0508e4c67 100644 --- a/code/ui/manager/src/components/sidebar/Menu.tsx +++ b/code/ui/manager/src/components/sidebar/Menu.tsx @@ -104,7 +104,7 @@ export const SidebarMenu: FC<{ } > @@ -121,7 +121,7 @@ export const ToolbarMenu: FC<{ = ({ error }) => (
From 398fb3b94f0ef75b920ebab7c57c2bbebdd5be0b Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 25 Jan 2023 18:48:00 +0100 Subject: [PATCH 02/16] Fix type issues --- code/ui/components/src/bar/button.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/code/ui/components/src/bar/button.tsx b/code/ui/components/src/bar/button.tsx index 4d3102e6a13d..a65e6fa7bc4e 100644 --- a/code/ui/components/src/bar/button.tsx +++ b/code/ui/components/src/bar/button.tsx @@ -7,6 +7,7 @@ import { auto } from '@popperjs/core'; interface BarButtonProps extends DetailedHTMLProps, HTMLButtonElement> { href?: void; + target?: void; } interface BarLinkProps extends DetailedHTMLProps, HTMLAnchorElement> { From bf3ea262ecf7bc47a7338123144c04668083f950 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 26 Jan 2023 09:52:22 +0100 Subject: [PATCH 03/16] Use ResizeObserver instead of observing the window width --- code/ui/components/package.json | 1 + .../src/hooks/useOnWindowResize.tsx | 9 -------- code/ui/components/src/tabs/tabs.hooks.tsx | 23 +++++++++---------- code/ui/components/src/tabs/tabs.stories.tsx | 15 +----------- code/ui/components/src/tabs/tabs.tsx | 8 +++---- .../ui/manager/src/components/panel/panel.tsx | 2 +- code/yarn.lock | 20 ++++++++++++++++ 7 files changed, 37 insertions(+), 41 deletions(-) delete mode 100644 code/ui/components/src/hooks/useOnWindowResize.tsx diff --git a/code/ui/components/package.json b/code/ui/components/package.json index 0064e3565258..e14279677e8a 100644 --- a/code/ui/components/package.json +++ b/code/ui/components/package.json @@ -54,6 +54,7 @@ "@storybook/theming": "7.0.0-beta.38", "@storybook/types": "7.0.0-beta.38", "memoizerific": "^1.11.3", + "use-resize-observer": "^9.1.0", "util-deprecate": "^1.0.2" }, "devDependencies": { diff --git a/code/ui/components/src/hooks/useOnWindowResize.tsx b/code/ui/components/src/hooks/useOnWindowResize.tsx deleted file mode 100644 index 8c2773a6d445..000000000000 --- a/code/ui/components/src/hooks/useOnWindowResize.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useEffect } from 'react'; - -export function useOnWindowResize(cb: (ev: UIEvent) => void) { - useEffect(() => { - window.addEventListener('resize', cb); - - return () => window.removeEventListener('resize', cb); - }, [cb]); -} diff --git a/code/ui/components/src/tabs/tabs.hooks.tsx b/code/ui/components/src/tabs/tabs.hooks.tsx index 10ea8a40b553..cdc46e2cbf71 100644 --- a/code/ui/components/src/tabs/tabs.hooks.tsx +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; import { sanitize } from '@storybook/csf'; import { styled } from '@storybook/theming'; +import useResizeObserver from 'use-resize-observer'; import { TabButton } from '../bar/button'; -import { useOnWindowResize } from '../hooks/useOnWindowResize'; import { TooltipLinkList } from '../tooltip/TooltipLinkList'; import { WithTooltip } from '../tooltip/WithTooltip'; import type { ChildrenList } from './tabs.helpers'; @@ -36,6 +36,9 @@ export function useList(list: ChildrenList) { const tabBarRef = useRef(); const addonsRef = useRef(); const tabRefs = useRef(new Map()); + const { width: tabBarWidth = 1 } = useResizeObserver({ + ref: tabBarRef, + }); const [visibleList, setVisibleList] = useState(list); const [invisibleList, setInvisibleList] = useState([]); @@ -60,14 +63,14 @@ export function useList(list: ChildrenList) { withArrows={false} visible={isTooltipVisible} onVisibleChange={setTooltipVisible} + placement="bottom" delayHide={100} tooltip={ { - const tabTitle = typeof title === 'function' ? title() : title; return { id, - title: tabTitle, + title, color, active, onClick: (e) => { @@ -96,14 +99,13 @@ export function useList(list: ChildrenList) { {invisibleList.map(({ title, id, color }) => { - const tabTitle = typeof title === 'function' ? title() : title; return ( { - tabRefs.current.set(tabTitle, ref); + tabRefs.current.set(title, ref); }} className="tabbutton" type="button" @@ -111,7 +113,7 @@ export function useList(list: ChildrenList) { textColor={color} role="tab" > - {tabTitle} + {title} ); })} @@ -133,8 +135,7 @@ export function useList(list: ChildrenList) { const newInvisibleList = list.filter((item) => { const { title } = item; - const tabTitle = typeof title === 'function' ? title() : title; - const tabButton = tabRefs.current.get(tabTitle); + const tabButton = tabRefs.current.get(title); if (!tabButton) { return false; @@ -159,9 +160,7 @@ export function useList(list: ChildrenList) { } }, [invisibleList.length, list, visibleList]); - useOnWindowResize(setTabLists); - - useLayoutEffect(setTabLists, [setTabLists]); + useLayoutEffect(setTabLists, [setTabLists, tabBarWidth]); return { tabRefs, diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 67f5baaf4bc2..f5da4ce482ad 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -120,20 +120,7 @@ const content = Object.entries(panels).map(([k, v]) => ( export default { title: 'Tabs', - decorators: [ - (story) => ( -
- {story()} -
- ), - ], + decorators: [(story) =>
{story()}
], args: { menuName: 'Addons', }, diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index c7e7e212570d..e7160bee5251 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -153,13 +153,11 @@ export const Tabs: FC = memo( {visibleList.map(({ title, id, active, color }, index) => { - const tabTitle = typeof title === 'function' ? title() : title; - return ( { - tabRefs.current.set(tabTitle, ref); + tabRefs.current.set(title, ref); }} className={`tabbutton ${active ? 'tabbutton-active' : ''}`} type="button" @@ -172,7 +170,7 @@ export const Tabs: FC = memo( }} role="tab" > - {tabTitle} + {title} ); })} diff --git a/code/ui/manager/src/components/panel/panel.tsx b/code/ui/manager/src/components/panel/panel.tsx index 2fe1171a5936..020ad08c4761 100644 --- a/code/ui/manager/src/components/panel/panel.tsx +++ b/code/ui/manager/src/components/panel/panel.tsx @@ -90,7 +90,7 @@ const AddonPanel = React.memo<{ id="storybook-panel-root" > {Object.entries(panels).map(([k, v]) => ( - + {v.render} ))} diff --git a/code/yarn.lock b/code/yarn.lock index 19bfea729063..83e05b0f685c 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -3076,6 +3076,13 @@ __metadata: languageName: node linkType: hard +"@juggle/resize-observer@npm:^3.3.1": + version: 3.4.0 + resolution: "@juggle/resize-observer@npm:3.4.0" + checksum: 12930242357298c6f2ad5d4ec7cf631dfb344ca7c8c830ab7f64e6ac11eb1aae486901d8d880fd08fb1b257800c160a0da3aee1e7ed9adac0ccbb9b7c5d93347 + languageName: node + linkType: hard + "@leichtgewicht/ip-codec@npm:^2.0.1": version: 2.0.4 resolution: "@leichtgewicht/ip-codec@npm:2.0.4" @@ -6018,6 +6025,7 @@ __metadata: react-textarea-autosize: ^8.3.0 ts-dedent: ^2.0.0 typescript: ~4.9.3 + use-resize-observer: ^9.1.0 util-deprecate: ^1.0.2 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -27845,6 +27853,18 @@ __metadata: languageName: node linkType: hard +"use-resize-observer@npm:^9.1.0": + version: 9.1.0 + resolution: "use-resize-observer@npm:9.1.0" + dependencies: + "@juggle/resize-observer": ^3.3.1 + peerDependencies: + react: 16.8.0 - 18 + react-dom: 16.8.0 - 18 + checksum: 6ccdeb09fe20566ec182b1635a22f189e13d46226b74610432590e69b31ef5d05d069badc3306ebd0d2bb608743b17981fb535763a1d7dc2c8ae462ee8e5999c + languageName: node + linkType: hard + "use@npm:^3.1.0": version: 3.1.1 resolution: "use@npm:3.1.1" From 30b0c672be4a93df51275e0d349ad845f0b43697 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 26 Jan 2023 11:32:14 +0100 Subject: [PATCH 04/16] Fix tab reference --- code/ui/components/src/tabs/tabs.hooks.tsx | 6 +++--- code/ui/components/src/tabs/tabs.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/code/ui/components/src/tabs/tabs.hooks.tsx b/code/ui/components/src/tabs/tabs.hooks.tsx index cdc46e2cbf71..68bb39ba908b 100644 --- a/code/ui/components/src/tabs/tabs.hooks.tsx +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -105,7 +105,7 @@ export function useList(list: ChildrenList) { style={{ visibility: 'hidden' }} tabIndex={-1} ref={(ref: HTMLButtonElement) => { - tabRefs.current.set(title, ref); + tabRefs.current.set(id, ref); }} className="tabbutton" type="button" @@ -134,8 +134,8 @@ export function useList(list: ChildrenList) { let widthSum = 0; const newInvisibleList = list.filter((item) => { - const { title } = item; - const tabButton = tabRefs.current.get(title); + const { id } = item; + const tabButton = tabRefs.current.get(id); if (!tabButton) { return false; diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index e7160bee5251..49b28998b02e 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -152,12 +152,12 @@ export const Tabs: FC = memo( - {visibleList.map(({ title, id, active, color }, index) => { + {visibleList.map(({ title, id, active, color }) => { return ( { - tabRefs.current.set(title, ref); + tabRefs.current.set(id, ref); }} className={`tabbutton ${active ? 'tabbutton-active' : ''}`} type="button" From becbe97f83c81236764ed8c598f17bf341ae68b6 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 26 Jan 2023 13:47:46 +0100 Subject: [PATCH 05/16] Fix e2e tests --- code/e2e-tests/addon-backgrounds.spec.ts | 2 +- code/e2e-tests/addon-viewport.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/e2e-tests/addon-backgrounds.spec.ts b/code/e2e-tests/addon-backgrounds.spec.ts index a3ea50f9d454..5dfb3e46c05d 100644 --- a/code/e2e-tests/addon-backgrounds.spec.ts +++ b/code/e2e-tests/addon-backgrounds.spec.ts @@ -14,7 +14,7 @@ test.describe('addon-backgrounds', () => { const sbPage = new SbPage(page); await sbPage.navigateToStory('example/button', 'primary'); - await sbPage.selectToolbar('[title="Change the background of the preview"]', '#dark'); + await sbPage.selectToolbar('[title="Change the background of the preview"]', '#list-item-dark'); await expect(sbPage.getCanvasBodyElement()).toHaveCSS('background-color', 'rgb(51, 51, 51)'); }); diff --git a/code/e2e-tests/addon-viewport.spec.ts b/code/e2e-tests/addon-viewport.spec.ts index f15b9fa55704..18d72e112646 100644 --- a/code/e2e-tests/addon-viewport.spec.ts +++ b/code/e2e-tests/addon-viewport.spec.ts @@ -15,7 +15,7 @@ test.describe('addon-viewport', () => { // Click on viewport button and select small mobile await sbPage.navigateToStory('example/button', 'primary'); - await sbPage.selectToolbar('[title="Change the size of the preview"]', '#mobile1'); + await sbPage.selectToolbar('[title="Change the size of the preview"]', '#list-item-mobile1'); // Check that Button story is still displayed await expect(sbPage.previewRoot()).toContainText('Button'); From 6ddbf2dfc90f1d6c654893671b56f35b1af27590 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 26 Jan 2023 17:07:24 +0100 Subject: [PATCH 06/16] Remove negative margin on TabBar --- code/ui/components/src/tabs/tabs.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index 49b28998b02e..a9218b82e9b4 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -53,10 +53,6 @@ export const TabBar = styled.div({ overflow: 'hidden', whiteSpace: 'nowrap', flexGrow: 1, - - '&:first-of-type': { - marginLeft: -3, - }, }); export interface ContentProps { From 12dec591616a7bce4257b5e2ba944a702c408be2 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 26 Jan 2023 17:09:50 +0100 Subject: [PATCH 07/16] Fix test run on chromatic --- code/ui/components/src/tabs/tabs.stories.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index f5da4ce482ad..8ff243ed1d92 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -182,6 +182,7 @@ export const StatefulDynamicWithOpenTooltip = { viewport: { defaultViewport: 'mobile2', }, + chromatic: { viewports: [414] }, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -211,6 +212,7 @@ export const StatefulDynamicWithSelectedAddon = { viewport: { defaultViewport: 'mobile2', }, + chromatic: { viewports: [414] }, }, play: async (context) => { await StatefulDynamicWithOpenTooltip.play(context); From 87f8101a525cddc87111137365679e3340ed2392 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 26 Jan 2023 19:31:51 +0100 Subject: [PATCH 08/16] Fix Story --- code/ui/manager/src/components/panel/panel.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/ui/manager/src/components/panel/panel.stories.tsx b/code/ui/manager/src/components/panel/panel.stories.tsx index a8a48426de7e..75a66c60942d 100644 --- a/code/ui/manager/src/components/panel/panel.stories.tsx +++ b/code/ui/manager/src/components/panel/panel.stories.tsx @@ -13,7 +13,7 @@ export default { }; export const Default = () => { - const [selectedPanel, setSelectedPanel] = useState('test10'); + const [selectedPanel, setSelectedPanel] = useState('test2'); return ( Date: Thu, 26 Jan 2023 19:34:29 +0100 Subject: [PATCH 09/16] Fix tabs if refs not set --- code/ui/components/src/tabs/tabs.hooks.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/ui/components/src/tabs/tabs.hooks.tsx b/code/ui/components/src/tabs/tabs.hooks.tsx index 68bb39ba908b..45cc97bdcac7 100644 --- a/code/ui/components/src/tabs/tabs.hooks.tsx +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -125,6 +125,9 @@ export function useList(list: ChildrenList) { const setTabLists = useCallback(() => { // get x and width from tabBarRef div + if (!tabBarRef.current || !addonsRef.current) { + return; + } const { x, width } = tabBarRef.current.getBoundingClientRect(); const { width: widthAddonsTab } = addonsRef.current.getBoundingClientRect(); const rightBorder = invisibleList.length ? x + width - widthAddonsTab : x + width; From 7383c1160ae7438c5fb62cfb2a7fa7aeab1ae151 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 27 Jan 2023 09:21:17 +0100 Subject: [PATCH 10/16] Fix snapshot diff --- code/ui/components/src/tabs/tabs.tsx | 65 +++++++++------ .../manager/src/components/hooks/useMedia.tsx | 19 +++++ .../ui/manager/src/components/panel/panel.tsx | 82 +++++++++---------- 3 files changed, 100 insertions(+), 66 deletions(-) create mode 100644 code/ui/manager/src/components/hooks/useMedia.tsx diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index a9218b82e9b4..683e8456478f 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -51,10 +51,23 @@ const WrapperChildren = styled.div<{ backgroundColor: string }>(({ theme, backgr export const TabBar = styled.div({ overflow: 'hidden', + + '&:first-of-type': { + marginLeft: -3, + }, + whiteSpace: 'nowrap', flexGrow: 1, }); +const TabBarSide = styled(Side)({ + flexGrow: 1, + flexShrink: 1, + maxWidth: '100%', +}); + +TabBar.displayName = 'TabBar'; + export interface ContentProps { absolute?: boolean; bordered?: boolean; @@ -147,31 +160,33 @@ export const Tabs: FC = memo( return list.length ? ( - - {visibleList.map(({ title, id, active, color }) => { - return ( - { - tabRefs.current.set(id, ref); - }} - className={`tabbutton ${active ? 'tabbutton-active' : ''}`} - type="button" - key={id} - active={active} - textColor={color} - onClick={(e: MouseEvent) => { - e.preventDefault(); - actions.onSelect(id); - }} - role="tab" - > - {title} - - ); - })} - - + + + {visibleList.map(({ title, id, active, color }) => { + return ( + { + tabRefs.current.set(id, ref); + }} + className={`tabbutton ${active ? 'tabbutton-active' : ''}`} + type="button" + key={id} + active={active} + textColor={color} + onClick={(e: MouseEvent) => { + e.preventDefault(); + actions.onSelect(id); + }} + role="tab" + > + {title} + + ); + })} + + + {tools ? {tools} : null} diff --git a/code/ui/manager/src/components/hooks/useMedia.tsx b/code/ui/manager/src/components/hooks/useMedia.tsx new file mode 100644 index 000000000000..240629e79cfe --- /dev/null +++ b/code/ui/manager/src/components/hooks/useMedia.tsx @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; + +const useMediaQuery = (query: string) => { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + window.addEventListener('resize', listener); + return () => window.removeEventListener('resize', listener); + }, [matches, query]); + + return matches; +}; + +export default useMediaQuery; diff --git a/code/ui/manager/src/components/panel/panel.tsx b/code/ui/manager/src/components/panel/panel.tsx index 020ad08c4761..6519ee9d9219 100644 --- a/code/ui/manager/src/components/panel/panel.tsx +++ b/code/ui/manager/src/components/panel/panel.tsx @@ -1,16 +1,9 @@ import type { ReactElement } from 'react'; import React, { Component, Fragment } from 'react'; -import { styled } from '@storybook/theming'; import { Tabs, Icons, IconButton } from '@storybook/components'; import type { State } from '@storybook/manager-api'; import { shortcutToHumanString } from '@storybook/manager-api'; - -const DesktopOnlyIconButton = styled(IconButton)({ - // Hides full screen icon at mobile breakpoint defined in app.js - '@media (max-width: 599px)': { - display: 'none', - }, -}); +import useMediaQuery from '../hooks/useMedia'; export interface SafeTabProps { title: (() => string) | string; @@ -63,39 +56,46 @@ const AddonPanel = React.memo<{ selectedPanel = null, panelPosition = 'right', absolute = true, - }) => ( - - - - - - - - - } - id="storybook-panel-root" - > - {Object.entries(panels).map(([k, v]) => ( - - {v.render} - - ))} - - ) + }) => { + const isTablet = useMediaQuery('(min-width: 599px)'); + return ( + + + + + + + + + ) : undefined + } + id="storybook-panel-root" + > + {Object.entries(panels).map(([k, v]) => ( + + {v.render} + + ))} + + ); + } ); AddonPanel.displayName = 'AddonPanel'; From b0f4a8fc0e166967d192c93fce162534c1fb78c8 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 27 Jan 2023 11:29:32 +0100 Subject: [PATCH 11/16] Revert Story decorator --- code/ui/components/src/tabs/tabs.stories.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 8ff243ed1d92..82088a57b120 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -120,7 +120,20 @@ const content = Object.entries(panels).map(([k, v]) => ( export default { title: 'Tabs', - decorators: [(story) =>
{story()}
], + decorators: [ + (story) => ( +
+ {story()} +
+ ), + ], args: { menuName: 'Addons', }, From 12f7fedf7728f204a6dbf17b3887675fe6e4b27c Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 27 Jan 2023 12:04:26 +0100 Subject: [PATCH 12/16] Adjust Tab component to match previous Stories --- code/ui/components/src/bar/bar.tsx | 33 ++++++++----- code/ui/components/src/tabs/tabs.tsx | 73 +++++++++++----------------- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/code/ui/components/src/bar/bar.tsx b/code/ui/components/src/bar/bar.tsx index 08c67aededfc..bdbb60757f38 100644 --- a/code/ui/components/src/bar/bar.tsx +++ b/code/ui/components/src/bar/bar.tsx @@ -7,6 +7,7 @@ import { ScrollArea } from '../ScrollArea/ScrollArea'; export interface SideProps { left?: boolean; right?: boolean; + scrollable?: boolean; } export const Side = styled.div( @@ -14,10 +15,10 @@ export const Side = styled.div( display: 'flex', whiteSpace: 'nowrap', flexBasis: 'auto', - flexShrink: 0, marginLeft: 3, marginRight: 3, }, + ({ scrollable }) => (scrollable ? { flexShrink: 0 } : {}), ({ left }) => left ? { @@ -38,18 +39,25 @@ export const Side = styled.div( ); Side.displayName = 'Side'; -const UnstyledBar: FC> = ({ children, className }) => ( - - {children} - -); -export const Bar = styled(UnstyledBar)<{ border?: boolean }>( - ({ theme }) => ({ +const UnstyledBar: FC & { scrollable?: boolean }> = ({ + children, + className, + scrollable, +}) => + scrollable ? ( + + {children} + + ) : ( +
{children}
+ ); +export const Bar = styled(UnstyledBar)<{ border?: boolean; scrollable?: boolean }>( + ({ theme, scrollable = true }) => ({ color: theme.barTextColor, width: '100%', height: 40, flexShrink: 0, - overflow: 'auto', + overflow: scrollable ? 'auto' : 'hidden', overflowY: 'hidden', }), ({ theme, border = false }) => @@ -72,9 +80,8 @@ const BarInner = styled.div<{ bgColor: string }>(({ bgColor }) => ({ backgroundColor: bgColor || '', })); -export interface FlexBarProps { +export interface FlexBarProps extends ComponentProps { border?: boolean; - children?: any; backgroundColor?: string; } @@ -83,7 +90,9 @@ export const FlexBar: FC = ({ children, backgroundColor, ...rest } return ( - {left} + + {left} + {right ? {right} : null} diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index 683e8456478f..80cf528aa901 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -5,7 +5,7 @@ import { sanitize } from '@storybook/csf'; import { Placeholder } from '../placeholder/placeholder'; import { TabButton } from '../bar/button'; -import { Side } from '../bar/bar'; +import { FlexBar, Side } from '../bar/bar'; import type { ChildrenList } from './tabs.helpers'; import { childrenToList, VisuallyHidden } from './tabs.helpers'; import { useList } from './tabs.hooks'; @@ -40,15 +40,6 @@ const Wrapper = styled.div( } ); -const WrapperChildren = styled.div<{ backgroundColor: string }>(({ theme, backgroundColor }) => ({ - color: theme.barTextColor, - display: 'flex', - width: '100%', - height: 40, - boxShadow: `${theme.appBorderColor} 0 -1px 0 0 inset`, - background: backgroundColor ?? theme.barBg, -})); - export const TabBar = styled.div({ overflow: 'hidden', @@ -60,12 +51,6 @@ export const TabBar = styled.div({ flexGrow: 1, }); -const TabBarSide = styled(Side)({ - flexGrow: 1, - flexShrink: 1, - maxWidth: '100%', -}); - TabBar.displayName = 'TabBar'; export interface ContentProps { @@ -159,36 +144,34 @@ export const Tabs: FC = memo( return list.length ? ( - - - - {visibleList.map(({ title, id, active, color }) => { - return ( - { - tabRefs.current.set(id, ref); - }} - className={`tabbutton ${active ? 'tabbutton-active' : ''}`} - type="button" - key={id} - active={active} - textColor={color} - onClick={(e: MouseEvent) => { - e.preventDefault(); - actions.onSelect(id); - }} - role="tab" - > - {title} - - ); - })} - - - + + + {visibleList.map(({ title, id, active, color }) => { + return ( + { + tabRefs.current.set(id, ref); + }} + className={`tabbutton ${active ? 'tabbutton-active' : ''}`} + type="button" + key={id} + active={active} + textColor={color} + onClick={(e: MouseEvent) => { + e.preventDefault(); + actions.onSelect(id); + }} + role="tab" + > + {title} + + ); + })} + + {tools ? {tools} : null} - + {list.map(({ id, active, render }) => render({ key: id, active }))} From c7419bf163d5f067f1bd1d59e69dd575236745d4 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 30 Jan 2023 14:34:50 +0100 Subject: [PATCH 13/16] Fix nesting of Tools in Addon Panel --- code/ui/components/src/tabs/tabs.hooks.tsx | 2 ++ code/ui/components/src/tabs/tabs.tsx | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/code/ui/components/src/tabs/tabs.hooks.tsx b/code/ui/components/src/tabs/tabs.hooks.tsx index 45cc97bdcac7..d6aeb270a672 100644 --- a/code/ui/components/src/tabs/tabs.hooks.tsx +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -87,6 +87,7 @@ export function useList(list: ChildrenList) { active={isAddonsActive} preActive={isTooltipVisible} style={{ visibility: invisibleList.length ? 'visible' : 'hidden' }} + aria-hidden={!invisibleList.length} className="tabbutton" type="button" role="tab" @@ -103,6 +104,7 @@ export function useList(list: ChildrenList) { { tabRefs.current.set(id, ref); diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index 80cf528aa901..33d12e468cc6 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -5,7 +5,7 @@ import { sanitize } from '@storybook/csf'; import { Placeholder } from '../placeholder/placeholder'; import { TabButton } from '../bar/button'; -import { FlexBar, Side } from '../bar/bar'; +import { FlexBar } from '../bar/bar'; import type { ChildrenList } from './tabs.helpers'; import { childrenToList, VisuallyHidden } from './tabs.helpers'; import { useList } from './tabs.hooks'; @@ -170,7 +170,7 @@ export const Tabs: FC = memo( })}
- {tools ? {tools} : null} + {tools} {list.map(({ id, active, render }) => render({ key: id, active }))} From 07cf50693c3e1f6fb1b28c107174abd943a69d75 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 30 Jan 2023 16:29:52 +0100 Subject: [PATCH 14/16] Change ListItem icon color --- code/ui/components/src/tooltip/ListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/ui/components/src/tooltip/ListItem.tsx b/code/ui/components/src/tooltip/ListItem.tsx index 0e2b13b4672f..c6d1a442154a 100644 --- a/code/ui/components/src/tooltip/ListItem.tsx +++ b/code/ui/components/src/tooltip/ListItem.tsx @@ -68,7 +68,7 @@ const Right = styled.span( opacity: 1, }, '& path': { - fill: theme.color.primary, + fill: theme.color.secondary, }, } : {} From 2ef671ed6c2509906ff56c3bd5603acf9df140af Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Tue, 31 Jan 2023 12:55:26 +0100 Subject: [PATCH 15/16] Add removed properties for backwards compat --- code/ui/components/src/tabs/tabs.tsx | 5 ++-- .../ui/components/src/tooltip/WithTooltip.tsx | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index 33d12e468cc6..284725a759fd 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -120,7 +120,7 @@ export interface TabsProps { backgroundColor?: string; absolute?: boolean; bordered?: boolean; - menuName: string; + menuName?: string; } export const Tabs: FC = memo( @@ -184,13 +184,14 @@ export const Tabs: FC = memo( } ); Tabs.displayName = 'Tabs'; -(Tabs as any).defaultProps = { +Tabs.defaultProps = { id: null, children: null, tools: null, selected: null, absolute: false, bordered: false, + menuName: 'Tabs', }; type FuncChildren = ({ active }: { active: boolean }) => JSX.Element; diff --git a/code/ui/components/src/tooltip/WithTooltip.tsx b/code/ui/components/src/tooltip/WithTooltip.tsx index b0a295f330a4..b60443a76124 100644 --- a/code/ui/components/src/tooltip/WithTooltip.tsx +++ b/code/ui/components/src/tooltip/WithTooltip.tsx @@ -35,6 +35,18 @@ export interface WithTooltipPureProps tooltip: ReactNode | ((p: WithHideFn) => ReactNode); children: ReactNode; onDoubleClick?: () => void; + /** + * @deprecated use `defaultVisible` property instead. This property will be removed in SB 8.0 + */ + tooltipShown?: boolean; + /** + * @deprecated use `closeOnOutsideClick` property instead. This property will be removed in SB 8.0 + */ + closeOnClick?: boolean; + /** + * @deprecated use `onVisibleChange` property instead. This property will be removed in SB 8.0 + */ + onVisibilityChange?: (visibility: boolean) => void | boolean; /** * If `true`, a click outside the trigger element closes the tooltip * @default false @@ -55,6 +67,9 @@ const WithTooltipPure: FC = ({ children, closeOnTriggerHidden, mutationObserverOptions, + closeOnClick, + tooltipShown, + onVisibilityChange, defaultVisible, delayHide, visible, @@ -78,12 +93,15 @@ const WithTooltipPure: FC = ({ { trigger, placement, - defaultVisible, + defaultVisible: defaultVisible ?? tooltipShown, delayHide, interactive, - closeOnOutsideClick, + closeOnOutsideClick: closeOnOutsideClick ?? closeOnClick, closeOnTriggerHidden, - onVisibleChange, + onVisibleChange: (_isVisible) => { + onVisibilityChange?.(_isVisible); + onVisibleChange?.(_isVisible); + }, delayShow, followCursor, mutationObserverOptions, From 2c2740e267affc96687a583d9d00d29676e382c7 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 3 Feb 2023 14:01:40 +0100 Subject: [PATCH 16/16] Fix play tests --- code/ui/components/src/tabs/tabs.stories.tsx | 23 ++++++++++++-------- code/ui/components/tsconfig.json | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 82088a57b120..d4412c56715f 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -1,3 +1,4 @@ +import { expect } from '@storybook/jest'; import type { Key } from 'react'; import React, { Fragment } from 'react'; import { action } from '@storybook/addon-actions'; @@ -199,15 +200,19 @@ export const StatefulDynamicWithOpenTooltip = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await new Promise((res) => - // The timeout is necessary to wait for Storybook to adjust the viewport - setTimeout(async () => { - const addonsTab = canvas.getByText('Addons'); - fireEvent(addonsTab, new MouseEvent('mouseenter', { bubbles: true })); - await waitFor(() => screen.getByTestId('tooltip')); - res(undefined); - }, 500) - ); + + await waitFor(async () => { + await expect(canvas.getAllByRole('tab')).toHaveLength(3); + await expect(canvas.getByRole('tab', { name: /Addons/ })).toBeInTheDocument(); + }); + + 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(); + }); }, render: (args) => ( diff --git a/code/ui/components/tsconfig.json b/code/ui/components/tsconfig.json index 65743dc000ea..e279989f36ce 100644 --- a/code/ui/components/tsconfig.json +++ b/code/ui/components/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "types": ["react-syntax-highlighter", "jest"] + "types": ["react-syntax-highlighter", "jest", "testing-library__jest-dom"] }, "include": ["src/**/*"] }