diff --git a/packages/edit-site/src/components/sidebar-edit-mode/index.js b/packages/edit-site/src/components/sidebar-edit-mode/index.js
index aaeb2fd875df5c..bd9c43cc456f73 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/index.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/index.js
@@ -1,10 +1,13 @@
/**
* WordPress dependencies
*/
-import { createSlotFill } from '@wordpress/components';
+import {
+ createSlotFill,
+ privateApis as componentsPrivateApis,
+} from '@wordpress/components';
import { isRTL, __ } from '@wordpress/i18n';
import { drawerLeft, drawerRight } from '@wordpress/icons';
-import { useEffect } from '@wordpress/element';
+import { useCallback, useContext, useEffect, useRef } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface';
import { store as blockEditorStore } from '@wordpress/block-editor';
@@ -22,12 +25,89 @@ import TemplatePanel from './template-panel';
import PluginTemplateSettingPanel from '../plugin-template-setting-panel';
import { SIDEBAR_BLOCK, SIDEBAR_TEMPLATE } from './constants';
import { store as editSiteStore } from '../../store';
+import { unlock } from '../../lock-unlock';
+
+const { Tabs } = unlock( componentsPrivateApis );
const { Slot: InspectorSlot, Fill: InspectorFill } = createSlotFill(
'EditSiteSidebarInspector'
);
export const SidebarInspectorFill = InspectorFill;
+const FillContents = ( {
+ sidebarName,
+ isEditingPage,
+ supportsGlobalStyles,
+} ) => {
+ const tabListRef = useRef( null );
+ // Because `DefaultSidebar` renders a `ComplementaryArea`, we
+ // need to forward the `Tabs` context so it can be passed through the
+ // underlying slot/fill.
+ const tabsContextValue = useContext( Tabs.Context );
+
+ // This effect addresses a race condition caused by tabbing from the last
+ // block in the editor into the settings sidebar. Without this effect, the
+ // selected tab and browser focus can become separated in an unexpected way.
+ // (e.g the "block" tab is focused, but the "post" tab is selected).
+ useEffect( () => {
+ const tabsElements = Array.from(
+ tabListRef.current?.querySelectorAll( '[role="tab"]' ) || []
+ );
+ const selectedTabElement = tabsElements.find(
+ // We are purposefully using a custom `data-tab-id` attribute here
+ // because we don't want rely on any assumptions about `Tabs`
+ // component internals.
+ ( element ) => element.getAttribute( 'data-tab-id' ) === sidebarName
+ );
+ const activeElement = selectedTabElement?.ownerDocument.activeElement;
+ const tabsHasFocus = tabsElements.some( ( element ) => {
+ return activeElement && activeElement.id === element.id;
+ } );
+ if (
+ tabsHasFocus &&
+ selectedTabElement &&
+ selectedTabElement.id !== activeElement?.id
+ ) {
+ selectedTabElement?.focus();
+ }
+ }, [ sidebarName ] );
+
+ return (
+ <>
+
+
+
+ }
+ headerClassName="edit-site-sidebar-edit-mode__panel-tabs"
+ // This classname is added so we can apply a corrective negative
+ // margin to the panel.
+ // see https://github.com/WordPress/gutenberg/pull/55360#pullrequestreview-1737671049
+ className="edit-site-sidebar__panel"
+ >
+
+
+ { isEditingPage ? : }
+
+
+
+
+
+
+
+ { supportsGlobalStyles && }
+ >
+ );
+};
+
export function SidebarComplementaryAreaFills() {
const {
sidebar,
@@ -35,13 +115,17 @@ export function SidebarComplementaryAreaFills() {
hasBlockSelection,
supportsGlobalStyles,
isEditingPage,
+ isEditorOpen,
} = useSelect( ( select ) => {
const _sidebar =
select( interfaceStore ).getActiveComplementaryArea( STORE_NAME );
+
const _isEditorSidebarOpened = [
SIDEBAR_BLOCK,
SIDEBAR_TEMPLATE,
].includes( _sidebar );
+ const { getCanvasMode } = unlock( select( editSiteStore ) );
+
return {
sidebar: _sidebar,
isEditorSidebarOpened: _isEditorSidebarOpened,
@@ -50,6 +134,7 @@ export function SidebarComplementaryAreaFills() {
supportsGlobalStyles:
select( coreStore ).getCurrentTheme()?.is_block_theme,
isEditingPage: select( editSiteStore ).isPage(),
+ isEditorOpen: getCanvasMode() === 'edit',
};
}, [] );
const { enableComplementaryArea } = useDispatch( interfaceStore );
@@ -79,27 +164,37 @@ export function SidebarComplementaryAreaFills() {
sidebarName = hasBlockSelection ? SIDEBAR_BLOCK : SIDEBAR_TEMPLATE;
}
+ // `newSelectedTabId` could technically be falsey if no tab is selected (i.e.
+ // the initial render) or when we don't want a tab displayed (i.e. the
+ // sidebar is closed). These cases should both be covered by the `!!` check
+ // below, so we shouldn't need any additional falsey handling.
+ const onTabSelect = useCallback(
+ ( newSelectedTabId ) => {
+ if ( !! newSelectedTabId ) {
+ enableComplementaryArea( STORE_NAME, newSelectedTabId );
+ }
+ },
+ [ enableComplementaryArea ]
+ );
+
return (
- <>
- }
- headerClassName="edit-site-sidebar-edit-mode__panel-tabs"
- >
- { sidebarName === SIDEBAR_TEMPLATE && (
- <>
- { isEditingPage ? : }
-
- >
- ) }
- { sidebarName === SIDEBAR_BLOCK && (
-
- ) }
-
- { supportsGlobalStyles && }
- >
+
+
+
);
}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
index b3bbf0dd035788..152b4191f9b0d5 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
@@ -1,82 +1,44 @@
-/**
- * External dependencies
- */
-import classnames from 'classnames';
-
/**
* WordPress dependencies
*/
-import { Button } from '@wordpress/components';
-import { __, sprintf } from '@wordpress/i18n';
-import { useSelect, useDispatch } from '@wordpress/data';
-import { store as interfaceStore } from '@wordpress/interface';
+import { privateApis as componentsPrivateApis } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
+import { forwardRef } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { STORE_NAME } from '../../../store/constants';
import { SIDEBAR_BLOCK, SIDEBAR_TEMPLATE } from '../constants';
+import { unlock } from '../../../lock-unlock';
-const SettingsHeader = ( { sidebarName } ) => {
+const { Tabs } = unlock( componentsPrivateApis );
+
+const SettingsHeader = ( _, ref ) => {
const postTypeLabel = useSelect(
( select ) => select( editorStore ).getPostTypeLabel(),
[]
);
- const { enableComplementaryArea } = useDispatch( interfaceStore );
- const openTemplateSettings = () =>
- enableComplementaryArea( STORE_NAME, SIDEBAR_TEMPLATE );
- const openBlockSettings = () =>
- enableComplementaryArea( STORE_NAME, SIDEBAR_BLOCK );
-
- const documentAriaLabel =
- sidebarName === SIDEBAR_TEMPLATE
- ? // translators: ARIA label for the Template sidebar tab, selected.
- sprintf( __( '%s (selected)' ), postTypeLabel )
- : postTypeLabel;
-
- /* Use a list so screen readers will announce how many tabs there are. */
return (
-
- -
-
-
- -
-
-
-
+
+
+ { postTypeLabel }
+
+
+ { __( 'Block' ) }
+
+
);
};
-export default SettingsHeader;
+export default forwardRef( SettingsHeader );
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/style.scss
index 01f074c73fca7a..d74432451e1d4c 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/style.scss
+++ b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/style.scss
@@ -1,20 +1,8 @@
.components-panel__header.edit-site-sidebar-edit-mode__panel-tabs {
- justify-content: flex-start;
padding-left: 0;
padding-right: $grid-unit-20;
- border-top: 0;
- margin-top: 0;
-
- ul {
- display: flex;
- }
- li {
- margin: 0;
- }
.components-button.has-icon {
- display: none;
- margin: 0 0 0 auto;
padding: 0;
min-width: $icon-size;
height: $icon-size;
@@ -24,78 +12,3 @@
}
}
}
-
-// This tab style CSS is duplicated verbatim in
-// /packages/components/src/tab-panel/style.scss
-.components-button.edit-site-sidebar-edit-mode__panel-tab {
- position: relative;
- border-radius: 0;
- height: $grid-unit-60;
- background: transparent;
- border: none;
- box-shadow: none;
- cursor: pointer;
- padding: 3px $grid-unit-20; // Use padding to offset the is-active border, this benefits Windows High Contrast mode
- margin-left: 0;
- font-weight: 500;
-
- &:focus:not(:disabled) {
- position: relative;
- box-shadow: none;
- outline: none;
- }
-
- // Tab indicator
- &::after {
- content: "";
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- pointer-events: none;
-
- // Draw the indicator.
- background: var(--wp-admin-theme-color);
- height: calc(0 * var(--wp-admin-border-width-focus));
- border-radius: 0;
-
- // Animation
- transition: all 0.1s linear;
- @include reduce-motion("transition");
- }
-
- // Active.
- &.is-active::after {
- height: calc(1 * var(--wp-admin-border-width-focus));
-
- // Windows high contrast mode.
- outline: 2px solid transparent;
- outline-offset: -1px;
- }
-
- // Focus.
- &::before {
- content: "";
- position: absolute;
- top: $grid-unit-15;
- right: $grid-unit-15;
- bottom: $grid-unit-15;
- left: $grid-unit-15;
- pointer-events: none;
-
- // Draw the indicator.
- box-shadow: 0 0 0 0 transparent;
- border-radius: $radius-block-ui;
-
- // Animation
- transition: all 0.1s linear;
- @include reduce-motion("transition");
- }
-
- &:focus-visible::before {
- box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
-
- // Windows high contrast mode.
- outline: 2px solid transparent;
- }
-}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/style.scss
index eeb5dc2d170cd2..fbc4954821e367 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/style.scss
+++ b/packages/edit-site/src/components/sidebar-edit-mode/style.scss
@@ -99,3 +99,7 @@
}
}
}
+
+.edit-site-sidebar__panel {
+ margin-top: -1px;
+}
diff --git a/test/e2e/specs/site-editor/settings-sidebar.spec.js b/test/e2e/specs/site-editor/settings-sidebar.spec.js
index f063603deacca2..63c37d7ee9ec0c 100644
--- a/test/e2e/specs/site-editor/settings-sidebar.spec.js
+++ b/test/e2e/specs/site-editor/settings-sidebar.spec.js
@@ -38,8 +38,8 @@ test.describe( 'Settings sidebar', () => {
await expect(
page
.getByRole( 'region', { name: 'Editor settings' } )
- .getByRole( 'button', { name: 'Template (selected)' } )
- ).toHaveClass( /is-active/ );
+ .getByRole( 'tab', { selected: true } )
+ ).toHaveText( 'Template' );
} );
test( `should show the currently selected template's title and description`, async ( {
@@ -90,8 +90,8 @@ test.describe( 'Settings sidebar', () => {
await expect(
page
.getByRole( 'region', { name: 'Editor settings' } )
- .getByRole( 'button', { name: 'Block (selected)' } )
- ).toHaveClass( /is-active/ );
+ .getByRole( 'tab', { selected: true } )
+ ).toHaveText( 'Block' );
} );
} );
@@ -105,16 +105,17 @@ test.describe( 'Settings sidebar', () => {
await expect(
page
.getByRole( 'region', { name: 'Editor settings' } )
- .getByRole( 'button', { name: 'Template (selected)' } )
- ).toHaveClass( /is-active/ );
+ .getByRole( 'tab', { selected: true } )
+ ).toHaveText( 'Template' );
// By inserting the block is also selected.
await editor.insertBlock( { name: 'core/heading' } );
+
await expect(
page
.getByRole( 'region', { name: 'Editor settings' } )
- .getByRole( 'button', { name: 'Block (selected)' } )
- ).toHaveClass( /is-active/ );
+ .getByRole( 'tab', { selected: true } )
+ ).toHaveText( 'Block' );
} );
test( 'should switch to Template tab when a block was selected and we select the Template', async ( {
@@ -129,8 +130,8 @@ test.describe( 'Settings sidebar', () => {
await expect(
page
.getByRole( 'region', { name: 'Editor settings' } )
- .getByRole( 'button', { name: 'Block (selected)' } )
- ).toHaveClass( /is-active/ );
+ .getByRole( 'tab', { selected: true } )
+ ).toHaveText( 'Block' );
await page.evaluate( () => {
window.wp.data
@@ -141,8 +142,8 @@ test.describe( 'Settings sidebar', () => {
await expect(
page
.getByRole( 'region', { name: 'Editor settings' } )
- .getByRole( 'button', { name: 'Template (selected)' } )
- ).toHaveClass( /is-active/ );
+ .getByRole( 'tab', { selected: true } )
+ ).toHaveText( 'Template' );
} );
} );
} );
diff --git a/test/e2e/specs/site-editor/template-revert.spec.js b/test/e2e/specs/site-editor/template-revert.spec.js
index c281b71d16a183..4cc18448608015 100644
--- a/test/e2e/specs/site-editor/template-revert.spec.js
+++ b/test/e2e/specs/site-editor/template-revert.spec.js
@@ -294,12 +294,12 @@ class TemplateRevertUtils {
await this.editor.openDocumentSettingsSidebar();
const isTemplateTabVisible = await this.page
.locator(
- 'role=region[name="Editor settings"i] >> role=button[name="Template"i]'
+ 'role=region[name="Editor settings"i] >> role=tab[name="Template"i]'
)
.isVisible();
if ( isTemplateTabVisible ) {
await this.page.click(
- 'role=region[name="Editor settings"i] >> role=button[name="Template"i]'
+ 'role=region[name="Editor settings"i] >> role=tab[name="Template"i]'
);
}
await this.page.click(
diff --git a/test/e2e/specs/site-editor/writing-flow.spec.js b/test/e2e/specs/site-editor/writing-flow.spec.js
index 8ee6ce0e565572..75f8766891156b 100644
--- a/test/e2e/specs/site-editor/writing-flow.spec.js
+++ b/test/e2e/specs/site-editor/writing-flow.spec.js
@@ -65,9 +65,9 @@ test.describe( 'Site editor writing flow', () => {
// Tab to the inspector, tabbing three times to go past the two resize handles.
await pageUtils.pressKeys( 'Tab', { times: 3 } );
- const inspectorTemplateTab = page.locator(
- 'role=region[name="Editor settings"i] >> role=button[name="Template part"i]'
+ const inspectorBlockTab = page.locator(
+ 'role=region[name="Editor settings"i] >> role=tab[name="Block"i]'
);
- await expect( inspectorTemplateTab ).toBeFocused();
+ await expect( inspectorBlockTab ).toBeFocused();
} );
} );