diff --git a/packages/e2e-tests/specs/editor/various/change-detection.test.js b/packages/e2e-tests/specs/editor/various/change-detection.test.js index 62057c4cbb2bc..0eb673671222f 100644 --- a/packages/e2e-tests/specs/editor/various/change-detection.test.js +++ b/packages/e2e-tests/specs/editor/various/change-detection.test.js @@ -370,7 +370,11 @@ describe( 'Change detection', () => { it( 'consecutive edits to the same attribute should mark the post as dirty after a save', async () => { // Open the sidebar block settings. await openDocumentSettingsSidebar(); - await page.click( '.edit-post-sidebar__panel-tab[data-label="Block"]' ); + + const blockInspectorTab = await page.waitForXPath( + '//button[@role="tab"][contains(text(), "Block")]' + ); + await blockInspectorTab.click(); // Insert a paragraph. await clickBlockAppender(); diff --git a/packages/e2e-tests/specs/editor/various/editor-modes.test.js b/packages/e2e-tests/specs/editor/various/editor-modes.test.js index 81878ebf7208e..aea6536f605bb 100644 --- a/packages/e2e-tests/specs/editor/various/editor-modes.test.js +++ b/packages/e2e-tests/specs/editor/various/editor-modes.test.js @@ -102,21 +102,24 @@ 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.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); expect( blockInspectorTab ).not.toBeNull(); await switchEditorModeTo( 'Code' ); // The Block inspector should not be active anymore. - blockInspectorTab = await page.$( - '.edit-post-sidebar__panel-tab.is-active[data-label="Block"]' + [ blockInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); - expect( blockInspectorTab ).toBeNull(); + expect( blockInspectorTab ).toBeUndefined(); // No block is selected. - await page.click( '.edit-post-sidebar__panel-tab[data-label="Block"]' ); + const inactiveBlockInspectorTab = await page.waitForXPath( + '//button[@role="tab"][contains(text(), "Block")]' + ); + inactiveBlockInspectorTab.click(); const noBlocksElement = await page.$( '.block-editor-block-inspector__no-blocks' ); diff --git a/packages/e2e-tests/specs/editor/various/preferences.test.js b/packages/e2e-tests/specs/editor/various/preferences.test.js index 98249637c7e96..54990a4004422 100644 --- a/packages/e2e-tests/specs/editor/various/preferences.test.js +++ b/packages/e2e-tests/specs/editor/various/preferences.test.js @@ -17,7 +17,7 @@ describe( 'preferences', () => { async function getActiveSidebarTabText() { try { return await page.$eval( - '.edit-post-sidebar__panel-tab.is-active', + 'div[aria-label="Editor settings"] [role="tab"][aria-selected="true"]', ( node ) => node.textContent ); } catch ( error ) { @@ -29,11 +29,15 @@ describe( 'preferences', () => { } it( 'remembers sidebar dismissal between sessions', async () => { + const blockTab = await page.waitForXPath( + `//button[@role="tab"][contains(text(), 'Block')]` + ); + // Open by default. expect( await getActiveSidebarTabText() ).toBe( 'Post' ); // Change to "Block" tab. - await page.click( '.edit-post-sidebar__panel-tab[aria-label="Block"]' ); + await blockTab.click(); expect( await getActiveSidebarTabText() ).toBe( 'Block' ); // Regression test: Reload resets to document tab. @@ -46,7 +50,7 @@ describe( 'preferences', () => { // Dismiss. await page.click( - '.edit-post-sidebar__panel-tabs [aria-label="Close Settings"]' + 'div[aria-label="Editor settings"] div[role="tablist"] + button[aria-label="Close Settings"]' ); expect( await getActiveSidebarTabText() ).toBe( null ); diff --git a/packages/e2e-tests/specs/editor/various/sidebar.test.js b/packages/e2e-tests/specs/editor/various/sidebar.test.js index 2e5d46eec2f7a..0cd39093aabb8 100644 --- a/packages/e2e-tests/specs/editor/various/sidebar.test.js +++ b/packages/e2e-tests/specs/editor/various/sidebar.test.js @@ -13,7 +13,8 @@ import { } from '@wordpress/e2e-test-utils'; const SIDEBAR_SELECTOR = '.edit-post-sidebar'; -const ACTIVE_SIDEBAR_TAB_SELECTOR = '.edit-post-sidebar__panel-tab.is-active'; +const ACTIVE_SIDEBAR_TAB_SELECTOR = + 'div[aria-label="Editor settings"] [role="tab"][aria-selected="true"]'; const ACTIVE_SIDEBAR_BUTTON_TEXT = 'Post'; describe( 'Sidebar', () => { @@ -99,22 +100,24 @@ describe( 'Sidebar', () => { // Tab lands at first (presumed selected) option "Post". await page.keyboard.press( 'Tab' ); - const isActiveDocumentTab = await page.evaluate( - () => - document.activeElement.textContent === 'Post' && - document.activeElement.classList.contains( 'is-active' ) + + // The Post tab should be focused and selected. + const [ documentInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Post")]' ); - expect( isActiveDocumentTab ).toBe( true ); + expect( documentInspectorTab ).toBeDefined(); + expect( documentInspectorTab ).toHaveFocus(); - // Tab into and activate "Block". - await page.keyboard.press( 'Tab' ); + // Arrow key into and activate "Block". + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Space' ); - const isActiveBlockTab = await page.evaluate( - () => - document.activeElement.textContent === 'Block' && - document.activeElement.classList.contains( 'is-active' ) + + // The Block tab should be focused and selected. + const [ blockInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); - expect( isActiveBlockTab ).toBe( true ); + expect( blockInspectorTab ).toBeDefined(); + expect( blockInspectorTab ).toHaveFocus(); } ); it( 'should be possible to programmatically remove Document Settings panels', async () => { 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 ef32450e7209f..368bd3e9e50db 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -1,22 +1,20 @@ /** * WordPress dependencies */ -import { Button } from '@wordpress/components'; -import { __, _x, sprintf } from '@wordpress/i18n'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { __, _x } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ -import { store as editPostStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; +import { sidebars } from '../settings-sidebar'; -const SettingsHeader = ( { sidebarName } ) => { - const { openGeneralSidebar } = useDispatch( editPostStore ); - const openDocumentSettings = () => - openGeneralSidebar( 'edit-post/document' ); - const openBlockSettings = () => openGeneralSidebar( 'edit-post/block' ); +const { Tabs } = unlock( componentsPrivateApis ); +const SettingsHeader = () => { const { documentLabel, isTemplateMode } = useSelect( ( select ) => { const { getPostTypeLabel, getRenderingMode } = select( editorStore ); @@ -27,66 +25,16 @@ const SettingsHeader = ( { sidebarName } ) => { }; }, [] ); - const [ documentAriaLabel, documentActiveClass ] = - sidebarName === 'edit-post/document' - ? // translators: ARIA label for the Document sidebar tab, selected. %s: Document label. - [ sprintf( __( '%s (selected)' ), documentLabel ), 'is-active' ] - : [ documentLabel, '' ]; - - const [ blockAriaLabel, blockActiveClass ] = - sidebarName === 'edit-post/block' - ? // translators: ARIA label for the Block Settings Sidebar tab, selected. - [ __( 'Block (selected)' ), 'is-active' ] - : // translators: ARIA label for the Block Settings Sidebar tab, not selected. - [ __( 'Block' ), '' ]; - - const [ templateAriaLabel, templateActiveClass ] = - sidebarName === 'edit-post/document' - ? [ __( 'Template (selected)' ), 'is-active' ] - : [ __( 'Template' ), '' ]; - - /* Use a list so screen readers will announce how many tabs there are. */ return ( - + + + { isTemplateMode ? __( 'Template' ) : documentLabel } + + + { /* translators: Text label for the Block Settings Sidebar tab. */ } + { __( 'Block' ) } + + ); }; diff --git a/packages/edit-post/src/components/sidebar/settings-header/style.scss b/packages/edit-post/src/components/sidebar/settings-header/style.scss deleted file mode 100644 index aaf7698cb6ddb..0000000000000 --- a/packages/edit-post/src/components/sidebar/settings-header/style.scss +++ /dev/null @@ -1,74 +0,0 @@ -// This tab style CSS is duplicated verbatim in -// /packages/components/src/tab-panel/style.scss -.components-button.edit-post-sidebar__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-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index e566ea400c12b..9fa27c6ac2ade 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -5,8 +5,8 @@ import { BlockInspector, store as blockEditorStore, } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; -import { Platform } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { Platform, useCallback, useContext } from '@wordpress/element'; import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as interfaceStore } from '@wordpress/interface'; @@ -29,54 +29,43 @@ import PluginDocumentSettingPanel from '../plugin-document-setting-panel'; import PluginSidebarEditPost from '../plugin-sidebar'; import TemplateSummary from '../template-summary'; import { store as editPostStore } from '../../../store'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { unlock } from '../../../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); const SIDEBAR_ACTIVE_BY_DEFAULT = Platform.select( { web: true, native: false, } ); +export const sidebars = { + document: 'edit-post/document', + block: 'edit-post/block', +}; -const SettingsSidebar = () => { - const { sidebarName, keyboardShortcut, isTemplateMode } = useSelect( - ( select ) => { - // The settings sidebar is used by the edit-post/document and edit-post/block sidebars. - // sidebarName represents the sidebar that is active or that should be active when the SettingsSidebar toggle button is pressed. - // If one of the two sidebars is active the component will contain the content of that sidebar. - // When neither of the two sidebars is active we can not simply return null, because the PluginSidebarEditPost - // component, besides being used to render the sidebar, also renders the toggle button. In that case sidebarName - // should contain the sidebar that will be active when the toggle button is pressed. If a block - // is selected, that should be edit-post/block otherwise it's edit-post/document. - let sidebar = select( interfaceStore ).getActiveComplementaryArea( - editPostStore.name - ); - if ( - ! [ 'edit-post/document', 'edit-post/block' ].includes( - sidebar - ) - ) { - if ( select( blockEditorStore ).getBlockSelectionStart() ) { - sidebar = 'edit-post/block'; - } - sidebar = 'edit-post/document'; - } - const shortcut = select( - keyboardShortcutsStore - ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ); - return { - sidebarName: sidebar, - keyboardShortcut: shortcut, - isTemplateMode: - select( editorStore ).getRenderingMode() === - 'template-only', - }; - }, - [] - ); +const SidebarContent = ( { + sidebarName, + keyboardShortcut, + isTemplateMode, +} ) => { + // Because `PluginSidebarEditPost` 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 ); return ( } + header={ + + + + } closeLabel={ __( 'Close Settings' ) } + // 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-post-sidebar__panel" headerClassName="edit-post-sidebar__panel-tabs" /* translators: button label text should, if possible, be under 16 characters. */ title={ __( 'Settings' ) } @@ -84,25 +73,96 @@ const SettingsSidebar = () => { icon={ isRTL() ? drawerLeft : drawerRight } isActiveByDefault={ SIDEBAR_ACTIVE_BY_DEFAULT } > - { ! isTemplateMode && sidebarName === 'edit-post/document' && ( - <> - - - - - - - - - - - ) } - { isTemplateMode && sidebarName === 'edit-post/document' && ( - - ) } - { sidebarName === 'edit-post/block' && } + + + { ! isTemplateMode && ( + <> + + + + + + + + + + + ) } + { isTemplateMode && } + + + + + ); }; +const SettingsSidebar = () => { + const { + sidebarName, + isSettingsSidebarActive, + keyboardShortcut, + isTemplateMode, + } = useSelect( ( select ) => { + // The settings sidebar is used by the edit-post/document and edit-post/block sidebars. + // sidebarName represents the sidebar that is active or that should be active when the SettingsSidebar toggle button is pressed. + // If one of the two sidebars is active the component will contain the content of that sidebar. + // When neither of the two sidebars is active we can not simply return null, because the PluginSidebarEditPost + // component, besides being used to render the sidebar, also renders the toggle button. In that case sidebarName + // should contain the sidebar that will be active when the toggle button is pressed. If a block + // is selected, that should be edit-post/block otherwise it's edit-post/document. + let sidebar = select( interfaceStore ).getActiveComplementaryArea( + editPostStore.name + ); + let isSettingsSidebar = true; + if ( ! [ sidebars.document, sidebars.block ].includes( sidebar ) ) { + isSettingsSidebar = false; + if ( select( blockEditorStore ).getBlockSelectionStart() ) { + sidebar = sidebars.block; + } + sidebar = sidebars.document; + } + const shortcut = select( + keyboardShortcutsStore + ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ); + return { + sidebarName: sidebar, + isSettingsSidebarActive: isSettingsSidebar, + keyboardShortcut: shortcut, + isTemplateMode: + select( editorStore ).getRenderingMode() === 'template-only', + }; + }, [] ); + + const { openGeneralSidebar } = useDispatch( editPostStore ); + + const onTabSelect = useCallback( + ( newSelectedTabId ) => { + if ( !! newSelectedTabId ) { + openGeneralSidebar( newSelectedTabId ); + } + }, + [ openGeneralSidebar ] + ); + + return ( + + + + ); +}; + export default SettingsSidebar; diff --git a/packages/edit-post/src/components/sidebar/style.scss b/packages/edit-post/src/components/sidebar/style.scss index 7b10eaec0d224..1921c5cfd7b31 100644 --- a/packages/edit-post/src/components/sidebar/style.scss +++ b/packages/edit-post/src/components/sidebar/style.scss @@ -1,20 +1,8 @@ .components-panel__header.edit-post-sidebar__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,3 +12,7 @@ } } } + +.edit-post-sidebar__panel { + margin-top: -1px; +} diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 53219bc6a3736..88916bf70f76d 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -12,7 +12,6 @@ @import "./components/sidebar/post-format/style.scss"; @import "./components/sidebar/post-slug/style.scss"; @import "./components/sidebar/post-visibility/style.scss"; -@import "./components/sidebar/settings-header/style.scss"; @import "./components/sidebar/template-summary/style.scss"; @import "./components/text-editor/style.scss"; @import "./components/visual-editor/style.scss"; diff --git a/test/e2e/specs/editor/plugins/custom-post-types.spec.js b/test/e2e/specs/editor/plugins/custom-post-types.spec.js index 17a497f26cee0..01dde03650ef7 100644 --- a/test/e2e/specs/editor/plugins/custom-post-types.spec.js +++ b/test/e2e/specs/editor/plugins/custom-post-types.spec.js @@ -31,7 +31,7 @@ test.describe( 'Test Custom Post Types', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { + .getByRole( 'tab', { name: 'Hierarchical No Title', } ) .click(); diff --git a/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js b/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js index f0bfe5bff203f..a695b0a9ead67 100644 --- a/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js +++ b/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js @@ -127,7 +127,7 @@ test.describe( 'Navigating the block hierarchy', () => { await pageUtils.pressKeys( 'ctrl+`' ); // Navigate to the block settings sidebar and tweak the column count. - await pageUtils.pressKeys( 'Tab', { times: 5 } ); + await pageUtils.pressKeys( 'Tab', { times: 4 } ); await expect( page.getByRole( 'slider', { name: 'Columns' } ) ).toBeFocused(); diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index 14a2fc653e387..6102f48749543 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -362,7 +362,7 @@ test.describe( 'Footnotes', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { name: 'Post' } ) + .getByRole( 'tab', { name: 'Post' } ) .click(); await page.locator( 'a:text("2 Revisions")' ).click(); await page.locator( '.revisions-controls .ui-slider-handle' ).focus(); @@ -440,7 +440,7 @@ test.describe( 'Footnotes', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { name: 'Post' } ) + .getByRole( 'tab', { name: 'Post' } ) .click(); // Visit the published post. diff --git a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js index 080abe011206a..84536c88227ce 100644 --- a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js +++ b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js @@ -75,9 +75,7 @@ test.describe( 'Order of block keyboard navigation', () => { ); await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( - 'Post (selected)' - ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' ); } ); test( 'allows tabbing in navigation mode if no block is selected (reverse)', async ( { @@ -151,7 +149,7 @@ test.describe( 'Order of block keyboard navigation', () => { ); await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Block' ); await pageUtils.pressKeys( 'shift+Tab' ); await KeyboardNavigableBlocks.expectLabelToHaveFocus( @@ -233,7 +231,7 @@ class KeyboardNavigableBlocks { await expect( activeElement ).toHaveText( paragraphText ); await this.page.keyboard.press( 'Tab' ); - await this.expectLabelToHaveFocus( 'Post' ); + await this.expectLabelToHaveFocus( 'Block' ); // Need to shift+tab here to end back in the block. If not, we'll be in the next region and it will only require 4 region jumps instead of 5. await this.pageUtils.pressKeys( 'shift+Tab' );