diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss
index 39c566be5ad526..9dec227a7cdc04 100644
--- a/packages/components/src/style.scss
+++ b/packages/components/src/style.scss
@@ -37,6 +37,7 @@
@import "./select-control/style.scss";
@import "./snackbar/style.scss";
@import "./spinner/style.scss";
+@import "./tab-panel/style.scss";
@import "./text-control/style.scss";
@import "./textarea-control/style.scss";
@import "./toggle-control/style.scss";
diff --git a/packages/components/src/tab-panel/README.md b/packages/components/src/tab-panel/README.md
index def626516b3a39..2ca75d149d4a1e 100644
--- a/packages/components/src/tab-panel/README.md
+++ b/packages/components/src/tab-panel/README.md
@@ -119,6 +119,7 @@ An array of objects containing the following properties:
- `name`: `(string)` Defines the key for the tab.
- `title`:`(string)` Defines the translated text for the tab.
- `className`:`(string)` Optional. Defines the class to put on the tab.
+- `onSelect`:`(function)` Optional. The function called when the tab is selected
>> **Note:** Other fields may be added to the object and accessed from the child function if desired.
@@ -133,9 +134,9 @@ The class to add to the active tab
- Required: No
- Default: `is-active`
-#### initialTabName
+#### controlledTabName
-Optionally provide a tab name for a tab to be selected upon mounting of component. If this prop is not set, the first tab will be selected by default.
+Provide a tab name for a tab to be selected upon mounting and controlled throughout the component's lifecycle. If this prop is not set, the first tab will be selected by default.
- Type: `String`
- Required: No
@@ -147,4 +148,4 @@ A function which renders the tabviews given the selected tab. The function is pa
The element to which the tooltip should anchor.
- Type: (`Object`) => `Element`
-- Required: Yes
+- Required: No
diff --git a/packages/components/src/tab-panel/index.js b/packages/components/src/tab-panel/index.js
index 71f4683bcbef80..77ac0c6ed1a1f8 100644
--- a/packages/components/src/tab-panel/index.js
+++ b/packages/components/src/tab-panel/index.js
@@ -2,13 +2,14 @@
* External dependencies
*/
import classnames from 'classnames';
-import { partial, noop, find } from 'lodash';
+import { noop, find } from 'lodash';
/**
* WordPress dependencies
*/
-import { Component } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
import { withInstanceId } from '@wordpress/compose';
+import { useState, useEffect } from '@wordpress/element';
/**
* Internal dependencies
@@ -28,77 +29,64 @@ const TabButton = ( { tabId, onClick, children, selected, ...rest } ) => (
);
-class TabPanel extends Component {
- constructor() {
- super( ...arguments );
- const { tabs, initialTabName } = this.props;
+const TabPanel = ( { tabs, initialTabName, controlledTabName, className, onSelect = noop, activeClass = 'is-active', orientation = 'horizontal', instanceId, children } ) => {
+ const [ selectedTabName, setSelectedTabName ] = useState( initialTabName || controlledTabName || tabs[ 0 ].name );
+ const selectedTab = find( tabs, { name: selectedTabName } );
+ const selectedId = selectedTab ? instanceId + '-' + selectedTab.name : '';
- this.handleClick = this.handleClick.bind( this );
- this.onNavigate = this.onNavigate.bind( this );
+ const onClick = ( tab ) => {
+ onSelect( tab.name );
+ setSelectedTabName( tab.name );
+ if ( tab.onSelect ) {
+ tab.onSelect();
+ }
+ };
- this.state = {
- selected: initialTabName || ( tabs.length > 0 ? tabs[ 0 ].name : null ),
- };
- }
-
- handleClick( tabKey ) {
- const { onSelect = noop } = this.props;
- this.setState( {
- selected: tabKey,
- } );
- onSelect( tabKey );
- }
-
- onNavigate( childIndex, child ) {
+ const onNavigate = ( childIndex, child ) => {
child.click();
- }
-
- render() {
- const { selected } = this.state;
- const {
- activeClass = 'is-active',
- className,
- instanceId,
- orientation = 'horizontal',
- tabs,
- } = this.props;
+ };
- const selectedTab = find( tabs, { name: selected } );
- const selectedId = instanceId + '-' + selectedTab.name;
+ useEffect(
+ () => {
+ if ( controlledTabName ) {
+ setSelectedTabName( controlledTabName );
+ }
+ }, [ controlledTabName ]
+ );
- return (
-
-
- { tabs.map( ( tab ) => (
-
- { tab.title }
-
- ) ) }
-
- { selectedTab && (
-
+
+ { tabs.map( ( tab ) => (
+ onClick( tab ) }
>
- { this.props.children( selectedTab ) }
-
- ) }
-
- );
- }
-}
+ { tab.title }
+
+ ) ) }
+
+ { children && selectedTab && (
+
+ { children( selectedTab ) }
+
+ ) }
+
+ );
+};
export default withInstanceId( TabPanel );
diff --git a/packages/components/src/tab-panel/style.scss b/packages/components/src/tab-panel/style.scss
new file mode 100644
index 00000000000000..da1ae369c99450
--- /dev/null
+++ b/packages/components/src/tab-panel/style.scss
@@ -0,0 +1,46 @@
+.components-tab-panel__tabs {
+ display: flex;
+ align-items: stretch;
+ background: $light-gray-200;
+ border-bottom: 1px solid #e2e4e7;
+}
+
+.components-tab-panel__tabs-item {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ cursor: pointer;
+ height: 50px;
+ line-height: 42px;
+ padding: 3px 15px; // Use padding to offset the is-active border, this benefits Windows High Contrast mode
+ margin-left: 0;
+ font-weight: 400;
+ @include square-style__neutral;
+ transition: box-shadow 0.1s linear;
+
+ &:focus:enabled {
+ color: $dark-gray-900;
+ outline-offset: -1px;
+ outline: 1px dotted $dark-gray-500;
+ }
+
+ &:focus:enabled,
+ &.is-active {
+ box-shadow: inset 0 -3px theme(outlines);
+ font-weight: 600;
+ position: relative;
+ background: transparent;
+
+ // This border appears in Windows High Contrast mode instead of the box-shadow.
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 1px;
+ right: 0;
+ left: 0;
+ border-bottom: 3px solid transparent;
+ }
+ }
+
+}
diff --git a/packages/components/src/tab-panel/test/index.js b/packages/components/src/tab-panel/test/index.js
index 153441cd195316..78630dcfdf8011 100644
--- a/packages/components/src/tab-panel/test/index.js
+++ b/packages/components/src/tab-panel/test/index.js
@@ -120,11 +120,11 @@ describe( 'TabPanel', () => {
} );
} );
- it( 'should render with a tab initially selected by prop initialTabIndex', () => {
+ it( 'should render with a tab initially selected by prop controlledTabName', () => {
const props = {
className: 'test-panel',
activeClass: 'active-tab',
- initialTabName: 'beta',
+ controlledTabName: 'beta',
tabs: [
{
name: 'alpha',
diff --git a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js
index 3f0e1c8c3f55c7..461edc082839ca 100644
--- a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js
+++ b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js
@@ -81,7 +81,7 @@ describe( 'Navigating the block hierarchy', () => {
await pressKeyWithModifier( 'ctrl', '`' );
await pressKeyWithModifier( 'ctrl', '`' );
await pressKeyWithModifier( 'ctrl', '`' );
- await pressKeyTimes( 'Tab', 4 );
+ await pressKeyTimes( 'Tab', 3 );
// Tweak the columns count by increasing it by one.
await page.keyboard.press( 'ArrowRight' );
diff --git a/packages/e2e-tests/specs/editor-modes.test.js b/packages/e2e-tests/specs/editor-modes.test.js
index 4096338ce9f64b..ce1ec41866750f 100644
--- a/packages/e2e-tests/specs/editor-modes.test.js
+++ b/packages/e2e-tests/specs/editor-modes.test.js
@@ -90,7 +90,7 @@ describe( 'Editing modes (visual/HTML)', () => {
expect( title ).toBe( 'Paragraph' );
// The Block inspector should be active
- let blockInspectorTab = await page.$( '.edit-post-sidebar__panel-tab.is-active[data-label="Block"]' );
+ let blockInspectorTab = await page.$( '.edit-post-sidebar__panel-tab.is-active[aria-label="Block (selected)"]' );
expect( blockInspectorTab ).not.toBeNull();
// Switch to Code Editor and hide More Menu
@@ -100,11 +100,11 @@ describe( 'Editing modes (visual/HTML)', () => {
);
// The Block inspector should not be active anymore
- blockInspectorTab = await page.$( '.edit-post-sidebar__panel-tab.is-active[data-label="Block"]' );
+ blockInspectorTab = await page.$( '.edit-post-sidebar__panel-tab.is-active[aria-label="Block"]' );
expect( blockInspectorTab ).toBeNull();
// No block is selected
- await page.click( '.edit-post-sidebar__panel-tab[data-label="Block"]' );
+ await page.click( '.edit-post-sidebar__panel-tab[aria-label="Block"]' );
const noBlocksElement = await page.$( '.block-editor-block-inspector__no-blocks' );
expect( noBlocksElement ).not.toBeNull();
diff --git a/packages/e2e-tests/specs/sidebar.test.js b/packages/e2e-tests/specs/sidebar.test.js
index def5d0f5011413..cbbf51f0bd0645 100644
--- a/packages/e2e-tests/specs/sidebar.test.js
+++ b/packages/e2e-tests/specs/sidebar.test.js
@@ -96,8 +96,7 @@ describe( 'Sidebar', () => {
expect( isActiveDocumentTab ).toBe( true );
// Tab into and activate "Block".
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Space' );
+ await page.keyboard.press( 'ArrowRight' );
const isActiveBlockTab = await page.evaluate( () => (
document.activeElement.textContent === 'Block' &&
document.activeElement.classList.contains( 'is-active' )
diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js
index 04132d6f2664ea..d40533cf14b31c 100644
--- a/packages/edit-post/src/components/sidebar/settings-header/index.js
+++ b/packages/edit-post/src/components/sidebar/settings-header/index.js
@@ -3,6 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { withDispatch } from '@wordpress/data';
+import { TabPanel } from '@wordpress/components';
/**
* Internal dependencies
@@ -10,47 +11,33 @@ import { withDispatch } from '@wordpress/data';
import SidebarHeader from '../sidebar-header';
const SettingsHeader = ( { openDocumentSettings, openBlockSettings, sidebarName } ) => {
- const blockLabel = __( 'Block' );
- const [ documentAriaLabel, documentActiveClass ] = sidebarName === 'edit-post/document' ?
- // translators: ARIA label for the Document sidebar tab, selected.
- [ __( 'Document (selected)' ), 'is-active' ] :
- // translators: ARIA label for the Document sidebar tab, not selected.
- [ __( 'Document' ), '' ];
-
- const [ blockAriaLabel, blockActiveClass ] = sidebarName === 'edit-post/block' ?
- // translators: ARIA label for the Settings Sidebar tab, selected.
- [ __( 'Block (selected)' ), 'is-active' ] :
- // translators: ARIA label for the Settings Sidebar tab, not selected.
- [ __( 'Block' ), '' ];
+ const tabs = [
+ {
+ name: 'edit-post/document',
+ className: 'edit-post-sidebar__panel-tab',
+ title: __( 'Document' ),
+ ariaLabel: __( 'Document' ),
+ onSelect: openDocumentSettings,
+ },
+ {
+ name: 'edit-post/block',
+ className: 'edit-post-sidebar__panel-tab',
+ title: __( 'Block' ),
+ ariaLabel: __( 'Block' ),
+ onSelect: openBlockSettings,
+ },
+ ];
return (
- { /* Use a list so screen readers will announce how many tabs there are. */ }
-
- -
-
-
- -
-
-
-
+
+
);
};
diff --git a/packages/edit-post/src/components/sidebar/settings-header/style.scss b/packages/edit-post/src/components/sidebar/settings-header/style.scss
index 73d9431661c267..85363b2f4d34be 100644
--- a/packages/edit-post/src/components/sidebar/settings-header/style.scss
+++ b/packages/edit-post/src/components/sidebar/settings-header/style.scss
@@ -6,59 +6,4 @@
position: sticky;
z-index: z-index(".components-panel__header.edit-post-sidebar__panel-tabs");
top: 0;
-
- ul {
- display: flex;
- }
- li {
- margin: 0;
- }
-}
-
-.edit-post-sidebar__panel-tab {
- background: transparent;
- border: none;
- box-shadow: none;
- cursor: pointer;
- padding: 3px 15px; // Use padding to offset the is-active border, this benefits Windows High Contrast mode
- margin-left: 0;
- font-weight: 400;
- color: $dark-gray-900;
- @include square-style__neutral;
- transition: box-shadow 0.1s linear;
- @include reduce-motion("transition");
-
- // This pseudo-element "duplicates" the tab label and sets the text to bold.
- // This ensures that the tab doesn't change width when selected.
- // See: https://github.com/WordPress/gutenberg/pull/9793
- &::after {
- content: attr(data-label);
- display: block;
- font-weight: 600;
- height: 0;
- overflow: hidden;
- speak: none;
- visibility: hidden;
- }
-
- &.is-active {
- box-shadow: inset 0 -3px theme(outlines);
- font-weight: 600;
- position: relative;
-
- // This border appears in Windows High Contrast mode instead of the box-shadow.
- &::before {
- content: "";
- position: absolute;
- top: 0;
- bottom: 1px;
- right: 0;
- left: 0;
- border-bottom: 3px solid transparent;
- }
- }
-
- &:focus {
- @include square-style__focus;
- }
}