@wordpress/ui 0.6.0 → 0.7.1-next.v.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/build/badge/badge.cjs +1 -1
- package/build/badge/badge.cjs.map +2 -2
- package/build/box/box.cjs +3 -7
- package/build/box/box.cjs.map +2 -2
- package/build/button/button.cjs +3 -3
- package/build/button/button.cjs.map +2 -2
- package/build/form/primitives/fieldset/root.cjs +3 -3
- package/build/form/primitives/fieldset/root.cjs.map +2 -2
- package/build/form/primitives/input-layout/input-layout.cjs +3 -3
- package/build/form/primitives/input-layout/input-layout.cjs.map +2 -2
- package/build/form/primitives/input-layout/slot.cjs +3 -3
- package/build/form/primitives/input-layout/slot.cjs.map +2 -2
- package/build/form/primitives/select/item.cjs +3 -3
- package/build/form/primitives/select/item.cjs.map +2 -2
- package/build/form/primitives/select/popup.cjs +3 -3
- package/build/form/primitives/select/popup.cjs.map +2 -2
- package/build/form/primitives/select/trigger.cjs +3 -3
- package/build/form/primitives/select/trigger.cjs.map +2 -2
- package/build/icon-button/icon-button.cjs +103 -0
- package/build/icon-button/icon-button.cjs.map +7 -0
- package/build/icon-button/index.cjs +31 -0
- package/build/icon-button/index.cjs.map +7 -0
- package/build/icon-button/types.cjs +19 -0
- package/build/icon-button/types.cjs.map +7 -0
- package/build/index.cjs +5 -0
- package/build/index.cjs.map +2 -2
- package/build/tabs/index.cjs +40 -0
- package/build/tabs/index.cjs.map +7 -0
- package/build/tabs/list.cjs +145 -0
- package/build/tabs/list.cjs.map +7 -0
- package/build/tabs/panel.cjs +67 -0
- package/build/tabs/panel.cjs.map +7 -0
- package/build/tabs/root.cjs +38 -0
- package/build/tabs/root.cjs.map +7 -0
- package/build/tabs/tab.cjs +71 -0
- package/build/tabs/tab.cjs.map +7 -0
- package/build/tabs/types.cjs +19 -0
- package/build/tabs/types.cjs.map +7 -0
- package/build/tooltip/popup.cjs +3 -3
- package/build/tooltip/popup.cjs.map +2 -2
- package/build-module/badge/badge.mjs +1 -1
- package/build-module/badge/badge.mjs.map +2 -2
- package/build-module/box/box.mjs +3 -7
- package/build-module/box/box.mjs.map +2 -2
- package/build-module/button/button.mjs +3 -3
- package/build-module/button/button.mjs.map +2 -2
- package/build-module/form/primitives/fieldset/root.mjs +3 -3
- package/build-module/form/primitives/fieldset/root.mjs.map +2 -2
- package/build-module/form/primitives/input-layout/input-layout.mjs +3 -3
- package/build-module/form/primitives/input-layout/input-layout.mjs.map +2 -2
- package/build-module/form/primitives/input-layout/slot.mjs +3 -3
- package/build-module/form/primitives/input-layout/slot.mjs.map +2 -2
- package/build-module/form/primitives/select/item.mjs +3 -3
- package/build-module/form/primitives/select/item.mjs.map +2 -2
- package/build-module/form/primitives/select/popup.mjs +3 -3
- package/build-module/form/primitives/select/popup.mjs.map +2 -2
- package/build-module/form/primitives/select/trigger.mjs +3 -3
- package/build-module/form/primitives/select/trigger.mjs.map +2 -2
- package/build-module/icon-button/icon-button.mjs +68 -0
- package/build-module/icon-button/icon-button.mjs.map +7 -0
- package/build-module/icon-button/index.mjs +6 -0
- package/build-module/icon-button/index.mjs.map +7 -0
- package/build-module/icon-button/types.mjs +1 -0
- package/build-module/icon-button/types.mjs.map +7 -0
- package/build-module/index.mjs +3 -0
- package/build-module/index.mjs.map +2 -2
- package/build-module/tabs/index.mjs +12 -0
- package/build-module/tabs/index.mjs.map +7 -0
- package/build-module/tabs/list.mjs +110 -0
- package/build-module/tabs/list.mjs.map +7 -0
- package/build-module/tabs/panel.mjs +32 -0
- package/build-module/tabs/panel.mjs.map +7 -0
- package/build-module/tabs/root.mjs +13 -0
- package/build-module/tabs/root.mjs.map +7 -0
- package/build-module/tabs/tab.mjs +36 -0
- package/build-module/tabs/tab.mjs.map +7 -0
- package/build-module/tabs/types.mjs +1 -0
- package/build-module/tabs/types.mjs.map +7 -0
- package/build-module/tooltip/popup.mjs +3 -3
- package/build-module/tooltip/popup.mjs.map +2 -2
- package/build-types/box/box.d.ts.map +1 -1
- package/build-types/box/stories/index.story.d.ts.map +1 -1
- package/build-types/button/stories/index.story.d.ts +1 -2
- package/build-types/button/stories/index.story.d.ts.map +1 -1
- package/build-types/form/primitives/field/stories/index.story.d.ts +0 -1
- package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
- package/build-types/form/primitives/select/stories/index.story.d.ts +0 -1
- package/build-types/form/primitives/select/stories/index.story.d.ts.map +1 -1
- package/build-types/icon-button/icon-button.d.ts +13 -0
- package/build-types/icon-button/icon-button.d.ts.map +1 -0
- package/build-types/icon-button/index.d.ts +2 -0
- package/build-types/icon-button/index.d.ts.map +1 -0
- package/build-types/icon-button/stories/index.story.d.ts +19 -0
- package/build-types/icon-button/stories/index.story.d.ts.map +1 -0
- package/build-types/icon-button/test/index.test.d.ts +2 -0
- package/build-types/icon-button/test/index.test.d.ts.map +1 -0
- package/build-types/icon-button/types.d.ts +36 -0
- package/build-types/icon-button/types.d.ts.map +1 -0
- package/build-types/index.d.ts +2 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/tabs/index.d.ts +6 -0
- package/build-types/tabs/index.d.ts.map +1 -0
- package/build-types/tabs/list.d.ts +16 -0
- package/build-types/tabs/list.d.ts.map +1 -0
- package/build-types/tabs/panel.d.ts +15 -0
- package/build-types/tabs/panel.d.ts.map +1 -0
- package/build-types/tabs/root.d.ts +15 -0
- package/build-types/tabs/root.d.ts.map +1 -0
- package/build-types/tabs/stories/index.story.d.ts +13 -0
- package/build-types/tabs/stories/index.story.d.ts.map +1 -0
- package/build-types/tabs/tab.d.ts +15 -0
- package/build-types/tabs/tab.d.ts.map +1 -0
- package/build-types/tabs/test/index.test.d.ts +2 -0
- package/build-types/tabs/test/index.test.d.ts.map +1 -0
- package/build-types/tabs/types.d.ts +33 -0
- package/build-types/tabs/types.d.ts.map +1 -0
- package/package.json +11 -9
- package/src/badge/badge.tsx +1 -1
- package/src/box/box.tsx +4 -15
- package/src/box/stories/index.story.tsx +9 -1
- package/src/button/stories/index.story.tsx +3 -16
- package/src/button/style.module.css +6 -3
- package/src/form/primitives/field/stories/index.story.tsx +0 -1
- package/src/form/primitives/fieldset/style.module.css +1 -1
- package/src/form/primitives/input-layout/style.module.css +5 -8
- package/src/form/primitives/select/stories/index.story.tsx +0 -1
- package/src/icon-button/icon-button.tsx +64 -0
- package/src/icon-button/index.ts +1 -0
- package/src/icon-button/stories/index.story.tsx +128 -0
- package/src/icon-button/style.module.css +9 -0
- package/src/icon-button/test/index.test.tsx +86 -0
- package/src/icon-button/types.ts +38 -0
- package/src/index.ts +2 -0
- package/src/tabs/index.ts +6 -0
- package/src/tabs/list.tsx +130 -0
- package/src/tabs/panel.tsx +23 -0
- package/src/tabs/root.tsx +15 -0
- package/src/tabs/stories/best-practices.mdx +85 -0
- package/src/tabs/stories/index.story.tsx +363 -0
- package/src/tabs/style.module.css +269 -0
- package/src/tabs/tab.tsx +29 -0
- package/src/tabs/test/index.test.tsx +2260 -0
- package/src/tabs/types.ts +36 -0
- package/src/tooltip/style.module.css +2 -2
- package/src/utils/css/item-popup.module.css +1 -1
- package/src/utils/css/select-trigger.module.css +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ButtonProps } from '../button/types';
|
|
2
|
+
import { type IconProps } from '../icon/types';
|
|
3
|
+
|
|
4
|
+
export type IconButtonProps = Omit< ButtonProps, 'children' > & {
|
|
5
|
+
/**
|
|
6
|
+
* A label describing the button's action, shown as a tooltip and to
|
|
7
|
+
* assistive technology.
|
|
8
|
+
*/
|
|
9
|
+
label: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The icon to display in the button.
|
|
13
|
+
*/
|
|
14
|
+
icon: IconProps[ 'icon' ];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The keyboard shortcut associated with this button. When provided, the
|
|
18
|
+
* shortcut is displayed in the tooltip and announced to assistive technology.
|
|
19
|
+
*
|
|
20
|
+
* **Note**: This prop is for display and accessibility purposes only — the
|
|
21
|
+
* consumer is responsible for implementing the actual keyboard event handler.
|
|
22
|
+
*/
|
|
23
|
+
shortcut?: {
|
|
24
|
+
/**
|
|
25
|
+
* The human-readable representation of the shortcut, displayed in the
|
|
26
|
+
* tooltip. Use platform-appropriate symbols (e.g., "⌘S" on macOS,
|
|
27
|
+
* "Ctrl+S" on Windows).
|
|
28
|
+
*/
|
|
29
|
+
displayShortcut: string;
|
|
30
|
+
/**
|
|
31
|
+
* The shortcut in a format compatible with the
|
|
32
|
+
* [aria-keyshortcuts](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-keyshortcuts)
|
|
33
|
+
* attribute. Use "+" to separate keys and standard key names
|
|
34
|
+
* (e.g., "Meta+S", "Control+Shift+P").
|
|
35
|
+
*/
|
|
36
|
+
ariaKeyShortcut: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ export * from './box';
|
|
|
3
3
|
export * from './button';
|
|
4
4
|
export * from './form/primitives';
|
|
5
5
|
export * from './icon';
|
|
6
|
+
export * from './icon-button';
|
|
6
7
|
export * from './stack';
|
|
8
|
+
export * as Tabs from './tabs';
|
|
7
9
|
export * as Tooltip from './tooltip';
|
|
8
10
|
export * from './visually-hidden';
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useState } from '@wordpress/element';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Tabs as _Tabs } from '@base-ui/react/tabs';
|
|
4
|
+
import { useMergeRefs } from '@wordpress/compose';
|
|
5
|
+
import styles from './style.module.css';
|
|
6
|
+
import type { TabListProps } from './types';
|
|
7
|
+
|
|
8
|
+
// Account for sub-pixel rounding errors.
|
|
9
|
+
const SCROLL_EPSILON = 1;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Groups the individual tab buttons.
|
|
13
|
+
*
|
|
14
|
+
* `Tabs` is a collection of React components that combine to render
|
|
15
|
+
* an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
|
|
16
|
+
*/
|
|
17
|
+
export const List = forwardRef< HTMLDivElement, TabListProps >(
|
|
18
|
+
function TabList(
|
|
19
|
+
{
|
|
20
|
+
children,
|
|
21
|
+
variant = 'default',
|
|
22
|
+
className,
|
|
23
|
+
activateOnFocus,
|
|
24
|
+
render,
|
|
25
|
+
...otherProps
|
|
26
|
+
},
|
|
27
|
+
forwardedRef
|
|
28
|
+
) {
|
|
29
|
+
const [ listEl, setListEl ] = useState< HTMLDivElement | null >( null );
|
|
30
|
+
const [ overflow, setOverflow ] = useState< {
|
|
31
|
+
first: boolean;
|
|
32
|
+
last: boolean;
|
|
33
|
+
isScrolling: boolean;
|
|
34
|
+
} >( {
|
|
35
|
+
first: false,
|
|
36
|
+
last: false,
|
|
37
|
+
isScrolling: false,
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
// Check if list is overflowing when it scrolls or resizes.
|
|
41
|
+
useEffect( () => {
|
|
42
|
+
if ( ! listEl ) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const measureOverflow = () => {
|
|
47
|
+
const { scrollWidth, clientWidth, scrollLeft } = listEl;
|
|
48
|
+
const maxScroll = Math.max( scrollWidth - clientWidth, 0 );
|
|
49
|
+
const direction =
|
|
50
|
+
listEl.dir ||
|
|
51
|
+
( typeof window !== 'undefined'
|
|
52
|
+
? window.getComputedStyle( listEl ).direction
|
|
53
|
+
: 'ltr' );
|
|
54
|
+
|
|
55
|
+
const scrollFromStart =
|
|
56
|
+
direction === 'rtl' && scrollLeft < 0
|
|
57
|
+
? // In RTL layouts, scrollLeft is typically 0 at the visual "start"
|
|
58
|
+
// (right edge) and becomes negative toward the "end" (left edge).
|
|
59
|
+
// Normalize value for correct first/last detection logic.
|
|
60
|
+
-scrollLeft
|
|
61
|
+
: scrollLeft;
|
|
62
|
+
|
|
63
|
+
// Use SCROLL_EPSILON to handle subpixel rendering differences.
|
|
64
|
+
setOverflow( {
|
|
65
|
+
first: scrollFromStart > SCROLL_EPSILON,
|
|
66
|
+
last: scrollFromStart < maxScroll - SCROLL_EPSILON,
|
|
67
|
+
isScrolling: scrollWidth > clientWidth,
|
|
68
|
+
} );
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const resizeObserver = new ResizeObserver( measureOverflow );
|
|
72
|
+
resizeObserver.observe( listEl );
|
|
73
|
+
|
|
74
|
+
let scrollTick = false;
|
|
75
|
+
const throttleMeasureOverflowOnScroll = () => {
|
|
76
|
+
if ( ! scrollTick ) {
|
|
77
|
+
requestAnimationFrame( () => {
|
|
78
|
+
measureOverflow();
|
|
79
|
+
scrollTick = false;
|
|
80
|
+
} );
|
|
81
|
+
scrollTick = true;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
listEl.addEventListener(
|
|
85
|
+
'scroll',
|
|
86
|
+
throttleMeasureOverflowOnScroll,
|
|
87
|
+
{ passive: true }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Initial check.
|
|
91
|
+
measureOverflow();
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
listEl.removeEventListener(
|
|
95
|
+
'scroll',
|
|
96
|
+
throttleMeasureOverflowOnScroll
|
|
97
|
+
);
|
|
98
|
+
resizeObserver.disconnect();
|
|
99
|
+
};
|
|
100
|
+
}, [ listEl ] );
|
|
101
|
+
|
|
102
|
+
const mergedListRef = useMergeRefs( [
|
|
103
|
+
forwardedRef,
|
|
104
|
+
( el: HTMLDivElement | null ) => setListEl( el ),
|
|
105
|
+
] );
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<_Tabs.List
|
|
109
|
+
ref={ mergedListRef }
|
|
110
|
+
activateOnFocus={ activateOnFocus }
|
|
111
|
+
data-select-on-move={ activateOnFocus ? 'true' : 'false' }
|
|
112
|
+
className={ clsx(
|
|
113
|
+
styles.tablist,
|
|
114
|
+
overflow.first && styles[ 'is-overflowing-first' ],
|
|
115
|
+
overflow.last && styles[ 'is-overflowing-last' ],
|
|
116
|
+
styles[ `is-${ variant }-variant` ],
|
|
117
|
+
className
|
|
118
|
+
) }
|
|
119
|
+
{ ...otherProps }
|
|
120
|
+
tabIndex={
|
|
121
|
+
otherProps.tabIndex ??
|
|
122
|
+
( overflow.isScrolling ? -1 : undefined )
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
{ children }
|
|
126
|
+
<_Tabs.Indicator className={ styles.indicator } />
|
|
127
|
+
</_Tabs.List>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { forwardRef } from '@wordpress/element';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Tabs as _Tabs } from '@base-ui/react/tabs';
|
|
4
|
+
import styles from './style.module.css';
|
|
5
|
+
import type { TabPanelProps } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A panel displayed when the corresponding tab is active.
|
|
9
|
+
*
|
|
10
|
+
* `Tabs` is a collection of React components that combine to render
|
|
11
|
+
* an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
|
|
12
|
+
*/
|
|
13
|
+
export const Panel = forwardRef< HTMLDivElement, TabPanelProps >(
|
|
14
|
+
function TabPanel( { className, ...otherProps }, forwardedRef ) {
|
|
15
|
+
return (
|
|
16
|
+
<_Tabs.Panel
|
|
17
|
+
ref={ forwardedRef }
|
|
18
|
+
className={ clsx( styles.tabpanel, className ) }
|
|
19
|
+
{ ...otherProps }
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { forwardRef } from '@wordpress/element';
|
|
2
|
+
import { Tabs as _Tabs } from '@base-ui/react/tabs';
|
|
3
|
+
import type { TabRootProps } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Groups the tabs and the corresponding panels.
|
|
7
|
+
*
|
|
8
|
+
* `Tabs` is a collection of React components that combine to render
|
|
9
|
+
* an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
|
|
10
|
+
*/
|
|
11
|
+
export const Root = forwardRef< HTMLDivElement, TabRootProps >(
|
|
12
|
+
function TabsRoot( { ...otherProps }, forwardedRef ) {
|
|
13
|
+
return <_Tabs.Root ref={ forwardedRef } { ...otherProps } />;
|
|
14
|
+
}
|
|
15
|
+
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Design System/Components/Tabs/Best Practices" />
|
|
4
|
+
|
|
5
|
+
# Tabs
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### Uncontrolled Mode
|
|
10
|
+
|
|
11
|
+
`Tabs` can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultValue` prop can be used to set the initially selected tab.
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
import { Tabs } from '@wordpress/ui';
|
|
15
|
+
|
|
16
|
+
const MyUncontrolledTabs = () => (
|
|
17
|
+
<Tabs.Root
|
|
18
|
+
onValueChange={ ( tab ) => console.log( 'New selected tab: ', tab ) }
|
|
19
|
+
defaultValue="tab2"
|
|
20
|
+
>
|
|
21
|
+
<Tabs.List>
|
|
22
|
+
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
|
|
23
|
+
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
|
|
24
|
+
<Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
|
|
25
|
+
</Tabs.List>
|
|
26
|
+
<Tabs.Panel value="tab1">
|
|
27
|
+
<p>Selected tab: Tab 1</p>
|
|
28
|
+
</Tabs.Panel>
|
|
29
|
+
<Tabs.Panel value="tab2">
|
|
30
|
+
<p>Selected tab: Tab 2</p>
|
|
31
|
+
</Tabs.Panel>
|
|
32
|
+
<Tabs.Panel value="tab3">
|
|
33
|
+
<p>Selected tab: Tab 3</p>
|
|
34
|
+
</Tabs.Panel>
|
|
35
|
+
</Tabs.Root>
|
|
36
|
+
);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Controlled Mode
|
|
40
|
+
|
|
41
|
+
Tabs can also be used in a controlled mode, where the parent component uses the `value` and `onValueChange` props to control tab selection. In this mode, the `defaultValue` prop will be ignored if it is provided. To indicate that no tabs are selected, pass `null` to the `value`.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useState } from 'react';
|
|
45
|
+
import { Tabs } from '@wordpress/ui';
|
|
46
|
+
|
|
47
|
+
const MyControlledTabs = () => {
|
|
48
|
+
const [ selectedTabId, setSelectedTabId ] = useState<
|
|
49
|
+
string | undefined | null
|
|
50
|
+
>( null );
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Tabs.Root
|
|
54
|
+
value={ selectedTabId }
|
|
55
|
+
onValueChange={ ( newSelectedTabId ) => {
|
|
56
|
+
setSelectedTabId( newSelectedTabId );
|
|
57
|
+
console.log( 'Selecting tab', newSelectedTabId );
|
|
58
|
+
} }
|
|
59
|
+
>
|
|
60
|
+
<Tabs.List>
|
|
61
|
+
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
|
|
62
|
+
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
|
|
63
|
+
<Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
|
|
64
|
+
</Tabs.List>
|
|
65
|
+
<Tabs.Panel value="tab1">
|
|
66
|
+
<p>Selected tab: Tab 1</p>
|
|
67
|
+
</Tabs.Panel>
|
|
68
|
+
<Tabs.Panel value="tab2">
|
|
69
|
+
<p>Selected tab: Tab 2</p>
|
|
70
|
+
</Tabs.Panel>
|
|
71
|
+
<Tabs.Panel value="tab3">
|
|
72
|
+
<p>Selected tab: Tab 3</p>
|
|
73
|
+
</Tabs.Panel>
|
|
74
|
+
</Tabs.Root>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Using `Tabs` with links
|
|
80
|
+
|
|
81
|
+
The semantics implemented by the `Tabs` component don't align well with the semantics needed by a list of links. Furthermore, end users usually expect every link to be tabbable, while `Tabs.List` is a [composite](https://w3c.github.io/aria/#composite) widget acting as a single tab stop.
|
|
82
|
+
|
|
83
|
+
For these reasons, even if the `Tabs` component is fully extensible, we don't recommend using `Tabs` with links, and we don't currently provide any related Storybook example.
|
|
84
|
+
|
|
85
|
+
We may provide a dedicated component for tabs-like links in the future based on the feedback received.
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { useState, cloneElement } from '@wordpress/element';
|
|
3
|
+
import { link, more, wordpress } from '@wordpress/icons';
|
|
4
|
+
import { Tabs, Tooltip } from '../..';
|
|
5
|
+
|
|
6
|
+
const meta: Meta< typeof Tabs.Root > = {
|
|
7
|
+
title: 'Design System/Components/Tabs',
|
|
8
|
+
component: Tabs.Root,
|
|
9
|
+
subcomponents: {
|
|
10
|
+
'Tabs.List': Tabs.List,
|
|
11
|
+
'Tabs.Tab': Tabs.Tab,
|
|
12
|
+
'Tabs.Panel': Tabs.Panel,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
const ThemedParagraph = ( { children }: { children: React.ReactNode } ) => {
|
|
18
|
+
return (
|
|
19
|
+
<p style={ { color: 'var( --wpds-color-fg-content-neutral )' } }>
|
|
20
|
+
{ children }
|
|
21
|
+
</p>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Default: StoryObj< typeof Tabs.Root > = {
|
|
26
|
+
args: {
|
|
27
|
+
defaultValue: 'tab1',
|
|
28
|
+
children: (
|
|
29
|
+
<>
|
|
30
|
+
<Tabs.List>
|
|
31
|
+
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
|
|
32
|
+
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
|
|
33
|
+
<Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
|
|
34
|
+
</Tabs.List>
|
|
35
|
+
<Tabs.Panel value="tab1">
|
|
36
|
+
<ThemedParagraph>Selected tab: Tab 1</ThemedParagraph>
|
|
37
|
+
</Tabs.Panel>
|
|
38
|
+
<Tabs.Panel value="tab2">
|
|
39
|
+
<ThemedParagraph>Selected tab: Tab 2</ThemedParagraph>
|
|
40
|
+
</Tabs.Panel>
|
|
41
|
+
<Tabs.Panel value="tab3">
|
|
42
|
+
<ThemedParagraph>Selected tab: Tab 3</ThemedParagraph>
|
|
43
|
+
</Tabs.Panel>
|
|
44
|
+
</>
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Minimal: StoryObj< typeof Tabs.Root > = {
|
|
50
|
+
args: {
|
|
51
|
+
...Default.args,
|
|
52
|
+
children: (
|
|
53
|
+
<>
|
|
54
|
+
<Tabs.List variant="minimal">
|
|
55
|
+
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
|
|
56
|
+
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
|
|
57
|
+
<Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
|
|
58
|
+
</Tabs.List>
|
|
59
|
+
<Tabs.Panel value="tab1">
|
|
60
|
+
<ThemedParagraph>Selected tab: Tab 1</ThemedParagraph>
|
|
61
|
+
</Tabs.Panel>
|
|
62
|
+
<Tabs.Panel value="tab2">
|
|
63
|
+
<ThemedParagraph>Selected tab: Tab 2</ThemedParagraph>
|
|
64
|
+
</Tabs.Panel>
|
|
65
|
+
<Tabs.Panel value="tab3">
|
|
66
|
+
<ThemedParagraph>Selected tab: Tab 3</ThemedParagraph>
|
|
67
|
+
</Tabs.Panel>
|
|
68
|
+
</>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const SizeAndOverflowPlayground: StoryObj< typeof Tabs.Root > = {
|
|
74
|
+
render: function SizeAndOverflowPlayground( props ) {
|
|
75
|
+
const [ fullWidth, setFullWidth ] = useState( false );
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
<div
|
|
79
|
+
style={ {
|
|
80
|
+
maxWidth: '40rem',
|
|
81
|
+
marginBottom: '1rem',
|
|
82
|
+
color: 'var( --wpds-color-fg-content-neutral )',
|
|
83
|
+
} }
|
|
84
|
+
>
|
|
85
|
+
<p>
|
|
86
|
+
This story helps understand how the TabList component
|
|
87
|
+
behaves under different conditions. The container below
|
|
88
|
+
(with the dotted red border) can be horizontally
|
|
89
|
+
resized, and it has a bit of padding to be out of the
|
|
90
|
+
way of the TabList.
|
|
91
|
+
</p>
|
|
92
|
+
<p>
|
|
93
|
+
The button will toggle between full width (adding{ ' ' }
|
|
94
|
+
<code>width: 100%</code>) and the default width.
|
|
95
|
+
</p>
|
|
96
|
+
<p>Try the following:</p>
|
|
97
|
+
<ul>
|
|
98
|
+
<li>
|
|
99
|
+
<strong>Small container</strong> that causes tabs to
|
|
100
|
+
overflow with scroll.
|
|
101
|
+
</li>
|
|
102
|
+
<li>
|
|
103
|
+
<strong>Large container</strong> that exceeds the
|
|
104
|
+
normal width of the tabs.
|
|
105
|
+
<ul>
|
|
106
|
+
<li>
|
|
107
|
+
<strong>
|
|
108
|
+
With <code>width: 100%</code>
|
|
109
|
+
</strong>{ ' ' }
|
|
110
|
+
set on the TabList (tabs fill up the space).
|
|
111
|
+
</li>
|
|
112
|
+
<li>
|
|
113
|
+
<strong>
|
|
114
|
+
Without <code>width: 100%</code>
|
|
115
|
+
</strong>{ ' ' }
|
|
116
|
+
(defaults to <code>auto</code>) set on the
|
|
117
|
+
TabList (tabs take up space proportional to
|
|
118
|
+
their content).
|
|
119
|
+
</li>
|
|
120
|
+
</ul>
|
|
121
|
+
</li>
|
|
122
|
+
</ul>
|
|
123
|
+
</div>
|
|
124
|
+
<button
|
|
125
|
+
style={ { marginBottom: '1rem' } }
|
|
126
|
+
onClick={ () => setFullWidth( ! fullWidth ) }
|
|
127
|
+
>
|
|
128
|
+
{ fullWidth
|
|
129
|
+
? 'Remove width: 100% from TabList'
|
|
130
|
+
: 'Set width: 100% in TabList' }
|
|
131
|
+
</button>
|
|
132
|
+
<Tabs.Root
|
|
133
|
+
{ ...props }
|
|
134
|
+
style={ {
|
|
135
|
+
...props.style,
|
|
136
|
+
width: '20rem',
|
|
137
|
+
border: '2px dotted red',
|
|
138
|
+
padding: '1rem',
|
|
139
|
+
resize: 'horizontal',
|
|
140
|
+
overflow: 'auto',
|
|
141
|
+
} }
|
|
142
|
+
>
|
|
143
|
+
<Tabs.List
|
|
144
|
+
style={ {
|
|
145
|
+
maxWidth: '100%',
|
|
146
|
+
width: fullWidth ? '100%' : undefined,
|
|
147
|
+
} }
|
|
148
|
+
>
|
|
149
|
+
<Tabs.Tab value="tab1">
|
|
150
|
+
Label with multiple words
|
|
151
|
+
</Tabs.Tab>
|
|
152
|
+
<Tabs.Tab value="tab2">Short</Tabs.Tab>
|
|
153
|
+
<Tabs.Tab value="tab3">
|
|
154
|
+
Hippopotomonstrosesquippedaliophobia
|
|
155
|
+
</Tabs.Tab>
|
|
156
|
+
<Tabs.Tab value="tab4">Tab 4</Tabs.Tab>
|
|
157
|
+
<Tabs.Tab value="tab5">Tab 5</Tabs.Tab>
|
|
158
|
+
</Tabs.List>
|
|
159
|
+
|
|
160
|
+
<Tabs.Panel value="tab1">
|
|
161
|
+
<ThemedParagraph>Selected tab: Tab 1</ThemedParagraph>
|
|
162
|
+
<ThemedParagraph>
|
|
163
|
+
(Label with multiple words)
|
|
164
|
+
</ThemedParagraph>
|
|
165
|
+
</Tabs.Panel>
|
|
166
|
+
<Tabs.Panel value="tab2">
|
|
167
|
+
<ThemedParagraph>Selected tab: Tab 2</ThemedParagraph>
|
|
168
|
+
<ThemedParagraph>(Short)</ThemedParagraph>
|
|
169
|
+
</Tabs.Panel>
|
|
170
|
+
<Tabs.Panel value="tab3">
|
|
171
|
+
<ThemedParagraph>Selected tab: Tab 3</ThemedParagraph>
|
|
172
|
+
<ThemedParagraph>
|
|
173
|
+
(Hippopotomonstrosesquippedaliophobia)
|
|
174
|
+
</ThemedParagraph>
|
|
175
|
+
</Tabs.Panel>
|
|
176
|
+
<Tabs.Panel value="tab4">
|
|
177
|
+
<ThemedParagraph>Selected tab: Tab 4</ThemedParagraph>
|
|
178
|
+
</Tabs.Panel>
|
|
179
|
+
<Tabs.Panel value="tab5">
|
|
180
|
+
<ThemedParagraph>Selected tab: Tab 5</ThemedParagraph>
|
|
181
|
+
</Tabs.Panel>
|
|
182
|
+
</Tabs.Root>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
args: {
|
|
187
|
+
...Default.args,
|
|
188
|
+
defaultValue: 'tab4',
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const Vertical: StoryObj< typeof Tabs.Root > = {
|
|
193
|
+
args: {
|
|
194
|
+
...Default.args,
|
|
195
|
+
orientation: 'vertical',
|
|
196
|
+
style: {
|
|
197
|
+
minWidth: '320px',
|
|
198
|
+
display: 'grid',
|
|
199
|
+
gridTemplateColumns: '120px 1fr',
|
|
200
|
+
gap: '24px',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const WithDisabledTab: StoryObj< typeof Tabs.Root > = {
|
|
206
|
+
args: {
|
|
207
|
+
...Default.args,
|
|
208
|
+
defaultValue: 'tab3',
|
|
209
|
+
children: (
|
|
210
|
+
<>
|
|
211
|
+
<Tabs.List>
|
|
212
|
+
<Tabs.Tab value="tab1" disabled>
|
|
213
|
+
Tab 1
|
|
214
|
+
</Tabs.Tab>
|
|
215
|
+
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
|
|
216
|
+
<Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
|
|
217
|
+
</Tabs.List>
|
|
218
|
+
<Tabs.Panel value="tab1">
|
|
219
|
+
<ThemedParagraph>Selected tab: Tab 1</ThemedParagraph>
|
|
220
|
+
</Tabs.Panel>
|
|
221
|
+
<Tabs.Panel value="tab2">
|
|
222
|
+
<ThemedParagraph>Selected tab: Tab 2</ThemedParagraph>
|
|
223
|
+
</Tabs.Panel>
|
|
224
|
+
<Tabs.Panel value="tab3">
|
|
225
|
+
<ThemedParagraph>Selected tab: Tab 3</ThemedParagraph>
|
|
226
|
+
</Tabs.Panel>
|
|
227
|
+
</>
|
|
228
|
+
),
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const LinkIcon = ( props: React.SVGProps< SVGSVGElement > ) => {
|
|
233
|
+
return cloneElement( link, props );
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const MoreIcon = ( props: React.SVGProps< SVGSVGElement > ) => {
|
|
237
|
+
return cloneElement( more, props );
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const WordpressIcon = ( props: React.SVGProps< SVGSVGElement > ) => {
|
|
241
|
+
return cloneElement( wordpress, props );
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const tabWithIconsData = [
|
|
245
|
+
{
|
|
246
|
+
value: 'tab1',
|
|
247
|
+
label: 'Tab one',
|
|
248
|
+
icon: WordpressIcon,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
value: 'tab2',
|
|
252
|
+
label: 'Tab two',
|
|
253
|
+
icon: LinkIcon,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
value: 'tab3',
|
|
257
|
+
label: 'Tab three',
|
|
258
|
+
icon: MoreIcon,
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
export const WithTabIconsAndTooltips: StoryObj< typeof Tabs.Root > = {
|
|
263
|
+
args: {
|
|
264
|
+
...Default.args,
|
|
265
|
+
children: (
|
|
266
|
+
<>
|
|
267
|
+
<Tabs.List>
|
|
268
|
+
{ tabWithIconsData.map(
|
|
269
|
+
( { value, label, icon: Icon } ) => (
|
|
270
|
+
<Tooltip.Root key={ value }>
|
|
271
|
+
<Tooltip.Trigger
|
|
272
|
+
aria-label={ label }
|
|
273
|
+
render={ <Tabs.Tab value={ value } /> }
|
|
274
|
+
>
|
|
275
|
+
{ /* TODO: potentially refactor with new Icon component */ }
|
|
276
|
+
<Icon
|
|
277
|
+
style={ {
|
|
278
|
+
width: '20px',
|
|
279
|
+
height: '20px',
|
|
280
|
+
} }
|
|
281
|
+
/>
|
|
282
|
+
</Tooltip.Trigger>
|
|
283
|
+
<Tooltip.Popup align="center" side="top">
|
|
284
|
+
{ label }
|
|
285
|
+
</Tooltip.Popup>
|
|
286
|
+
</Tooltip.Root>
|
|
287
|
+
)
|
|
288
|
+
) }
|
|
289
|
+
</Tabs.List>
|
|
290
|
+
{ tabWithIconsData.map( ( { value, label } ) => (
|
|
291
|
+
<Tabs.Panel value={ value } key={ value }>
|
|
292
|
+
<ThemedParagraph>
|
|
293
|
+
Selected tab: { label }
|
|
294
|
+
</ThemedParagraph>
|
|
295
|
+
</Tabs.Panel>
|
|
296
|
+
) ) }
|
|
297
|
+
</>
|
|
298
|
+
),
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export const WithPanelsAlwaysMounted: StoryObj< typeof Tabs.Root > = {
|
|
303
|
+
args: {
|
|
304
|
+
...Default.args,
|
|
305
|
+
children: (
|
|
306
|
+
<>
|
|
307
|
+
<Tabs.List>
|
|
308
|
+
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
|
|
309
|
+
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
|
|
310
|
+
<Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
|
|
311
|
+
</Tabs.List>
|
|
312
|
+
<Tabs.Panel value="tab1" keepMounted>
|
|
313
|
+
<ThemedParagraph>Selected tab: Tab 1</ThemedParagraph>
|
|
314
|
+
</Tabs.Panel>
|
|
315
|
+
<Tabs.Panel value="tab2" keepMounted>
|
|
316
|
+
<ThemedParagraph>Selected tab: Tab 2</ThemedParagraph>
|
|
317
|
+
</Tabs.Panel>
|
|
318
|
+
<Tabs.Panel value="tab3" keepMounted>
|
|
319
|
+
<ThemedParagraph>Selected tab: Tab 3</ThemedParagraph>
|
|
320
|
+
</Tabs.Panel>
|
|
321
|
+
</>
|
|
322
|
+
),
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const WithNonFocusablePanels: StoryObj< typeof Tabs.Root > = {
|
|
327
|
+
args: {
|
|
328
|
+
...Default.args,
|
|
329
|
+
children: (
|
|
330
|
+
<>
|
|
331
|
+
<Tabs.List>
|
|
332
|
+
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
|
|
333
|
+
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
|
|
334
|
+
<Tabs.Tab value="tab3">Tab 3</Tabs.Tab>
|
|
335
|
+
</Tabs.List>
|
|
336
|
+
<Tabs.Panel value="tab1" tabIndex={ -1 }>
|
|
337
|
+
<ThemedParagraph>Selected tab: Tab 1</ThemedParagraph>
|
|
338
|
+
<ThemedParagraph>
|
|
339
|
+
This tabpanel is not focusable, therefore tabbing into
|
|
340
|
+
it will focus its first tabbable child.
|
|
341
|
+
</ThemedParagraph>
|
|
342
|
+
<button>Focus me</button>
|
|
343
|
+
</Tabs.Panel>
|
|
344
|
+
<Tabs.Panel value="tab2" tabIndex={ -1 }>
|
|
345
|
+
<ThemedParagraph>Selected tab: Tab 2</ThemedParagraph>
|
|
346
|
+
<ThemedParagraph>
|
|
347
|
+
This tabpanel is not focusable, therefore tabbing into
|
|
348
|
+
it will focus its first tabbable child.
|
|
349
|
+
</ThemedParagraph>
|
|
350
|
+
<button>Focus me</button>
|
|
351
|
+
</Tabs.Panel>
|
|
352
|
+
<Tabs.Panel value="tab3" tabIndex={ -1 }>
|
|
353
|
+
<ThemedParagraph>Selected tab: Tab 3</ThemedParagraph>
|
|
354
|
+
<ThemedParagraph>
|
|
355
|
+
This tabpanel is not focusable, therefore tabbing into
|
|
356
|
+
it will focus its first tabbable child.
|
|
357
|
+
</ThemedParagraph>
|
|
358
|
+
<button>Focus me</button>
|
|
359
|
+
</Tabs.Panel>
|
|
360
|
+
</>
|
|
361
|
+
),
|
|
362
|
+
},
|
|
363
|
+
};
|