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