@@ -222,7 +231,6 @@ const MediaReplaceFlow = ( {
showSuggestions={ false }
onChange={ ( { url } ) => {
onSelectURL( url );
- editMediaButtonRef.current.focus();
} }
/>
diff --git a/packages/block-library/src/post-comments-form/block.json b/packages/block-library/src/post-comments-form/block.json
index af893ccb67a082..4b6b333b75cfab 100644
--- a/packages/block-library/src/post-comments-form/block.json
+++ b/packages/block-library/src/post-comments-form/block.json
@@ -56,5 +56,10 @@
"wp-block-post-comments-form",
"wp-block-buttons",
"wp-block-button"
- ]
+ ],
+ "example": {
+ "attributes": {
+ "textAlign": "center"
+ }
+ }
}
diff --git a/packages/block-library/src/post-comments-link/block.json b/packages/block-library/src/post-comments-link/block.json
index 67831b1d15c5d5..8e23bc7a695070 100644
--- a/packages/block-library/src/post-comments-link/block.json
+++ b/packages/block-library/src/post-comments-link/block.json
@@ -42,6 +42,13 @@
},
"interactivity": {
"clientNavigation": true
+ },
+ "__experimentalBorder": {
+ "radius": true,
+ "color": true,
+ "width": true,
+ "style": true
}
- }
+ },
+ "style": "wp-block-post-comments-link"
}
diff --git a/packages/block-library/src/post-comments-link/style.scss b/packages/block-library/src/post-comments-link/style.scss
new file mode 100644
index 00000000000000..110179d3ee1df9
--- /dev/null
+++ b/packages/block-library/src/post-comments-link/style.scss
@@ -0,0 +1,4 @@
+.wp-block-post-comments-link {
+ // This block has customizable padding, border-box makes that more predictable.
+ box-sizing: border-box;
+}
diff --git a/packages/block-library/src/post-navigation-link/block.json b/packages/block-library/src/post-navigation-link/block.json
index 5f1b295119822a..ce733759846fee 100644
--- a/packages/block-library/src/post-navigation-link/block.json
+++ b/packages/block-library/src/post-navigation-link/block.json
@@ -34,12 +34,6 @@
"default": ""
}
},
- "example": {
- "attributes": {
- "label": "Next post",
- "arrow": "arrow"
- }
- },
"usesContext": [ "postType" ],
"supports": {
"reusable": false,
diff --git a/packages/block-library/src/post-navigation-link/index.js b/packages/block-library/src/post-navigation-link/index.js
index e85e594990adba..4bcb1999067053 100644
--- a/packages/block-library/src/post-navigation-link/index.js
+++ b/packages/block-library/src/post-navigation-link/index.js
@@ -1,3 +1,8 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
/**
* Internal dependencies
*/
@@ -12,6 +17,12 @@ export { metadata, name };
export const settings = {
edit,
variations,
+ example: {
+ attributes: {
+ label: __( 'Next post' ),
+ arrow: 'arrow',
+ },
+ },
};
export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/post-navigation-link/variations.js b/packages/block-library/src/post-navigation-link/variations.js
index 4f52b21338af1e..e49be1542685e7 100644
--- a/packages/block-library/src/post-navigation-link/variations.js
+++ b/packages/block-library/src/post-navigation-link/variations.js
@@ -17,7 +17,7 @@ const variations = [
scope: [ 'inserter', 'transform' ],
example: {
attributes: {
- label: 'Next post',
+ label: __( 'Next post' ),
arrow: 'arrow',
},
},
@@ -33,7 +33,7 @@ const variations = [
scope: [ 'inserter', 'transform' ],
example: {
attributes: {
- label: 'Previous post',
+ label: __( 'Previous post' ),
arrow: 'arrow',
},
},
diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json
index 57664b271bca46..d379a46d3142f8 100644
--- a/packages/block-library/src/post-template/block.json
+++ b/packages/block-library/src/post-template/block.json
@@ -50,8 +50,8 @@
},
"__experimentalDefaultControls": {
"blockGap": true,
- "padding" : false,
- "margin" : false
+ "padding": false,
+ "margin": false
}
},
"interactivity": {
diff --git a/packages/block-library/src/query-no-results/block.json b/packages/block-library/src/query-no-results/block.json
index c7d3ff500e0f43..c2b7224aa40b14 100644
--- a/packages/block-library/src/query-no-results/block.json
+++ b/packages/block-library/src/query-no-results/block.json
@@ -8,16 +8,6 @@
"ancestor": [ "core/query" ],
"textdomain": "default",
"usesContext": [ "queryId", "query" ],
- "example": {
- "innerBlocks": [
- {
- "name": "core/paragraph",
- "attributes": {
- "content": "No posts were found."
- }
- }
- ]
- },
"supports": {
"align": true,
"reusable": false,
diff --git a/packages/block-library/src/query-no-results/index.js b/packages/block-library/src/query-no-results/index.js
index 1c56638cdfdba8..fab5993148470e 100644
--- a/packages/block-library/src/query-no-results/index.js
+++ b/packages/block-library/src/query-no-results/index.js
@@ -1,6 +1,7 @@
/**
* WordPress dependencies
*/
+import { __ } from '@wordpress/i18n';
import { loop as icon } from '@wordpress/icons';
/**
@@ -18,6 +19,16 @@ export const settings = {
icon,
edit,
save,
+ example: {
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ attributes: {
+ content: __( 'No posts were found.' ),
+ },
+ },
+ ],
+ },
};
export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss
index 663626caaac87e..c61049c23151b9 100644
--- a/packages/block-library/src/style.scss
+++ b/packages/block-library/src/style.scss
@@ -37,6 +37,7 @@
@import "./post-author-biography/style.scss";
@import "./post-comments-form/style.scss";
@import "./post-content/style.scss";
+@import "./post-comments-link/style.scss";
@import "./post-date/style.scss";
@import "./post-excerpt/style.scss";
@import "./post-featured-image/style.scss";
diff --git a/packages/block-library/src/table-of-contents/block.json b/packages/block-library/src/table-of-contents/block.json
index 5eb6e729d3f03e..68266166080bbd 100644
--- a/packages/block-library/src/table-of-contents/block.json
+++ b/packages/block-library/src/table-of-contents/block.json
@@ -62,57 +62,5 @@
}
}
},
- "example": {
- "innerBlocks": [
- {
- "name": "core/heading",
- "attributes": {
- "level": 2,
- "content": "Heading"
- }
- },
- {
- "name": "core/heading",
- "attributes": {
- "level": 3,
- "content": "Subheading"
- }
- },
- {
- "name": "core/heading",
- "attributes": {
- "level": 2,
- "content": "Heading"
- }
- },
- {
- "name": "core/heading",
- "attributes": {
- "level": 3,
- "content": "Subheading"
- }
- }
- ],
- "attributes": {
- "headings": [
- {
- "content": "Heading",
- "level": 2
- },
- {
- "content": "Subheading",
- "level": 3
- },
- {
- "content": "Heading",
- "level": 2
- },
- {
- "content": "Subheading",
- "level": 3
- }
- ]
- }
- },
"style": "wp-block-table-of-contents"
}
diff --git a/packages/block-library/src/table-of-contents/index.js b/packages/block-library/src/table-of-contents/index.js
index 408538a7dcadbd..ff1b658966f19f 100644
--- a/packages/block-library/src/table-of-contents/index.js
+++ b/packages/block-library/src/table-of-contents/index.js
@@ -1,6 +1,7 @@
/**
* WordPress dependencies
*/
+import { __ } from '@wordpress/i18n';
import { tableOfContents as icon } from '@wordpress/icons';
/**
@@ -19,6 +20,58 @@ export const settings = {
icon,
edit,
save,
+ example: {
+ innerBlocks: [
+ {
+ name: 'core/heading',
+ attributes: {
+ level: 2,
+ content: __( 'Heading' ),
+ },
+ },
+ {
+ name: 'core/heading',
+ attributes: {
+ level: 3,
+ content: __( 'Subheading' ),
+ },
+ },
+ {
+ name: 'core/heading',
+ attributes: {
+ level: 2,
+ content: __( 'Heading' ),
+ },
+ },
+ {
+ name: 'core/heading',
+ attributes: {
+ level: 3,
+ content: __( 'Subheading' ),
+ },
+ },
+ ],
+ attributes: {
+ headings: [
+ {
+ content: __( 'Heading' ),
+ level: 2,
+ },
+ {
+ content: __( 'Subheading' ),
+ level: 3,
+ },
+ {
+ content: __( 'Heading' ),
+ level: 2,
+ },
+ {
+ content: __( 'Subheading' ),
+ level: 3,
+ },
+ ],
+ },
+ },
};
export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md
index 9c7e846046c904..7f5f3219adfd1e 100644
--- a/packages/components/src/tabs/README.md
+++ b/packages/components/src/tabs/README.md
@@ -1,254 +1,218 @@
# 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.
+🔒 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.
-If left `undefined`, it will be automatically set to the first enabled tab, and the component assumes it is being used in "uncontrolled" mode.
+See the WordPress Storybook for more detailed, interactive documentation.
-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 is a collection of React components that combine to render
+an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
-- Required: No
+Tabs organizes content across different screens, data sets, and interactions.
+It has two sections: a list of tabs, and the view to show when a tab is chosen.
-###### `defaultTabId`: `string | null`
+`Tabs` itself is a wrapper component and context provider.
+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.
-The id of the tab whose panel is currently visible.
+## Props
-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.
+### `activeTabId`
-_Note: this prop will be overridden by the `selectedTabId` prop if it is provided (meaning the component will be used in "controlled" mode)._
+ - Type: `string`
+ - Required: No
-- Required: No
+The current active tab `id`. The active tab is the tab element within the
+tablist widget that has DOM focus.
-###### `onSelect`: `( ( selectedId: string | null | undefined ) => void )`
+- `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.
-The function called when the `selectedTabId` changes.
+### `children`
-- Required: No
-- Default: `noop`
+ - Type: `ReactNode`
+ - Required: Yes
-###### `activeTabId`: `string | null`
+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.
-The current active tab `id`. The active tab is the tab element within the tablist widget that has DOM focus.
+### `defaultTabId`
-- `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.
+ - Type: `string`
+ - Required: No
+
+The id of the tab whose panel is currently visible.
-- Required: No
+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.
-###### `defaultActiveTabId`: `string | null`
+Note: this prop will be overridden by the `selectedTabId` prop if it is
+provided (meaning the component will be used in "controlled" mode).
-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._
+ - Type: `string`
+ - Required: No
-- 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.
-###### `onActiveTabIdChange`: `( ( activeId: string | null | undefined ) => void )`
+Note: this prop will be overridden by the `activeTabId` prop if it is
+provided.
+
+### `onSelect`
+
+ - Type: `(selectedId: string) => void`
+ - Required: No
The function called when the `selectedTabId` changes.
-- Required: No
-- Default: `noop`
+### `onActiveTabIdChange`
+
+ - Type: `(activeId: string) => void`
+ - Required: No
+
+A callback that gets called when the `activeTabId` state changes.
-###### `orientation`: `'horizontal' | 'vertical' | 'both'`
+### `orientation`
-Defines the orientation of the tablist and determines which arrow keys can be used to move focus:
+ - Type: `"horizontal" | "vertical" | "both"`
+ - Required: No
+ - Default: `"horizontal"`
-- `both`: all arrow keys work;
-- `horizontal`: only left and right arrow keys work;
+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`
+### `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.
+
+### `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
+tab, and the component assumes it is being used in "uncontrolled" mode.
-#### TabList
+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.
-##### Props
+## Subcomponents
-###### `children`: `React.ReactNode`
+### Tabs.TabList
-The children elements, which should include one or more instances of the `Tabs.Tab` component.
+A wrapper component for the `Tab` components.
-- Required: No
+It is responsible for rendering the list of tabs.
-#### Tab
+#### Props
-##### Props
+##### `children`
-###### `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.
+The children elements, which should include one or more instances of the
+`Tabs.Tab` component.
-- Required: Yes
+### Tabs.Tab
-###### `children`: `React.ReactNode`
+Renders a single tab.
+
+The currently active tab receives default styling that can be
+overridden with CSS targeting `[aria-selected="true"]`.
+
+#### Props
+
+##### `children`
+
+ - Type: `ReactNode`
+ - Required: No
The contents of the tab.
-- Required: No
+##### `disabled`
-###### `disabled`: `boolean`
+ - 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.
+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`
+##### `render`
-###### `render`: `React.ReactNode`
+ - 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 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
+##### `tabId`
-#### TabPanel
+ - Type: `string`
+ - Required: Yes
-##### 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`
+### Tabs.TabPanel
-The contents of the tab panel.
+Renders the content to display for a single tab once that tab is selected.
-- Required: No
+#### Props
-###### `tabId`: `string`
+##### `children`
-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.
+ - Type: `ReactNode`
+ - Required: No
-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
+##### `focusable`
-###### `focusable`: `boolean`
+ - 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
-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).
+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.
-- Required: No
-- Default: `true`
+If not provided, this link is automatically established by matching the
+order of `Tabs.Tab` and `Tabs.TabPanel` elements in the DOM.
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..2cbe487976c59e 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 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 one instance of the `Tabs.TabList` component and one or more instances of the `Tab.TabPanel` component.
*/
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..a8bb9cf20a5f0e
--- /dev/null
+++ b/packages/components/src/tabs/stories/best-practices.mdx
@@ -0,0 +1,99 @@
+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
+
+
+);
+```
+
+### 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.
diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts
index 3e52f11481e85f..7ef0f919322c04 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.TabProviderProps[ 'selectOnMove' ];
/**
@@ -63,11 +61,12 @@ 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
* itself will have focus and users will be able to navigate to it using
- * arrow keys.activeTabId
+ * arrow keys.
*/
activeTabId?: Ariakit.TabProviderProps[ 'activeId' ];
/**
@@ -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.
@@ -105,7 +105,6 @@ export type TabListProps = {
};
// TODO: consider prop name changes (tabId, selectedTabId)
-// switch to auto-generated README
// compound technique
export type TabProps = {
diff --git a/packages/edit-site/src/components/canvas-loader/style.scss b/packages/edit-site/src/components/canvas-loader/style.scss
index 3d74d408aeceda..33ff6dc38c3f51 100644
--- a/packages/edit-site/src/components/canvas-loader/style.scss
+++ b/packages/edit-site/src/components/canvas-loader/style.scss
@@ -9,9 +9,10 @@
align-items: center;
justify-content: center;
- animation: 0.5s ease 0.2s edit-site-canvas-loader__fade-in-animation;
- animation-fill-mode: forwards;
- @include reduce-motion("animation");
+ @media not (prefers-reduced-motion) {
+ animation: 0.5s ease 0.2s edit-site-canvas-loader__fade-in-animation;
+ animation-fill-mode: forwards;
+ }
& > div {
width: 160px;
diff --git a/packages/edit-site/src/components/editor-canvas-container/style.scss b/packages/edit-site/src/components/editor-canvas-container/style.scss
index 07d666fb293c59..c544f88f6bcd58 100644
--- a/packages/edit-site/src/components/editor-canvas-container/style.scss
+++ b/packages/edit-site/src/components/editor-canvas-container/style.scss
@@ -26,7 +26,9 @@
position: absolute;
right: 0;
top: 0;
- transition: all 0.3s; // Match .block-editor-iframe__body transition.
+ @media not (prefers-reduced-motion) {
+ transition: all 0.3s; // Match .block-editor-iframe__body transition.
+ }
}
.edit-site-editor-canvas-container__close-button {
diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss
index a6cc5084966947..625b2633ab7244 100644
--- a/packages/edit-site/src/components/editor/style.scss
+++ b/packages/edit-site/src/components/editor/style.scss
@@ -1,7 +1,9 @@
.edit-site-editor__editor-interface {
opacity: 1;
- transition: opacity 0.1s ease-out;
- @include reduce-motion( "transition" );
+
+ @media not (prefers-reduced-motion) {
+ transition: opacity 0.1s ease-out;
+ }
&.is-loading {
opacity: 0;
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss
index 7568fea2b6f805..11a1c6d6689370 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss
@@ -129,8 +129,10 @@ $footer-height: 70px;
.font-library-modal__font-variant_demo-text {
white-space: nowrap;
flex-shrink: 0;
- transition: opacity 0.3s ease-in-out;
- @include reduce-motion( "transition" );
+
+ @media not (prefers-reduced-motion) {
+ transition: opacity 0.3s ease-in-out;
+ }
}
}
diff --git a/packages/edit-site/src/components/global-styles/variations/style.scss b/packages/edit-site/src/components/global-styles/variations/style.scss
index 5f57c72f180b12..b092e09e487508 100644
--- a/packages/edit-site/src/components/global-styles/variations/style.scss
+++ b/packages/edit-site/src/components/global-styles/variations/style.scss
@@ -9,9 +9,10 @@
outline-offset: -$border-width;
overflow: hidden;
position: relative;
- // Add the same transition that block style variations and other buttons have.
- transition: outline 0.1s linear;
- @include reduce-motion("transition");
+ @media not (prefers-reduced-motion) {
+ // Add the same transition that block style variations and other buttons have.
+ transition: outline 0.1s linear;
+ }
&.is-pill {
height: $button-size-compact;
diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss
index 2c7e6ce1b10c8b..caf7dd78da4b34 100644
--- a/packages/edit-site/src/components/layout/style.scss
+++ b/packages/edit-site/src/components/layout/style.scss
@@ -115,10 +115,13 @@
.edit-site-resizable-frame__inner-content {
box-shadow: $elevation-x-small;
- transition: border-radius, box-shadow 0.4s;
// This ensure the radius work properly.
overflow: hidden;
+ @media not (prefers-reduced-motion) {
+ transition: border-radius, box-shadow 0.4s;
+ }
+
.edit-site-layout:not(.is-full-canvas) & {
border-radius: $radius-large;
}
@@ -195,8 +198,6 @@ html.canvas-mode-edit-transition::view-transition-group(toggle) {
}
&::before {
- transition: box-shadow 0.1s ease;
- @include reduce-motion("transition");
content: "";
display: block;
position: absolute;
@@ -206,6 +207,10 @@ html.canvas-mode-edit-transition::view-transition-group(toggle) {
left: 9px;
border-radius: $radius-medium;
box-shadow: none;
+
+ @media not (prefers-reduced-motion) {
+ transition: box-shadow 0.1s ease;
+ }
}
.edit-site-layout__view-mode-toggle-icon {
diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss
index d5520e5d97cdfc..2cacc8fab607c2 100644
--- a/packages/edit-site/src/components/page-patterns/style.scss
+++ b/packages/edit-site/src/components/page-patterns/style.scss
@@ -26,10 +26,12 @@
top: 0;
z-index: 2;
flex-shrink: 0;
- transition: padding ease-out 0.1s;
- @include reduce-motion("transition");
min-height: $grid-unit-50;
+ @media not (prefers-reduced-motion) {
+ transition: padding ease-out 0.1s;
+ }
+
.edit-site-patterns__title {
min-height: $grid-unit-50;
diff --git a/packages/edit-site/src/components/page-templates/style.scss b/packages/edit-site/src/components/page-templates/style.scss
index 29df1f5bd0803c..bb9069e2c5038a 100644
--- a/packages/edit-site/src/components/page-templates/style.scss
+++ b/packages/edit-site/src/components/page-templates/style.scss
@@ -67,9 +67,11 @@
height: $grid-unit-20;
object-fit: cover;
opacity: 0;
- transition: opacity 0.1s linear;
- @include reduce-motion("transition");
border-radius: 100%;
+
+ @media not (prefers-reduced-motion) {
+ transition: opacity 0.1s linear;
+ }
}
&.is-loaded {
diff --git a/packages/edit-site/src/components/page/style.scss b/packages/edit-site/src/components/page/style.scss
index 7759081e36f592..23e79420a7fbb2 100644
--- a/packages/edit-site/src/components/page/style.scss
+++ b/packages/edit-site/src/components/page/style.scss
@@ -4,8 +4,10 @@
height: calc(100% - #{$header-height});
/* stylelint-disable-next-line property-no-unknown -- '@container' not globally permitted */
container: edit-site-page / inline-size;
- transition: width ease-out 0.2s;
- @include reduce-motion("transition");
+
+ @media not (prefers-reduced-motion) {
+ transition: width ease-out 0.2s;
+ }
@include break-medium() {
height: 100%;
@@ -19,8 +21,10 @@
position: sticky;
top: 0;
z-index: z-index(".edit-site-page-header");
- transition: padding ease-out 0.1s;
- @include reduce-motion("transition");
+
+ @media not (prefers-reduced-motion) {
+ transition: padding ease-out 0.1s;
+ }
.components-heading {
color: $gray-900;
diff --git a/packages/edit-site/src/components/site-hub/style.scss b/packages/edit-site/src/components/site-hub/style.scss
index 39f44d2bef7bb5..4099f7064ff05f 100644
--- a/packages/edit-site/src/components/site-hub/style.scss
+++ b/packages/edit-site/src/components/site-hub/style.scss
@@ -65,8 +65,10 @@
opacity: 0;
position: absolute;
right: 0;
- transition: opacity 0.1s linear;
- @include reduce-motion("transition");
+
+ @media not (prefers-reduced-motion) {
+ transition: opacity 0.1s linear;
+ }
}
&:hover::after,