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 && ( ) } { ( shouldShowContextualToolbar || isForcingContextualToolbar.current ) && ( @@ -522,6 +556,7 @@ function BlockListBlock( { /> ) } { + ! isNavigationMode && ! shouldShowContextualToolbar && isSelected && ! hasFixedToolbar && @@ -604,6 +639,7 @@ const applyWithSelect = withSelect( getBlockIndex, getBlockOrder, __unstableGetBlockWithoutInnerBlocks, + isNavigationMode, } = select( 'core/block-editor' ); const block = __unstableGetBlockWithoutInnerBlocks( clientId ); const isSelected = isBlockSelected( clientId ); @@ -637,6 +673,7 @@ const applyWithSelect = withSelect( isFocusMode: focusMode && isLargeViewport, hasFixedToolbar: hasFixedToolbar && isLargeViewport, isLast: index === blockOrder.length - 1, + isNavigationMode: isNavigationMode(), isRTL, // Users of the editor.BlockListBlock filter used to be able to access the block prop @@ -664,6 +701,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { mergeBlocks, replaceBlocks, toggleSelection, + setNavigationMode, } = dispatch( 'core/block-editor' ); return { @@ -737,6 +775,9 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { toggleSelection( selectionEnabled ) { toggleSelection( selectionEnabled ); }, + enableNavigationMode() { + setNavigationMode( true ); + }, }; } ); diff --git a/packages/block-editor/src/components/block-list/breadcrumb.js b/packages/block-editor/src/components/block-list/breadcrumb.js index a0eb3a385407c..81f05b40bf708 100644 --- a/packages/block-editor/src/components/block-list/breadcrumb.js +++ b/packages/block-editor/src/components/block-list/breadcrumb.js @@ -1,10 +1,9 @@ /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; -import { Toolbar } from '@wordpress/components'; -import { withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { Toolbar, Button } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { forwardRef } from '@wordpress/element'; /** * Internal dependencies @@ -17,62 +16,31 @@ import BlockTitle from '../block-title'; * the root block. * * @param {string} props.clientId Client ID of block. - * @param {string} props.rootClientId Client ID of block's root. - * @param {Function} props.selectRootBlock Callback to select root block. + * @return {WPElement} Block Breadcrumb. */ -export class BlockBreadcrumb extends Component { - constructor() { - super( ...arguments ); - this.state = { - isFocused: false, +const BlockBreadcrumb = forwardRef( ( { clientId }, ref ) => { + const { setNavigationMode } = useDispatch( 'core/block-editor' ); + const { rootClientId } = useSelect( ( select ) => { + return { + rootClientId: select( 'core/block-editor' ).getBlockRootClientId( clientId ), }; - this.onFocus = this.onFocus.bind( this ); - this.onBlur = this.onBlur.bind( this ); - } - - onFocus( event ) { - this.setState( { - isFocused: true, - } ); - - // This is used for improved interoperability - // with the block's `onFocus` handler which selects the block, thus conflicting - // with the intention to select the root block. - event.stopPropagation(); - } - - onBlur() { - this.setState( { - isFocused: false, - } ); - } - - render() { - const { clientId, rootClientId } = this.props; - - return ( -
- - { rootClientId && ( - <> - - - - ) } + } ); + + return ( +
+ + { rootClientId && ( + <> + + + + ) } +
- ); - } -} - -export default compose( [ - withSelect( ( select, ownProps ) => { - const { getBlockRootClientId } = select( 'core/block-editor' ); - const { clientId } = ownProps; + +
+
+ ); +} ); - return { - rootClientId: getBlockRootClientId( clientId ), - }; - } ), -] )( BlockBreadcrumb ); +export default BlockBreadcrumb; diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index 909d4bc7a1380..5e1ef9da6eeac 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -146,10 +146,19 @@ } } } + + &.is-navigate-mode > .block-editor-block-list__block-edit::before { + border-color: $blue-medium-focus; + box-shadow: inset $block-left-border-width 0 0 0 $blue-medium-focus; + + @include break-small() { + box-shadow: -$block-left-border-width 0 0 0 $blue-medium-focus; + } + } } // Hover style. - &.is-hovered > .block-editor-block-list__block-edit::before { + &.is-hovered:not(.is-navigate-mode) > .block-editor-block-list__block-edit::before { box-shadow: -$block-left-border-width 0 0 0 $dark-opacity-light-500; .is-dark-theme & { @@ -1042,17 +1051,19 @@ padding: 4px 4px; background: $light-gray-500; color: $dark-gray-900; + transition: box-shadow 0.1s linear; + @include reduce-motion("transition"); + + .components-button { + font-size: inherit; + line-height: inherit; + padding: 0; + } .is-dark-theme & { background: $dark-gray-600; color: $white; } - - // Animate in - .block-editor-block-list__block:hover & { - opacity: 0; - @include edit-post__fade-in-animation(60ms, 0.5s); - } } // Position this above the toolbar of parent blocks. @@ -1070,6 +1081,38 @@ left: auto; right: 0; } + + // In navigation mode, this should appear similarly to the block toolbar. + .is-navigate-mode & { + + // Position in the top left of the border. + left: -$block-padding; + top: -$block-toolbar-height - $block-padding; + + .components-toolbar { + background: $white; + border: $border-width solid $blue-medium-focus; + border-left: none; + box-shadow: inset $block-left-border-width 0 0 0 $blue-medium-focus; + height: $block-toolbar-height + $border-width; + font-size: $default-font-size; + line-height: $block-toolbar-height - $grid-size; + padding-left: $grid-size; + padding-right: $grid-size; + + .components-button { + box-shadow: none; + } + + .is-dark-theme & { + border-color: $light-opacity-light-800; + } + + @include break-small() { + box-shadow: -$block-left-border-width 0 0 0 $blue-medium-focus; + } + } + } } .block-editor-block-list__descendant-arrow::before { diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index 21c6b5dd841e4..26984e217f3d0 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { cond, matchesProperty, omit } from 'lodash'; +import { omit } from 'lodash'; /** * WordPress dependencies @@ -9,24 +9,13 @@ import { cond, matchesProperty, omit } from 'lodash'; import { NavigableMenu, KeyboardShortcuts } from '@wordpress/components'; import { Component, createRef } from '@wordpress/element'; import { focus } from '@wordpress/dom'; -import { ESCAPE } from '@wordpress/keycodes'; - -/** - * Browser dependencies - */ - -const { Node, getSelection } = window; class NavigableToolbar extends Component { constructor() { super( ...arguments ); this.focusToolbar = this.focusToolbar.bind( this ); - this.focusSelection = this.focusSelection.bind( this ); - this.switchOnKeyDown = cond( [ - [ matchesProperty( [ 'keyCode' ], ESCAPE ), this.focusSelection ], - ] ); this.toolbar = createRef(); } @@ -37,30 +26,6 @@ class NavigableToolbar extends Component { } } - /** - * Programmatically shifts focus to the element where the current selection - * exists, if there is a selection. - */ - focusSelection() { - // Ensure that a selection exists. - const selection = getSelection(); - if ( ! selection ) { - return; - } - - // Focus node may be a text node, which cannot be focused directly. - // Find its parent element instead. - const { focusNode } = selection; - let focusElement = focusNode; - if ( focusElement.nodeType !== Node.ELEMENT_NODE ) { - focusElement = focusElement.parentElement; - } - - if ( focusElement ) { - focusElement.focus(); - } - } - componentDidMount() { if ( this.props.focusOnMount ) { this.focusToolbar(); diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index f4c4844f5bf72..cbc531ef754a8 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -6,7 +6,7 @@ import { overEvery, find, findLast, reverse, first, last } from 'lodash'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { Component, createRef } from '@wordpress/element'; import { computeCaretRect, focus, @@ -17,7 +17,7 @@ import { placeCaretAtVerticalEdge, isEntirelySelected, } from '@wordpress/dom'; -import { UP, DOWN, LEFT, RIGHT, isKeyboardEvent } from '@wordpress/keycodes'; +import { UP, DOWN, LEFT, RIGHT, TAB, isKeyboardEvent } from '@wordpress/keycodes'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -78,7 +78,7 @@ class WritingFlow extends Component { this.onKeyDown = this.onKeyDown.bind( this ); this.bindContainer = this.bindContainer.bind( this ); - this.clearVerticalRect = this.clearVerticalRect.bind( this ); + this.onMouseDown = this.onMouseDown.bind( this ); this.focusLastTextField = this.focusLastTextField.bind( this ); /** @@ -89,14 +89,28 @@ class WritingFlow extends Component { * @type {?DOMRect} */ this.verticalRect = null; + + /** + * Reference of the writing flow appender element. + * The reference is used to focus the first tabbable element after the block list + * once we hit `tab` on the last block in navigation mode. + */ + this.appender = createRef(); } bindContainer( ref ) { this.container = ref; } - clearVerticalRect() { + onMouseDown() { this.verticalRect = null; + this.disableNavigationMode(); + } + + disableNavigationMode() { + if ( this.props.isNavigationMode ) { + this.props.disableNavigationMode(); + } } /** @@ -224,8 +238,10 @@ class WritingFlow extends Component { hasMultiSelection, onMultiSelect, blocks, + selectedBlockClientId, selectionBeforeEndClientId, selectionAfterEndClientId, + isNavigationMode, } = this.props; const { keyCode, target } = event; @@ -233,6 +249,7 @@ class WritingFlow extends Component { const isDown = keyCode === DOWN; const isLeft = keyCode === LEFT; const isRight = keyCode === RIGHT; + const isTab = keyCode === TAB; const isReverse = isUp || isLeft; const isHorizontal = isLeft || isRight; const isVertical = isUp || isDown; @@ -241,6 +258,28 @@ class WritingFlow extends Component { const hasModifier = isShift || event.ctrlKey || event.altKey || event.metaKey; const isNavEdge = isVertical ? isVerticalEdge : isHorizontalEdge; + // In navigation mode, tab and arrows navigate from block to block. + if ( isNavigationMode ) { + const navigateUp = ( isTab && isShift ) || isUp; + const navigateDown = ( isTab && ! isShift ) || isDown; + const focusedBlockUid = navigateUp ? selectionBeforeEndClientId : selectionAfterEndClientId; + + if ( + ( navigateDown || navigateUp ) && + focusedBlockUid + ) { + event.preventDefault(); + this.props.onSelectBlock( focusedBlockUid ); + } + + // Special case when reaching the end of the blocks (navigate to the next tabbable outside of the writing flow) + if ( navigateDown && selectedBlockClientId && ! selectionAfterEndClientId && [ UP, DOWN ].indexOf( keyCode ) === -1 ) { + this.props.clearSelectedBlock(); + this.appender.current.focus(); + } + return; + } + // When presing any key other than up or down, the initial vertical // position must ALWAYS be reset. The vertical position is saved so it // can be restored as well as possible on sebsequent vertical arrow key @@ -356,11 +395,12 @@ class WritingFlow extends Component {
{ children }
{ - const { multiSelect, selectBlock } = dispatch( 'core/block-editor' ); + const { multiSelect, selectBlock, setNavigationMode, clearSelectedBlock } = dispatch( 'core/block-editor' ); return { onMultiSelect: multiSelect, onSelectBlock: selectBlock, + disableNavigationMode: () => setNavigationMode( false ), + clearSelectedBlock, }; } ), ] )( WritingFlow ); diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 4dafaa291fcf3..c9d02e0b200a2 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -705,3 +705,16 @@ export function __unstableMarkLastChangeAsPersistent() { return { type: 'MARK_LAST_CHANGE_AS_PERSISTENT' }; } +/** + * Returns an action object used to enable or disable the navigation mode. + * + * @param {string} isNavigationMode Enable/Disable navigation mode. + * + * @return {Object} Action object + */ +export function setNavigationMode( isNavigationMode = true ) { + return { + type: 'SET_NAVIGATION_MODE', + isNavigationMode, + }; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index df81bd5df22f0..3ef595d48c9a2 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1207,6 +1207,22 @@ export const blockListSettings = ( state = {}, action ) => { return state; }; +/** + * Reducer returning whether the navigation mode is enabled or not. + * + * @param {string} state Current state. + * @param {Object} action Dispatched action. + * + * @return {string} Updated state. + */ +export function isNavigationMode( state = true, action ) { + if ( action.type === 'SET_NAVIGATION_MODE' ) { + return action.isNavigationMode; + } + + return state; +} + /** * Reducer return an updated state representing the most recent block attribute * update. The state is structured as an object where the keys represent the @@ -1247,4 +1263,5 @@ export default combineReducers( { settings, preferences, lastBlockAttributesChange, + isNavigationMode, } ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 5e8771ff94935..9fbc0c68e0162 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1405,3 +1405,14 @@ export function __experimentalGetLastBlockAttributeChanges( state ) { function getReusableBlocks( state ) { return get( state, [ 'settings', '__experimentalReusableBlocks' ], EMPTY_ARRAY ); } + +/** + * Returns whether the navigation mode is enabled. + * + * @param {Object} state Editor state. + * + * @return {boolean} Is navigation mode enabled. + */ +export function isNavigationMode( state ) { + return state.isNavigationMode; +} diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index d8f14e5e6c343..d932adca1f1ad 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -137,6 +137,14 @@ _Parameters_ - _slug_ `string`: Plugin slug. +# **disableNavigationMode** + +Triggers edit mode if not already active. + +_Returns_ + +- `Promise`: Promise resolving after enabling the keyboard edit mode. + # **disablePrePublishChecks** Disables Pre-publish checks. diff --git a/packages/e2e-test-utils/src/click-block-toolbar-button.js b/packages/e2e-test-utils/src/click-block-toolbar-button.js index 679727189f741..fcdd9d2af91ed 100644 --- a/packages/e2e-test-utils/src/click-block-toolbar-button.js +++ b/packages/e2e-test-utils/src/click-block-toolbar-button.js @@ -7,8 +7,9 @@ export async function clickBlockToolbarButton( buttonAriaLabel ) { const BLOCK_TOOLBAR_SELECTOR = '.block-editor-block-toolbar'; const BUTTON_SELECTOR = `${ BLOCK_TOOLBAR_SELECTOR } button[aria-label="${ buttonAriaLabel }"]`; if ( await page.$( BLOCK_TOOLBAR_SELECTOR ) === null ) { - // 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.waitForSelector( BUTTON_SELECTOR ); await page.click( BUTTON_SELECTOR ); diff --git a/packages/e2e-test-utils/src/create-new-post.js b/packages/e2e-test-utils/src/create-new-post.js index 91a95bf5216a4..ddc2ad64ea6c1 100644 --- a/packages/e2e-test-utils/src/create-new-post.js +++ b/packages/e2e-test-utils/src/create-new-post.js @@ -7,6 +7,7 @@ import { addQueryArgs } from '@wordpress/url'; * Internal dependencies */ import { visitAdminPage } from './visit-admin-page'; +import { disableNavigationMode } from './keyboard-mode'; /** * Creates new post. @@ -36,4 +37,6 @@ export async function createNewPost( { if ( enableTips ) { await page.reload(); } + + await disableNavigationMode(); } diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index 64e0a22a585b1..c6839ac84f1d5 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -42,6 +42,7 @@ export { selectBlockByClientId } from './select-block-by-client-id'; export { setBrowserViewport } from './set-browser-viewport'; export { setPostContent } from './set-post-content'; export { switchEditorModeTo } from './switch-editor-mode-to'; +export { disableNavigationMode } from './keyboard-mode'; export { switchUserToAdmin } from './switch-user-to-admin'; export { switchUserToTest } from './switch-user-to-test'; export { toggleScreenOption } from './toggle-screen-option'; diff --git a/packages/e2e-test-utils/src/keyboard-mode.js b/packages/e2e-test-utils/src/keyboard-mode.js new file mode 100644 index 0000000000000..8928c220b28f6 --- /dev/null +++ b/packages/e2e-test-utils/src/keyboard-mode.js @@ -0,0 +1,12 @@ +/** + * Triggers edit mode if not already active. + * + * @return {Promise} Promise resolving after enabling the keyboard edit mode. + */ +export async function disableNavigationMode() { + const focusedElement = await page.$( ':focus' ); + await page.click( '.editor-post-title' ); + if ( focusedElement ) { + await focusedElement.focus(); + } +} diff --git a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap index f5e1b7e1afd79..77f59df47a901 100644 --- a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`adding blocks Should navigate inner blocks with arrow keys 1`] = ` +exports[`Writing Flow Should navigate inner blocks with arrow keys 1`] = ` "

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

" `; -exports[`adding blocks should insert line break at end and continue writing 1`] = ` +exports[`Writing Flow should insert line break at end and continue writing 1`] = ` "

a
b

" `; -exports[`adding blocks should insert line break at start 1`] = ` +exports[`Writing Flow should insert line break at start 1`] = ` "


a

" `; -exports[`adding blocks should insert line break in empty container 1`] = ` +exports[`Writing Flow should insert line break in empty container 1`] = ` "


" `; -exports[`adding blocks should insert line break mid text 1`] = ` +exports[`Writing Flow should insert line break mid text 1`] = ` "

a
b

" `; -exports[`adding blocks should merge forwards 1`] = ` +exports[`Writing Flow should merge forwards 1`] = ` "

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(); } );