diff --git a/packages/block-editor/src/components/block-tools/block-selection-button.js b/packages/block-editor/src/components/block-tools/block-selection-button.js index d4ec0f8cf79fb6..805e41c580f950 100644 --- a/packages/block-editor/src/components/block-tools/block-selection-button.js +++ b/packages/block-editor/src/components/block-tools/block-selection-button.js @@ -9,7 +9,7 @@ import clsx from 'clsx'; import { dragHandle, trash } from '@wordpress/icons'; import { Button, Flex, FlexItem, ToolbarButton } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useRef } from '@wordpress/element'; +import { forwardRef, useEffect } from '@wordpress/element'; import { BACKSPACE, DELETE, @@ -48,10 +48,11 @@ import Shuffle from '../block-toolbar/shuffle'; * * @param {string} props Component props. * @param {string} props.clientId Client ID of block. + * @param {Object} ref Reference to the component. * * @return {Component} The component to be rendered. */ -function BlockSelectionButton( { clientId, rootClientId } ) { +function BlockSelectionButton( { clientId, rootClientId }, ref ) { const selected = useSelect( ( select ) => { const { @@ -125,7 +126,6 @@ function BlockSelectionButton( { clientId, rootClientId } ) { canMove, } = selected; const { setNavigationMode, removeBlock } = useDispatch( blockEditorStore ); - const ref = useRef(); // Focus the breadcrumb in navigation mode. useEffect( () => { @@ -164,11 +164,6 @@ function BlockSelectionButton( { clientId, rootClientId } ) { const isEnter = keyCode === ENTER; const isSpace = keyCode === SPACE; const isShift = event.shiftKey; - if ( isEscape && editorMode === 'navigation' ) { - setNavigationMode( false ); - event.preventDefault(); - return; - } if ( keyCode === BACKSPACE || keyCode === DELETE ) { removeBlock( clientId ); @@ -368,4 +363,4 @@ function BlockSelectionButton( { clientId, rootClientId } ) { ); } -export default BlockSelectionButton; +export default forwardRef( BlockSelectionButton ); diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js index 0ae67e1be0001e..ae03bdb4f51647 100644 --- a/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js +++ b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js @@ -3,6 +3,11 @@ */ import clsx from 'clsx'; +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + /** * Internal dependencies */ @@ -11,10 +16,7 @@ import { PrivateBlockPopover } from '../block-popover'; import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; import useSelectedBlockToolProps from './use-selected-block-tool-props'; -export default function BlockToolbarBreadcrumb( { - clientId, - __unstableContentRef, -} ) { +function BlockToolbarBreadcrumb( { clientId, __unstableContentRef }, ref ) { const { capturingClientId, isInsertionPointVisible, @@ -38,9 +40,12 @@ export default function BlockToolbarBreadcrumb( { { ...popoverProps } > ); } + +export default forwardRef( BlockToolbarBreadcrumb ); diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index ad744a81cca623..0e89e7991320e0 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -81,6 +81,7 @@ export default function BlockTools( { } = useShowBlockTools(); const { + clearSelectedBlock, duplicateBlocks, removeBlocks, replaceBlocks, @@ -92,6 +93,8 @@ export default function BlockTools( { expandBlock, } = unlock( useDispatch( blockEditorStore ) ); + const blockSelectionButtonRef = useRef(); + function onKeyDown( event ) { if ( event.defaultPrevented ) { return; @@ -152,6 +155,39 @@ export default function BlockTools( { // block so that focus is directed back to the beginning of the selection. // In effect, to the user this feels like deselecting the multi-selection. selectBlock( clientIds[ 0 ] ); + } else if ( + clientIds.length === 1 && + event.target === blockSelectionButtonRef?.current + ) { + event.preventDefault(); + clearSelectedBlock(); + // If there are multiple editors, we need to find the iframe that contains our contentRef to make sure + // we're focusing the region that contains this editor. + const editorCanvas = + Array.from( + document + .querySelectorAll( 'iframe[name="editor-canvas"]' ) + .values() + ).find( ( iframe ) => { + // Find the iframe that contains our contentRef + const iframeDocument = + iframe.contentDocument || + iframe.contentWindow.document; + + return ( + iframeDocument === + __unstableContentRef.current.ownerDocument + ); + } ) ?? __unstableContentRef.current; + + // The region is provivided by the editor, not the block-editor. + // We should send focus to the region if one is available to reuse the + // same interface for navigating landmarks. If no region is available, + // use the canvas instead. + const focusableWrapper = + editorCanvas?.closest( '[role="region"]' ) ?? editorCanvas; + + focusableWrapper.focus(); } } else if ( isMatch( 'core/block-editor/collapse-list-view', event ) ) { // If focus is currently within a text field, such as a rich text block or other editable field, @@ -182,7 +218,6 @@ export default function BlockTools( { } } } - const blockToolbarRef = usePopoverScroll( __unstableContentRef ); const blockToolbarAfterRef = usePopoverScroll( __unstableContentRef ); @@ -213,6 +248,7 @@ export default function BlockTools( { { showBreadcrumb && ( diff --git a/packages/components/src/higher-order/navigate-regions/style.scss b/packages/components/src/higher-order/navigate-regions/style.scss index b3a4a0c1a9d1b5..5c3767e310b8f4 100644 --- a/packages/components/src/higher-order/navigate-regions/style.scss +++ b/packages/components/src/higher-order/navigate-regions/style.scss @@ -1,22 +1,35 @@ // Allow the position to be easily overridden to e.g. fixed. + +@mixin region-selection-outline { + outline: 4px solid $components-color-accent; + outline-offset: -4px; +} + +@mixin region-selection-focus { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + content: ""; + pointer-events: none; + @include region-selection-outline; + z-index: z-index(".is-focusing-regions {region} :focus::after"); +} + [role="region"] { position: relative; + + // Handles the focus when we programatically send focus to this region + &.interface-interface-skeleton__content:focus-visible::after { + @include region-selection-focus; + } } .is-focusing-regions { [role="region"]:focus::after { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - content: ""; - pointer-events: none; - outline: 4px solid $components-color-accent; - outline-offset: -4px; - z-index: z-index(".is-focusing-regions {region} :focus::after"); + @include region-selection-focus; } - // Fixes for edge cases. // Some of the regions are currently used for layout purposes as 'interface skeleton' // items. When they're absolutely positioned or when they contain absolutely @@ -33,7 +46,6 @@ .interface-interface-skeleton__actions .editor-layout__toggle-publish-panel, .interface-interface-skeleton__actions .editor-layout__toggle-entities-saved-states-panel, .editor-post-publish-panel { - outline: 4px solid $components-color-accent; - outline-offset: -4px; + @include region-selection-outline; } } diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js index 1af46a80896f07..bd1552ad4cb66a 100644 --- a/test/e2e/specs/editor/various/writing-flow.spec.js +++ b/test/e2e/specs/editor/various/writing-flow.spec.js @@ -958,7 +958,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { ` ); } ); - test( 'escape should toggle between edit and navigation modes', async ( { + test( 'escape should set select mode and then focus the canvas', async ( { page, writingFlowUtils, } ) => { @@ -975,15 +975,13 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { .poll( writingFlowUtils.getActiveBlockName ) .toBe( 'core/paragraph' ); - // Second escape Toggles back to Edit Mode + // Second escape should send focus to the canvas await page.keyboard.press( 'Escape' ); + // The navigation button should be hidden. await expect( navigationButton ).toBeHidden(); - const blockToolbar = page.getByLabel( 'Block tools' ); - - await expect( blockToolbar ).toBeVisible(); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/paragraph' ); + await expect( + page.getByRole( 'region', { name: 'Editor content' } ) + ).toBeFocused(); } ); // Checks for regressions of https://github.com/WordPress/gutenberg/issues/40091.