Skip to content

Commit 2153f74

Browse files
committed
WIP: Revert breaking changes on Button, Tabs, Modal
1 parent 7d94edd commit 2153f74

File tree

8 files changed

+269
-28
lines changed

8 files changed

+269
-28
lines changed

MIGRATION.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- [Added: ariaLabel](#added-arialabel)
1414
- [Added: shortcut](#added-shortcut)
1515
- [Added: tooltip](#added-tooltip)
16-
- [Removed: active](#removed-active)
16+
- [Deprecated: active](#deprecated-active)
1717
- [IconButton is deprecated](#iconbutton-is-deprecated)
1818
- [Bar Component API Changes](#bar-component-api-changes)
1919
- [Added: innerStyle](#added-innerstyle)
@@ -22,7 +22,7 @@
2222
- [TabsState is deprecated](#tabsstate-is-deprecated)
2323
- [TabWrapper is deprecated](#tabwrapper-is-deprecated)
2424
- [TabButton is deprecated](#tabbutton-is-deprecated)
25-
- [TabBar is removed](#tabbar-is-removed)
25+
- [TabBar is deprecated](#tabbar-is-deprecated)
2626
- [Modal Component API Changes](#modal-component-api-changes)
2727
- [Removed: container and portalSelector](#removed-container-and-portalselector)
2828
- [Removed: onInteractOutside](#removed-oninteractoutside)
@@ -629,7 +629,7 @@ During development of Storybook [Tags](https://storybook.js.org/docs/writing-sto
629629
#### Button Component API Changes
630630

631631
##### Added: ariaLabel
632-
The Button component now has a mandatory `ariaLabel` prop, to ensure that Storybook UI code is accessible to screenreader users.
632+
The Button component now has an `ariaLabel` prop, to ensure that Storybook UI code is accessible to screenreader users. The prop will become mandatory in Storybook 11.
633633

634634
When buttons have text content as children, and when that text content does not rely on visual context to be understood, you may pass `false` to the `ariaLabel` prop to indicate that an ARIA label is not necessary.
635635

@@ -643,13 +643,11 @@ An optional `shortcut` prop was added for internal use. When `shortcut` is set,
643643

644644
Button now displays a tooltip whenever `ariaLabel` or `shortcut` is set. The tooltip can be customised by passing a string to the optional `tooltip` prop.
645645

646-
##### Removed: active
646+
##### Deprecated: active
647647

648-
The `active` prop was removed from Button.
648+
The `active` prop is deprecated and will be removed in Storybook 11.
649649

650-
The Button component has historically been used to implement Toggle and Select interactions. When you need a Button to have an active state, use ToggleButton if the active state denotes that a state or feature is enabled after pressing the Button.
651-
652-
Use Select if the active state denotes that the Button is open while a selection is being made, or that the Button currently has a selected value.
650+
The Button component has historically been used to implement Toggle and Select interactions. When you need a Button to have an active state, use ToggleButton if the active state denotes that a state or feature is enabled after pressing the Button. Use Select if the active state denotes that the Button is open while a selection is being made, or that the Button currently has a selected value.
653651

654652
#### IconButton is deprecated
655653

@@ -695,9 +693,9 @@ The `TabWrapper` component is deprecated as it was not accessible. Instead, use
695693

696694
The `TabButton` class is deprecated as it was not accessible. It does not have a replacement, as the new `TabList` component handles tab buttons internally.
697695

698-
#### TabBar is removed
696+
#### TabBar is deprecated
699697

700-
The `TabBar` component, a styled bar used inside `Tabs` and not intended to be public, has been removed. It has no replacement.
698+
The `TabBar` component, a styled bar used inside `Tabs` and not intended to be public, is deprecated and will be hidden in Storybook 11. Use `TabsView` instead.
701699

702700
#### Modal Component API Changes
703701

@@ -711,10 +709,10 @@ The `onInteractOutside` prop is removed in favor of `dismissOnClickOutside`, bec
711709
The `onEscapeKeyDown` prop is removed in favor of `dismissOnEscape`, because it was only used to close the modal when pressing Escape. Use `dismissOnEscape` to control whether pressing Escape should close it or not.
712710

713711
##### Added: `ariaLabel`
714-
Modal elements must have a title to be accessible. Set that title through the mandatory `ariaLabel` prop.
712+
Modal elements must have a title to be accessible. Set that title through the `ariaLabel` prop. It will become mandatory in Storybook 11.
715713

716714
##### Renamed: Modal.Dialog.Close and Modal.CloseButton
717-
The `Modal.Dialog.Close` component and `Modal.CloseButton` components are replaced by `Modal.Close` for consistency with other components. Those names are deprecated and will be removed in a future version. You may call `<Modal.Close />` for a default close button, or `<Modal.Close asChild>...</Modal.Close>` to wrap your own custom button.
715+
The `Modal.Dialog.Close` component and `Modal.CloseButton` components are replaced by `Modal.Close` for consistency with other components. Those names are deprecated and will be removed in Storybook 11. You may call `<Modal.Close />` for a default close button, or `<Modal.Close asChild>...</Modal.Close>` to wrap your own custom button.
718716

719717
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.
720718
#### ListItem, TooltipLinkList and TooltipMessage are deprecated

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

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,13 @@ export const PseudoStates = meta.story({
101101
</Row>
102102
<Row id="active">
103103
<Button ariaLabel={false} variant="solid">
104-
active
104+
Active
105105
</Button>
106106
<Button ariaLabel={false} variant="outline">
107-
active
107+
Active
108108
</Button>
109109
<Button ariaLabel={false} variant="ghost">
110-
active
110+
Active
111111
</Button>
112112
</Row>
113113
<Row id="focus">
@@ -144,6 +144,78 @@ export const PseudoStates = meta.story({
144144
},
145145
});
146146

147+
export const Active = meta.story({
148+
name: 'Active (deprecated)',
149+
args: { ariaLabel: false, active: true },
150+
render: (args) => (
151+
<Stack>
152+
<Row>
153+
<Button {...args} variant="solid">
154+
Button
155+
</Button>
156+
<Button {...args} variant="outline">
157+
Button
158+
</Button>
159+
<Button {...args} variant="ghost">
160+
Button
161+
</Button>
162+
</Row>
163+
<Row id="hover">
164+
<Button {...args} variant="solid">
165+
Hover
166+
</Button>
167+
<Button {...args} variant="outline">
168+
Hover
169+
</Button>
170+
<Button {...args} variant="ghost">
171+
Hover
172+
</Button>
173+
</Row>
174+
<Row id="active">
175+
<Button {...args} variant="solid">
176+
active
177+
</Button>
178+
<Button {...args} variant="outline">
179+
active
180+
</Button>
181+
<Button {...args} variant="ghost">
182+
active
183+
</Button>
184+
</Row>
185+
<Row id="focus">
186+
<Button {...args} variant="solid">
187+
Focus
188+
</Button>
189+
<Button {...args} variant="outline">
190+
Focus
191+
</Button>
192+
<Button {...args} variant="ghost">
193+
Focus
194+
</Button>
195+
</Row>
196+
<Row id="focus-visible">
197+
<Button {...args} variant="solid">
198+
Focus Visible
199+
</Button>
200+
<Button {...args} variant="outline">
201+
Focus Visible
202+
</Button>
203+
<Button {...args} variant="ghost">
204+
Focus Visible
205+
</Button>
206+
</Row>
207+
</Stack>
208+
),
209+
parameters: {
210+
pseudo: {
211+
hover: '#hover button',
212+
active: '#active button',
213+
focus: '#focus button',
214+
focusVisible: '#focus-visible button',
215+
},
216+
},
217+
});
218+
147219
export const WithIcon = meta.story({
148220
args: {
149221
ariaLabel: false,

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

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ButtonHTMLAttributes, SyntheticEvent } from 'react';
22
import React, { forwardRef, useEffect, useMemo, useState } from 'react';
33

4-
import { deprecate } from 'storybook/internal/client-logger';
4+
import { deprecate, logger } from 'storybook/internal/client-logger';
55

66
import { Slot } from '@radix-ui/react-slot';
77
import { darken, lighten, rgba, transparentize } from 'polished';
@@ -17,6 +17,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
1717
padding?: 'small' | 'medium' | 'none';
1818
variant?: 'outline' | 'solid' | 'ghost';
1919
onClick?: (event: SyntheticEvent) => void;
20+
active?: boolean;
2021
disabled?: boolean;
2122
animation?: 'none' | 'rotate360' | 'glow' | 'jiggle';
2223

@@ -26,7 +27,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
2627
* Button's content is already accessible to all. When a string is passed, it is also used as the
2728
* default tooltip text.
2829
*/
29-
ariaLabel: string | false;
30+
ariaLabel?: string | false;
3031

3132
/**
3233
* An optional tooltip to display when the Button is hovered. If the Button has no text content,
@@ -64,6 +65,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
6465
variant = 'outline',
6566
padding = 'medium',
6667
disabled = false,
68+
active,
6769
onClick,
6870
ariaLabel,
6971
ariaDescription = undefined,
@@ -76,17 +78,29 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
7678
) => {
7779
let Comp: 'button' | 'a' | typeof Slot = 'button';
7880

81+
// FIXME / TODO : reenable this but its too verbose and i cant work rn
82+
// if (ariaLabel === undefined || ariaLabel === '') {
83+
// logger.warn(ariaLabel, props.children);
84+
// logger.warn(
85+
// 'The `ariaLabel` prop on `Button` will become mandatory in Storybook 11. Buttons with text content should set `ariaLabel={false}` to indicate that they are accessible as-is. Buttons without text content must provide a meaningful `ariaLabel` for accessibility.'
86+
// );
87+
// // TODO in Storybook 11
88+
// // throw new Error(
89+
// // 'Button requires an ARIA label to be accessible. Please provide a valid ariaLabel prop.'
90+
// // );
91+
// }
92+
93+
if (active !== undefined) {
94+
deprecate(
95+
'The `active` prop on `Button` is deprecated and will be removed in Storybook 11. Use specialized components like `ToggleButton` or `Select` instead.'
96+
);
97+
}
98+
7999
if (asChild) {
80100
Comp = Slot;
81101
}
82102
const { ariaDescriptionAttrs, AriaDescription } = useAriaDescription(ariaDescription);
83103

84-
if (ariaLabel === '') {
85-
throw new Error(
86-
'Button requires an ARIA label to be accessible. Please provide a valid ariaLabel prop.'
87-
);
88-
}
89-
90104
const shortcutAttribute = useMemo(() => {
91105
return shortcut ? shortcutToAriaKeyshortcuts(shortcut) : undefined;
92106
}, [shortcut]);
@@ -129,6 +143,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
129143
size={size}
130144
padding={padding}
131145
disabled={disabled}
146+
active={active}
132147
animating={isAnimating}
133148
animation={animation}
134149
onClick={handleClick}
@@ -153,7 +168,7 @@ const StyledButton = styled('button', {
153168
animating: boolean;
154169
animation: ButtonProps['animation'];
155170
}
156-
>(({ theme, variant, size, disabled, animating, animation = 'none', padding }) => ({
171+
>(({ theme, variant, size, disabled, active, animating, animation = 'none', padding }) => ({
157172
border: 0,
158173
cursor: disabled ? 'not-allowed' : 'pointer',
159174
display: 'inline-flex',
@@ -203,6 +218,10 @@ const StyledButton = styled('button', {
203218
return theme.button.background;
204219
}
205220

221+
if (variant === 'ghost' && active) {
222+
return transparentize(0.93, theme.barSelectedColor);
223+
}
224+
206225
return 'transparent';
207226
})(),
208227
color: (() => {
@@ -214,6 +233,10 @@ const StyledButton = styled('button', {
214233
return theme.input.color;
215234
}
216235

236+
if (variant === 'ghost' && active) {
237+
return theme.base === 'light' ? darken(0.1, theme.color.secondary) : theme.color.secondary;
238+
}
239+
217240
if (variant === 'ghost') {
218241
return theme.textMutedColor;
219242
}

code/core/src/components/components/Modal/Modal.stories.tsx

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
33
import { Button } from 'storybook/internal/components';
44

55
import { action } from 'storybook/actions';
6-
import { expect, fn, getByRole, screen, userEvent, waitFor, within } from 'storybook/test';
6+
import { expect, fn, screen, userEvent, waitFor, within } from 'storybook/test';
77

88
import preview from '../../../../../.storybook/preview';
99
import { Modal } from './Modal';
@@ -221,6 +221,91 @@ export const DismissalBehavior = meta.story({
221221
),
222222
});
223223

224+
export const OnInteractOutside = meta.story({
225+
name: 'OnInteractOutside (deprecated)',
226+
args: {
227+
children: <SampleModalContent />,
228+
onInteractOutside: fn(),
229+
},
230+
render: (args) => {
231+
const [isOpen, setOpen] = useState(false);
232+
233+
return (
234+
<>
235+
<Modal {...args} open={isOpen} onOpenChange={setOpen} />
236+
<Button ariaLabel={false} onClick={() => setOpen(true)}>
237+
Open Modal
238+
</Button>
239+
<Button ariaLabel={false} style={{ marginLeft: '1rem' }}>
240+
Outside Button
241+
</Button>
242+
</>
243+
);
244+
},
245+
play: async ({ args, canvas, step }) => {
246+
await step('Open modal', async () => {
247+
const trigger = canvas.getByText('Open Modal');
248+
await userEvent.click(trigger);
249+
await waitFor(() => {
250+
expect(screen.queryByText('Sample Modal')).toBeInTheDocument();
251+
});
252+
});
253+
254+
await step('Click outside to close', async () => {
255+
const outsideButton = canvas.getByText('Outside Button');
256+
await userEvent.click(outsideButton);
257+
expect(args.onInteractOutside).toHaveBeenCalled();
258+
await waitFor(() => {
259+
expect(screen.queryByText('Sample Modal')).not.toBeInTheDocument();
260+
});
261+
});
262+
},
263+
});
264+
265+
export const OnInteractOutsidePreventDefault = meta.story({
266+
name: 'OnInteractOutside - e.preventDefault (deprecated)',
267+
args: {
268+
children: <SampleModalContent />,
269+
onInteractOutside: (e) => e.preventDefault(),
270+
},
271+
render: (args) => {
272+
const [isOpen, setOpen] = useState(false);
273+
274+
return (
275+
<>
276+
<Modal {...args} open={isOpen} onOpenChange={setOpen} />
277+
<Button ariaLabel={false} onClick={() => setOpen(true)}>
278+
Open Modal
279+
</Button>
280+
<Button ariaLabel={false} style={{ marginLeft: '1rem' }}>
281+
Outside Button
282+
</Button>
283+
</>
284+
);
285+
},
286+
play: async ({ canvas, step }) => {
287+
await step('Open modal', async () => {
288+
const trigger = canvas.getByText('Open Modal');
289+
await userEvent.click(trigger);
290+
await waitFor(() => {
291+
expect(screen.queryByText('Sample Modal')).toBeInTheDocument();
292+
});
293+
});
294+
295+
await step('Click outside to close but modal stays open', async () => {
296+
const outsideButton = canvas.getByText('Outside Button');
297+
await userEvent.click(outsideButton);
298+
// Wait a bit to ensure the modal close animation would've had time to play.
299+
await new Promise((r) => setTimeout(r, 300));
300+
await waitFor(() => {
301+
expect(screen.queryByText('Sample Modal')).toBeInTheDocument();
302+
});
303+
});
304+
},
305+
});
306+
307+
// TODO what when dismissOutside is disabled
308+
224309
const ModalWithTrigger = ({
225310
triggerText,
226311
...modalProps

0 commit comments

Comments
 (0)