From 869d26bebdb7d39f2cba51b13a7187e8f5f15814 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 21 Dec 2024 02:29:36 +0900 Subject: [PATCH 01/12] Components: Normalize newlines --- bin/api-docs/gen-components-docs/markdown/index.mjs | 8 ++++++-- .../components/src/alignment-matrix-control/README.md | 1 + packages/components/src/angle-picker-control/README.md | 1 + packages/components/src/base-control/README.md | 2 ++ packages/components/src/box-control/README.md | 1 + packages/components/src/button/README.md | 1 + packages/components/src/form-file-upload/README.md | 1 + packages/components/src/gradient-picker/README.md | 1 + packages/components/src/icon/README.md | 1 + packages/components/src/tree-select/README.md | 1 + 10 files changed, 16 insertions(+), 2 deletions(-) diff --git a/bin/api-docs/gen-components-docs/markdown/index.mjs b/bin/api-docs/gen-components-docs/markdown/index.mjs index 126fdf0057b6e5..8fd61a25fcace8 100644 --- a/bin/api-docs/gen-components-docs/markdown/index.mjs +++ b/bin/api-docs/gen-components-docs/markdown/index.mjs @@ -8,6 +8,10 @@ import json2md from 'json2md'; */ import { generateMarkdownPropsJson } from './props.mjs'; +function normalizeTrailingNewline( str ) { + return str?.length ? str.replace( /\n*$/, '\n' ) : undefined; +} + export function generateMarkdownDocs( { typeDocs, subcomponentTypeDocs } ) { const mainDocsJson = [ { h1: typeDocs.displayName }, @@ -15,7 +19,7 @@ export function generateMarkdownDocs( { typeDocs, subcomponentTypeDocs } ) { { p: `

See the WordPress Storybook for more detailed, interactive documentation.

`, }, - typeDocs.description, + normalizeTrailingNewline( typeDocs.description ), ...generateMarkdownPropsJson( typeDocs.props ), ]; @@ -26,7 +30,7 @@ export function generateMarkdownDocs( { typeDocs, subcomponentTypeDocs } ) { { h3: subcomponentTypeDoc.displayName, }, - subcomponentTypeDoc.description, + normalizeTrailingNewline( subcomponentTypeDoc.description ), ...generateMarkdownPropsJson( subcomponentTypeDoc.props, { headingLevel: 4, } ), diff --git a/packages/components/src/alignment-matrix-control/README.md b/packages/components/src/alignment-matrix-control/README.md index af97e3ae0607cd..267b368454c499 100644 --- a/packages/components/src/alignment-matrix-control/README.md +++ b/packages/components/src/alignment-matrix-control/README.md @@ -21,6 +21,7 @@ const Example = () => { ); }; ``` + ## Props ### `defaultValue` diff --git a/packages/components/src/angle-picker-control/README.md b/packages/components/src/angle-picker-control/README.md index d9389c6564338f..8b98ba813adde8 100644 --- a/packages/components/src/angle-picker-control/README.md +++ b/packages/components/src/angle-picker-control/README.md @@ -23,6 +23,7 @@ function Example() { ); } ``` + ## Props ### `as` diff --git a/packages/components/src/base-control/README.md b/packages/components/src/base-control/README.md index 839464b41260b5..9c8920fbc06896 100644 --- a/packages/components/src/base-control/README.md +++ b/packages/components/src/base-control/README.md @@ -25,6 +25,7 @@ const MyCustomTextareaControl = ({ children, ...baseProps }) => ( ); ); ``` + ## Props ### `__nextHasNoMarginBottom` @@ -113,6 +114,7 @@ const MyBaseControl = () => ( ); ``` + #### Props ##### `as` diff --git a/packages/components/src/box-control/README.md b/packages/components/src/box-control/README.md index da08cafceee42f..6192cfa47367ba 100644 --- a/packages/components/src/box-control/README.md +++ b/packages/components/src/box-control/README.md @@ -28,6 +28,7 @@ function Example() { ); }; ``` + ## Props ### `__next40pxDefaultSize` diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md index 99a6d0f9c24cfb..d63dee9007c2f0 100644 --- a/packages/components/src/button/README.md +++ b/packages/components/src/button/README.md @@ -17,6 +17,7 @@ const Mybutton = () => ( ); ``` + ## Props ### `__next40pxDefaultSize` diff --git a/packages/components/src/form-file-upload/README.md b/packages/components/src/form-file-upload/README.md index c6a7205815de53..d281b1ca639274 100644 --- a/packages/components/src/form-file-upload/README.md +++ b/packages/components/src/form-file-upload/README.md @@ -19,6 +19,7 @@ const MyFormFileUpload = () => ( ); ``` + ## Props ### `__next40pxDefaultSize` diff --git a/packages/components/src/gradient-picker/README.md b/packages/components/src/gradient-picker/README.md index ec0210d03c0a43..652fcbf30ae135 100644 --- a/packages/components/src/gradient-picker/README.md +++ b/packages/components/src/gradient-picker/README.md @@ -43,6 +43,7 @@ const MyGradientPicker = () => { ); }; ``` + ## Props ### `__experimentalIsRenderedInSidebar` diff --git a/packages/components/src/icon/README.md b/packages/components/src/icon/README.md index 63d52c1fd20b13..29e621f80c1fd9 100644 --- a/packages/components/src/icon/README.md +++ b/packages/components/src/icon/README.md @@ -11,6 +11,7 @@ import { wordpress } from '@wordpress/icons'; ``` + ## Props ### `icon` diff --git a/packages/components/src/tree-select/README.md b/packages/components/src/tree-select/README.md index 493c83bf993b0c..5f3247bdb3322b 100644 --- a/packages/components/src/tree-select/README.md +++ b/packages/components/src/tree-select/README.md @@ -51,6 +51,7 @@ const MyTreeSelect = () => { ); } ``` + ## Props ### `__next40pxDefaultSize` From c3b9f31fa2d7ca04edcdc66059f9e7afac95f4c2 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 21 Dec 2024 02:31:54 +0900 Subject: [PATCH 02/12] Tabs: Auto-generate README --- packages/components/src/tabs/README.md | 336 ++++++++---------- .../components/src/tabs/docs-manifest.json | 22 ++ packages/components/src/tabs/index.tsx | 25 +- .../src/tabs/stories/best-practices.mdx | 92 +++++ packages/components/src/tabs/tab.tsx | 6 + packages/components/src/tabs/tablist.tsx | 5 + packages/components/src/tabs/tabpanel.tsx | 3 + packages/components/src/tabs/types.ts | 6 +- 8 files changed, 301 insertions(+), 194 deletions(-) create mode 100644 packages/components/src/tabs/docs-manifest.json create mode 100644 packages/components/src/tabs/stories/best-practices.mdx diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md index 9c7e846046c904..7fdfc8dc42c56c 100644 --- a/packages/components/src/tabs/README.md +++ b/packages/components/src/tabs/README.md @@ -1,254 +1,216 @@ # Tabs -
-This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. -
- -Tabs is a collection of React components that combine to render an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). - -Tabs organizes content across different screens, data sets, and interactions. It has two sections: a list of tabs, and the view to show when tabs are chosen. - -## Development guidelines - -### Usage - -#### Uncontrolled Mode - -Tabs can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultTabId` prop can be used to set the initially selected tab. If this prop is not set, the first tab will be selected by default. In addition, in most cases where the currently active tab becomes disabled or otherwise unavailable, uncontrolled mode will automatically fall back to selecting the first available tab. - -```jsx -import { Tabs } from '@wordpress/components'; - -const onSelect = ( tabName ) => { - console.log( 'Selecting tab', tabName ); -}; - -const MyUncontrolledTabs = () => ( - - - - Tab 1 - - - Tab 2 - - - Tab 3 - - - -

Selected tab: Tab 1

-
- -

Selected tab: Tab 2

-
- -

Selected tab: Tab 3

-
-
- ); -``` - -#### Controlled Mode - -Tabs can also be used in a controlled mode, where the parent component specifies the `selectedTabId` and the `onSelect` props to control tab selection. In this mode, the `defaultTabId` prop will be ignored if it is provided. If the `selectedTabId` is `null`, no tab is selected. In this mode, if the currently selected tab becomes disabled or otherwise unavailable, the component will _not_ fall back to another available tab, leaving the controlling component in charge of implementing the desired logic. - -```jsx -import { Tabs } from '@wordpress/components'; - const [ selectedTabId, setSelectedTabId ] = useState< - string | undefined | null - >(); - -const onSelect = ( tabName ) => { - console.log( 'Selecting tab', tabName ); -}; - -const MyControlledTabs = () => ( - { - setSelectedTabId( selectedId ); - onSelect( selectedId ); - } } - > - - - Tab 1 - - - Tab 2 - - - Tab 3 - - - -

Selected tab: Tab 1

-
- -

Selected tab: Tab 2

-
- -

Selected tab: Tab 3

-
-
- ); -``` - -### Components and Sub-components - -Tabs is comprised of four individual components: -- `Tabs`: a wrapper component and context provider. It is responsible for managing the state of the tabs and rendering the `TabList` and `TabPanels`. -- `TabList`: a wrapper component for the `Tab` components. It is responsible for rendering the list of tabs. -- `Tab`: renders a single tab. The currently active tab receives default styling that can be overridden with CSS targeting [aria-selected="true"]. -- `TabPanel`: renders the content to display for a single tab once that tab is selected. - -#### Tabs - -##### Props - -###### `children`: `React.ReactNode` - -The children elements, which should include one instance of the `Tabs.Tablist` component and as many instances of the `Tabs.TabPanel` components as there are `Tabs.Tab` components. - -- Required: Yes - -###### `selectOnMove`: `boolean` - -Determines if the tab should be selected when it receives focus. If set to `false`, the tab will only be selected upon clicking, not when using arrow keys to shift focus (manual tab activation). See the [official W3C docs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) for more info. - -- Required: No -- Default: `true` - -###### `selectedTabId`: `string | null` + -The id of the tab whose panel is currently visible. +

See the WordPress Storybook for more detailed, interactive documentation.

-If left `undefined`, it will be automatically set to the first enabled tab, and the component assumes it is being used in "uncontrolled" mode. +Tabs is a collection of React components that combine to render +an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). -Consequently, any value different than `undefined` will set the component in "controlled" mode. When in "controlled" mode, the `null` value will result in no tabs being selected, and the tablist becoming tabbable. +Tabs organizes content across different screens, data sets, and interactions. +It has two sections: a list of tabs, and the view to show when tabs are chosen. -- Required: No +`Tabs` itself is a wrapper component and context provider. +It is responsible for managing the state of the tabs, and rendering the `TabList` and `TabPanels`. -###### `defaultTabId`: `string | null` +## Props -The id of the tab whose panel is currently visible. +### `activeTabId` -If left `undefined`, it will be automatically set to the first enabled tab. If set to `null`, no tab will be selected, and the tablist will be tabbable. +The current active tab `id`. The active tab is the tab element within the +tablist widget that has DOM focus. -_Note: this prop will be overridden by the `selectedTabId` prop if it is provided (meaning the component will be used in "controlled" mode)._ +- `null` represents the tablist (ie. the base composite element). Users + will be able to navigate out of it using arrow keys. +- If `activeTabId` is initially set to `null`, the base composite element + itself will have focus and users will be able to navigate to it using + arrow keys.activeTabId -- Required: No + - Type: `string` + - Required: No -###### `onSelect`: `( ( selectedId: string | null | undefined ) => void )` +### `children` -The function called when the `selectedTabId` changes. +The children elements, which should include one instance of the +`Tabs.Tablist` component and as many instances of the `Tabs.TabPanel` +components as there are `Tabs.Tab` components. -- Required: No -- Default: `noop` + - Type: `ReactNode` + - Required: Yes -###### `activeTabId`: `string | null` +### `defaultTabId` -The current active tab `id`. The active tab is the tab element within the tablist widget that has DOM focus. +The id of the tab whose panel is currently visible. -- `null` represents the tablist (ie. the base composite element). Users - will be able to navigate out of it using arrow keys; -- If `activeTabId` is initially set to `null`, the base composite element - itself will have focus and users will be able to navigate to it using - arrow keys. +If left `undefined`, it will be automatically set to the first enabled +tab. If set to `null`, no tab will be selected, and the tablist will be +tabbable. -- Required: No +Note: this prop will be overridden by the `selectedTabId` prop if it is +provided (meaning the component will be used in "controlled" mode). -###### `defaultActiveTabId`: `string | null` + - Type: `string` + - Required: No -The tab id that should be active by default when the composite widget is rendered. If `null`, the tablist element itself will have focus and users will be able to navigate to it using arrow keys. If `undefined`, the first enabled item will be focused. +### `defaultActiveTabId` -_Note: this prop will be overridden by the `activeTabId` prop if it is provided._ +The tab id that should be active by default when the composite widget is +rendered. If `null`, the tablist element itself will have focus +and users will be able to navigate to it using arrow keys. If `undefined`, +the first enabled item will be focused. -- Required: No +Note: this prop will be overridden by the `activeTabId` prop if it is +provided. -###### `onActiveTabIdChange`: `( ( activeId: string | null | undefined ) => void )` + - Type: `string` + - Required: No + +### `onSelect` The function called when the `selectedTabId` changes. -- Required: No -- Default: `noop` + - Type: `(selectedId: string) => void` + - Required: No + +### `onActiveTabIdChange` -###### `orientation`: `'horizontal' | 'vertical' | 'both'` +A callback that gets called when the `activeTabId` state changes. -Defines the orientation of the tablist and determines which arrow keys can be used to move focus: + - Type: `(activeId: string) => void` + - Required: No -- `both`: all arrow keys work; -- `horizontal`: only left and right arrow keys work; +### `orientation` + +Defines the orientation of the tablist and determines which arrow keys +can be used to move focus: + +- `both`: all arrow keys work. +- `horizontal`: only left and right arrow keys work. - `vertical`: only up and down arrow keys work. -- Required: No -- Default: `horizontal` + - Type: `"horizontal" | "vertical" | "both"` + - Required: No + - Default: `"horizontal"` + +### `selectOnMove` -#### TabList +Determines if the tab should be selected when it receives focus. If set to +`false`, the tab will only be selected upon clicking, not when using arrow +keys to shift focus (manual tab activation). See the [official W3C docs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) +for more info. -##### Props + - Type: `boolean` + - Required: No + - Default: `true` -###### `children`: `React.ReactNode` +### `selectedTabId` -The children elements, which should include one or more instances of the `Tabs.Tab` component. +The id of the tab whose panel is currently visible. + +If left `undefined`, it will be automatically set to the first enabled +tab, and the component assumes it is being used in "uncontrolled" mode. + +Consequently, any value different than `undefined` will set the component +in "controlled" mode. When in "controlled" mode, the `null` value will +result in no tabs being selected, and the tablist becoming tabbable. + + - Type: `string` + - Required: No + +## Subcomponents + +### Tabs.TabList + +A wrapper component for the `Tab` components. + +It is responsible for rendering the list of tabs. -- Required: No +#### Props -#### Tab +##### `children` -##### Props +The children elements, which should include one or more instances of the +`Tabs.Tab` component. -###### `tabId`: `string` + - Type: `ReactNode` + - Required: Yes -The unique ID of the tab. It will be used to register the tab and match it to a corresponding `Tabs.TabPanel` component. If not provided, a unique ID will be automatically generated. +### Tabs.Tab -- Required: Yes +Renders a single tab. -###### `children`: `React.ReactNode` +The currently active tab receives default styling that can be +overridden with CSS targeting `[aria-selected="true"]`. + +#### Props + +##### `children` The contents of the tab. -- Required: No + - Type: `ReactNode` + - Required: No -###### `disabled`: `boolean` +##### `disabled` -Determines if the tab should be disabled. Note that disabled tabs can still be accessed via the keyboard when navigating through the tablist. +Determines if the tab should be disabled. Note that disabled tabs can +still be accessed via the keyboard when navigating through the tablist. -- Required: No -- Default: `false` + - Type: `boolean` + - Required: No + - Default: `false` -###### `render`: `React.ReactNode` +##### `render` -Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. +Allows the component to be rendered as a different HTML element or React +component. The value can be a React element or a function that takes in the +original component props and gives back a React element with the props +merged. By default, the tab will be rendered as a `button` element. -- Required: No + - Type: `RenderProp & { ref?: Ref; }> | ReactElement>` + - Required: No -#### TabPanel +##### `tabId` -##### Props +The unique ID of the tab. It will be used to register the tab and match +it to a corresponding `Tabs.TabPanel` component. -###### `children`: `React.ReactNode` + - Type: `string` + - Required: Yes -The contents of the tab panel. +### Tabs.TabPanel -- Required: No +Renders the content to display for a single tab once that tab is selected. -###### `tabId`: `string` +#### Props -The unique `id` of the `Tabs.Tab` component controlling this panel. This connection is used to assign the `aria-labelledby` attribute to the tab panel and to determine if the tab panel should be visible. +##### `children` -If not provided, this link is automatically established by matching the order of `Tabs.Tab` and `Tabs.TabPanel` elements in the DOM. +The contents of the tab panel. -- Required: Yes + - Type: `ReactNode` + - Required: No -###### `focusable`: `boolean` +##### `focusable` Determines whether or not the tabpanel element should be focusable. +If `false`, pressing the tab key will skip over the tabpanel, and instead +focus on the first focusable element in the panel (if there is one). + + - Type: `boolean` + - Required: No + - Default: `true` + +##### `tabId` + +The unique `id` of the `Tabs.Tab` component controlling this panel. This +connection is used to assign the `aria-labelledby` attribute to the tab +panel and to determine if the tab panel should be visible. -If `false`, pressing the tab key will skip over the tabpanel, and instead focus on the first focusable element in the panel (if there is one). +If not provided, this link is automatically established by matching the +order of `Tabs.Tab` and `Tabs.TabPanel` elements in the DOM. -- Required: No -- Default: `true` + - Type: `string` + - Required: Yes diff --git a/packages/components/src/tabs/docs-manifest.json b/packages/components/src/tabs/docs-manifest.json new file mode 100644 index 00000000000000..fc24b177ef6163 --- /dev/null +++ b/packages/components/src/tabs/docs-manifest.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../schemas/docs-manifest.json", + "displayName": "Tabs", + "filePath": "./index.tsx", + "subcomponents": [ + { + "displayName": "TabList", + "preferredDisplayName": "Tabs.TabList", + "filePath": "./tablist.tsx" + }, + { + "displayName": "Tab", + "preferredDisplayName": "Tabs.Tab", + "filePath": "./tab.tsx" + }, + { + "displayName": "TabPanel", + "preferredDisplayName": "Tabs.TabPanel", + "filePath": "./tabpanel.tsx" + } + ] +} diff --git a/packages/components/src/tabs/index.tsx b/packages/components/src/tabs/index.tsx index 819d259395daf8..295614c5230a0d 100644 --- a/packages/components/src/tabs/index.tsx +++ b/packages/components/src/tabs/index.tsx @@ -36,11 +36,14 @@ function internalToExternalTabId( } /** - * Display one panel of content at a time with a tabbed interface, based on the - * WAI-ARIA Tabs Pattern⁠. + * Tabs is a collection of React components that combine to render + * an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). * - * @see https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ - * ``` + * Tabs organizes content across different screens, data sets, and interactions. + * It has two sections: a list of tabs, and the view to show when tabs are chosen. + * + * `Tabs` itself is a wrapper component and context provider. + * It is responsible for managing the state of the tabs, and rendering the `TabList` and `TabPanels`. */ export const Tabs = Object.assign( function Tabs( { @@ -121,12 +124,26 @@ export const Tabs = Object.assign( ); }, { + /** + * Renders a single tab. + * + * The currently active tab receives default styling that can be + * overridden with CSS targeting `[aria-selected="true"]`. + */ Tab: Object.assign( Tab, { displayName: 'Tabs.Tab', } ), + /** + * A wrapper component for the `Tab` components. + * + * It is responsible for rendering the list of tabs. + */ TabList: Object.assign( TabList, { displayName: 'Tabs.TabList', } ), + /** + * Renders the content to display for a single tab once that tab is selected. + */ TabPanel: Object.assign( TabPanel, { displayName: 'Tabs.TabPanel', } ), diff --git a/packages/components/src/tabs/stories/best-practices.mdx b/packages/components/src/tabs/stories/best-practices.mdx new file mode 100644 index 00000000000000..c0529766238e5f --- /dev/null +++ b/packages/components/src/tabs/stories/best-practices.mdx @@ -0,0 +1,92 @@ +import { Meta } from '@storybook/blocks'; + +import * as TabsStories from './index.story'; + + + +# Tabs + +## Usage + +### Uncontrolled Mode + +Tabs can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultTabId` prop can be used to set the initially selected tab. If this prop is not set, the first tab will be selected by default. In addition, in most cases where the currently active tab becomes disabled or otherwise unavailable, uncontrolled mode will automatically fall back to selecting the first available tab. + +```jsx +import { Tabs } from '@wordpress/components'; + +const onSelect = ( tabName ) => { + console.log( 'Selecting tab', tabName ); +}; + +const MyUncontrolledTabs = () => ( + + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + +

Selected tab: Tab 1

+
+ +

Selected tab: Tab 2

+
+ +

Selected tab: Tab 3

+
+
+ ); +``` + +### Controlled Mode + +Tabs can also be used in a controlled mode, where the parent component specifies the `selectedTabId` and the `onSelect` props to control tab selection. In this mode, the `defaultTabId` prop will be ignored if it is provided. If the `selectedTabId` is `null`, no tab is selected. In this mode, if the currently selected tab becomes disabled or otherwise unavailable, the component will _not_ fall back to another available tab, leaving the controlling component in charge of implementing the desired logic. + +```tsx +import { Tabs } from '@wordpress/components'; + const [ selectedTabId, setSelectedTabId ] = useState< + string | undefined | null + >(); + +const onSelect = ( tabName ) => { + console.log( 'Selecting tab', tabName ); +}; + +const MyControlledTabs = () => ( + { + setSelectedTabId( selectedId ); + onSelect( selectedId ); + } } + > + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + +

Selected tab: Tab 1

+
+ +

Selected tab: Tab 2

+
+ +

Selected tab: Tab 3

+
+
+ ); +``` \ No newline at end of file diff --git a/packages/components/src/tabs/tab.tsx b/packages/components/src/tabs/tab.tsx index 8226d0589f08c8..2e1ec828c9f63c 100644 --- a/packages/components/src/tabs/tab.tsx +++ b/packages/components/src/tabs/tab.tsx @@ -18,6 +18,12 @@ import { import type { WordPressComponentProps } from '../context'; import { chevronRight } from '@wordpress/icons'; +/** + * Renders a single tab. + * + * The currently active tab receives default styling that can be + * overridden with CSS targeting `[aria-selected="true"]`. + */ export const Tab = forwardRef< HTMLButtonElement, Omit< WordPressComponentProps< TabProps, 'button', false >, 'id' > diff --git a/packages/components/src/tabs/tablist.tsx b/packages/components/src/tabs/tablist.tsx index b7cfef7e19a096..1fe87213762588 100644 --- a/packages/components/src/tabs/tablist.tsx +++ b/packages/components/src/tabs/tablist.tsx @@ -67,6 +67,11 @@ function useScrollRectIntoView( }, [ margin, parent, rect ] ); } +/** + * A wrapper component for the `Tab` components. + * + * It is responsible for rendering the list of tabs. + */ export const TabList = forwardRef< HTMLDivElement, WordPressComponentProps< TabListProps, 'div', false > diff --git a/packages/components/src/tabs/tabpanel.tsx b/packages/components/src/tabs/tabpanel.tsx index 512b2609682722..6b9cbbf5df8d98 100644 --- a/packages/components/src/tabs/tabpanel.tsx +++ b/packages/components/src/tabs/tabpanel.tsx @@ -18,6 +18,9 @@ import warning from '@wordpress/warning'; import { useTabsContext } from './context'; import type { WordPressComponentProps } from '../context'; +/** + * Renders the content to display for a single tab once that tab is selected. + */ export const TabPanel = forwardRef< HTMLDivElement, Omit< WordPressComponentProps< TabPanelProps, 'div', false >, 'id' > diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts index 959a82509a05d6..7f8a55e28bf9b8 100644 --- a/packages/components/src/tabs/types.ts +++ b/packages/components/src/tabs/types.ts @@ -26,12 +26,10 @@ export type TabsProps = { /** * Determines if the tab should be selected when it receives focus. If set to * `false`, the tab will only be selected upon clicking, not when using arrow - * keys to shift focus (manual tab activation). See the official W3C docs + * keys to shift focus (manual tab activation). See the [official W3C docs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) * for more info. * * @default true - * - * @see https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/ */ selectOnMove?: Ariakit.TabStoreProps[ 'selectOnMove' ]; /** @@ -63,6 +61,7 @@ export type TabsProps = { /** * The current active tab `id`. The active tab is the tab element within the * tablist widget that has DOM focus. + * * - `null` represents the tablist (ie. the base composite element). Users * will be able to navigate out of it using arrow keys. * - If `activeTabId` is initially set to `null`, the base composite element @@ -87,6 +86,7 @@ export type TabsProps = { /** * Defines the orientation of the tablist and determines which arrow keys * can be used to move focus: + * * - `both`: all arrow keys work. * - `horizontal`: only left and right arrow keys work. * - `vertical`: only up and down arrow keys work. From bb01937489fcdd0a65d9eabbcba8338d2acde2e4 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 21 Dec 2024 03:10:16 +0900 Subject: [PATCH 03/12] Remove redundant JSDocs --- packages/components/src/tabs/tab.tsx | 6 ------ packages/components/src/tabs/tablist.tsx | 5 ----- packages/components/src/tabs/tabpanel.tsx | 3 --- 3 files changed, 14 deletions(-) diff --git a/packages/components/src/tabs/tab.tsx b/packages/components/src/tabs/tab.tsx index 2e1ec828c9f63c..8226d0589f08c8 100644 --- a/packages/components/src/tabs/tab.tsx +++ b/packages/components/src/tabs/tab.tsx @@ -18,12 +18,6 @@ import { import type { WordPressComponentProps } from '../context'; import { chevronRight } from '@wordpress/icons'; -/** - * Renders a single tab. - * - * The currently active tab receives default styling that can be - * overridden with CSS targeting `[aria-selected="true"]`. - */ export const Tab = forwardRef< HTMLButtonElement, Omit< WordPressComponentProps< TabProps, 'button', false >, 'id' > diff --git a/packages/components/src/tabs/tablist.tsx b/packages/components/src/tabs/tablist.tsx index 1fe87213762588..b7cfef7e19a096 100644 --- a/packages/components/src/tabs/tablist.tsx +++ b/packages/components/src/tabs/tablist.tsx @@ -67,11 +67,6 @@ function useScrollRectIntoView( }, [ margin, parent, rect ] ); } -/** - * A wrapper component for the `Tab` components. - * - * It is responsible for rendering the list of tabs. - */ export const TabList = forwardRef< HTMLDivElement, WordPressComponentProps< TabListProps, 'div', false > diff --git a/packages/components/src/tabs/tabpanel.tsx b/packages/components/src/tabs/tabpanel.tsx index 6b9cbbf5df8d98..512b2609682722 100644 --- a/packages/components/src/tabs/tabpanel.tsx +++ b/packages/components/src/tabs/tabpanel.tsx @@ -18,9 +18,6 @@ import warning from '@wordpress/warning'; import { useTabsContext } from './context'; import type { WordPressComponentProps } from '../context'; -/** - * Renders the content to display for a single tab once that tab is selected. - */ export const TabPanel = forwardRef< HTMLDivElement, Omit< WordPressComponentProps< TabPanelProps, 'div', false >, 'id' > From 4ddf77124f08199b74557ec7ac8781ef6000bb0b Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 20 Dec 2024 22:01:15 +0100 Subject: [PATCH 04/12] Add "Using `Tabs` with links" section --- packages/components/src/tabs/stories/best-practices.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/components/src/tabs/stories/best-practices.mdx b/packages/components/src/tabs/stories/best-practices.mdx index c0529766238e5f..533995660b6ec2 100644 --- a/packages/components/src/tabs/stories/best-practices.mdx +++ b/packages/components/src/tabs/stories/best-practices.mdx @@ -89,4 +89,10 @@ const MyControlledTabs = () => ( ); -``` \ No newline at end of file +``` + +### Using `Tabs` with links + +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.Tablist` is a [composite](https://w3c.github.io/aria/#composite) widget acting as a single tab stop. + +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. From 0a3011166aa8e4a1bfb413fc0161402a7e8e180e Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 24 Dec 2024 03:39:18 +0900 Subject: [PATCH 05/12] Tweak wording Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> --- packages/components/src/tabs/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/tabs/index.tsx b/packages/components/src/tabs/index.tsx index 295614c5230a0d..8688c2ccaa24a1 100644 --- a/packages/components/src/tabs/index.tsx +++ b/packages/components/src/tabs/index.tsx @@ -40,7 +40,7 @@ function internalToExternalTabId( * an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). * * Tabs organizes content across different screens, data sets, and interactions. - * It has two sections: a list of tabs, and the view to show when tabs are chosen. + * It has two sections: a list of tabs, and the view to show when a tab is chosen. * * `Tabs` itself is a wrapper component and context provider. * It is responsible for managing the state of the tabs, and rendering the `TabList` and `TabPanels`. From 5b579c7908560453888c6cc11a26ba31148bea11 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 24 Dec 2024 03:39:33 +0900 Subject: [PATCH 06/12] Tweak wording Co-authored-by: Marco Ciampini --- packages/components/src/tabs/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/tabs/index.tsx b/packages/components/src/tabs/index.tsx index 8688c2ccaa24a1..2cbe487976c59e 100644 --- a/packages/components/src/tabs/index.tsx +++ b/packages/components/src/tabs/index.tsx @@ -43,7 +43,7 @@ function internalToExternalTabId( * It has two sections: a list of tabs, and the view to show when a tab is chosen. * * `Tabs` itself is a wrapper component and context provider. - * It is responsible for managing the state of the tabs, and rendering the `TabList` and `TabPanels`. + * It is responsible for managing the state of the tabs, and rendering one instance of the `Tabs.TabList` component and one or more instances of the `Tab.TabPanel` component. */ export const Tabs = Object.assign( function Tabs( { From 817b6084fcde26e75bfcc8ab0ac058644f45d5fe Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 24 Dec 2024 03:48:11 +0900 Subject: [PATCH 07/12] Fix typo --- packages/components/src/tabs/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts index 7f8a55e28bf9b8..547efaa380dc2f 100644 --- a/packages/components/src/tabs/types.ts +++ b/packages/components/src/tabs/types.ts @@ -66,7 +66,7 @@ export type TabsProps = { * will be able to navigate out of it using arrow keys. * - If `activeTabId` is initially set to `null`, the base composite element * itself will have focus and users will be able to navigate to it using - * arrow keys.activeTabId + * arrow keys. */ activeTabId?: Ariakit.TabStoreProps[ 'activeId' ]; /** From 19940d11f84c683197111ace9d462c041b4e7668 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 24 Dec 2024 04:00:05 +0900 Subject: [PATCH 08/12] Regenerate readme --- packages/components/src/tabs/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md index 7fdfc8dc42c56c..e9ed5fd9819cda 100644 --- a/packages/components/src/tabs/README.md +++ b/packages/components/src/tabs/README.md @@ -8,10 +8,10 @@ Tabs is a collection of React components that combine to render an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). Tabs organizes content across different screens, data sets, and interactions. -It has two sections: a list of tabs, and the view to show when tabs are chosen. +It has two sections: a list of tabs, and the view to show when a tab is chosen. `Tabs` itself is a wrapper component and context provider. -It is responsible for managing the state of the tabs, and rendering the `TabList` and `TabPanels`. +It is responsible for managing the state of the tabs, and rendering one instance of the `Tabs.TabList` component and one or more instances of the `Tab.TabPanel` component. ## Props @@ -24,7 +24,7 @@ tablist widget that has DOM focus. will be able to navigate out of it using arrow keys. - If `activeTabId` is initially set to `null`, the base composite element itself will have focus and users will be able to navigate to it using - arrow keys.activeTabId + arrow keys. - Type: `string` - Required: No From 20fc6d192babfd8e8dddb60cffe6d5fd030ccdb3 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Fri, 27 Dec 2024 02:24:36 +0900 Subject: [PATCH 09/12] Fix lists --- packages/components/src/tabs/README.md | 98 +++++++++++++------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md index e9ed5fd9819cda..c8e5348f8a0edc 100644 --- a/packages/components/src/tabs/README.md +++ b/packages/components/src/tabs/README.md @@ -17,6 +17,9 @@ It is responsible for managing the state of the tabs, and rendering one instance ### `activeTabId` + - Type: `string` + - Required: No + The current active tab `id`. The active tab is the tab element within the tablist widget that has DOM focus. @@ -26,20 +29,20 @@ tablist widget that has DOM focus. itself will have focus and users will be able to navigate to it using arrow keys. - - Type: `string` - - Required: No - ### `children` + - Type: `ReactNode` + - Required: Yes + The children elements, which should include one instance of the `Tabs.Tablist` component and as many instances of the `Tabs.TabPanel` components as there are `Tabs.Tab` components. - - Type: `ReactNode` - - Required: Yes - ### `defaultTabId` + - Type: `string` + - Required: No + The id of the tab whose panel is currently visible. If left `undefined`, it will be automatically set to the first enabled @@ -49,11 +52,11 @@ tabbable. Note: this prop will be overridden by the `selectedTabId` prop if it is provided (meaning the component will be used in "controlled" mode). +### `defaultActiveTabId` + - Type: `string` - Required: No -### `defaultActiveTabId` - The tab id that should be active by default when the composite widget is rendered. If `null`, the tablist element itself will have focus and users will be able to navigate to it using arrow keys. If `undefined`, @@ -62,25 +65,26 @@ the first enabled item will be focused. Note: this prop will be overridden by the `activeTabId` prop if it is provided. - - Type: `string` - - Required: No - ### `onSelect` -The function called when the `selectedTabId` changes. - - Type: `(selectedId: string) => void` - Required: No -### `onActiveTabIdChange` +The function called when the `selectedTabId` changes. -A callback that gets called when the `activeTabId` state changes. +### `onActiveTabIdChange` - Type: `(activeId: string) => void` - Required: No +A callback that gets called when the `activeTabId` state changes. + ### `orientation` + - Type: `"horizontal" | "vertical" | "both"` + - Required: No + - Default: `"horizontal"` + Defines the orientation of the tablist and determines which arrow keys can be used to move focus: @@ -88,23 +92,22 @@ can be used to move focus: - `horizontal`: only left and right arrow keys work. - `vertical`: only up and down arrow keys work. - - Type: `"horizontal" | "vertical" | "both"` - - Required: No - - Default: `"horizontal"` - ### `selectOnMove` + - Type: `boolean` + - Required: No + - Default: `true` + Determines if the tab should be selected when it receives focus. If set to `false`, the tab will only be selected upon clicking, not when using arrow keys to shift focus (manual tab activation). See the [official W3C docs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) for more info. - - Type: `boolean` - - Required: No - - Default: `true` - ### `selectedTabId` + - Type: `string` + - Required: No + The id of the tab whose panel is currently visible. If left `undefined`, it will be automatically set to the first enabled @@ -114,9 +117,6 @@ Consequently, any value different than `undefined` will set the component in "controlled" mode. When in "controlled" mode, the `null` value will result in no tabs being selected, and the tablist becoming tabbable. - - Type: `string` - - Required: No - ## Subcomponents ### Tabs.TabList @@ -129,12 +129,12 @@ It is responsible for rendering the list of tabs. ##### `children` -The children elements, which should include one or more instances of the -`Tabs.Tab` component. - - Type: `ReactNode` - Required: Yes +The children elements, which should include one or more instances of the +`Tabs.Tab` component. + ### Tabs.Tab Renders a single tab. @@ -146,22 +146,25 @@ overridden with CSS targeting `[aria-selected="true"]`. ##### `children` -The contents of the tab. - - Type: `ReactNode` - Required: No -##### `disabled` +The contents of the tab. -Determines if the tab should be disabled. Note that disabled tabs can -still be accessed via the keyboard when navigating through the tablist. +##### `disabled` - Type: `boolean` - Required: No - Default: `false` +Determines if the tab should be disabled. Note that disabled tabs can +still be accessed via the keyboard when navigating through the tablist. + ##### `render` + - Type: `RenderProp & { ref?: Ref; }> | ReactElement>` + - Required: No + Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props @@ -169,17 +172,14 @@ merged. By default, the tab will be rendered as a `button` element. - - Type: `RenderProp & { ref?: Ref; }> | ReactElement>` - - Required: No - ##### `tabId` -The unique ID of the tab. It will be used to register the tab and match -it to a corresponding `Tabs.TabPanel` component. - - Type: `string` - Required: Yes +The unique ID of the tab. It will be used to register the tab and match +it to a corresponding `Tabs.TabPanel` component. + ### Tabs.TabPanel Renders the content to display for a single tab once that tab is selected. @@ -188,29 +188,29 @@ Renders the content to display for a single tab once that tab is selected. ##### `children` -The contents of the tab panel. - - Type: `ReactNode` - Required: No -##### `focusable` +The contents of the tab panel. -Determines whether or not the tabpanel element should be focusable. -If `false`, pressing the tab key will skip over the tabpanel, and instead -focus on the first focusable element in the panel (if there is one). +##### `focusable` - Type: `boolean` - Required: No - Default: `true` +Determines whether or not the tabpanel element should be focusable. +If `false`, pressing the tab key will skip over the tabpanel, and instead +focus on the first focusable element in the panel (if there is one). + ##### `tabId` + - Type: `string` + - Required: Yes + The unique `id` of the `Tabs.Tab` component controlling this panel. This connection is used to assign the `aria-labelledby` attribute to the tab panel and to determine if the tab panel should be visible. If not provided, this link is automatically established by matching the order of `Tabs.Tab` and `Tabs.TabPanel` elements in the DOM. - - - Type: `string` - - Required: Yes From e7fa2d1cb13b46e96734b8ec11a7091bf8d15d13 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 28 Dec 2024 02:55:13 +0900 Subject: [PATCH 10/12] Convert code snippets back to tab indentation --- .../src/tabs/stories/best-practices.mdx | 115 +++++++++--------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/packages/components/src/tabs/stories/best-practices.mdx b/packages/components/src/tabs/stories/best-practices.mdx index 533995660b6ec2..a8bb9cf20a5f0e 100644 --- a/packages/components/src/tabs/stories/best-practices.mdx +++ b/packages/components/src/tabs/stories/best-practices.mdx @@ -16,33 +16,33 @@ Tabs can be used in an uncontrolled mode, where the component manages its own st import { Tabs } from '@wordpress/components'; const onSelect = ( tabName ) => { - console.log( 'Selecting tab', tabName ); + console.log( 'Selecting tab', tabName ); }; const MyUncontrolledTabs = () => ( - - - - Tab 1 - - - Tab 2 - - - Tab 3 - - - -

Selected tab: Tab 1

-
- -

Selected tab: Tab 2

-
- -

Selected tab: Tab 3

-
-
- ); + + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + +

Selected tab: Tab 1

+
+ +

Selected tab: Tab 2

+
+ +

Selected tab: Tab 3

+
+
+); ``` ### Controlled Mode @@ -51,44 +51,45 @@ Tabs can also be used in a controlled mode, where the parent component specifies ```tsx import { Tabs } from '@wordpress/components'; - const [ selectedTabId, setSelectedTabId ] = useState< - string | undefined | null - >(); + +const [ selectedTabId, setSelectedTabId ] = useState< + string | undefined | null +>(); const onSelect = ( tabName ) => { - console.log( 'Selecting tab', tabName ); + console.log( 'Selecting tab', tabName ); }; const MyControlledTabs = () => ( - { - setSelectedTabId( selectedId ); - onSelect( selectedId ); - } } - > - - - Tab 1 - - - Tab 2 - - - Tab 3 - - - -

Selected tab: Tab 1

-
- -

Selected tab: Tab 2

-
- -

Selected tab: Tab 3

-
-
- ); + { + setSelectedTabId( selectedId ); + onSelect( selectedId ); + } } + > + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + +

Selected tab: Tab 1

+
+ +

Selected tab: Tab 2

+
+ +

Selected tab: Tab 3

+
+
+); ``` ### Using `Tabs` with links From 94bbdfbc2acea811c9cbc818f381a341b2d10b68 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 28 Dec 2024 02:55:57 +0900 Subject: [PATCH 11/12] Update readme --- packages/components/src/tabs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md index c8e5348f8a0edc..7f5f3219adfd1e 100644 --- a/packages/components/src/tabs/README.md +++ b/packages/components/src/tabs/README.md @@ -2,6 +2,8 @@ +🔒 This component is locked as a [private API](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-private-apis/). We do not yet recommend using this outside of the Gutenberg project. +

See the WordPress Storybook for more detailed, interactive documentation.

Tabs is a collection of React components that combine to render From 31a04e102fa3c59eb5264a3177cfb841a80cd1b6 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 28 Dec 2024 02:56:07 +0900 Subject: [PATCH 12/12] Remove todo item --- packages/components/src/tabs/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts index 0e2645a05aab49..7ef0f919322c04 100644 --- a/packages/components/src/tabs/types.ts +++ b/packages/components/src/tabs/types.ts @@ -105,7 +105,6 @@ export type TabListProps = { }; // TODO: consider prop name changes (tabId, selectedTabId) -// switch to auto-generated README // compound technique export type TabProps = {