Skip to content

Commit

Permalink
Ensure focus remains within list view after cut and paste
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewserong committed Jan 23, 2024
1 parent a12cac8 commit 46b59f2
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 26 deletions.
2 changes: 1 addition & 1 deletion packages/block-editor/src/components/list-view/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ function ListViewBlock( {
selectBlock( undefined, focusClientId, null, null );
}

focusListItem( focusClientId, treeGridElementRef );
focusListItem( focusClientId, treeGridElementRef?.current );
},
[ selectBlock, treeGridElementRef ]
);
Expand Down
39 changes: 21 additions & 18 deletions packages/block-editor/src/components/list-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -54,7 +65,7 @@ export default function useClipboardHandler() {
blocksToUpdate,
firstBlockClientId,
firstBlockRootClientId,
selectedBlockClientIds,
originallySelectedBlockClientIds: selectedBlockClientIds,
};
}

Expand Down Expand Up @@ -83,7 +94,9 @@ export default function useClipboardHandler() {

const {
blocksToUpdate: selectedBlockClientIds,
firstBlockClientId,
firstBlockRootClientId,
originallySelectedBlockClientIds,
} = getBlocksToUpdate( clientId );

if ( selectedBlockClientIds.length === 0 ) {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -189,6 +227,7 @@ export default function useClipboardHandler() {
undefined,
selectedBlockClientId
);
updateFocusAndSelection( blocks[ 0 ]?.clientId, false );
return;
}
}
Expand All @@ -199,6 +238,7 @@ export default function useClipboardHandler() {
blocks.length - 1,
-1
);
updateFocusAndSelection( blocks[ 0 ]?.clientId, false );
}
}

Expand Down
8 changes: 4 additions & 4 deletions packages/block-editor/src/components/list-view/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>} 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;
Expand Down

0 comments on commit 46b59f2

Please sign in to comment.