diff --git a/packages/block-editor/src/components/inserter/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js index 456fb533ac467..4c68990dae12b 100644 --- a/packages/block-editor/src/components/inserter/quick-inserter.js +++ b/packages/block-editor/src/components/inserter/quick-inserter.js @@ -196,8 +196,12 @@ function QuickInserter( { const onBrowseAll = () => { // We have to select the previous block because the menu inserter // inserts the new block after the selected one. + // Ideally, this selection shouldn't focus the block to avoid the setTimeout. selectBlock( previousBlockClientId ); - setInserterIsOpened( true ); + // eslint-disable-next-line @wordpress/react-no-unsafe-timeout + setTimeout( () => { + setInserterIsOpened( true ); + } ); }; // Disable reason (no-autofocus): The inserter menu is a modal display, not one which diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js index acd4bb14007d9..e1e737ee12ae9 100644 --- a/packages/e2e-test-utils/src/inserter.js +++ b/packages/e2e-test-utils/src/inserter.js @@ -58,6 +58,7 @@ async function toggleGlobalBlockInserter() { */ export async function searchForBlock( searchTerm ) { await openGlobalBlockInserter(); + await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); await page.focus( INSERTER_SEARCH_SELECTOR ); await pressKeyWithModifier( 'primary', 'a' ); await page.keyboard.type( searchTerm ); @@ -71,10 +72,11 @@ export async function searchForBlock( searchTerm ) { export async function searchForPattern( searchTerm ) { await openGlobalBlockInserter(); // Select the patterns tab - const [ tab ] = await page.$x( + const tab = await page.waitForXPath( '//div[contains(@class, "block-editor-inserter__tabs")]//button[.="Patterns"]' ); await tab.click(); + await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); await page.focus( INSERTER_SEARCH_SELECTOR ); await pressKeyWithModifier( 'primary', 'a' ); await page.keyboard.type( searchTerm ); @@ -96,10 +98,11 @@ export async function searchForReusableBlock( searchTerm ) { ); // Select the reusable blocks tab. - const [ tab ] = await page.$x( + const tab = await page.waitForXPath( '//div[contains(@class, "block-editor-inserter__tabs")]//button[text()="Reusable"]' ); await tab.click(); + await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); await page.focus( INSERTER_SEARCH_SELECTOR ); await pressKeyWithModifier( 'primary', 'a' ); await page.keyboard.type( searchTerm ); @@ -117,6 +120,12 @@ export async function insertBlock( searchTerm ) { await page.$x( `//button//span[contains(text(), '${ searchTerm }')]` ) )[ 0 ]; await insertButton.click(); + // We should wait until the inserter closes and the focus moves to the content. + await page.waitForFunction( () => + document.body + .querySelector( '.block-editor-block-list__layout' ) + .contains( document.activeElement ) + ); } /** @@ -133,6 +142,12 @@ export async function insertPattern( searchTerm ) { ) )[ 0 ]; await insertButton.click(); + // We should wait until the inserter closes and the focus moves to the content. + await page.waitForFunction( () => + document.body + .querySelector( '.block-editor-block-list__layout' ) + .contains( document.activeElement ) + ); } /** @@ -148,4 +163,10 @@ export async function insertReusableBlock( searchTerm ) { await page.$x( `//button//span[contains(text(), '${ searchTerm }')]` ) )[ 0 ]; await insertButton.click(); + // We should wait until the inserter closes and the focus moves to the content. + await page.waitForFunction( () => + document.body + .querySelector( '.block-editor-block-list__layout' ) + .contains( document.activeElement ) + ); } diff --git a/packages/e2e-tests/specs/editor/plugins/align-hook.test.js b/packages/e2e-tests/specs/editor/plugins/align-hook.test.js index 87af1f940aa50..6b0c41ae9115d 100644 --- a/packages/e2e-tests/specs/editor/plugins/align-hook.test.js +++ b/packages/e2e-tests/specs/editor/plugins/align-hook.test.js @@ -10,6 +10,7 @@ import { insertBlock, selectBlockByClientId, setPostContent, + clickBlockToolbarButton, } from '@wordpress/e2e-test-utils'; const alignLabels = { @@ -21,9 +22,6 @@ const alignLabels = { }; describe( 'Align Hook Works As Expected', () => { - const CHANGE_ALIGNMENT_BUTTON_SELECTOR = - '.block-editor-block-toolbar .components-dropdown-menu__toggle[aria-label="Change alignment"]'; - beforeAll( async () => { await activatePlugin( 'gutenberg-test-align-hook' ); } ); @@ -37,11 +35,7 @@ describe( 'Align Hook Works As Expected', () => { } ); const getAlignmentToolbarLabels = async () => { - const element = await page.waitForSelector( - CHANGE_ALIGNMENT_BUTTON_SELECTOR - ); - await element.click(); - + await clickBlockToolbarButton( 'Change alignment' ); const buttonLabels = await page.evaluate( () => { return Array.from( document.querySelectorAll( @@ -57,7 +51,6 @@ describe( 'Align Hook Works As Expected', () => { const createShowsTheExpectedButtonsTest = ( blockName, buttonLabels ) => { it( 'Shows the expected buttons on the alignment toolbar', async () => { await insertBlock( blockName ); - expect( await getAlignmentToolbarLabels() ).toEqual( buttonLabels ); } ); }; @@ -65,12 +58,7 @@ describe( 'Align Hook Works As Expected', () => { const createDoesNotApplyAlignmentByDefaultTest = ( blockName ) => { it( 'Does not apply any alignment by default', async () => { await insertBlock( blockName ); - - // verify no alignment button is in pressed state - const element = await page.waitForSelector( - CHANGE_ALIGNMENT_BUTTON_SELECTOR - ); - await element.click(); + await clickBlockToolbarButton( 'Change alignment' ); const pressedButtons = await page.$$( '.components-dropdown-menu__menu button.is-active' ); @@ -95,14 +83,11 @@ describe( 'Align Hook Works As Expected', () => { '.components-dropdown-menu__menu button.is-active'; // set the specified alignment. await insertBlock( blockName ); - const element = await page.waitForSelector( - CHANGE_ALIGNMENT_BUTTON_SELECTOR - ); - await element.click(); + await clickBlockToolbarButton( 'Change alignment' ); await ( await page.$x( BUTTON_XPATH ) )[ 0 ].click(); // verify the button of the specified alignment is pressed. - await page.click( CHANGE_ALIGNMENT_BUTTON_SELECTOR ); + await clickBlockToolbarButton( 'Change alignment' ); let pressedButtons = await page.$$( BUTTON_PRESSED_SELECTOR ); expect( pressedButtons ).toHaveLength( 1 ); @@ -119,11 +104,11 @@ describe( 'Align Hook Works As Expected', () => { ); // remove the alignment. - await page.click( CHANGE_ALIGNMENT_BUTTON_SELECTOR ); + await clickBlockToolbarButton( 'Change alignment' ); await ( await page.$x( BUTTON_XPATH ) )[ 0 ].click(); // verify no alignment button is in pressed state. - await page.click( CHANGE_ALIGNMENT_BUTTON_SELECTOR ); + await clickBlockToolbarButton( 'Change alignment' ); pressedButtons = await page.$$( BUTTON_PRESSED_SELECTOR ); expect( pressedButtons ).toHaveLength( 0 ); @@ -139,7 +124,7 @@ describe( 'Align Hook Works As Expected', () => { ); // verify no alignment button is in pressed state after parsing the block. - await page.click( CHANGE_ALIGNMENT_BUTTON_SELECTOR ); + await clickBlockToolbarButton( 'Change alignment' ); pressedButtons = await page.$$( BUTTON_PRESSED_SELECTOR ); expect( pressedButtons ).toHaveLength( 0 ); } ); @@ -149,6 +134,8 @@ describe( 'Align Hook Works As Expected', () => { const BLOCK_NAME = 'Test No Alignment Set'; it( 'Shows no alignment buttons on the alignment toolbar', async () => { await insertBlock( BLOCK_NAME ); + const CHANGE_ALIGNMENT_BUTTON_SELECTOR = + '.block-editor-block-toolbar .components-dropdown-menu__toggle[aria-label="Change alignment"]'; const changeAlignmentButton = await page.$( CHANGE_ALIGNMENT_BUTTON_SELECTOR ); @@ -205,10 +192,7 @@ describe( 'Align Hook Works As Expected', () => { it( 'Applies the selected alignment by default', async () => { await insertBlock( BLOCK_NAME ); // verify the correct alignment button is pressed - const element = await page.waitForSelector( - CHANGE_ALIGNMENT_BUTTON_SELECTOR - ); - await element.click(); + await clickBlockToolbarButton( 'Change alignment' ); const selectedAlignmentControls = await page.$x( SELECTED_ALIGNMENT_CONTROL_SELECTOR ); @@ -225,10 +209,7 @@ describe( 'Align Hook Works As Expected', () => { it( 'Can remove the default alignment and the align attribute equals none but alignnone class is not applied', async () => { await insertBlock( BLOCK_NAME ); // remove the alignment. - const element = await page.waitForSelector( - CHANGE_ALIGNMENT_BUTTON_SELECTOR - ); - await element.click(); + await clickBlockToolbarButton( 'Change alignment' ); const [ selectedAlignmentControl ] = await page.$x( SELECTED_ALIGNMENT_CONTROL_SELECTOR ); diff --git a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js b/packages/e2e-tests/specs/editor/various/adding-blocks.test.js index cb92fb24bae2f..370b945f9059c 100644 --- a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/adding-blocks.test.js @@ -248,13 +248,12 @@ describe( 'adding blocks', () => { ); await browseAll.click(); const inserterMenuInputSelector = - '.block-editor-inserter__menu .block-editor-inserter__search-input'; - await page.waitForSelector( inserterMenuInputSelector ); - const inserterMenuSearchInput = await page.$( + '.edit-post-layout__inserter-panel .block-editor-inserter__search-input'; + const inserterMenuSearchInput = await page.waitForSelector( inserterMenuInputSelector ); inserterMenuSearchInput.type( 'cover' ); - const coverBlock = await page.$( + const coverBlock = await page.waitForSelector( '.block-editor-block-types-list .editor-block-list-item-cover' ); await coverBlock.click(); diff --git a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js index d0b73c6259749..99a9be8268fe1 100644 --- a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js @@ -23,7 +23,6 @@ const navigateToContentEditorTop = async () => { // Use 'Ctrl+`' to return to the top of the editor await pressKeyWithModifier( 'ctrl', '`' ); await pressKeyWithModifier( 'ctrl', '`' ); - await pressKeyWithModifier( 'ctrl', '`' ); }; const tabThroughParagraphBlock = async ( paragraphText ) => { diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index 6c178f193ba16..b77fdc567814f 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -20,8 +20,10 @@ import { __experimentalToolbarItem as ToolbarItem, } from '@wordpress/components'; import { plus } from '@wordpress/icons'; +import { useRef } from '@wordpress/element'; function HeaderToolbar() { + const inserterButton = useRef(); const { setIsInserterOpened } = useDispatch( 'core/edit-post' ); const { hasFixedToolbar, @@ -72,11 +74,22 @@ function HeaderToolbar() { aria-label={ toolbarAriaLabel } > setIsInserterOpened( ! isInserterOpened ) } + onMouseDown={ ( event ) => { + event.preventDefault(); + } } + onClick={ () => { + if ( isInserterOpened ) { + // Focusing the inserter button closes the inserter popover + inserterButton.current.focus(); + } else { + setIsInserterOpened( true ); + } + } } disabled={ ! isInserterEnabled } icon={ plus } label={ _x( diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 11182f3e4a93d..9f5831b37de42 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -50,6 +50,7 @@ import SettingsSidebar from '../sidebar/settings-sidebar'; import MetaBoxes from '../meta-boxes'; import WelcomeGuide from '../welcome-guide'; import ActionsPanel from './actions-panel'; +import PopoverWrapper from './popover-wrapper'; const interfaceLabels = { leftSidebar: __( 'Block library' ), @@ -178,29 +179,36 @@ function Layout() { leftSidebar={ mode === 'visual' && isInserterOpened && ( -
-
-
-
- { - if ( isMobileViewport ) { - setIsInserterOpened( false ); + setIsInserterOpened( false ) } + > +
+
+
+
+ + showInserterHelpPanel + onSelect={ () => { + if ( isMobileViewport ) { + setIsInserterOpened( + false + ); + } + } } + /> +
-
+ ) } sidebar={ diff --git a/packages/edit-post/src/components/layout/popover-wrapper.js b/packages/edit-post/src/components/layout/popover-wrapper.js new file mode 100644 index 0000000000000..375ba0ace1179 --- /dev/null +++ b/packages/edit-post/src/components/layout/popover-wrapper.js @@ -0,0 +1,57 @@ +/** + * WordPress dependencies + */ +import { + withConstrainedTabbing, + withFocusReturn, + withFocusOutside, +} from '@wordpress/components'; +import { Component } from '@wordpress/element'; +import { ESCAPE } from '@wordpress/keycodes'; + +function stopPropagation( event ) { + event.stopPropagation(); +} + +const DetectOutside = withFocusOutside( + class extends Component { + handleFocusOutside( event ) { + this.props.onFocusOutside( event ); + } + + render() { + return this.props.children; + } + } +); + +const FocusManaged = withConstrainedTabbing( + withFocusReturn( ( { children } ) => children ) +); + +export default function PopoverWrapper( { onClose, children, className } ) { + // Event handlers + const maybeClose = ( event ) => { + // Close on escape + if ( event.keyCode === ESCAPE && onClose ) { + event.stopPropagation(); + onClose(); + } + }; + + // Disable reason: this stops certain events from propagating outside of the component. + // - onMouseDown is disabled as this can cause interactions with other DOM elements + /* eslint-disable jsx-a11y/no-static-element-interactions */ + return ( +
+ + { children } + +
+ ); + /* eslint-enable jsx-a11y/no-static-element-interactions */ +} diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index 741f4d095de89..8bbae2f3254b8 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -112,6 +112,17 @@ background-color: $light-gray-700; } +// Ideally we don't need all these nested divs. +// Removing these requires a refactoring of the different a11y HoCs. +.edit-post-layout__inserter-panel-popover-wrapper { + &, + & > div, + & > div > div, + & > div > div > div { + height: 100%; + } +} + .edit-post-layout__inserter-panel { height: 100%; display: flex;