diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index bb1d22b2865de..558182b7b5ac4 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -116,17 +116,53 @@ export default function useTabNav() { return; } + // We want to constrain the tabbing to the block and its child blocks. + // If the preceding form element is within a different block, + // such as two sibling image blocks in the placeholder state, + // we want shift + tab from the first form element to move to the image + // block toolbar and not the previous image block's form element. + // TODO: Should this become a utility function? + /** + * Determine whether an element is part of or is the selected block. + * + * @param {Object} selectedBlockElement + * @param {Object} element + * @return {boolean} Whether the element is part of or is the selected block. + */ + const isElementPartOfSelectedBlock = ( + selectedBlockElement, + element + ) => { + // Check if the element is or is within the selected block by finding the + // closest element with a data-block attribute and seeing if + // it matches our current selected block ID + const elementBlockId = element + .closest( '[data-block]' ) + ?.getAttribute( 'data-block' ); + const isElementSameBlock = + elementBlockId === getSelectedBlockClientId(); + + // Check if the element is a child of the selected block. This could be a + // child block in a group or column block, etc. + const isElementChildOfBlock = + selectedBlockElement.contains( element ); + + return isElementSameBlock || isElementChildOfBlock; + }; + + const nextTabbable = focus.tabbable[ direction ]( event.target ); // Allow tabbing from the block wrapper to a form element, - // and between form elements rendered in a block, + // and between form elements rendered in a block and its child blocks, // such as inside a placeholder. Form elements are generally // meant to be UI rather than part of the content. Ideally // these are not rendered in the content and perhaps in the // future they can be rendered in an iframe or shadow DOM. if ( - ( isFormElement( event.target ) || - event.target.getAttribute( 'data-block' ) === - getSelectedBlockClientId() ) && - isFormElement( focus.tabbable[ direction ]( event.target ) ) + isFormElement( nextTabbable ) && + isElementPartOfSelectedBlock( + event.target.closest( '[data-block]' ), + nextTabbable + ) ) { return; }