From 35b0ba06a6efc3e1c4a5299a6db88e888f20792c Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 19 Jun 2023 20:46:00 +0200 Subject: [PATCH 1/6] useFocusReturn: pass focus restoration default target to the onFocusReturn callback --- packages/compose/src/hooks/use-focus-return/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/compose/src/hooks/use-focus-return/index.js b/packages/compose/src/hooks/use-focus-return/index.js index 66751b7028d329..e588f653e90b8b 100644 --- a/packages/compose/src/hooks/use-focus-return/index.js +++ b/packages/compose/src/hooks/use-focus-return/index.js @@ -9,7 +9,7 @@ import { useRef, useEffect, useCallback } from '@wordpress/element'; * previously focused element when closed. * The current hook implements the returning behavior. * - * @param {() => void} [onFocusReturn] Overrides the default return behavior. + * @param {( defaultElementToFocus: Element | null ) => void} [onFocusReturn] Overrides the default return behavior. * @return {import('react').RefCallback} Element Ref. * * @example @@ -62,12 +62,14 @@ function useFocusReturn( onFocusReturn ) { // decides to allow the default behavior to occur under some // conditions. if ( onFocusReturnRef.current ) { - onFocusReturnRef.current(); + onFocusReturnRef.current( focusedBeforeMount.current ); } else { /** @type {null | HTMLElement} */ ( focusedBeforeMount.current )?.focus(); } + } else if ( onFocusReturnRef.current ) { + onFocusReturnRef.current( focusedBeforeMount.current ); } }, [] ); } From ac099f497b0bcd669fb5754cf26004c09c4bf142 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 19 Jun 2023 20:46:21 +0200 Subject: [PATCH 2/6] Modal: pass onFocusReturn callback --- packages/components/src/modal/index.tsx | 3 ++- packages/components/src/modal/types.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index d9c7b602b83920..75d5bd1043ee0c 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -67,6 +67,7 @@ function UnforwardedModal( onKeyDown, isFullScreen = false, __experimentalHideHeader = false, + onFocusReturn, } = props; const ref = useRef< HTMLDivElement >(); @@ -76,7 +77,7 @@ function UnforwardedModal( : aria.labelledby; const focusOnMountRef = useFocusOnMount( focusOnMount ); const constrainedTabbingRef = useConstrainedTabbing(); - const focusReturnRef = useFocusReturn(); + const focusReturnRef = useFocusReturn( onFocusReturn ); const focusOutsideProps = useFocusOutside( onRequestClose ); const contentRef = useRef< HTMLDivElement >( null ); const childrenContainerRef = useRef< HTMLDivElement >( null ); diff --git a/packages/components/src/modal/types.ts b/packages/components/src/modal/types.ts index 6169e42a8a2d4c..46497471ab0e3a 100644 --- a/packages/components/src/modal/types.ts +++ b/packages/components/src/modal/types.ts @@ -143,4 +143,9 @@ export type ModalProps = { * @default false */ __experimentalHideHeader?: boolean; + /** + * Callback called when the modal is dismissed. Used to implement custom + * focus restoration behavior. + */ + onFocusReturn?: ( defaultFocusReturnElement: Element | null ) => void; }; From af51379fbed18551d8896b01550048afb59d4708 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 19 Jun 2023 20:50:28 +0200 Subject: [PATCH 3/6] BlockLockModal: restore focus to first focusable item when unlocking block from toolbar button --- .../src/components/block-lock/modal.js | 3 +- .../src/components/block-lock/toolbar.js | 35 +++++++++++++++++-- .../src/components/block-toolbar/index.js | 5 ++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/block-lock/modal.js b/packages/block-editor/src/components/block-lock/modal.js index cfafa6c031bbd1..e0884300f1b37f 100644 --- a/packages/block-editor/src/components/block-lock/modal.js +++ b/packages/block-editor/src/components/block-lock/modal.js @@ -41,7 +41,7 @@ function getTemplateLockValue( lock ) { return false; } -export default function BlockLockModal( { clientId, onClose } ) { +export default function BlockLockModal( { clientId, onClose, onFocusReturn } ) { const [ lock, setLock ] = useState( { move: false, remove: false } ); const { canEdit, canMove, canRemove } = useBlockLock( clientId ); const { allowsEditLocking, templateLock, hasTemplateLock } = useSelect( @@ -89,6 +89,7 @@ export default function BlockLockModal( { clientId, onClose } ) { ) } overlayClassName="block-editor-block-lock-modal" onRequestClose={ onClose } + onFocusReturn={ onFocusReturn } >

{ __( diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js index d0a9d3d8bf3d3d..05e84062d90976 100644 --- a/packages/block-editor/src/components/block-lock/toolbar.js +++ b/packages/block-editor/src/components/block-lock/toolbar.js @@ -3,7 +3,8 @@ */ import { __ } from '@wordpress/i18n'; import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; -import { useReducer } from '@wordpress/element'; +import { focus } from '@wordpress/dom'; +import { useReducer, useRef } from '@wordpress/element'; import { lock } from '@wordpress/icons'; /** @@ -12,7 +13,7 @@ import { lock } from '@wordpress/icons'; import BlockLockModal from './modal'; import useBlockLock from './use-block-lock'; -export default function BlockLockToolbar( { clientId } ) { +export default function BlockLockToolbar( { clientId, wrapperRef } ) { const { canEdit, canMove, canRemove, canLock } = useBlockLock( clientId ); const [ isModalOpen, toggleModal ] = useReducer( @@ -20,6 +21,8 @@ export default function BlockLockToolbar( { clientId } ) { false ); + const lockButtonRef = useRef( null ); + if ( ! canLock ) { return null; } @@ -35,10 +38,36 @@ export default function BlockLockToolbar( { clientId } ) { icon={ lock } label={ __( 'Unlock' ) } onClick={ toggleModal } + ref={ lockButtonRef } /> { isModalOpen && ( - + { + if ( defaultFocusReturnElement ) { + defaultFocusReturnElement.focus(); + } + + if ( + defaultFocusReturnElement.ownerDocument + .activeElement !== defaultFocusReturnElement && + wrapperRef.current + ) { + focus.focusable + .find( wrapperRef.current, { + sequential: false, + } ) + .find( + ( element ) => + element.tagName === 'BUTTON' && + element !== lockButtonRef.current + ) + ?.focus(); + } + } } + /> ) } ); diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 29726ea6e3d3d5..0f968b8a209a73 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -78,6 +78,8 @@ const BlockToolbar = ( { hideDragHandle } ) => { }; }, [] ); + const toolbarWrapperRef = useRef( null ); + // Handles highlighting the current block outline on hover or focus of the // block type toolbar area. const { toggleBlockHighlight } = useDispatch( blockEditorStore ); @@ -123,7 +125,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { } ); return ( -

+
{ ! isMultiToolbar && isLargeViewport && blockEditingMode === 'default' && } @@ -135,6 +137,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { { ! isMultiToolbar && ( ) } Date: Tue, 20 Jun 2023 09:39:37 +0200 Subject: [PATCH 4/6] Add comments --- packages/block-editor/src/components/block-lock/toolbar.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js index 05e84062d90976..41fccbdbe5611c 100644 --- a/packages/block-editor/src/components/block-lock/toolbar.js +++ b/packages/block-editor/src/components/block-lock/toolbar.js @@ -46,15 +46,22 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) { clientId={ clientId } onClose={ toggleModal } onFocusReturn={ ( defaultFocusReturnElement ) => { + // Try to focus the element that should have received + // focus by default. if ( defaultFocusReturnElement ) { defaultFocusReturnElement.focus(); } + // Check if the element that should have received focus is effectively + // the current active element. This check is useful when the element + // that should have received focus is not being rendered in the DOM. if ( defaultFocusReturnElement.ownerDocument .activeElement !== defaultFocusReturnElement && wrapperRef.current ) { + // As a fallback, focus the first focusable button + // found in the toolbar focus.focusable .find( wrapperRef.current, { sequential: false, From cff16b35ab6c68ac10d03418c346a84fa905ec6b Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 26 Jun 2023 15:24:38 +0200 Subject: [PATCH 5/6] Revert changes to `useFocusReturn` and `Modal` component, just add logic to the BlockLockToolbar instead --- .../src/components/block-lock/modal.js | 3 +- .../src/components/block-lock/toolbar.js | 65 ++++++++----------- packages/components/src/modal/index.tsx | 3 +- packages/components/src/modal/types.ts | 5 -- .../src/hooks/use-focus-return/index.js | 6 +- 5 files changed, 31 insertions(+), 51 deletions(-) diff --git a/packages/block-editor/src/components/block-lock/modal.js b/packages/block-editor/src/components/block-lock/modal.js index e0884300f1b37f..cfafa6c031bbd1 100644 --- a/packages/block-editor/src/components/block-lock/modal.js +++ b/packages/block-editor/src/components/block-lock/modal.js @@ -41,7 +41,7 @@ function getTemplateLockValue( lock ) { return false; } -export default function BlockLockModal( { clientId, onClose, onFocusReturn } ) { +export default function BlockLockModal( { clientId, onClose } ) { const [ lock, setLock ] = useState( { move: false, remove: false } ); const { canEdit, canMove, canRemove } = useBlockLock( clientId ); const { allowsEditLocking, templateLock, hasTemplateLock } = useSelect( @@ -89,7 +89,6 @@ export default function BlockLockModal( { clientId, onClose, onFocusReturn } ) { ) } overlayClassName="block-editor-block-lock-modal" onRequestClose={ onClose } - onFocusReturn={ onFocusReturn } >

{ __( diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js index 41fccbdbe5611c..2dcbb18a202234 100644 --- a/packages/block-editor/src/components/block-lock/toolbar.js +++ b/packages/block-editor/src/components/block-lock/toolbar.js @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; import { focus } from '@wordpress/dom'; -import { useReducer, useRef } from '@wordpress/element'; +import { useReducer, useRef, useEffect } from '@wordpress/element'; import { lock } from '@wordpress/icons'; /** @@ -22,12 +22,33 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) { ); const lockButtonRef = useRef( null ); + const isFirstRender = useRef( true ); - if ( ! canLock ) { - return null; - } + const shouldHideBlockLockUI = + ! canLock || ( canEdit && canMove && canRemove ); + + useEffect( () => { + if ( isFirstRender.current ) { + isFirstRender.current = false; + return; + } - if ( canEdit && canMove && canRemove ) { + if ( ! isModalOpen && shouldHideBlockLockUI ) { + focus.focusable + .find( wrapperRef.current, { + sequential: false, + } ) + .find( + ( element ) => + element.tagName === 'BUTTON' && + element !== lockButtonRef.current + ) + ?.focus(); + } + // wrapperRef is a reference object and should be stable + }, [ isModalOpen, shouldHideBlockLockUI, wrapperRef ] ); + + if ( shouldHideBlockLockUI ) { return null; } @@ -42,39 +63,7 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) { /> { isModalOpen && ( - { - // Try to focus the element that should have received - // focus by default. - if ( defaultFocusReturnElement ) { - defaultFocusReturnElement.focus(); - } - - // Check if the element that should have received focus is effectively - // the current active element. This check is useful when the element - // that should have received focus is not being rendered in the DOM. - if ( - defaultFocusReturnElement.ownerDocument - .activeElement !== defaultFocusReturnElement && - wrapperRef.current - ) { - // As a fallback, focus the first focusable button - // found in the toolbar - focus.focusable - .find( wrapperRef.current, { - sequential: false, - } ) - .find( - ( element ) => - element.tagName === 'BUTTON' && - element !== lockButtonRef.current - ) - ?.focus(); - } - } } - /> + ) } ); diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index 75d5bd1043ee0c..d9c7b602b83920 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -67,7 +67,6 @@ function UnforwardedModal( onKeyDown, isFullScreen = false, __experimentalHideHeader = false, - onFocusReturn, } = props; const ref = useRef< HTMLDivElement >(); @@ -77,7 +76,7 @@ function UnforwardedModal( : aria.labelledby; const focusOnMountRef = useFocusOnMount( focusOnMount ); const constrainedTabbingRef = useConstrainedTabbing(); - const focusReturnRef = useFocusReturn( onFocusReturn ); + const focusReturnRef = useFocusReturn(); const focusOutsideProps = useFocusOutside( onRequestClose ); const contentRef = useRef< HTMLDivElement >( null ); const childrenContainerRef = useRef< HTMLDivElement >( null ); diff --git a/packages/components/src/modal/types.ts b/packages/components/src/modal/types.ts index 46497471ab0e3a..6169e42a8a2d4c 100644 --- a/packages/components/src/modal/types.ts +++ b/packages/components/src/modal/types.ts @@ -143,9 +143,4 @@ export type ModalProps = { * @default false */ __experimentalHideHeader?: boolean; - /** - * Callback called when the modal is dismissed. Used to implement custom - * focus restoration behavior. - */ - onFocusReturn?: ( defaultFocusReturnElement: Element | null ) => void; }; diff --git a/packages/compose/src/hooks/use-focus-return/index.js b/packages/compose/src/hooks/use-focus-return/index.js index e588f653e90b8b..66751b7028d329 100644 --- a/packages/compose/src/hooks/use-focus-return/index.js +++ b/packages/compose/src/hooks/use-focus-return/index.js @@ -9,7 +9,7 @@ import { useRef, useEffect, useCallback } from '@wordpress/element'; * previously focused element when closed. * The current hook implements the returning behavior. * - * @param {( defaultElementToFocus: Element | null ) => void} [onFocusReturn] Overrides the default return behavior. + * @param {() => void} [onFocusReturn] Overrides the default return behavior. * @return {import('react').RefCallback} Element Ref. * * @example @@ -62,14 +62,12 @@ function useFocusReturn( onFocusReturn ) { // decides to allow the default behavior to occur under some // conditions. if ( onFocusReturnRef.current ) { - onFocusReturnRef.current( focusedBeforeMount.current ); + onFocusReturnRef.current(); } else { /** @type {null | HTMLElement} */ ( focusedBeforeMount.current )?.focus(); } - } else if ( onFocusReturnRef.current ) { - onFocusReturnRef.current( focusedBeforeMount.current ); } }, [] ); } From 24e0904ec90f9bdc25f35125254f211f7a228e1c Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 26 Jun 2023 15:34:17 +0200 Subject: [PATCH 6/6] Comment --- packages/block-editor/src/components/block-lock/toolbar.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js index 2dcbb18a202234..0abd693b5a7c99 100644 --- a/packages/block-editor/src/components/block-lock/toolbar.js +++ b/packages/block-editor/src/components/block-lock/toolbar.js @@ -27,6 +27,9 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) { const shouldHideBlockLockUI = ! canLock || ( canEdit && canMove && canRemove ); + // Restore focus manually on the first focusable element in the toolbar + // when the block lock modal is closed and the block is not locked anymore. + // See https://github.com/WordPress/gutenberg/issues/51447 useEffect( () => { if ( isFirstRender.current ) { isFirstRender.current = false;