diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md
index 5a0a7339ac3fa7..cc1ee80f19420c 100644
--- a/packages/block-editor/README.md
+++ b/packages/block-editor/README.md
@@ -528,7 +528,8 @@ _Returns_
# **WritingFlow**
-Undocumented declaration.
+Handles selection and navigation across blocks. This component should be
+wrapped around BlockList.
diff --git a/packages/block-editor/src/components/block-list-appender/index.js b/packages/block-editor/src/components/block-list-appender/index.js
index 94cc4505dd0b01..14b6579d8f5a45 100644
--- a/packages/block-editor/src/components/block-list-appender/index.js
+++ b/packages/block-editor/src/components/block-list-appender/index.js
@@ -12,10 +12,13 @@ import { getDefaultBlockName } from '@wordpress/blocks';
/**
* Internal dependencies
*/
-import IgnoreNestedEvents from '../ignore-nested-events';
import DefaultBlockAppender from '../default-block-appender';
import ButtonBlockAppender from '../button-block-appender';
+function stopPropagation( event ) {
+ event.stopPropagation();
+}
+
function BlockListAppender( {
blockClientIds,
rootClientId,
@@ -51,26 +54,24 @@ function BlockListAppender( {
);
}
- // IgnoreNestedEvents is used to treat interactions within the appender as
- // subject to the same conditions as those which occur within nested blocks.
- // Notably, this effectively prevents event bubbling to block ancestors
- // which can otherwise interfere with the intended behavior of the appender
- // (e.g. focus handler on the ancestor block).
- //
- // A `tabIndex` is used on the wrapping `div` element in order to force a
- // focus event to occur when an appender `button` element is clicked. In
- // some browsers (Firefox, Safari), button clicks do not emit a focus event,
- // which could cause this event to propagate unexpectedly. The `tabIndex`
- // ensures that the interaction is captured as a focus, without also adding
- // an extra tab stop.
- //
- // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
return (
-
-
- { appender }
-
-
+
+ { appender }
+
);
}
diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js
index 2c8675ba48a5a2..76e4abf20df040 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, useLayoutEffect, useState, useCallback } from '@wordpress/element';
+import { useRef, useEffect, useLayoutEffect, useState, useCallback, useContext } from '@wordpress/element';
import {
focus,
isTextField,
placeCaretAtHorizontalEdge,
} from '@wordpress/dom';
-import { BACKSPACE, DELETE, ENTER, ESCAPE } from '@wordpress/keycodes';
+import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes';
import {
getBlockType,
getSaveElement,
@@ -45,20 +45,11 @@ import BlockHtml from './block-html';
import BlockBreadcrumb from './breadcrumb';
import BlockContextualToolbar from './block-contextual-toolbar';
import BlockInsertionPoint from './insertion-point';
-import IgnoreNestedEvents from '../ignore-nested-events';
import Inserter from '../inserter';
import { isInsideRootBlock } from '../../utils/dom';
import useMovingAnimation from './moving-animation';
import { ChildToolbar, ChildToolbarSlot } from './block-child-toolbar';
-/**
- * Prevents default dragging behavior within a block to allow for multi-
- * selection to take effect unhampered.
- *
- * @param {DragEvent} event Drag event.
- */
-const preventDrag = ( event ) => {
- event.preventDefault();
-};
+import { Context } from './root-container';
function BlockListBlock( {
mode,
@@ -94,17 +85,15 @@ function BlockListBlock( {
onRemove,
onInsertDefaultBlockAfter,
toggleSelection,
- onShiftSelection,
- onSelectionStart,
animateOnChange,
enableAnimation,
isNavigationMode,
- setNavigationMode,
isMultiSelecting,
isLargeViewport,
hasSelectedUI = true,
hasMovers = true,
} ) {
+ const onSelectionStart = useContext( Context );
// In addition to withSelect, we should favor using useSelect in this component going forward
// to avoid leaking new props to the public API (editor.BlockListBlock filter)
const { isDraggingBlocks } = useSelect( ( select ) => {
@@ -231,17 +220,6 @@ function BlockListBlock( {
// Other event handlers
- /**
- * Marks the block as selected when focused and not already selected. This
- * specifically handles the case where block does not set focus on its own
- * (via `setFocus`), typically if there is no focusable input in the block.
- */
- const onFocus = () => {
- if ( ! isSelected && ! isPartOfMultiSelection ) {
- onSelect();
- }
- };
-
/**
* Interprets keydown event intent to remove or insert after block if key
* event occurs on wrapper node. This can occur when the block has no text
@@ -253,13 +231,9 @@ function BlockListBlock( {
const onKeyDown = ( event ) => {
const { keyCode, target } = event;
- // 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 )
- );
+ // ENTER/BACKSPACE Shortcuts are only available if the wrapper or
+ // breadcrumb is focused.
+ const canUseShortcuts = ( target === wrapper.current || target === breadcrumb.current );
const isEditMode = ! isNavigationMode;
switch ( keyCode ) {
@@ -279,51 +253,6 @@ function BlockListBlock( {
event.preventDefault();
}
break;
- case ESCAPE:
- if (
- isSelected &&
- isEditMode
- ) {
- setNavigationMode( true );
- wrapper.current.focus();
- }
- break;
- }
- };
-
- /**
- * Begins tracking cursor multi-selection when clicking down within block.
- *
- * @param {MouseEvent} event A mousedown event.
- */
- const onMouseDown = ( event ) => {
- // Not the main button.
- // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
- if ( event.button !== 0 ) {
- return;
- }
-
- if (
- isNavigationMode &&
- isSelected &&
- isInsideRootBlock( blockNodeRef.current, event.target )
- ) {
- setNavigationMode( false );
- }
-
- if ( event.shiftKey ) {
- if ( ! isSelected ) {
- onShiftSelection();
- event.preventDefault();
- }
-
- // Allow user to escape out of a multi-selection to a singular
- // selection of a block via click. This is handled here since
- // onFocus excludes blocks involved in a multiselection, as
- // focus can be incurred by starting a multiselection (focus
- // moved to first block's multi-controls).
- } else if ( isPartOfMultiSelection ) {
- onSelect();
}
};
@@ -333,7 +262,7 @@ function BlockListBlock( {
// cases where Firefox might always set `buttons` to `0`.
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/which
- if ( isSelected && ( buttons || which ) === 1 ) {
+ if ( ( buttons || which ) === 1 ) {
onSelectionStart( clientId );
}
};
@@ -469,18 +398,16 @@ function BlockListBlock( {
);
return (
-
) }
-
@@ -565,7 +491,7 @@ function BlockListBlock( {
] }
{ !! hasError && }
-
+
{ showEmptyBlockSideInserter && (
) }
-
+
);
}
@@ -680,14 +606,12 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {
const {
updateBlockAttributes,
selectBlock,
- multiSelect,
insertBlocks,
insertDefaultBlock,
removeBlock,
mergeBlocks,
replaceBlocks,
toggleSelection,
- setNavigationMode,
__unstableMarkLastChangeAsPersistent,
} = dispatch( 'core/block-editor' );
@@ -750,25 +674,9 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {
}
replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect );
},
- onShiftSelection() {
- if ( ! ownProps.isSelectionEnabled ) {
- return;
- }
-
- const {
- getBlockSelectionStart,
- } = select( 'core/block-editor' );
-
- if ( getBlockSelectionStart() ) {
- multiSelect( getBlockSelectionStart(), ownProps.clientId );
- } else {
- selectBlock( ownProps.clientId );
- }
- },
toggleSelection( selectionEnabled ) {
toggleSelection( selectionEnabled );
},
- setNavigationMode,
};
} );
diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js
index 7100e7ced012f2..dff5d1c980298f 100644
--- a/packages/block-editor/src/components/block-list/index.js
+++ b/packages/block-editor/src/components/block-list/index.js
@@ -6,7 +6,6 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { useRef } from '@wordpress/element';
import { AsyncModeProvider, useSelect } from '@wordpress/data';
/**
@@ -15,7 +14,7 @@ import { AsyncModeProvider, useSelect } from '@wordpress/data';
import BlockListBlock from './block';
import BlockListAppender from '../block-list-appender';
import __experimentalBlockListFooter from '../block-list-footer';
-import useMultiSelection from './use-multi-selection';
+import RootContainer from './root-container';
/**
* If the block count exceeds the threshold, we disable the reordering animation
@@ -71,8 +70,6 @@ function BlockList( {
hasMultiSelection,
enableAnimation,
} = useSelect( selector, [ rootClientId ] );
- const ref = useRef();
- const onSelectionStart = useMultiSelection( { ref, rootClientId } );
const uiParts = {
hasMovers: true,
@@ -80,9 +77,10 @@ function BlockList( {
...__experimentalUIParts,
};
+ const Container = rootClientId ? 'div' : RootContainer;
+
return (
-
<__experimentalBlockListFooter.Slot />
-
+
);
}
diff --git a/packages/block-editor/src/components/block-list/root-container.js b/packages/block-editor/src/components/block-list/root-container.js
new file mode 100644
index 00000000000000..8c5c5c371f662a
--- /dev/null
+++ b/packages/block-editor/src/components/block-list/root-container.js
@@ -0,0 +1,84 @@
+/**
+ * WordPress dependencies
+ */
+import { useRef, createContext } from '@wordpress/element';
+import { useSelect, useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import useMultiSelection from './use-multi-selection';
+import { getBlockClientId } from '../../utils/dom';
+
+/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */
+
+export const Context = createContext();
+
+function selector( select ) {
+ const {
+ getSelectedBlockClientId,
+ hasMultiSelection,
+ } = select( 'core/block-editor' );
+
+ return {
+ selectedBlockClientId: getSelectedBlockClientId(),
+ hasMultiSelection: hasMultiSelection(),
+ };
+}
+
+/**
+ * Prevents default dragging behavior within a block.
+ * To do: we must handle this in the future and clean up the drag target.
+ * Previously dragging was prevented for multi-selected, but this is no longer
+ * needed.
+ *
+ * @param {WPSyntheticEvent} event Synthetic drag event.
+ */
+function onDragStart( event ) {
+ // Ensure we target block content, not block controls.
+ if ( getBlockClientId( event.target ) ) {
+ event.preventDefault();
+ }
+}
+
+export default function RootContainer( { children, className } ) {
+ const ref = useRef();
+ const {
+ selectedBlockClientId,
+ hasMultiSelection,
+ } = useSelect( selector, [] );
+ const { selectBlock } = useDispatch( 'core/block-editor' );
+ const onSelectionStart = useMultiSelection( ref );
+
+ /**
+ * Marks the block as selected when focused and not already selected. This
+ * specifically handles the case where block does not set focus on its own
+ * (via `setFocus`), typically if there is no focusable input in the block.
+ *
+ * @param {WPSyntheticEvent} event
+ */
+ function onFocus( event ) {
+ if ( hasMultiSelection ) {
+ return;
+ }
+
+ const clientId = getBlockClientId( event.target );
+
+ if ( clientId && clientId !== selectedBlockClientId ) {
+ selectBlock( clientId );
+ }
+ }
+
+ return (
+
+
+ { children }
+
+
+ );
+}
diff --git a/packages/block-editor/src/components/block-list/use-multi-selection.js b/packages/block-editor/src/components/block-list/use-multi-selection.js
index ca2ccdf13b98a1..9d158a53d5dca9 100644
--- a/packages/block-editor/src/components/block-list/use-multi-selection.js
+++ b/packages/block-editor/src/components/block-list/use-multi-selection.js
@@ -4,6 +4,11 @@
import { useEffect, useRef, useCallback } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
+/**
+ * Internal dependencies
+ */
+import { getBlockClientId, getBlockDOMNode } from '../../utils/dom';
+
/**
* Returns for the deepest node at the start or end of a container node. Ignores
* any text nodes that only contain HTML formatting whitespace.
@@ -30,35 +35,32 @@ function getDeepestNode( node, type ) {
return node;
}
-export default function useMultiSelection( { ref, rootClientId } ) {
- function selector( select ) {
- const {
- getBlockOrder,
- isSelectionEnabled,
- isMultiSelecting,
- getMultiSelectedBlockClientIds,
- hasMultiSelection,
- getBlockParents,
- } = select( 'core/block-editor' );
-
- return {
- blockClientIds: getBlockOrder( rootClientId ),
- isSelectionEnabled: isSelectionEnabled(),
- isMultiSelecting: isMultiSelecting(),
- multiSelectedBlockClientIds: getMultiSelectedBlockClientIds(),
- hasMultiSelection: hasMultiSelection(),
- getBlockParents,
- };
- }
+function selector( select ) {
+ const {
+ isSelectionEnabled,
+ isMultiSelecting,
+ getMultiSelectedBlockClientIds,
+ hasMultiSelection,
+ getBlockParents,
+ } = select( 'core/block-editor' );
+
+ return {
+ isSelectionEnabled: isSelectionEnabled(),
+ isMultiSelecting: isMultiSelecting(),
+ multiSelectedBlockClientIds: getMultiSelectedBlockClientIds(),
+ hasMultiSelection: hasMultiSelection(),
+ getBlockParents,
+ };
+}
+export default function useMultiSelection( ref ) {
const {
- blockClientIds,
isSelectionEnabled,
isMultiSelecting,
multiSelectedBlockClientIds,
hasMultiSelection,
getBlockParents,
- } = useSelect( selector, [ rootClientId ] );
+ } = useSelect( selector, [] );
const {
startMultiSelect,
stopMultiSelect,
@@ -78,22 +80,17 @@ export default function useMultiSelection( { ref, rootClientId } ) {
}
const { length } = multiSelectedBlockClientIds;
- // These must be in the right DOM order.
- const start = multiSelectedBlockClientIds[ 0 ];
- const end = multiSelectedBlockClientIds[ length - 1 ];
- const startIndex = blockClientIds.indexOf( start );
- // The selected block is not in this block list.
- if ( startIndex === -1 ) {
+ if ( length < 2 ) {
return;
}
- let startNode = ref.current.querySelector(
- `[data-block="${ start }"]`
- );
- let endNode = ref.current.querySelector(
- `[data-block="${ end }"]`
- );
+ // These must be in the right DOM order.
+ const start = multiSelectedBlockClientIds[ 0 ];
+ const end = multiSelectedBlockClientIds[ length - 1 ];
+
+ let startNode = getBlockDOMNode( start );
+ let endNode = getBlockDOMNode( end );
const selection = window.getSelection();
const range = document.createRange();
@@ -112,7 +109,6 @@ export default function useMultiSelection( { ref, rootClientId } ) {
hasMultiSelection,
isMultiSelecting,
multiSelectedBlockClientIds,
- blockClientIds,
selectBlock,
] );
@@ -124,16 +120,7 @@ export default function useMultiSelection( { ref, rootClientId } ) {
return;
}
- let { focusNode } = selection;
- let clientId;
-
- // Find the client ID of the block where the selection ends.
- do {
- focusNode = focusNode.parentElement;
- } while (
- focusNode &&
- ! ( clientId = focusNode.getAttribute( 'data-block' ) )
- );
+ const clientId = getBlockClientId( selection.focusNode );
if ( startClientId.current === clientId ) {
selectBlock( clientId );
diff --git a/packages/block-editor/src/components/ignore-nested-events/index.js b/packages/block-editor/src/components/ignore-nested-events/index.js
deleted file mode 100644
index c419d282a76020..00000000000000
--- a/packages/block-editor/src/components/ignore-nested-events/index.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * External dependencies
- */
-import { reduce } from 'lodash';
-
-/**
- * WordPress dependencies
- */
-import { Component, forwardRef, createElement } from '@wordpress/element';
-
-/**
- * Component which renders a div with passed props applied except the optional
- * `childHandledEvents` prop. Event prop handlers are replaced with a proxying
- * event handler to capture and prevent events from being handled by ancestor
- * `IgnoreNestedEvents` elements by testing the presence of a private property
- * assigned on the event object.
- *
- * Optionally accepts an `childHandledEvents` prop array, which can be used in
- * instances where an inner `IgnoreNestedEvents` element exists and the outer
- * element should stop propagation but not invoke a callback handler, since it
- * would be assumed these are invoked by the child element.
- *
- * @type {WPComponent}
- */
-export class IgnoreNestedEvents extends Component {
- constructor() {
- super( ...arguments );
-
- this.proxyEvent = this.proxyEvent.bind( this );
-
- // The event map is responsible for tracking an event type to a React
- // component prop name, since it is easy to determine event type from
- // a React prop name, but not the other way around.
- this.eventMap = {};
- }
-
- /**
- * General event handler which only calls to its original props callback if
- * it has not already been handled by a descendant IgnoreNestedEvents.
- *
- * @param {Event} event Event object.
- */
- proxyEvent( event ) {
- // Skip if already handled (i.e. assume nested block)
- if ( event.nativeEvent._blockHandled ) {
- return;
- }
-
- // Assign into the native event, since React will reuse their synthetic
- // event objects and this property assignment could otherwise leak.
- //
- // See: https://reactjs.org/docs/events.html#event-pooling
- event.nativeEvent._blockHandled = true;
-
- // Invoke original prop handler
- const propKey = this.eventMap[ event.type ];
-
- if ( this.props[ propKey ] ) {
- this.props[ propKey ]( event );
- }
- }
-
- render() {
- const { childHandledEvents = [], forwardedRef, tagName = 'div', ...props } = this.props;
-
- const eventHandlers = reduce( [
- ...childHandledEvents,
- ...Object.keys( props ),
- ], ( result, key ) => {
- // Try to match prop key as event handler
- const match = key.match( /^on([A-Z][a-zA-Z]+)$/ );
- if ( match ) {
- // Re-map the prop to the local proxy handler to check whether
- // the event has already been handled.
- result[ key ] = this.proxyEvent;
-
- // Assign event -> propName into an instance variable, so as to
- // avoid re-renders which could be incurred either by setState
- // or in mapping values to a newly created function.
- this.eventMap[ match[ 1 ].toLowerCase() ] = key;
- }
-
- return result;
- }, {} );
-
- return createElement( tagName, { ref: forwardedRef, ...props, ...eventHandlers } );
- }
-}
-
-const forwardedIgnoreNestedEvents = ( props, ref ) => {
- return ;
-};
-forwardedIgnoreNestedEvents.displayName = 'IgnoreNestedEvents';
-
-export default forwardRef( forwardedIgnoreNestedEvents );
diff --git a/packages/block-editor/src/components/ignore-nested-events/test/index.js b/packages/block-editor/src/components/ignore-nested-events/test/index.js
deleted file mode 100644
index 367570c89646e6..00000000000000
--- a/packages/block-editor/src/components/ignore-nested-events/test/index.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * External dependencies
- */
-import { shallow } from 'enzyme';
-
-/**
- * Internal dependencies
- */
-import { IgnoreNestedEvents } from '../';
-
-describe( 'IgnoreNestedEvents', () => {
- it( 'passes props to its rendered div', () => {
- const wrapper = shallow(
-
- );
-
- expect( wrapper.type() ).toBe( 'div' );
- expect( wrapper.prop( 'className' ) ).toBe( 'foo' );
- } );
-
- it( 'stops propagation of events to ancestor IgnoreNestedEvents', () => {
- const spyOuter = jest.fn();
- const spyInner = jest.fn();
- const wrapper = shallow(
-
-
-
- );
-
- wrapper.childAt( 0 ).simulate( 'click' );
-
- expect( spyInner ).toHaveBeenCalled();
- expect( spyOuter ).not.toHaveBeenCalled();
- } );
-
- it( 'stops propagation of child handled events', () => {
- const spyOuter = jest.fn();
- const spyInner = jest.fn();
- const wrapper = shallow(
-
-
-
-
-
-
- );
-
- const div = wrapper.childAt( 0 ).childAt( 0 );
- div.simulate( 'click' );
-
- expect( spyInner ).not.toHaveBeenCalled();
- expect( spyOuter ).not.toHaveBeenCalled();
- } );
-} );
diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js
index 0a1c5ffcf3cef0..b89340927086d5 100644
--- a/packages/block-editor/src/components/writing-flow/index.js
+++ b/packages/block-editor/src/components/writing-flow/index.js
@@ -17,7 +17,7 @@ import {
placeCaretAtVerticalEdge,
isEntirelySelected,
} from '@wordpress/dom';
-import { UP, DOWN, LEFT, RIGHT, TAB, isKeyboardEvent } from '@wordpress/keycodes';
+import { UP, DOWN, LEFT, RIGHT, TAB, isKeyboardEvent, ESCAPE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
/**
@@ -28,6 +28,9 @@ import {
isInSameBlock,
hasInnerBlocksContext,
getBlockFocusableWrapper,
+ isInsideRootBlock,
+ getBlockDOMNode,
+ getBlockClientId,
} from '../../utils/dom';
import FocusCapture from './focus-capture';
@@ -160,6 +163,8 @@ function selector( select ) {
hasMultiSelection,
getBlockOrder,
isNavigationMode,
+ isSelectionEnabled,
+ getBlockSelectionStart,
} = select( 'core/block-editor' );
const selectedBlockClientId = getSelectedBlockClientId();
@@ -176,9 +181,15 @@ function selector( select ) {
hasMultiSelection: hasMultiSelection(),
blocks: getBlockOrder(),
isNavigationMode: isNavigationMode(),
+ isSelectionEnabled: isSelectionEnabled(),
+ blockSelectionStart: getBlockSelectionStart(),
};
}
+/**
+ * Handles selection and navigation across blocks. This component should be
+ * wrapped around BlockList.
+ */
export default function WritingFlow( { children } ) {
const container = useRef();
const focusCaptureBeforeRef = useRef();
@@ -195,10 +206,6 @@ export default function WritingFlow( { children } ) {
// browser behaviour across blocks.
const verticalRect = useRef();
- function onMouseDown() {
- verticalRect.current = null;
- }
-
const {
selectedBlockClientId,
selectionStartClientId,
@@ -209,13 +216,55 @@ export default function WritingFlow( { children } ) {
hasMultiSelection,
blocks,
isNavigationMode,
- } = useSelect( selector );
+ isSelectionEnabled,
+ blockSelectionStart,
+ } = useSelect( selector, [] );
const {
multiSelect,
selectBlock,
clearSelectedBlock,
+ setNavigationMode,
} = useDispatch( 'core/block-editor' );
+ function onMouseDown( event ) {
+ verticalRect.current = null;
+
+ // Clicking inside a selected block should exit navigation mode.
+ if (
+ isNavigationMode &&
+ selectedBlockClientId &&
+ isInsideRootBlock( getBlockDOMNode( selectedBlockClientId ), event.target )
+ ) {
+ setNavigationMode( false );
+ }
+
+ // Multi-select blocks when Shift+clicking.
+ if (
+ isSelectionEnabled &&
+ // The main button.
+ // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
+ event.button === 0
+ ) {
+ const clientId = getBlockClientId( event.target );
+
+ if ( clientId ) {
+ if ( event.shiftKey ) {
+ if ( blockSelectionStart !== clientId ) {
+ multiSelect( blockSelectionStart, clientId );
+ event.preventDefault();
+ }
+ // Allow user to escape out of a multi-selection to a singular
+ // selection of a block via click. This is handled here since
+ // focus handling excludes blocks when there is multiselection,
+ // as focus can be incurred by starting a multiselection (focus
+ // moved to first block's multi-controls).
+ } else if ( hasMultiSelection ) {
+ selectBlock( clientId );
+ }
+ }
+ }
+ }
+
function expandSelection( isReverse ) {
const nextSelectionEndClientId = isReverse ?
selectionBeforeEndClientId :
@@ -260,6 +309,7 @@ export default function WritingFlow( { children } ) {
const isLeft = keyCode === LEFT;
const isRight = keyCode === RIGHT;
const isTab = keyCode === TAB;
+ const isEscape = keyCode === ESCAPE;
const isReverse = isUp || isLeft;
const isHorizontal = isLeft || isRight;
const isVertical = isUp || isDown;
@@ -307,27 +357,31 @@ export default function WritingFlow( { children } ) {
// which is normally the block toolbar.
// Arrow keys can be used, and Tab and arrow keys can be used in
// Navigation mode (press Esc), to navigate through blocks.
- if ( isTab && clientId ) {
+ if ( clientId ) {
const wrapper = getBlockFocusableWrapper( clientId );
- if ( isShift ) {
- if ( target === wrapper ) {
- // Disable focus capturing on the focus capture element, so
- // it doesn't refocus this block and so it allows default
- // behaviour (moving focus to the next tabbable element).
- noCapture.current = true;
- focusCaptureBeforeRef.current.focus();
- return;
- }
- } else {
- const tabbables = focus.tabbable.find( wrapper );
-
- if ( target === last( tabbables ) ) {
- // See comment above.
- noCapture.current = true;
- focusCaptureAfterRef.current.focus();
- return;
+ if ( isTab ) {
+ if ( isShift ) {
+ if ( target === wrapper ) {
+ // Disable focus capturing on the focus capture element, so
+ // it doesn't refocus this block and so it allows default
+ // behaviour (moving focus to the next tabbable element).
+ noCapture.current = true;
+ focusCaptureBeforeRef.current.focus();
+ return;
+ }
+ } else {
+ const tabbables = focus.tabbable.find( wrapper );
+
+ if ( target === last( tabbables ) ) {
+ // See comment above.
+ noCapture.current = true;
+ focusCaptureAfterRef.current.focus();
+ return;
+ }
}
+ } else if ( isEscape ) {
+ setNavigationMode( true );
}
}
diff --git a/packages/block-editor/src/utils/dom.js b/packages/block-editor/src/utils/dom.js
index fd732b40c5318d..ec26f836f47394 100644
--- a/packages/block-editor/src/utils/dom.js
+++ b/packages/block-editor/src/utils/dom.js
@@ -86,3 +86,24 @@ export function isInsideRootBlock( blockElement, element ) {
export function hasInnerBlocksContext( element ) {
return !! element.querySelector( '.block-editor-block-list__layout' );
}
+
+/**
+ * Finds the block client ID given any DOM node inside the block.
+ *
+ * @param {Node} node DOM node.
+ *
+ * @return {string|undefined} Client ID or undefined if the node is not part of a block.
+ */
+export function getBlockClientId( node ) {
+ if ( node.nodeType !== node.ELEMENT_NODE ) {
+ node = node.parentElement;
+ }
+
+ const blockNode = node.closest( '.wp-block' );
+
+ if ( ! blockNode ) {
+ return;
+ }
+
+ return blockNode.id.slice( 'block-'.length );
+}