@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.
Files changed (147) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/build/badge/badge.cjs +1 -1
  3. package/build/badge/badge.cjs.map +2 -2
  4. package/build/box/box.cjs +3 -7
  5. package/build/box/box.cjs.map +2 -2
  6. package/build/button/button.cjs +3 -3
  7. package/build/button/button.cjs.map +2 -2
  8. package/build/form/primitives/fieldset/root.cjs +3 -3
  9. package/build/form/primitives/fieldset/root.cjs.map +2 -2
  10. package/build/form/primitives/input-layout/input-layout.cjs +3 -3
  11. package/build/form/primitives/input-layout/input-layout.cjs.map +2 -2
  12. package/build/form/primitives/input-layout/slot.cjs +3 -3
  13. package/build/form/primitives/input-layout/slot.cjs.map +2 -2
  14. package/build/form/primitives/select/item.cjs +3 -3
  15. package/build/form/primitives/select/item.cjs.map +2 -2
  16. package/build/form/primitives/select/popup.cjs +3 -3
  17. package/build/form/primitives/select/popup.cjs.map +2 -2
  18. package/build/form/primitives/select/trigger.cjs +3 -3
  19. package/build/form/primitives/select/trigger.cjs.map +2 -2
  20. package/build/icon-button/icon-button.cjs +103 -0
  21. package/build/icon-button/icon-button.cjs.map +7 -0
  22. package/build/icon-button/index.cjs +31 -0
  23. package/build/icon-button/index.cjs.map +7 -0
  24. package/build/icon-button/types.cjs +19 -0
  25. package/build/icon-button/types.cjs.map +7 -0
  26. package/build/index.cjs +5 -0
  27. package/build/index.cjs.map +2 -2
  28. package/build/tabs/index.cjs +40 -0
  29. package/build/tabs/index.cjs.map +7 -0
  30. package/build/tabs/list.cjs +145 -0
  31. package/build/tabs/list.cjs.map +7 -0
  32. package/build/tabs/panel.cjs +67 -0
  33. package/build/tabs/panel.cjs.map +7 -0
  34. package/build/tabs/root.cjs +38 -0
  35. package/build/tabs/root.cjs.map +7 -0
  36. package/build/tabs/tab.cjs +71 -0
  37. package/build/tabs/tab.cjs.map +7 -0
  38. package/build/tabs/types.cjs +19 -0
  39. package/build/tabs/types.cjs.map +7 -0
  40. package/build/tooltip/popup.cjs +3 -3
  41. package/build/tooltip/popup.cjs.map +2 -2
  42. package/build-module/badge/badge.mjs +1 -1
  43. package/build-module/badge/badge.mjs.map +2 -2
  44. package/build-module/box/box.mjs +3 -7
  45. package/build-module/box/box.mjs.map +2 -2
  46. package/build-module/button/button.mjs +3 -3
  47. package/build-module/button/button.mjs.map +2 -2
  48. package/build-module/form/primitives/fieldset/root.mjs +3 -3
  49. package/build-module/form/primitives/fieldset/root.mjs.map +2 -2
  50. package/build-module/form/primitives/input-layout/input-layout.mjs +3 -3
  51. package/build-module/form/primitives/input-layout/input-layout.mjs.map +2 -2
  52. package/build-module/form/primitives/input-layout/slot.mjs +3 -3
  53. package/build-module/form/primitives/input-layout/slot.mjs.map +2 -2
  54. package/build-module/form/primitives/select/item.mjs +3 -3
  55. package/build-module/form/primitives/select/item.mjs.map +2 -2
  56. package/build-module/form/primitives/select/popup.mjs +3 -3
  57. package/build-module/form/primitives/select/popup.mjs.map +2 -2
  58. package/build-module/form/primitives/select/trigger.mjs +3 -3
  59. package/build-module/form/primitives/select/trigger.mjs.map +2 -2
  60. package/build-module/icon-button/icon-button.mjs +68 -0
  61. package/build-module/icon-button/icon-button.mjs.map +7 -0
  62. package/build-module/icon-button/index.mjs +6 -0
  63. package/build-module/icon-button/index.mjs.map +7 -0
  64. package/build-module/icon-button/types.mjs +1 -0
  65. package/build-module/icon-button/types.mjs.map +7 -0
  66. package/build-module/index.mjs +3 -0
  67. package/build-module/index.mjs.map +2 -2
  68. package/build-module/tabs/index.mjs +12 -0
  69. package/build-module/tabs/index.mjs.map +7 -0
  70. package/build-module/tabs/list.mjs +110 -0
  71. package/build-module/tabs/list.mjs.map +7 -0
  72. package/build-module/tabs/panel.mjs +32 -0
  73. package/build-module/tabs/panel.mjs.map +7 -0
  74. package/build-module/tabs/root.mjs +13 -0
  75. package/build-module/tabs/root.mjs.map +7 -0
  76. package/build-module/tabs/tab.mjs +36 -0
  77. package/build-module/tabs/tab.mjs.map +7 -0
  78. package/build-module/tabs/types.mjs +1 -0
  79. package/build-module/tabs/types.mjs.map +7 -0
  80. package/build-module/tooltip/popup.mjs +3 -3
  81. package/build-module/tooltip/popup.mjs.map +2 -2
  82. package/build-types/box/box.d.ts.map +1 -1
  83. package/build-types/box/stories/index.story.d.ts.map +1 -1
  84. package/build-types/button/stories/index.story.d.ts +1 -2
  85. package/build-types/button/stories/index.story.d.ts.map +1 -1
  86. package/build-types/form/primitives/field/stories/index.story.d.ts +0 -1
  87. package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
  88. package/build-types/form/primitives/select/stories/index.story.d.ts +0 -1
  89. package/build-types/form/primitives/select/stories/index.story.d.ts.map +1 -1
  90. package/build-types/icon-button/icon-button.d.ts +13 -0
  91. package/build-types/icon-button/icon-button.d.ts.map +1 -0
  92. package/build-types/icon-button/index.d.ts +2 -0
  93. package/build-types/icon-button/index.d.ts.map +1 -0
  94. package/build-types/icon-button/stories/index.story.d.ts +19 -0
  95. package/build-types/icon-button/stories/index.story.d.ts.map +1 -0
  96. package/build-types/icon-button/test/index.test.d.ts +2 -0
  97. package/build-types/icon-button/test/index.test.d.ts.map +1 -0
  98. package/build-types/icon-button/types.d.ts +36 -0
  99. package/build-types/icon-button/types.d.ts.map +1 -0
  100. package/build-types/index.d.ts +2 -0
  101. package/build-types/index.d.ts.map +1 -1
  102. package/build-types/tabs/index.d.ts +6 -0
  103. package/build-types/tabs/index.d.ts.map +1 -0
  104. package/build-types/tabs/list.d.ts +16 -0
  105. package/build-types/tabs/list.d.ts.map +1 -0
  106. package/build-types/tabs/panel.d.ts +15 -0
  107. package/build-types/tabs/panel.d.ts.map +1 -0
  108. package/build-types/tabs/root.d.ts +15 -0
  109. package/build-types/tabs/root.d.ts.map +1 -0
  110. package/build-types/tabs/stories/index.story.d.ts +13 -0
  111. package/build-types/tabs/stories/index.story.d.ts.map +1 -0
  112. package/build-types/tabs/tab.d.ts +15 -0
  113. package/build-types/tabs/tab.d.ts.map +1 -0
  114. package/build-types/tabs/test/index.test.d.ts +2 -0
  115. package/build-types/tabs/test/index.test.d.ts.map +1 -0
  116. package/build-types/tabs/types.d.ts +33 -0
  117. package/build-types/tabs/types.d.ts.map +1 -0
  118. package/package.json +11 -9
  119. package/src/badge/badge.tsx +1 -1
  120. package/src/box/box.tsx +4 -15
  121. package/src/box/stories/index.story.tsx +9 -1
  122. package/src/button/stories/index.story.tsx +3 -16
  123. package/src/button/style.module.css +6 -3
  124. package/src/form/primitives/field/stories/index.story.tsx +0 -1
  125. package/src/form/primitives/fieldset/style.module.css +1 -1
  126. package/src/form/primitives/input-layout/style.module.css +5 -8
  127. package/src/form/primitives/select/stories/index.story.tsx +0 -1
  128. package/src/icon-button/icon-button.tsx +64 -0
  129. package/src/icon-button/index.ts +1 -0
  130. package/src/icon-button/stories/index.story.tsx +128 -0
  131. package/src/icon-button/style.module.css +9 -0
  132. package/src/icon-button/test/index.test.tsx +86 -0
  133. package/src/icon-button/types.ts +38 -0
  134. package/src/index.ts +2 -0
  135. package/src/tabs/index.ts +6 -0
  136. package/src/tabs/list.tsx +130 -0
  137. package/src/tabs/panel.tsx +23 -0
  138. package/src/tabs/root.tsx +15 -0
  139. package/src/tabs/stories/best-practices.mdx +85 -0
  140. package/src/tabs/stories/index.story.tsx +363 -0
  141. package/src/tabs/style.module.css +269 -0
  142. package/src/tabs/tab.tsx +29 -0
  143. package/src/tabs/test/index.test.tsx +2260 -0
  144. package/src/tabs/types.ts +36 -0
  145. package/src/tooltip/style.module.css +2 -2
  146. package/src/utils/css/item-popup.module.css +1 -1
  147. 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,6 @@
1
+ import { List } from './list';
2
+ import { Panel } from './panel';
3
+ import { Root } from './root';
4
+ import { Tab } from './tab';
5
+
6
+ export { Root, List, Panel, Tab };
@@ -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
+ };