diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 930720fe56582..e558b951f5459 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -6,6 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { hasBlockSupport } from '@wordpress/blocks'; import { Button, __experimentalHStack as HStack, @@ -55,13 +56,15 @@ function ListViewBlockSelectButton( } ); const { isLocked } = useBlockLock( clientId ); const { + canInsertBlockType, getSelectedBlockClientIds, getPreviousBlockClientId, getBlockRootClientId, getBlockOrder, + getBlocksByClientId, canRemoveBlocks, } = useSelect( blockEditorStore ); - const { removeBlocks } = useDispatch( blockEditorStore ); + const { duplicateBlocks, removeBlocks } = useDispatch( blockEditorStore ); const isMatch = useShortcutEventMatch(); const isSticky = blockInformation?.positionType === 'sticky'; const images = useListViewImages( { clientId, isExpanded } ); @@ -83,10 +86,35 @@ function ListViewBlockSelectButton( onDragStart?.( event ); }; + // 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. + function getBlocksToUpdate() { + const selectedBlockClientIds = getSelectedBlockClientIds(); + const isUpdatingSelectedBlocks = + selectedBlockClientIds.includes( clientId ); + const firstBlockClientId = isUpdatingSelectedBlocks + ? selectedBlockClientIds[ 0 ] + : clientId; + const firstBlockRootClientId = + getBlockRootClientId( firstBlockClientId ); + + const blocksToUpdate = isUpdatingSelectedBlocks + ? selectedBlockClientIds + : [ clientId ]; + + return { + blocksToUpdate, + firstBlockClientId, + firstBlockRootClientId, + selectedBlockClientIds, + }; + } + /** * @param {KeyboardEvent} event */ - function onKeyDownHandler( event ) { + async function onKeyDownHandler( event ) { if ( event.keyCode === ENTER || event.keyCode === SPACE ) { onClick( event ); } else if ( @@ -94,18 +122,12 @@ function ListViewBlockSelectButton( event.keyCode === DELETE || isMatch( 'core/block-editor/remove', event ) ) { - const selectedBlockClientIds = getSelectedBlockClientIds(); - const isDeletingSelectedBlocks = - selectedBlockClientIds.includes( clientId ); - const firstBlockClientId = isDeletingSelectedBlocks - ? selectedBlockClientIds[ 0 ] - : clientId; - const firstBlockRootClientId = - getBlockRootClientId( firstBlockClientId ); - - const blocksToDelete = isDeletingSelectedBlocks - ? selectedBlockClientIds - : [ clientId ]; + const { + blocksToUpdate: blocksToDelete, + firstBlockClientId, + firstBlockRootClientId, + selectedBlockClientIds, + } = getBlocksToUpdate(); // Don't update the selection if the blocks cannot be deleted. if ( ! canRemoveBlocks( blocksToDelete, firstBlockRootClientId ) ) { @@ -131,6 +153,36 @@ function ListViewBlockSelectButton( } updateFocusAndSelection( blockToFocus, shouldUpdateSelection ); + } else if ( isMatch( 'core/block-editor/duplicate', event ) ) { + if ( event.defaultPrevented ) { + return; + } + event.preventDefault(); + + const { blocksToUpdate, firstBlockRootClientId } = + getBlocksToUpdate(); + + const canDuplicate = getBlocksByClientId( blocksToUpdate ).every( + ( block ) => { + return ( + !! block && + hasBlockSupport( block.name, 'multiple', true ) && + canInsertBlockType( block.name, firstBlockRootClientId ) + ); + } + ); + + if ( canDuplicate ) { + const updatedBlocks = await duplicateBlocks( + blocksToUpdate, + false + ); + + if ( updatedBlocks?.length ) { + // If blocks have been duplicated, focus the first duplicated block. + updateFocusAndSelection( updatedBlocks[ 0 ], false ); + } + } } } diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index 40f7cce283210..304007f4d0d25 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -431,7 +431,7 @@ test.describe( 'List View', () => { ).toBeFocused(); } ); - test( 'should delete blocks using keyboard', async ( { + test( 'should duplicate and delete blocks using keyboard', async ( { editor, page, pageUtils, @@ -474,6 +474,22 @@ test.describe( 'List View', () => { { name: 'core/file', selected: true, focused: true }, ] ); + await pageUtils.pressKeys( 'primaryShift+d' ); + + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Duplicating a block should retain selection on existing block, move focus to duplicated block.' + ) + .toMatchObject( [ + { name: 'core/group' }, + { name: 'core/columns' }, + { name: 'core/file', selected: true }, + { name: 'core/file', focused: true }, + ] ); + + // Move focus to the first file block, and then delete it. + await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'Delete' ); await expect .poll( @@ -483,6 +499,7 @@ test.describe( 'List View', () => { .toMatchObject( [ { name: 'core/group' }, { name: 'core/columns', selected: true, focused: true }, + { name: 'core/file' }, ] ); // Expand the current column. @@ -504,6 +521,7 @@ test.describe( 'List View', () => { { name: 'core/column', focused: true }, ], }, + { name: 'core/file' }, ] ); await page.keyboard.press( 'Delete' ); @@ -525,6 +543,7 @@ test.describe( 'List View', () => { }, ], }, + { name: 'core/file' }, ] ); // Expand the current column. @@ -555,6 +574,7 @@ test.describe( 'List View', () => { }, ], }, + { name: 'core/file' }, ] ); // Move focus and select the first block. @@ -573,14 +593,17 @@ test.describe( 'List View', () => { selected: true, focused: true, }, + { name: 'core/file' }, ] ); + // Delete remaining blocks. // Keyboard shortcut should also work. await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'access+z' ); await expect .poll( listViewUtils.getBlocksWithA11yAttributes, - 'Deleting the only block left will create a default block and focus/select it' + 'Deleting the only blocks left will create a default block and focus/select it' ) .toMatchObject( [ {