Tab UI Improvements

This commit is contained in:
Valentin Palkovic 2023-01-25 15:47:55 +01:00
parent 32ad0aacf6
commit 8ab49a0f5c
23 changed files with 491 additions and 158 deletions

View File

@ -141,7 +141,7 @@ export const VisionSimulator = () => {
});
return <TooltipLinkList links={colorList} />;
}}
closeOnClick
closeOnOutsideClick
onDoubleClick={() => setFilter(null)}
>
<IconButton key="filter" active={!!filter} title="Vision simulator">

View File

@ -117,7 +117,7 @@ export const BackgroundSelector: FC = memo(function BackgroundSelector() {
<WithTooltip
placement="top"
trigger="click"
closeOnClick
closeOnOutsideClick
tooltip={({ onHide }) => {
return (
<TooltipLinkList

View File

@ -84,7 +84,7 @@ export const ToolbarMenuList: FC<ToolbarMenuListProps> = withKeyboardCycle(
});
return <TooltipLinkList links={links} />;
}}
closeOnClick
closeOnOutsideClick
>
<ToolbarMenuButton
active={hasGlobalValue}

View File

@ -184,7 +184,7 @@ export const ViewportTool: FC = memo(
tooltip={({ onHide }) => (
<TooltipLinkList links={toLinks(list, item, setState, state, onHide)} />
)}
closeOnClick
closeOnOutsideClick
>
<IconButtonWithLabel
key="viewport"

View File

@ -160,11 +160,11 @@ const ArgSummary: FC<ArgSummaryProps> = ({ value, initialExpandedArgs }) => {
return (
<WithTooltipPure
closeOnClick
closeOnOutsideClick
trigger="click"
placement="bottom"
tooltipShown={isOpen}
onVisibilityChange={(isVisible) => {
visible={isOpen}
onVisibleChange={(isVisible) => {
setIsOpen(isVisible);
}}
tooltip={

View File

@ -322,8 +322,8 @@ export const ColorControl: FC<ColorControlProps> = ({
<PickerTooltip
trigger="click"
startOpen={startOpen}
closeOnClick
onVisibilityChange={() => addPreset(color)}
closeOnOutsideClick
onVisibleChange={() => addPreset(color)}
tooltip={
<TooltipContent>
<Picker

View File

@ -9,7 +9,7 @@ export interface SideProps {
right?: boolean;
}
const Side = styled.div<SideProps>(
export const Side = styled.div<SideProps>(
{
display: 'flex',
whiteSpace: 'nowrap',

View File

@ -10,17 +10,30 @@ interface BarButtonProps
}
interface BarLinkProps
extends DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
disabled?: void;
href: string;
}
const ButtonOrLink = ({ children, ...restProps }: BarButtonProps | BarLinkProps) =>
restProps.href != null ? (
<a {...(restProps as BarLinkProps)}>{children}</a>
const ButtonOrLink = React.forwardRef<
HTMLAnchorElement | HTMLButtonElement,
BarLinkProps | BarButtonProps
>(({ children, ...restProps }, ref) => {
return restProps.href != null ? (
<a ref={ref as React.ForwardedRef<HTMLAnchorElement>} {...(restProps as BarLinkProps)}>
{children}
</a>
) : (
<button type="button" {...(restProps as BarButtonProps)}>
<button
ref={ref as React.ForwardedRef<HTMLButtonElement>}
type="button"
{...(restProps as BarButtonProps)}
>
{children}
</button>
);
});
ButtonOrLink.displayName = 'ButtonOrLink';
export interface TabButtonProps {
active?: boolean;

View File

@ -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]);
}

View File

@ -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<VisuallyHiddenProps>(({ 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) => (
<VisuallyHidden key={key} active={active} role="tabpanel">
{content}
</VisuallyHidden>
),
};
}
);
export type ChildrenList = ReturnType<typeof childrenToList>;

View File

@ -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<HTMLDivElement>();
const addonsRef = useRef<HTMLButtonElement>();
const tabRefs = useRef(new Map<string, HTMLButtonElement>());
const [visibleList, setVisibleList] = useState(list);
const [invisibleList, setInvisibleList] = useState<ChildrenList>([]);
const previousList = useRef<ChildrenList>(list);
const AddonTab = useCallback(
({
menuName,
actions,
}: {
menuName: string;
actions?: {
onSelect: (id: string) => void;
} & Record<string, any>;
}) => {
const isAddonsActive = invisibleList.some(({ active }) => active);
const [isTooltipVisible, setTooltipVisible] = useState(false);
return (
<>
<WithTooltip
interactive
withArrows={false}
visible={isTooltipVisible}
onVisibleChange={setTooltipVisible}
delayHide={100}
tooltip={
<TooltipLinkList
links={invisibleList.map(({ title, id, color, active }) => {
const tabTitle = typeof title === 'function' ? title() : title;
return {
id,
title: tabTitle,
color,
active,
onClick: (e) => {
e.preventDefault();
actions.onSelect(id);
},
} as Link;
})}
/>
}
>
<AddonButton
ref={addonsRef}
active={isAddonsActive}
preActive={isTooltipVisible}
style={{ visibility: invisibleList.length ? 'visible' : 'hidden' }}
className="tabbutton"
type="button"
role="tab"
>
{menuName}
<CollapseIcon
className="addon-collapsible-icon"
isActive={isAddonsActive || isTooltipVisible}
/>
</AddonButton>
</WithTooltip>
{invisibleList.map(({ title, id, color }) => {
const tabTitle = typeof title === 'function' ? title() : title;
return (
<TabButton
id={`tabbutton-${sanitize(tabTitle)}`}
style={{ visibility: 'hidden' }}
tabIndex={-1}
ref={(ref: HTMLButtonElement) => {
tabRefs.current.set(tabTitle, ref);
}}
className="tabbutton"
type="button"
key={id}
textColor={color}
role="tab"
>
{tabTitle}
</TabButton>
);
})}
</>
);
},
[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,
};
}

View File

@ -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 ? (
<div id="test3" key={key}>
@ -133,10 +134,15 @@ export default {
</div>
),
],
} as Meta;
args: {
menuName: 'Addons',
},
} satisfies Meta<typeof TabsState>;
type Story = StoryObj<typeof TabsState>;
export const StatefulStatic = {
render: (args: ComponentProps<typeof TabsState>) => (
render: (args) => (
<TabsState initial="test2" {...args}>
<div id="test1" title="With a function">
{({ active, selected }: { active: boolean; selected: string }) =>
@ -148,10 +154,10 @@ export const StatefulStatic = {
</div>
</TabsState>
),
};
} satisfies Story;
export const StatefulStaticWithSetButtonTextColors = {
render: (args: ComponentProps<typeof TabsState>) => (
render: (args) => (
<div>
<TabsState initial="test2" {...args}>
<div id="test1" title="With a function" color="#e00000">
@ -165,9 +171,10 @@ export const StatefulStaticWithSetButtonTextColors = {
</TabsState>
</div>
),
};
} satisfies Story;
export const StatefulStaticWithSetBackgroundColor = {
render: (args: ComponentProps<typeof TabsState>) => (
render: (args) => (
<div>
<TabsState initial="test2" backgroundColor="rgba(0,0,0,.05)" {...args}>
<div id="test1" title="With a function" color="#e00000">
@ -181,11 +188,28 @@ export const StatefulStaticWithSetBackgroundColor = {
</TabsState>
</div>
),
};
} satisfies Story;
export const StatefulDynamic = {
render: (args: ComponentProps<typeof TabsState>) => (
<TabsState initial="test3" {...args}>
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) => (
<TabsState initial="test1" {...args}>
{Object.entries(panels).map(([k, v]) => (
<div key={k} id={k} title={v.title}>
{v.render}
@ -193,16 +217,47 @@ export const StatefulDynamic = {
))}
</TabsState>
),
};
} 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) => (
<TabsState initial="test1" {...args}>
{Object.entries(panels).map(([k, v]) => (
<div key={k} id={k} title={v.title}>
{v.render}
</div>
))}
</TabsState>
),
} satisfies Story;
export const StatefulNoInitial = {
render: (args: ComponentProps<typeof TabsState>) => <TabsState {...args}>{content}</TabsState>,
};
render: (args) => <TabsState {...args}>{content}</TabsState>,
} satisfies Story;
export const StatelessBordered = {
render: (args: ComponentProps<typeof TabsState>) => (
render: (args) => (
<Tabs
bordered
absolute={false}
selected="test3"
menuName="Addons"
actions={{
onSelect,
}}
@ -211,11 +266,13 @@ export const StatelessBordered = {
{content}
</Tabs>
),
};
} satisfies Story;
export const StatelessWithTools = {
render: (args: ComponentProps<typeof TabsState>) => (
render: (args) => (
<Tabs
selected="test3"
menuName="Addons"
actions={{
onSelect,
}}
@ -234,12 +291,14 @@ export const StatelessWithTools = {
{content}
</Tabs>
),
};
} satisfies Story;
export const StatelessAbsolute = {
render: (args: ComponentProps<typeof TabsState>) => (
render: (args) => (
<Tabs
absolute
selected="test3"
menuName="Addons"
actions={{
onSelect,
}}
@ -248,12 +307,14 @@ export const StatelessAbsolute = {
{content}
</Tabs>
),
};
} satisfies Story;
export const StatelessAbsoluteBordered = {
render: (args: ComponentProps<typeof TabsState>) => (
render: (args) => (
<Tabs
absolute
bordered
menuName="Addons"
selected="test3"
actions={{
onSelect,
@ -263,16 +324,18 @@ export const StatelessAbsoluteBordered = {
{content}
</Tabs>
),
};
} satisfies Story;
export const StatelessEmpty = {
render: (args: ComponentProps<typeof TabsState>) => (
render: (args) => (
<Tabs
actions={{
onSelect,
}}
bordered
menuName="Addons"
absolute
{...args}
/>
),
};
} satisfies Story;

View File

@ -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<WrapperProps>(
}
);
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<ContentProps>(
: {}
);
export interface VisuallyHiddenProps {
active?: boolean;
}
const VisuallyHidden = styled.div<VisuallyHiddenProps>(({ active }) =>
active ? { display: 'block' } : { display: 'none' }
);
export interface TabWrapperProps {
active: boolean;
render?: () => JSX.Element;
@ -109,27 +115,6 @@ export const TabWrapper: FC<TabWrapperProps> = ({ 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) => (
<VisuallyHidden key={key} active={active} role="tabpanel">
{content}
</VisuallyHidden>
),
};
}
);
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<TabsProps> = 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<ChildrenList>(
() => childrenToList(children, selected),
[children, selected]
);
const { visibleList, tabBarRef, tabRefs, AddonTab } = useList(list);
return list.length ? (
<Wrapper absolute={absolute} bordered={bordered} id={htmlId}>
<FlexBar border backgroundColor={backgroundColor}>
<TabBar role="tablist">
{list.map(({ title, id, active, color }) => {
<WrapperChildren backgroundColor={backgroundColor}>
<TabBar ref={tabBarRef} role="tablist">
{visibleList.map(({ title, id, active, color }, index) => {
const tabTitle = typeof title === 'function' ? title() : title;
return (
<TabButton
id={`tabbutton-${sanitize(tabTitle)}`}
ref={(ref: HTMLButtonElement) => {
tabRefs.current.set(tabTitle, ref);
}}
className={`tabbutton ${active ? 'tabbutton-active' : ''}`}
type="button"
key={id}
@ -171,9 +176,10 @@ export const Tabs: FC<TabsProps> = memo(
</TabButton>
);
})}
<AddonTab menuName={menuName} actions={actions} />
</TabBar>
{tools ? <Fragment>{tools}</Fragment> : null}
</FlexBar>
{tools ? <Side right>{tools}</Side> : null}
</WrapperChildren>
<Content id="panel-tab-content" bordered={bordered} absolute={absolute}>
{list.map(({ id, active, render }) => render({ key: id, active }))}
</Content>
@ -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<TabsStateProps, TabsStateState> {
absolute: false,
bordered: false,
backgroundColor: '',
menuName: undefined,
};
constructor(props: TabsStateProps) {
@ -231,7 +239,7 @@ export class TabsState extends Component<TabsStateProps, TabsStateState> {
};
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 (
<Tabs
@ -239,6 +247,7 @@ export class TabsState extends Component<TabsStateProps, TabsStateState> {
absolute={absolute}
selected={selected}
backgroundColor={backgroundColor}
menuName={menuName}
actions={this.handlers}
>
{children}

View File

@ -23,7 +23,7 @@ const Title = styled(({ active, loading, disabled, ...rest }: TitleProps) => <sp
({ active, theme }) =>
active
? {
color: theme.color.primary,
color: theme.color.secondary,
fontWeight: theme.typography.weight.bold,
}
: {},
@ -97,7 +97,7 @@ const CenterText = styled.span<CenterTextProps>(
({ active, theme }) =>
active
? {
color: theme.color.primary,
color: theme.color.secondary,
}
: {},
({ theme, disabled }) =>

View File

@ -121,13 +121,17 @@ export interface TooltipProps {
arrowProps?: any;
placement?: string;
color?: keyof Color;
withArrows?: boolean;
}
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
({ placement, hasChrome, children, arrowProps, tooltipRef, color, ...props }, ref) => {
(
{ placement, hasChrome, children, arrowProps, tooltipRef, color, withArrows = true, ...props },
ref
) => {
return (
<Wrapper hasChrome={hasChrome} ref={ref} {...props} color={color}>
{hasChrome && <Arrow placement={placement} {...arrowProps} color={color} />}
<Wrapper data-testid="tooltip" hasChrome={hasChrome} ref={ref} {...props} color={color}>
{hasChrome && withArrows && <Arrow placement={placement} {...arrowProps} color={color} />}
{children}
</Wrapper>
);

View File

@ -29,7 +29,7 @@ export interface TooltipLinkListProps {
}
const Item: FC<TooltipLinkListProps['links'][number]> = (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<TooltipLinkListProps['links'][number]> = (props) => {
title={title}
active={active}
href={href}
id={`list-item-${id}`}
LinkWrapper={LinkWrapper}
{...rest}
{...(hasOnClick ? { onClick } : {})}

View File

@ -112,7 +112,7 @@ export const SimpleClickCloseOnClick: StoryObj<ComponentProps<typeof WithTooltip
args: {
placement: 'top',
trigger: 'click',
closeOnClick: true,
closeOnOutsideClick: true,
},
render: (args) => (
<WithTooltip tooltip={<Tooltip />} {...args}>

View File

@ -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<ReactPopperTooltipConfig, 'closeOnOutsideClick'>,
PopperOptions {
svg?: boolean;
trigger?: TriggerType;
closeOnClick?: boolean;
placement?: Placement;
modifiers?: Array<Partial<Modifier<string, {}>>>;
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<WithTooltipPureProps> = ({
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 = (
<Tooltip
placement={state?.placement}
ref={setTooltipRef}
hasChrome={hasChrome}
arrowProps={getArrowProps()}
withArrows={withArrows}
{...getTooltipProps()}
>
{typeof tooltip === 'function' ? tooltip({ onHide: () => onVisibleChange(false) }) : tooltip}
</Tooltip>
);
return (
<>
<Container mode={trigger} ref={setTriggerRef as any} {...props}>
<Container trigger={trigger} ref={setTriggerRef as any} {...props}>
{children}
</Container>
{visible &&
ReactDOM.createPortal(
<Tooltip
placement={state?.placement}
ref={setTooltipRef}
hasChrome={hasChrome}
arrowProps={getArrowProps()}
{...getTooltipProps()}
>
{typeof tooltip === 'function'
? tooltip({ onHide: () => onVisibilityChange(false) })
: tooltip}
</Tooltip>,
document.body
)}
{isVisible && ReactDOM.createPortal(tooltipComponent, document.body)}
</>
);
};
@ -95,7 +122,7 @@ const WithTooltipPure: FC<WithTooltipPureProps> = ({
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<WithTooltipPureProps, 'onVisibleChange'> & {
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 (
<WithTooltipPure
{...rest}
tooltipShown={tooltipShown}
onVisibilityChange={onVisibilityChange}
/>
<WithTooltipPure {...rest} defaultVisible={tooltipShown} onVisibleChange={onVisibilityChange} />
);
};

View File

@ -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)
<DismissButtonWrapper
title="Dismiss notification"
onClick={(e: SyntheticEvent) => {

View File

@ -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 = () => (
<Panel
absolute={false}
panels={panels}
actions={{ onSelect, toggleVisibility, togglePosition }}
selectedPanel="test2"
shortcuts={shortcuts}
/>
);
export const Default = () => {
const [selectedPanel, setSelectedPanel] = useState('test10');
return (
<Panel
absolute={false}
panels={panels}
actions={{ onSelect: setSelectedPanel, toggleVisibility, togglePosition }}
selectedPanel={selectedPanel}
shortcuts={shortcuts}
/>
);
};
export const NoPanels = () => (
<Panel

View File

@ -67,6 +67,7 @@ const AddonPanel = React.memo<{
<Tabs
absolute={absolute}
{...(selectedPanel ? { selected: selectedPanel } : {})}
menuName="Addons"
actions={actions}
tools={
<Fragment>

View File

@ -104,7 +104,7 @@ export const SidebarMenu: FC<{
<WithTooltip
placement="top"
trigger="click"
closeOnClick
closeOnOutsideClick
tooltip={({ onHide }) => <SidebarMenuList onHide={onHide} menu={menu} />}
>
<SidebarIconButton title="Shortcuts" aria-label="Shortcuts" highlighted={isHighlighted}>
@ -121,7 +121,7 @@ export const ToolbarMenu: FC<{
<WithTooltip
placement="bottom"
trigger="click"
closeOnClick
closeOnOutsideClick
modifiers={[
{
name: 'flip',

View File

@ -170,7 +170,6 @@ export const ErrorBlock: FC<{ error: Error }> = ({ error }) => (
<br />
<WithTooltip
trigger="click"
closeOnClick={false}
tooltip={
<ErrorDisplay>
<ErrorFormatter error={error} />