From 4230e46964795bf99e98e73fbe712ed2e7d7fa50 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:07:55 +1000 Subject: [PATCH 1/6] List View: Add keyboard shortcut for duplicating blocks --- .../list-view/block-select-button.js | 79 ++++++++++++++++--- 1 file changed, 66 insertions(+), 13 deletions(-) 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..8cd8b82eab100 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,6 +86,33 @@ function ListViewBlockSelectButton( onDragStart?.( event ); }; + // Determine which blocks to update using logic that if the current block + // is part of the block selection, then use the whole block selection for the + // update. Otherwise, only update the current block. This way, where the user is + // currently focused has a logical impact on what happens when they perform an action. + // This is used by actions to delete and duplicate blocks. + 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 */ @@ -94,18 +124,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 +155,35 @@ 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 duplicatedBlocks = duplicateBlocks( + blocksToUpdate, + false + ); + + if ( duplicatedBlocks ) { + updateFocusAndSelection( duplicatedBlocks, true ); + } + } } } From 14f837623aade5802d06a4aa49e9424f288f6bf7 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:11:51 +1000 Subject: [PATCH 2/6] Fix focus issue after duplicating blocks --- .../list-view/block-select-button.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) 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 8cd8b82eab100..7cfdff17afea6 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 @@ -86,11 +86,9 @@ function ListViewBlockSelectButton( onDragStart?.( event ); }; - // Determine which blocks to update using logic that if the current block - // is part of the block selection, then use the whole block selection for the - // update. Otherwise, only update the current block. This way, where the user is - // currently focused has a logical impact on what happens when they perform an action. - // This is used by actions to delete and duplicate blocks. + // 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 = @@ -175,14 +173,8 @@ function ListViewBlockSelectButton( ); if ( canDuplicate ) { - const duplicatedBlocks = duplicateBlocks( - blocksToUpdate, - false - ); - - if ( duplicatedBlocks ) { - updateFocusAndSelection( duplicatedBlocks, true ); - } + // Duplicate blocks, but do not update the selection. + duplicateBlocks( blocksToUpdate, false ); } } } From 754fbf8f145bfe57766521b7ec82883073ef2075 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:10:41 +1000 Subject: [PATCH 3/6] Update existing test to cover duplication shortcut --- .../specs/editor/various/list-view.spec.js | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index 40f7cce283210..6a25f8c06ab9e 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,20 @@ test.describe( 'List View', () => { { name: 'core/file', selected: true, focused: true }, ] ); + await page.keyboard.press( 'Meta+Shift+d' ); + + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Duplicating a block should retain focus and selection on existing block.' + ) + .toMatchObject( [ + { name: 'core/group' }, + { name: 'core/columns' }, + { name: 'core/file', selected: true, focused: true }, + { name: 'core/file' }, + ] ); + await page.keyboard.press( 'Delete' ); await expect .poll( @@ -483,6 +497,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 +519,7 @@ test.describe( 'List View', () => { { name: 'core/column', focused: true }, ], }, + { name: 'core/file' }, ] ); await page.keyboard.press( 'Delete' ); @@ -525,6 +541,7 @@ test.describe( 'List View', () => { }, ], }, + { name: 'core/file' }, ] ); // Expand the current column. @@ -555,6 +572,7 @@ test.describe( 'List View', () => { }, ], }, + { name: 'core/file' }, ] ); // Move focus and select the first block. @@ -573,14 +591,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( [ { From 7cbb7d015b54c882c98b21eb2bf67b48910fb324 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:07:54 +1000 Subject: [PATCH 4/6] Use pressKeys instead --- test/e2e/specs/editor/various/list-view.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index 6a25f8c06ab9e..b5e43489755da 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -474,7 +474,7 @@ test.describe( 'List View', () => { { name: 'core/file', selected: true, focused: true }, ] ); - await page.keyboard.press( 'Meta+Shift+d' ); + await pageUtils.pressKeys( 'primaryShift+d' ); await expect .poll( From 8bd95b53db5b0ad6b6a9845bdc06c2f3f498f3cd Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:33:53 +1000 Subject: [PATCH 5/6] Focus first duplicated block on success --- .../src/components/list-view/block-select-button.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 7cfdff17afea6..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 @@ -114,7 +114,7 @@ function ListViewBlockSelectButton( /** * @param {KeyboardEvent} event */ - function onKeyDownHandler( event ) { + async function onKeyDownHandler( event ) { if ( event.keyCode === ENTER || event.keyCode === SPACE ) { onClick( event ); } else if ( @@ -173,8 +173,15 @@ function ListViewBlockSelectButton( ); if ( canDuplicate ) { - // Duplicate blocks, but do not update the selection. - duplicateBlocks( blocksToUpdate, false ); + const updatedBlocks = await duplicateBlocks( + blocksToUpdate, + false + ); + + if ( updatedBlocks?.length ) { + // If blocks have been duplicated, focus the first duplicated block. + updateFocusAndSelection( updatedBlocks[ 0 ], false ); + } } } } From c96f39b09944fc51b437bf54eb5185ab5a7b8c16 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 16 Aug 2023 12:06:29 +1000 Subject: [PATCH 6/6] Fix tests --- test/e2e/specs/editor/various/list-view.spec.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index b5e43489755da..304007f4d0d25 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -479,15 +479,17 @@ test.describe( 'List View', () => { await expect .poll( listViewUtils.getBlocksWithA11yAttributes, - 'Duplicating a block should retain focus and selection on existing block.' + '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, focused: true }, - { name: 'core/file' }, + { 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(