Skip to content

Commit 496cf94

Browse files
authored
UI: Port WithTooltip from popper.js to react-aria and adjust internals (#32493)
2 parents b981972 + e914108 commit 496cf94

File tree

21 files changed

+651
-703
lines changed

21 files changed

+651
-703
lines changed

MIGRATION.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@
2121
- [Removed: onEscapeKeyDown](#removed-onescapekeydown)
2222
- [Added: `ariaLabel`](#added-arialabel-1)
2323
- [Renamed: Modal.Dialog.Close and Modal.CloseButton](#renamed-modaldialogclose-and-modalclosebutton)
24+
- [ListItem, TooltipLinkList and TooltipMessage are deprecated](#listitem-tooltiplinklist-and-tooltipmessage-are-deprecated)
25+
- [Tooltip Component API Changes](#tooltip-component-api-changes)
26+
- [Renamed: tooltipRef](#renamed-tooltipref)
27+
- [Removed: arrowProps and withArrows](#removed-arrowprops-and-witharrows)
28+
- [Removed: placement](#removed-placement)
29+
- [Changed type: color](#changed-type-color)
30+
- [WithPopover Component Added](#withpopover-component-added)
31+
- [WithTooltip Component API Changes](#withtooltip-component-api-changes)
32+
- [Removed: trigger](#removed-trigger)
33+
- [Added: triggerOnFocusOnly](#added-triggeronfocusonly)
34+
- [Renamed: startOpen](#renamed-startopen)
35+
- [Removed: svg, strategy, withArrows, mutationObserverOptions](#removed-svg-strategy-witharrows-mutationobserveroptions)
36+
- [Removed: hasChrome](#removed-haschrome)
37+
- [Removed: closeOnTriggerHidden, followCursor, closeOnOutsideClick](#removed-closeontriggerhidden-followcursor-closeonoutsideclick)
38+
- [Removed: interactive](#removed-interactive)
39+
- [Other changes](#other-changes)
40+
- [WithTooltipPure and WithTooltipState are removed](#withtooltippure-and-withtooltipstate-are-removed)
2441
- [From version 8.x to 9.0.0](#from-version-8x-to-900)
2542
- [Core Changes and Removals](#core-changes-and-removals)
2643
- [Dropped support for legacy packages](#dropped-support-for-legacy-packages)
@@ -649,6 +666,65 @@ Modal elements must have a title to be accessible. Set that title through the ma
649666
The `Modal.Dialog.Close` component and `Modal.CloseButton` components are replaced by `Modal.Close` for consistency with other components. You may call `<Modal.Close />` for a default close button, or `<Modal.Close asChild>...</Modal.Close>` to wrap your own custom button.
650667

651668
The `Modal.Close` component no longer requires an `onClick` handler to close the modal. It will automatically close the modal when clicked. If you need to perform additional actions when the close button is clicked, you can still provide an `onClick` handler, and it will be called in addition to closing the modal.
669+
#### ListItem, TooltipLinkList and TooltipMessage are deprecated
670+
671+
The ListItem and TooltipLinkList components were used in Storybook to make menus, and TooltipMessage to make message popovers. However, WithTooltip does not support keyboard interactions, so these components were not accessible.
672+
673+
These components are now deprecated and will be removed in future versions. To replace TooltipMessage, replace WithTooltip with WithPopover, and use Popover as a base component for your popovers. To replace ListItem and TooltipLinkList, a dedicated menu component will be introduced in a future version, and Popover can be used in the meantime.
674+
675+
#### Tooltip Component API Changes
676+
677+
##### Renamed: tooltipRef
678+
Tooltip's `ref` prop is now named `ref` for consistency.
679+
680+
##### Removed: arrowProps and withArrows
681+
The `arrowProps` and `withArrows` props were not used in Storybook, so they have been removed.
682+
683+
We recommend you do not use arrows in your addon tooltips for better consistency with the Storybook UI.
684+
685+
##### Removed: placement
686+
The `placement` prop was passed to help position the arrow. It has also been removed. WithToolip now entirely handles the placement of its tooltip on its own.
687+
688+
##### Changed type: color
689+
The `color` prop used to accept arbitrary colors and theme background color names. This made it difficult to use.
690+
691+
The prop was to the background color of the tooltip, and it was not possible to set the text color in a consistent fashion. To ensure Tooltip uses accessible colors, the prop has been limited to the following values: `'default'`, `'inverse'`, `'positive'`, `'negative'`, `'warning'` and `'none'`. The prop now controls both background and foreground colors.
692+
693+
#### WithPopover Component Added
694+
695+
The WithPopover component acts as a counterpoint to WithTooltip. When you want an interactive overlay with buttons or inputs, use WithPopover and Popover. When you want a static overlay that shows on focus or hover, use WithTooltip with TooltipNote or Tooltip.
696+
697+
WithPopover is based on react-aria. It must have a single child that acts as a trigger. This child must have a pressable role (can be clicked or pressed) and must be able to receive React refs. Wrap your trigger component in `forwardRef` if you notice placement issues for your popover.
698+
699+
#### WithTooltip Component API Changes
700+
701+
##### Removed: trigger
702+
The `trigger` prop was removed to enforce better accessibility compliance. WithTooltip must not be triggered on click, as it is not reachable by keyboard. Buttons that open a popover, menu or select must use appropriate components instead.
703+
704+
#### Added: triggerOnFocusOnly
705+
The `triggerOnFocusOnly` prop was added. When set, tooltips will only show on focus. Use this to provide keyboard navigation hints to keyboard users. Do not use it for other purposes.
706+
707+
#### Renamed: startOpen
708+
The `startOpen` prop was renamed `defaultVisible` to match naming in other components that expose both controlled and uncontrolled visibility.
709+
710+
#### Removed: svg, strategy, withArrows, mutationObserverOptions
711+
These prop were not used inside Storybook and have been removed.
712+
713+
#### Removed: hasChrome
714+
The `hasChrome` prop was removed because it should be handled by the tooltip being shown instead. Popover and Tooltip both have a `hasChrome` prop. TooltipNote never needs this prop and does not have it.
715+
716+
#### Removed: closeOnTriggerHidden, followCursor, closeOnOutsideClick
717+
The `closeOnTriggerHidden`, `followCursor` and `closeOnOutsideClick` prop has been removed. WithTooltip will now authoritatively decide when and where to show or hide its tooltip. It will always close on clicks outside the tooltip, because tooltips should never be modal.
718+
719+
#### Removed: interactive
720+
Thed `interactive` prop has been removed as it does not align with our vision for accessible components with a well-defined role. Use WithPopover instead of WithTooltip to show interactive overlays.
721+
722+
##### Other changes
723+
The underlying implementation was switched from Popper.js to react-aria. Due to these changes, WithTooltip must now have a single child that has a focusable role and that can receive React refs. Wrap your trigger component in `forwardRef` if you notice placement issues for your tooltip.
724+
725+
#### WithTooltipPure and WithTooltipState are removed
726+
727+
Instead, use WithTooltip. For a controlled tooltip, use the `onVisibleChange` and `visible` props. For an uncontrolled tooltip with a default open state, use the `defaultVisible` prop.
652728

653729
## From version 8.x to 9.0.0
654730

code/addons/docs/src/blocks/components/ArgsTable/ArgValue.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FC } from 'react';
22
import React, { useState } from 'react';
33

4-
import { SyntaxHighlighter, WithTooltipPure, codeCommon } from 'storybook/internal/components';
4+
import { SyntaxHighlighter, WithTooltip, codeCommon } from 'storybook/internal/components';
55

66
import { ChevronSmallDownIcon, ChevronSmallUpIcon } from '@storybook/icons';
77

@@ -171,8 +171,7 @@ const ArgSummary: FC<ArgSummaryProps> = ({ value, initialExpandedArgs }) => {
171171
}
172172

173173
return (
174-
<WithTooltipPure
175-
closeOnOutsideClick
174+
<WithTooltip
176175
placement="bottom"
177176
visible={isOpen}
178177
onVisibleChange={(isVisible) => {
@@ -190,7 +189,7 @@ const ArgSummary: FC<ArgSummaryProps> = ({ value, initialExpandedArgs }) => {
190189
<span>{summaryAsString}</span>
191190
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
192191
</Expandable>
193-
</WithTooltipPure>
192+
</WithTooltip>
194193
);
195194
};
196195

code/core/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,6 @@
341341
"react-dom": "^18.2.0",
342342
"react-helmet-async": "^1.3.0",
343343
"react-inspector": "^6.0.0",
344-
"react-popper-tooltip": "^4.4.2",
345344
"react-router-dom": "6.15.0",
346345
"react-stately": "^3.41.0",
347346
"react-syntax-highlighter": "^15.4.5",

code/core/src/components/components/Button/Button.tsx

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -109,24 +109,26 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
109109
const finalTooltip = tooltip || (ariaLabel !== false ? ariaLabel : undefined);
110110

111111
return (
112-
<InteractiveTooltipWrapper shortcut={shortcut} tooltip={finalTooltip}>
113-
<StyledButton
114-
as={Comp}
115-
ref={ref}
116-
variant={variant}
117-
size={size}
118-
padding={padding}
119-
disabled={disabled}
120-
animating={isAnimating}
121-
animation={animation}
122-
onClick={handleClick}
123-
aria-label={ariaLabel !== false ? ariaLabel : undefined}
124-
aria-keyshortcuts={shortcutAttribute}
125-
{...ariaDescriptionAttrs}
126-
{...props}
127-
/>
112+
<>
113+
<InteractiveTooltipWrapper shortcut={shortcut} tooltip={finalTooltip}>
114+
<StyledButton
115+
as={Comp}
116+
ref={ref}
117+
variant={variant}
118+
size={size}
119+
padding={padding}
120+
disabled={disabled}
121+
animating={isAnimating}
122+
animation={animation}
123+
onClick={handleClick}
124+
aria-label={ariaLabel !== false ? ariaLabel : undefined}
125+
aria-keyshortcuts={shortcutAttribute}
126+
{...ariaDescriptionAttrs}
127+
{...props}
128+
/>
129+
</InteractiveTooltipWrapper>
128130
<AriaDescription />
129-
</InteractiveTooltipWrapper>
131+
</>
130132
);
131133
}
132134
);
@@ -291,7 +293,7 @@ const StyledButton = styled('button', {
291293

292294
export const IconButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
293295
deprecate(
294-
'`IconButton` is deprecated and will be removed in Storybook 10, use `Button` instead.'
296+
'`IconButton` is deprecated and will be removed in Storybook 11, use `Button` instead.'
295297
);
296298

297299
return <Button ref={ref} {...props} />;

code/core/src/components/components/Button/helpers/InteractiveTooltipWrapper.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,11 @@
1-
import React, { useMemo } from 'react';
1+
import React, { type DOMAttributes, type ReactElement, useMemo } from 'react';
22

3+
import { TooltipNote, WithTooltip } from 'storybook/internal/components';
34
import { shortcutToHumanString } from 'storybook/internal/manager-api';
45
import type { API_KeyCollection } from 'storybook/internal/manager-api';
56

6-
import { styled } from 'storybook/theming';
7-
8-
import { TooltipNote } from '../../tooltip/TooltipNote';
9-
import { WithTooltip } from '../../tooltip/WithTooltip';
10-
11-
const NoMarginNote = styled(TooltipNote)(() => ({
12-
margin: 0,
13-
}));
14-
15-
// TODO: Improve delay management; make the delay near-instantaneous if any instance of this component has been recently shown.
16-
// TODO: Find way to trigger the tooltip when the child component is focused too. Will need this for general a11y but particularly for Sidebar nav secondary actions.
177
export const InteractiveTooltipWrapper: React.FC<{
18-
children: React.ReactNode;
8+
children: ReactElement<DOMAttributes<Element>, string>;
199
shortcut?: API_KeyCollection;
2010
tooltip?: string;
2111
}> = ({ children, shortcut, tooltip }) => {
@@ -37,7 +27,7 @@ export const InteractiveTooltipWrapper: React.FC<{
3727
}, [shortcut, tooltip]);
3828

3929
return tooltipLabel ? (
40-
<WithTooltip trigger="hover" hasChrome={false} tooltip={<NoMarginNote note={tooltipLabel} />}>
30+
<WithTooltip placement="top" tooltip={<TooltipNote note={tooltipLabel} />}>
4131
{children}
4232
</WithTooltip>
4333
) : (

code/core/src/components/components/tabs/tabs.hooks.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
22

3+
import type { TooltipLinkListLink as Link } from 'storybook/internal/components';
4+
import { TooltipLinkList, WithTooltip } from 'storybook/internal/components';
35
import { sanitize } from 'storybook/internal/csf';
46

57
import { styled } from 'storybook/theming';
68
import useResizeObserver from 'use-resize-observer';
79

810
import { TabButton } from '../bar/button';
9-
import { TooltipLinkList } from '../tooltip/TooltipLinkList';
10-
import type { Link } from '../tooltip/TooltipLinkList';
11-
import { WithTooltip } from '../tooltip/WithTooltip';
1211
import type { ChildrenListComplete } from './tabs.helpers';
1312

1413
const CollapseIcon = styled.span<{ isActive: boolean }>(({ theme, isActive }) => ({

code/core/src/components/components/tooltip/ListItem.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, { type ComponentProps, type ReactNode, type SyntheticEvent } from 'react';
22

3+
import { deprecate } from 'storybook/internal/client-logger';
4+
35
import memoize from 'memoizerific';
46
import { styled } from 'storybook/theming';
57

@@ -210,6 +212,10 @@ const ListItem = (props: ListItemProps) => {
210212
const itemProps = getItemProps(props);
211213
const left = icon || input;
212214

215+
deprecate(
216+
'`ListItem` is deprecated and will be removed in Storybook 11, use `MenuItem` instead.'
217+
);
218+
213219
return (
214220
<Item {...rest} {...commonProps} {...itemProps}>
215221
<>
Lines changed: 63 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,83 @@
1-
import type { ComponentProps } from 'react';
21
import React from 'react';
32

4-
import { styled } from 'storybook/theming';
5-
3+
import preview from '../../../../../.storybook/preview';
64
import { Tooltip } from './Tooltip';
75

8-
// Popper would position the tooltip absolutely. We just need to make sure we are pos:rel
9-
const mockPopperProps = {
10-
style: {
11-
position: 'relative',
12-
top: 20,
13-
left: 20,
6+
const SampleTooltip = () => (
7+
<div>
8+
<h3>Lorem ipsum dolor sit amet</h3>
9+
<p>Consectatur vestibulum concet durum politu coret weirom</p>
10+
</div>
11+
);
12+
13+
const meta = preview.meta({
14+
id: 'overlay-Tooltip',
15+
title: 'Overlay/Tooltip',
16+
component: Tooltip,
17+
args: {
18+
children: <SampleTooltip />,
19+
color: undefined,
20+
hasChrome: true,
21+
},
22+
argTypes: {
23+
color: {
24+
type: 'string',
25+
control: 'select',
26+
options: ['default', 'inverse', 'positive', 'negative', 'warning', 'none'],
27+
},
1428
},
15-
};
16-
const Content = styled.div({
17-
width: '100px',
18-
height: '100px',
19-
fontSize: '16px',
20-
textAlign: 'center',
21-
lineHeight: '100px',
2229
});
2330

24-
export default {
25-
component: Tooltip,
26-
args: mockPopperProps,
27-
};
31+
export const Base = meta.story({
32+
args: {
33+
children: <SampleTooltip />,
34+
},
35+
});
36+
37+
export const WithChrome = meta.story({
38+
args: {
39+
hasChrome: true,
40+
},
41+
});
42+
43+
export const WithoutChrome = meta.story({
44+
args: {
45+
hasChrome: false,
46+
},
47+
});
2848

29-
export const BasicDefault = {
30-
// args: mockPopperProps,
31-
render: (args: ComponentProps<typeof Tooltip>) => (
32-
<Tooltip {...args}>
33-
<Content>Text</Content>
34-
</Tooltip>
35-
),
36-
};
49+
export const ColorDefault = meta.story({
50+
args: {
51+
color: 'default',
52+
},
53+
});
3754

38-
export const BasicDefaultBottom = {
55+
export const ColorInverse = meta.story({
3956
args: {
40-
// ...mockPopperProps,
41-
placement: 'bottom',
57+
color: 'inverse',
4258
},
43-
render: (args: ComponentProps<typeof Tooltip>) => (
44-
<Tooltip {...args}>
45-
<Content>Text</Content>
46-
</Tooltip>
47-
),
48-
};
59+
});
4960

50-
export const BasicDefaultLeft = {
61+
export const ColorPositive = meta.story({
5162
args: {
52-
// ...mockPopperProps,
53-
placement: 'left',
63+
color: 'positive',
5464
},
55-
render: (args: ComponentProps<typeof Tooltip>) => (
56-
<Tooltip {...args}>
57-
<Content>Text</Content>
58-
</Tooltip>
59-
),
60-
};
65+
});
6166

62-
export const BasicDefaultRight = {
67+
export const ColorNegative = meta.story({
6368
args: {
64-
// ...mockPopperProps,
65-
placement: 'right',
69+
color: 'negative',
6670
},
67-
render: (args: ComponentProps<typeof Tooltip>) => (
68-
<Tooltip {...args}>
69-
<Content>Text</Content>
70-
</Tooltip>
71-
),
72-
};
71+
});
7372

74-
export const WithoutChrome = {
73+
export const ColorWarning = meta.story({
7574
args: {
76-
// ...mockPopperProps,
77-
hasChrome: false,
75+
color: 'warning',
7876
},
79-
render: (args: ComponentProps<typeof Tooltip>) => (
80-
<Tooltip {...args}>
81-
<Content>Text</Content>
82-
</Tooltip>
83-
),
84-
};
77+
});
78+
79+
export const WithoutColor = meta.story({
80+
args: {
81+
color: 'none',
82+
},
83+
});

0 commit comments

Comments
 (0)