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 > { 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'); diff --git a/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx b/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx index c4f3e3cd4343..e2961973c9fa 100644 --- a/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx +++ b/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx @@ -160,11 +160,11 @@ const ArgSummary: FC = ({ 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', flexBasis: 'auto', - flexShrink: 0, marginLeft: 3, marginRight: 3, }, + ({ scrollable }) => (scrollable ? { flexShrink: 0 } : {}), ({ left }) => left ? { @@ -38,18 +39,25 @@ 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/bar/button.tsx b/code/ui/components/src/bar/button.tsx index 34126b1ca54b..a65e6fa7bc4e 100644 --- a/code/ui/components/src/bar/button.tsx +++ b/code/ui/components/src/bar/button.tsx @@ -7,20 +7,34 @@ import { auto } from '@popperjs/core'; interface BarButtonProps extends DetailedHTMLProps, HTMLButtonElement> { href?: void; + target?: void; } 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/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..d6aeb270a672 --- /dev/null +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -0,0 +1,178 @@ +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 { 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 { width: tabBarWidth = 1 } = useResizeObserver({ + ref: tabBarRef, + }); + + 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 ( + <> + { + return { + id, + title, + color, + active, + onClick: (e) => { + e.preventDefault(); + actions.onSelect(id); + }, + } as Link; + })} + /> + } + > + + {menuName} + + + + {invisibleList.map(({ title, id, color }) => { + return ( + { + tabRefs.current.set(id, ref); + }} + className="tabbutton" + type="button" + key={id} + textColor={color} + role="tab" + > + {title} + + ); + })} + + ); + }, + [invisibleList] + ); + + 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; + + const newVisibleList: ChildrenList = []; + + let widthSum = 0; + + const newInvisibleList = list.filter((item) => { + const { id } = item; + const tabButton = tabRefs.current.get(id); + + 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]); + + useLayoutEffect(setTabLists, [setTabLists, tabBarWidth]); + + 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..d4412c56715f 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -1,8 +1,10 @@ -import type { ComponentProps, Key } from 'react'; +import { expect } from '@storybook/jest'; +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 +69,7 @@ const panels: Panels = { ), }, test3: { - title: 'Tab with scroll!', + title: 'Tab title #3', render: ({ active, key }) => active ? (
@@ -133,10 +135,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 +155,10 @@ export const StatefulStatic = {
), -}; +} satisfies Story; export const StatefulStaticWithSetButtonTextColors = { - render: (args: ComponentProps) => ( + render: (args) => (
@@ -165,9 +172,10 @@ export const StatefulStaticWithSetButtonTextColors = {
), -}; +} satisfies Story; + export const StatefulStaticWithSetBackgroundColor = { - render: (args: ComponentProps) => ( + render: (args) => (
@@ -181,11 +189,33 @@ export const StatefulStaticWithSetBackgroundColor = {
), -}; +} satisfies Story; + +export const StatefulDynamicWithOpenTooltip = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + chromatic: { viewports: [414] }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor(async () => { + await expect(canvas.getAllByRole('tab')).toHaveLength(3); + await expect(canvas.getByRole('tab', { name: /Addons/ })).toBeInTheDocument(); + }); -export const StatefulDynamic = { - render: (args: ComponentProps) => ( - + 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) => ( + {Object.entries(panels).map(([k, v]) => (
{v.render} @@ -193,16 +223,48 @@ export const StatefulDynamic = { ))} ), -}; +} satisfies Story; + +export const StatefulDynamicWithSelectedAddon = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + chromatic: { viewports: [414] }, + }, + 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..284725a759fd 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 { FlexBar } 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; @@ -43,8 +46,13 @@ export const TabBar = styled.div({ '&:first-of-type': { marginLeft: -3, }, + + whiteSpace: 'nowrap', + flexGrow: 1, }); +TabBar.displayName = 'TabBar'; + export interface ContentProps { absolute?: boolean; bordered?: boolean; @@ -89,14 +97,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 +109,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 +120,39 @@ 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 }) => { - const tabTitle = typeof title === 'function' ? title() : title; + + + {visibleList.map(({ title, id, active, color }) => { return ( { + tabRefs.current.set(id, ref); + }} className={`tabbutton ${active ? 'tabbutton-active' : ''}`} type="button" key={id} @@ -167,12 +164,13 @@ export const Tabs: FC = memo( }} role="tab" > - {tabTitle} + {title} ); })} + - {tools ? {tools} : null} + {tools} {list.map(({ id, active, render }) => render({ key: id, active }))} @@ -186,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; @@ -203,6 +202,7 @@ export interface TabsStateProps { absolute: boolean; bordered: boolean; backgroundColor: string; + menuName: string; } export interface TabsStateState { @@ -216,6 +216,7 @@ export class TabsState extends Component { absolute: false, bordered: false, backgroundColor: '', + menuName: undefined, }; constructor(props: TabsStateProps) { @@ -231,7 +232,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..c6d1a442154a 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, } : {}, @@ -68,7 +68,7 @@ const Right = styled.span( opacity: 1, }, '& path': { - fill: theme.color.primary, + fill: theme.color.secondary, }, } : {} @@ -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..b60443a76124 100644 --- a/code/ui/components/src/tooltip/WithTooltip.tsx +++ b/code/ui/components/src/tooltip/WithTooltip.tsx @@ -4,90 +4,135 @@ 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; + 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; - 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, + closeOnTriggerHidden, + mutationObserverOptions, + closeOnClick, tooltipShown, onVisibilityChange, + 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, + const { + getArrowProps, + getTooltipProps, + setTooltipRef, + setTriggerRef, + visible: isVisible, + state, + } = usePopperTooltip( + { + trigger, + placement, + defaultVisible: defaultVisible ?? tooltipShown, + delayHide, + interactive, + closeOnOutsideClick: closeOnOutsideClick ?? closeOnClick, + closeOnTriggerHidden, + onVisibleChange: (_isVisible) => { + onVisibilityChange?.(_isVisible); + onVisibleChange?.(_isVisible); }, - { - modifiers, - } - ); + 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 +140,7 @@ const WithTooltipPure: FC = ({ WithTooltipPure.defaultProps = { svg: false, trigger: 'hover', - closeOnClick: false, + closeOnOutsideClick: false, placement: 'top', modifiers: [ { @@ -118,17 +163,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 +222,7 @@ const WithToolTipState: FC< }); return ( - + ); }; 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/**/*"] } 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/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..75a66c60942d 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('test2'); + return ( + + ); +}; export const NoPanels = () => ( string) | string; @@ -63,38 +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'; 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 }) => (
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"