diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md
index 1b42d76df3e3b..005c474f570f6 100644
--- a/docs/designers-developers/developers/data/data-core-block-editor.md
+++ b/docs/designers-developers/developers/data/data-core-block-editor.md
@@ -761,6 +761,18 @@ _Returns_
- `boolean`: True if multi-selecting, false if not.
+# **isNavigationMode**
+
+Returns whether the navigation mode is enabled.
+
+_Parameters_
+
+- _state_ `Object`: Editor state.
+
+_Returns_
+
+- `boolean`: Is navigation mode enabled.
+
# **isSelectionEnabled**
Selector that returns if multi-selection is enabled or not.
@@ -1071,6 +1083,18 @@ _Parameters_
- _clientId_ `string`: Block client ID.
+# **setNavigationMode**
+
+Returns an action object used to enable or disable the navigation mode.
+
+_Parameters_
+
+- _isNavigationMode_ `string`: Enable/Disable navigation mode.
+
+_Returns_
+
+- `Object`: Action object
+
# **setTemplateValidity**
Returns an action object resetting the template validity.
diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js
index 890bfa4ff7aea..81ecc7b7a6f92 100644
--- a/packages/block-editor/src/components/block-list/block.js
+++ b/packages/block-editor/src/components/block-list/block.js
@@ -8,13 +8,13 @@ import { animated } from 'react-spring/web.cjs';
/**
* WordPress dependencies
*/
-import { useRef, useEffect, useState } from '@wordpress/element';
+import { useRef, useEffect, useLayoutEffect, useState } from '@wordpress/element';
import {
focus,
isTextField,
placeCaretAtHorizontalEdge,
} from '@wordpress/dom';
-import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes';
+import { BACKSPACE, DELETE, ENTER, ESCAPE } from '@wordpress/keycodes';
import {
getBlockType,
getSaveElement,
@@ -101,6 +101,8 @@ function BlockListBlock( {
onSelectionStart,
animateOnChange,
enableAnimation,
+ isNavigationMode,
+ enableNavigationMode,
} ) {
// Random state used to rerender the component if needed, ideally we don't need this
const [ , updateRerenderState ] = useState( {} );
@@ -118,6 +120,8 @@ function BlockListBlock( {
// Hovered area of the block
const hoverArea = useHoveredArea( wrapper );
+ const breadcrumb = useRef();
+
// Keep track of touchstart to disable hover on iOS
const hadTouchStart = useRef( false );
const onTouchStart = () => {
@@ -215,6 +219,11 @@ function BlockListBlock( {
return;
}
+ if ( isNavigationMode ) {
+ breadcrumb.current.focus();
+ return;
+ }
+
// Find all tabbables within node.
const textInputs = focus.tabbable
.find( blockNodeRef.current )
@@ -254,6 +263,18 @@ function BlockListBlock( {
// Block Reordering animation
const animationStyle = useMovingAnimation( wrapper, isSelected || isPartOfMultiSelection, enableAnimation, animateOnChange );
+ // Focus the breadcrumb if the wrapper is focused on navigation mode.
+ // Focus the first editable or the wrapper if edit mode.
+ useLayoutEffect( () => {
+ if ( isSelected ) {
+ if ( isNavigationMode ) {
+ breadcrumb.current.focus();
+ } else {
+ focusTabbable( true );
+ }
+ }
+ }, [ isSelected, isNavigationMode ] );
+
// Other event handlers
/**
@@ -275,32 +296,43 @@ function BlockListBlock( {
*
* @param {KeyboardEvent} event Keydown event.
*/
- const deleteOrInsertAfterWrapper = ( event ) => {
+ const onKeyDown = ( event ) => {
const { keyCode, target } = event;
- // These block shortcuts should only trigger if the wrapper of the block is selected
- // And when it's not a multi-selection to avoid conflicting with RichText/Inputs and multiselection.
- if (
- ! isSelected ||
- target !== wrapper.current ||
- isLocked
- ) {
- return;
- }
+ // ENTER/BACKSPACE Shortcuts are only available if the wrapper is focused
+ // and the block is not locked.
+ const canUseShortcuts = (
+ isSelected &&
+ ! isLocked &&
+ ( target === wrapper.current || target === breadcrumb.current )
+ );
+ const isEditMode = ! isNavigationMode;
switch ( keyCode ) {
case ENTER:
- // Insert default block after current block if enter and event
- // not already handled by descendant.
- onInsertDefaultBlockAfter();
- event.preventDefault();
+ if ( canUseShortcuts && isEditMode ) {
+ // Insert default block after current block if enter and event
+ // not already handled by descendant.
+ onInsertDefaultBlockAfter();
+ event.preventDefault();
+ }
break;
-
case BACKSPACE:
case DELETE:
- // Remove block on backspace.
- onRemove( clientId );
- event.preventDefault();
+ if ( canUseShortcuts ) {
+ // Remove block on backspace.
+ onRemove( clientId );
+ event.preventDefault();
+ }
+ break;
+ case ESCAPE:
+ if (
+ isSelected &&
+ isEditMode
+ ) {
+ enableNavigationMode();
+ wrapper.current.focus();
+ }
break;
}
};
@@ -357,8 +389,8 @@ function BlockListBlock( {
// If the block is selected and we're typing the block should not appear.
// Empty paragraph blocks should always show up as unselected.
- const showInserterShortcuts = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid;
- const showEmptyBlockSideInserter = ( isSelected || isHovered || isLast ) && isEmptyDefaultBlock && isValid;
+ const showInserterShortcuts = ! isNavigationMode && ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid;
+ const showEmptyBlockSideInserter = ! isNavigationMode && ( isSelected || isHovered || isLast ) && isEmptyDefaultBlock && isValid;
const shouldAppearSelected =
! isFocusMode &&
! showEmptyBlockSideInserter &&
@@ -371,20 +403,23 @@ function BlockListBlock( {
! isEmptyDefaultBlock;
// We render block movers and block settings to keep them tabbale even if hidden
const shouldRenderMovers =
+ ! isNavigationMode &&
( isSelected || hoverArea === ( isRTL ? 'right' : 'left' ) ) &&
! showEmptyBlockSideInserter &&
! isPartOfMultiSelection &&
! isTypingWithinBlock;
const shouldShowBreadcrumb =
- ! isFocusMode && isHovered && ! isEmptyDefaultBlock;
+ ( isSelected && isNavigationMode ) ||
+ ( ! isNavigationMode && ! isFocusMode && isHovered && ! isEmptyDefaultBlock );
const shouldShowContextualToolbar =
+ ! isNavigationMode &&
! hasFixedToolbar &&
! showEmptyBlockSideInserter &&
(
( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) ||
isFirstMultiSelected
);
- const shouldShowMobileToolbar = shouldAppearSelected;
+ const shouldShowMobileToolbar = ! isNavigationMode && shouldAppearSelected;
// Insertion point can only be made visible if the block is at the
// the extent of a multi-selection, or not in a multi-selection.
@@ -399,6 +434,7 @@ function BlockListBlock( {
{
'has-warning': ! isValid || !! hasError || isUnregisteredBlock,
'is-selected': shouldAppearSelected,
+ 'is-navigate-mode': isNavigationMode,
'is-multi-selected': isPartOfMultiSelection,
'is-hovered': shouldAppearHovered,
'is-reusable': isReusableBlock( blockType ),
@@ -464,7 +500,7 @@ function BlockListBlock( {
onTouchStart={ onTouchStart }
onFocus={ onFocus }
onClick={ onTouchStop }
- onKeyDown={ deleteOrInsertAfterWrapper }
+ onKeyDown={ onKeyDown }
tabIndex="0"
aria-label={ blockLabel }
childHandledEvents={ [ 'onDragStart', 'onMouseDown' ] }
@@ -509,9 +545,7 @@ function BlockListBlock( {
{ shouldShowBreadcrumb && (
First paragraph
@@ -24,7 +24,7 @@ exports[`adding blocks Should navigate inner blocks with arrow keys 1`] = ` " `; -exports[`adding blocks should create valid paragraph blocks when rapidly pressing Enter 1`] = ` +exports[`Writing Flow should create valid paragraph blocks when rapidly pressing Enter 1`] = ` " @@ -70,43 +70,43 @@ exports[`adding blocks should create valid paragraph blocks when rapidly pressin " `; -exports[`adding blocks should insert line break at end 1`] = ` +exports[`Writing Flow should insert line break at end 1`] = ` "a
a
b
a
a
b
123
" `; -exports[`adding blocks should navigate around inline boundaries 1`] = ` +exports[`Writing Flow should navigate around inline boundaries 1`] = ` "FirstAfter
@@ -120,19 +120,19 @@ exports[`adding blocks should navigate around inline boundaries 1`] = ` " `; -exports[`adding blocks should navigate around nested inline boundaries 1`] = ` +exports[`Writing Flow should navigate around nested inline boundaries 1`] = ` "1 2
" `; -exports[`adding blocks should navigate around nested inline boundaries 2`] = ` +exports[`Writing Flow should navigate around nested inline boundaries 2`] = ` "abc1de fg2hij
" `; -exports[`adding blocks should navigate contenteditable with padding 1`] = ` +exports[`Writing Flow should navigate contenteditable with padding 1`] = ` "1
@@ -142,7 +142,7 @@ exports[`adding blocks should navigate contenteditable with padding 1`] = ` " `; -exports[`adding blocks should navigate contenteditable with side padding 1`] = ` +exports[`Writing Flow should navigate contenteditable with side padding 1`] = ` "1
@@ -156,7 +156,7 @@ exports[`adding blocks should navigate contenteditable with side padding 1`] = ` " `; -exports[`adding blocks should navigate empty paragraph 1`] = ` +exports[`Writing Flow should navigate empty paragraph 1`] = ` "1
@@ -166,49 +166,49 @@ exports[`adding blocks should navigate empty paragraph 1`] = ` " `; -exports[`adding blocks should not create extra line breaks in multiline value 1`] = ` +exports[`Writing Flow should not create extra line breaks in multiline value 1`] = ` "" `; -exports[`adding blocks should not delete surrounding space when deleting a selected word 1`] = ` +exports[`Writing Flow should not delete surrounding space when deleting a selected word 1`] = ` "
alpha gamma
" `; -exports[`adding blocks should not delete surrounding space when deleting a selected word 2`] = ` +exports[`Writing Flow should not delete surrounding space when deleting a selected word 2`] = ` "alpha beta gamma
" `; -exports[`adding blocks should not delete surrounding space when deleting a word with Alt+Backspace 1`] = ` +exports[`Writing Flow should not delete surrounding space when deleting a word with Alt+Backspace 1`] = ` "alpha gamma
" `; -exports[`adding blocks should not delete surrounding space when deleting a word with Alt+Backspace 2`] = ` +exports[`Writing Flow should not delete surrounding space when deleting a word with Alt+Backspace 2`] = ` "alpha beta gamma
" `; -exports[`adding blocks should not delete surrounding space when deleting a word with Backspace 1`] = ` +exports[`Writing Flow should not delete surrounding space when deleting a word with Backspace 1`] = ` "1 3
" `; -exports[`adding blocks should not delete surrounding space when deleting a word with Backspace 2`] = ` +exports[`Writing Flow should not delete surrounding space when deleting a word with Backspace 2`] = ` "1 2 3
" `; -exports[`adding blocks should not prematurely multi-select 1`] = ` +exports[`Writing Flow should not prematurely multi-select 1`] = ` "1
@@ -218,7 +218,7 @@ exports[`adding blocks should not prematurely multi-select 1`] = ` " `; -exports[`adding blocks should preserve horizontal position when navigating vertically between blocks 1`] = ` +exports[`Writing Flow should preserve horizontal position when navigating vertically between blocks 1`] = ` "abc
@@ -228,7 +228,7 @@ exports[`adding blocks should preserve horizontal position when navigating verti " `; -exports[`adding blocks should remember initial vertical position 1`] = ` +exports[`Writing Flow should remember initial vertical position 1`] = ` "1x
diff --git a/packages/e2e-tests/specs/adding-inline-tokens.test.js b/packages/e2e-tests/specs/adding-inline-tokens.test.js index 5bcd55498489c..387c8e63ab6cb 100644 --- a/packages/e2e-tests/specs/adding-inline-tokens.test.js +++ b/packages/e2e-tests/specs/adding-inline-tokens.test.js @@ -18,7 +18,7 @@ import { } from '@wordpress/e2e-test-utils'; describe( 'adding inline tokens', () => { - beforeAll( async () => { + beforeEach( async () => { await createNewPost(); } ); diff --git a/packages/e2e-tests/specs/block-deletion.test.js b/packages/e2e-tests/specs/block-deletion.test.js index 4466c03783f29..a231977ed171a 100644 --- a/packages/e2e-tests/specs/block-deletion.test.js +++ b/packages/e2e-tests/specs/block-deletion.test.js @@ -64,8 +64,9 @@ describe( 'block deletion -', () => { // The blocks can't be empty to trigger the toolbar await page.keyboard.type( 'Paragraph to remove' ); - // Press Escape to show the block toolbar - await page.keyboard.press( 'Escape' ); + // Move the mouse to show the block toolbar + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); await clickOnBlockSettingsMenuRemoveBlockButton(); @@ -143,12 +144,17 @@ describe( 'block deletion -', () => { } ); describe( 'deleting all blocks', () => { - it( 'results in the default block getting selected', async () => { + beforeEach( async () => { await createNewPost(); + } ); + + it( 'results in the default block getting selected', async () => { await clickBlockAppender(); await page.keyboard.type( 'Paragraph' ); - await page.keyboard.press( 'Escape' ); + // Move the mouse to show the block toolbar + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); await clickOnBlockSettingsMenuRemoveBlockButton(); @@ -168,7 +174,6 @@ describe( 'deleting all blocks', () => { // // See: https://github.com/WordPress/gutenberg/issues/15458 // See: https://github.com/WordPress/gutenberg/pull/15543 - await createNewPost(); // Unregister default block type. This may happen if the editor is // configured to not allow the default (paragraph) block type, either diff --git a/packages/e2e-tests/specs/editor-modes.test.js b/packages/e2e-tests/specs/editor-modes.test.js index 4096338ce9f64..9fa866e893e9f 100644 --- a/packages/e2e-tests/specs/editor-modes.test.js +++ b/packages/e2e-tests/specs/editor-modes.test.js @@ -20,8 +20,9 @@ describe( 'Editing modes (visual/HTML)', () => { let visualBlock = await page.$$( '.block-editor-block-list__layout .block-editor-block-list__block .block-editor-rich-text' ); expect( visualBlock ).toHaveLength( 1 ); - // Press Escape to show the block toolbar - await page.keyboard.press( 'Escape' ); + // Move the mouse to show the block toolbar + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); // Change editing mode from "Visual" to "HTML". await clickBlockToolbarButton( 'More options' ); @@ -32,8 +33,9 @@ describe( 'Editing modes (visual/HTML)', () => { const htmlBlock = await page.$$( '.block-editor-block-list__layout .block-editor-block-list__block .block-editor-block-list__block-html-textarea' ); expect( htmlBlock ).toHaveLength( 1 ); - // Press Escape to show the block toolbar - await page.keyboard.press( 'Escape' ); + // Move the mouse to show the block toolbar + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); // Change editing mode from "HTML" back to "Visual". await clickBlockToolbarButton( 'More options' ); @@ -46,8 +48,9 @@ describe( 'Editing modes (visual/HTML)', () => { } ); it( 'should display sidebar in HTML mode', async () => { - // Press Escape to show the block toolbar - await page.keyboard.press( 'Escape' ); + // Move the mouse to show the block toolbar + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); // Change editing mode from "Visual" to "HTML". await clickBlockToolbarButton( 'More options' ); @@ -61,8 +64,9 @@ describe( 'Editing modes (visual/HTML)', () => { } ); it( 'should update HTML in HTML mode when sidebar is used', async () => { - // Press Escape to show the block toolbar - await page.keyboard.press( 'Escape' ); + // Move the mouse to show the block toolbar + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); // Change editing mode from "Visual" to "HTML". await clickBlockToolbarButton( 'More options' ); diff --git a/packages/e2e-tests/specs/links.test.js b/packages/e2e-tests/specs/links.test.js index a1d1dd3a46934..d55ec0a02e9b0 100644 --- a/packages/e2e-tests/specs/links.test.js +++ b/packages/e2e-tests/specs/links.test.js @@ -238,8 +238,9 @@ describe( 'Links', () => { // Make a collapsed selection inside the link await page.keyboard.press( 'ArrowLeft' ); await page.keyboard.press( 'ArrowRight' ); - // Press escape to show the block toolbar - await page.keyboard.press( 'Escape' ); + // Move the mouse to show the block toolbar + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); await page.click( 'button[aria-label="Edit"]' ); await waitForAutoFocus(); await page.keyboard.type( '/handbook' ); diff --git a/packages/e2e-tests/specs/navigable-toolbar.test.js b/packages/e2e-tests/specs/navigable-toolbar.test.js index e3fb884185008..1936fc5064caa 100644 --- a/packages/e2e-tests/specs/navigable-toolbar.test.js +++ b/packages/e2e-tests/specs/navigable-toolbar.test.js @@ -25,10 +25,6 @@ describe( 'block toolbar', () => { }, isUnifiedToolbar ); } ); - const isInRichTextEditable = () => page.evaluate( () => ( - document.activeElement.contentEditable === 'true' - ) ); - const isInBlockToolbar = () => page.evaluate( () => ( !! document.activeElement.closest( '.block-editor-block-toolbar' ) ) ); @@ -46,10 +42,6 @@ describe( 'block toolbar', () => { // Upward await pressKeyWithModifier( 'alt', 'F10' ); expect( await isInBlockToolbar() ).toBe( true ); - - // Downward - await page.keyboard.press( 'Escape' ); - expect( await isInRichTextEditable() ).toBe( true ); } ); } ); } ); diff --git a/packages/e2e-tests/specs/preview.test.js b/packages/e2e-tests/specs/preview.test.js index 5a1d7f4cc2754..255f6200d116a 100644 --- a/packages/e2e-tests/specs/preview.test.js +++ b/packages/e2e-tests/specs/preview.test.js @@ -14,6 +14,7 @@ import { saveDraft, clickOnMoreMenuItem, pressKeyWithModifier, + disableNavigationMode, } from '@wordpress/e2e-test-utils'; async function openPreviewPage( editorPage ) { @@ -203,6 +204,7 @@ describe( 'Preview with Custom Fields enabled', () => { beforeEach( async () => { await createNewPost(); await toggleCustomFieldsOption( true ); + await disableNavigationMode(); } ); afterEach( async () => { diff --git a/packages/e2e-tests/specs/rich-text.test.js b/packages/e2e-tests/specs/rich-text.test.js index 6dce07f78146c..a7acef1d2aeaf 100644 --- a/packages/e2e-tests/specs/rich-text.test.js +++ b/packages/e2e-tests/specs/rich-text.test.js @@ -80,10 +80,12 @@ describe( 'RichText', () => { it( 'should return focus when pressing formatting button', async () => { await clickBlockAppender(); await page.keyboard.type( 'Some ' ); - await page.keyboard.press( 'Escape' ); + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); await page.click( '[aria-label="Bold"]' ); await page.keyboard.type( 'bold' ); - await page.keyboard.press( 'Escape' ); + await page.mouse.move( 0, 0 ); + await page.mouse.move( 10, 10 ); await page.click( '[aria-label="Bold"]' ); await page.keyboard.type( '.' ); diff --git a/packages/e2e-tests/specs/undo.test.js b/packages/e2e-tests/specs/undo.test.js index 60e9ff64bfdeb..4200e31f1ca6b 100644 --- a/packages/e2e-tests/specs/undo.test.js +++ b/packages/e2e-tests/specs/undo.test.js @@ -9,6 +9,7 @@ import { selectBlockByClientId, getAllBlocks, saveDraft, + disableNavigationMode, } from '@wordpress/e2e-test-utils'; describe( 'undo', () => { @@ -79,6 +80,7 @@ describe( 'undo', () => { await page.keyboard.type( 'original' ); await saveDraft(); await page.reload(); + await disableNavigationMode(); // Issue is demonstrated by forcing state merges (multiple inputs) on // an existing text after a fresh reload. diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js index a9923f1d7b6e8..3b238abceaafc 100644 --- a/packages/e2e-tests/specs/writing-flow.test.js +++ b/packages/e2e-tests/specs/writing-flow.test.js @@ -10,7 +10,7 @@ import { insertBlock, } from '@wordpress/e2e-test-utils'; -describe( 'adding blocks', () => { +describe( 'Writing Flow', () => { beforeEach( async () => { await createNewPost(); } );