-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
List View: Add keyboard clipboard events for cut, copy, paste (#57838)
* List View: Try adding keyboard clipboard events for cut, copy, paste * Make useClipboardHandler that works with the list view * Ensure focus remains within list view after cut and paste * Add e2e tests * Fix linting issue * Update packages/block-editor/src/components/list-view/utils.js Co-authored-by: Robert Anderson <[email protected]> * Try consolidating some of the copy and paste behaviour * Add comment references the other useClipboardHandler * Add JSDoc comments for the new utility functions * Tidy up comment --------- Co-authored-by: Robert Anderson <[email protected]>
- Loading branch information
1 parent
2e01f22
commit a2d9738
Showing
7 changed files
with
430 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
199 changes: 199 additions & 0 deletions
199
packages/block-editor/src/components/list-view/use-clipboard-handler.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useDispatch, useSelect } from '@wordpress/data'; | ||
import { useRefEffect } from '@wordpress/compose'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { store as blockEditorStore } from '../../store'; | ||
import { useNotifyCopy } from '../../utils/use-notify-copy'; | ||
import { focusListItem } from './utils'; | ||
import { getPasteBlocks, setClipboardBlocks } from '../writing-flow/utils'; | ||
|
||
// This hook borrows from useClipboardHandler in ../writing-flow/use-clipboard-handler.js | ||
// and adds behaviour for the list view, while skipping partial selection. | ||
export default function useClipboardHandler( { selectBlock } ) { | ||
const { | ||
getBlockOrder, | ||
getBlockRootClientId, | ||
getBlocksByClientId, | ||
getPreviousBlockClientId, | ||
getSelectedBlockClientIds, | ||
getSettings, | ||
canInsertBlockType, | ||
canRemoveBlocks, | ||
} = useSelect( blockEditorStore ); | ||
const { flashBlock, removeBlocks, replaceBlocks, insertBlocks } = | ||
useDispatch( blockEditorStore ); | ||
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. | ||
function getBlocksToUpdate( clientId ) { | ||
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, | ||
originallySelectedBlockClientIds: selectedBlockClientIds, | ||
}; | ||
} | ||
|
||
function handler( event ) { | ||
if ( event.defaultPrevented ) { | ||
// This was possibly already handled in rich-text/use-paste-handler.js. | ||
return; | ||
} | ||
|
||
// Only handle events that occur within the list view. | ||
if ( ! node.contains( event.target.ownerDocument.activeElement ) ) { | ||
return; | ||
} | ||
|
||
// Retrieve the block clientId associated with the focused list view row. | ||
// This enables applying copy / cut / paste behavior to the focused block, | ||
// rather than just the blocks that are currently selected. | ||
const listViewRow = | ||
event.target.ownerDocument.activeElement?.closest( | ||
'[role=row]' | ||
); | ||
const clientId = listViewRow?.dataset?.block; | ||
if ( ! clientId ) { | ||
return; | ||
} | ||
|
||
const { | ||
blocksToUpdate: selectedBlockClientIds, | ||
firstBlockClientId, | ||
firstBlockRootClientId, | ||
originallySelectedBlockClientIds, | ||
} = getBlocksToUpdate( clientId ); | ||
|
||
if ( selectedBlockClientIds.length === 0 ) { | ||
return; | ||
} | ||
|
||
event.preventDefault(); | ||
|
||
if ( event.type === 'copy' || event.type === 'cut' ) { | ||
if ( selectedBlockClientIds.length === 1 ) { | ||
flashBlock( selectedBlockClientIds[ 0 ] ); | ||
} | ||
|
||
notifyCopy( event.type, selectedBlockClientIds ); | ||
const blocks = getBlocksByClientId( selectedBlockClientIds ); | ||
setClipboardBlocks( event, blocks ); | ||
} | ||
|
||
if ( event.type === 'cut' ) { | ||
// Don't update the selection if the blocks cannot be deleted. | ||
if ( | ||
! canRemoveBlocks( | ||
selectedBlockClientIds, | ||
firstBlockRootClientId | ||
) | ||
) { | ||
return; | ||
} | ||
|
||
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: | ||
canUserUseUnfilteredHTML, | ||
} = getSettings(); | ||
const blocks = getPasteBlocks( | ||
event, | ||
canUserUseUnfilteredHTML | ||
); | ||
|
||
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( | ||
block.name, | ||
selectedBlockClientId | ||
) | ||
) | ||
) { | ||
insertBlocks( | ||
blocks, | ||
undefined, | ||
selectedBlockClientId | ||
); | ||
updateFocusAndSelection( blocks[ 0 ]?.clientId, false ); | ||
return; | ||
} | ||
} | ||
|
||
replaceBlocks( | ||
selectedBlockClientIds, | ||
blocks, | ||
blocks.length - 1, | ||
-1 | ||
); | ||
updateFocusAndSelection( blocks[ 0 ]?.clientId, false ); | ||
} | ||
} | ||
|
||
node.ownerDocument.addEventListener( 'copy', handler ); | ||
node.ownerDocument.addEventListener( 'cut', handler ); | ||
node.ownerDocument.addEventListener( 'paste', handler ); | ||
|
||
return () => { | ||
node.ownerDocument.removeEventListener( 'copy', handler ); | ||
node.ownerDocument.removeEventListener( 'cut', handler ); | ||
node.ownerDocument.removeEventListener( 'paste', handler ); | ||
}; | ||
}, [] ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
a2d9738
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flaky tests detected in a2d9738.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.
🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7648054136
📝 Reported issues:
/test/e2e/specs/editor/blocks/paragraph.spec.js