From 46b59f208d38e691f58775c55cd22853a6d2f12a Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:06:29 +1100 Subject: [PATCH] Ensure focus remains within list view after cut and paste --- .../src/components/list-view/block.js | 2 +- .../src/components/list-view/index.js | 39 ++++++++-------- .../list-view/use-clipboard-handler.js | 46 +++++++++++++++++-- .../src/components/list-view/utils.js | 8 ++-- 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index aab9d444a5948f..6dedabb48f8b85 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -187,7 +187,7 @@ function ListViewBlock( { selectBlock( undefined, focusClientId, null, null ); } - focusListItem( focusClientId, treeGridElementRef ); + focusListItem( focusClientId, treeGridElementRef?.current ); }, [ selectBlock, treeGridElementRef ] ); diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 915857505169be..f11eca023463f3 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -138,23 +138,6 @@ function ListViewComponent( const [ expandedState, setExpandedState ] = useReducer( expanded, {} ); - const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone( { - dropZoneElement, - expandedState, - setExpandedState, - } ); - const elementRef = useRef(); - - // Allow handling of copy, cut, and paste events. - const clipBoardRef = useClipboardHandler(); - - const treeGridRef = useMergeRefs( [ - clipBoardRef, - elementRef, - dropZoneRef, - ref, - ] ); - const [ insertedBlock, setInsertedBlock ] = useState( null ); const { setSelectedTreeId } = useListViewExpandSelectedItem( { @@ -176,11 +159,31 @@ function ListViewComponent( }, [ setSelectedTreeId, updateBlockSelection, onSelect, getBlock ] ); + + const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone( { + dropZoneElement, + expandedState, + setExpandedState + } ); + const elementRef = useRef(); + + // Allow handling of copy, cut, and paste events. + const clipBoardRef = useClipboardHandler( { + selectBlock: selectEditorBlock, + } ); + + const treeGridRef = useMergeRefs( [ + clipBoardRef, + elementRef, + dropZoneRef, + ref, + ] ); + useEffect( () => { // If a blocks are already selected when the list view is initially // mounted, shift focus to the first selected block. if ( selectedClientIds?.length ) { - focusListItem( selectedClientIds[ 0 ], elementRef ); + focusListItem( selectedClientIds[ 0 ], elementRef?.current ); } // Disable reason: Only focus on the selected item when the list view is mounted. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/block-editor/src/components/list-view/use-clipboard-handler.js b/packages/block-editor/src/components/list-view/use-clipboard-handler.js index 0b33569a06227b..0a5d61c9dae4d0 100644 --- a/packages/block-editor/src/components/list-view/use-clipboard-handler.js +++ b/packages/block-editor/src/components/list-view/use-clipboard-handler.js @@ -18,11 +18,14 @@ import { useRefEffect } from '@wordpress/compose'; import { getPasteEventData } from '../../utils/pasting'; import { store as blockEditorStore } from '../../store'; import { useNotifyCopy } from '../../utils/use-notify-copy'; +import { focusListItem } from './utils'; -export default function useClipboardHandler() { +export default function useClipboardHandler( { selectBlock } ) { const { + getBlockOrder, getBlockRootClientId, getBlocksByClientId, + getPreviousBlockClientId, getSelectedBlockClientIds, getSettings, canInsertBlockType, @@ -33,6 +36,14 @@ export default function useClipboardHandler() { const notifyCopy = useNotifyCopy(); return useRefEffect( ( node ) => { + function updateFocusAndSelection( focusClientId, shouldSelectBlock ) { + if ( shouldSelectBlock ) { + selectBlock( undefined, focusClientId, null, null ); + } + + focusListItem( focusClientId, node ); + } + // Determine which blocks to update: // If the current (focused) block is part of the block selection, use the whole selection. // If the focused block is not part of the block selection, only update the focused block. @@ -54,7 +65,7 @@ export default function useClipboardHandler() { blocksToUpdate, firstBlockClientId, firstBlockRootClientId, - selectedBlockClientIds, + originallySelectedBlockClientIds: selectedBlockClientIds, }; } @@ -83,7 +94,9 @@ export default function useClipboardHandler() { const { blocksToUpdate: selectedBlockClientIds, + firstBlockClientId, firstBlockRootClientId, + originallySelectedBlockClientIds, } = getBlocksToUpdate( clientId ); if ( selectedBlockClientIds.length === 0 ) { @@ -137,7 +150,27 @@ export default function useClipboardHandler() { ) { return; } - removeBlocks( selectedBlockClientIds ); + + let blockToFocus = + getPreviousBlockClientId( firstBlockClientId ) ?? + // If the previous block is not found (when the first block is deleted), + // fallback to focus the parent block. + firstBlockRootClientId; + + // Remove blocks, but don't update selection, and it will be handled below. + removeBlocks( selectedBlockClientIds, false ); + + // Update the selection if the original selection has been removed. + const shouldUpdateSelection = + originallySelectedBlockClientIds.length > 0 && + getSelectedBlockClientIds().length === 0; + + // If there's no previous block nor parent block, focus the first block. + if ( ! blockToFocus ) { + blockToFocus = getBlockOrder()[ 0 ]; + } + + updateFocusAndSelection( blockToFocus, shouldUpdateSelection ); } else if ( event.type === 'paste' ) { const { __experimentalCanUserUseUnfilteredHTML: @@ -176,6 +209,11 @@ export default function useClipboardHandler() { if ( selectedBlockClientIds.length === 1 ) { const [ selectedBlockClientId ] = selectedBlockClientIds; + // If a single block is focused, and the blocks to be posted can + // be inserted within the block, then append the pasted blocks + // within the focused block. For example, if you have copied a paragraph + // block and paste it within a single Group block, this will append + // the paragraph block within the Group block. if ( blocks.every( ( block ) => canInsertBlockType( @@ -189,6 +227,7 @@ export default function useClipboardHandler() { undefined, selectedBlockClientId ); + updateFocusAndSelection( blocks[ 0 ]?.clientId, false ); return; } } @@ -199,6 +238,7 @@ export default function useClipboardHandler() { blocks.length - 1, -1 ); + updateFocusAndSelection( blocks[ 0 ]?.clientId, false ); } } diff --git a/packages/block-editor/src/components/list-view/utils.js b/packages/block-editor/src/components/list-view/utils.js index ed7a321dea0c86..3e596ca5015465 100644 --- a/packages/block-editor/src/components/list-view/utils.js +++ b/packages/block-editor/src/components/list-view/utils.js @@ -63,12 +63,12 @@ export function getCommonDepthClientIds( * * @typedef {import('@wordpress/element').RefObject} RefObject * - * @param {string} focusClientId The client ID of the block to focus. - * @param {RefObject} treeGridElementRef The container element to search within. + * @param {string} focusClientId The client ID of the block to focus. + * @param {HTMLElement} treeGridElement The container element to search within. */ -export function focusListItem( focusClientId, treeGridElementRef ) { +export function focusListItem( focusClientId, treeGridElement ) { const getFocusElement = () => { - const row = treeGridElementRef.current?.querySelector( + const row = treeGridElement?.querySelector( `[role=row][data-block="${ focusClientId }"]` ); if ( ! row ) return null;