diff --git a/packages/block-editor/src/components/copy-handler/index.js b/packages/block-editor/src/components/copy-handler/index.js
index 9138000500f8f..f3494db3b4a13 100644
--- a/packages/block-editor/src/components/copy-handler/index.js
+++ b/packages/block-editor/src/components/copy-handler/index.js
@@ -113,6 +113,7 @@ export function useClipboardHandler() {
return;
}
+ const eventDefaultPrevented = event.defaultPrevented;
event.preventDefault();
if ( event.type === 'copy' || event.type === 'cut' ) {
@@ -130,6 +131,10 @@ export function useClipboardHandler() {
if ( event.type === 'cut' ) {
removeBlocks( selectedBlockClientIds );
} else if ( event.type === 'paste' ) {
+ if ( eventDefaultPrevented ) {
+ // This was likely already handled in rich-text/use-paste-handler.js
+ return;
+ }
const {
__experimentalCanUserUseUnfilteredHTML: canUserUseUnfilteredHTML,
} = getSettings();
diff --git a/packages/e2e-test-utils/src/press-key-with-modifier.js b/packages/e2e-test-utils/src/press-key-with-modifier.js
index a26a648aab9a0..24eb4c3c86e48 100644
--- a/packages/e2e-test-utils/src/press-key-with-modifier.js
+++ b/packages/e2e-test-utils/src/press-key-with-modifier.js
@@ -126,6 +126,7 @@ async function emulateClipboard( type ) {
document.activeElement.dispatchEvent(
new ClipboardEvent( _type, {
bubbles: true,
+ cancelable: true,
clipboardData: window._clipboardData,
} )
);
diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/copy-cut-paste-whole-blocks.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/copy-cut-paste-whole-blocks.test.js.snap
index ccc1cfe05a31e..3f7451bd87db4 100644
--- a/packages/e2e-tests/specs/editor/various/__snapshots__/copy-cut-paste-whole-blocks.test.js.snap
+++ b/packages/e2e-tests/specs/editor/various/__snapshots__/copy-cut-paste-whole-blocks.test.js.snap
@@ -1,5 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Copy/cut/paste of whole blocks can copy group onto non textual element (image, spacer) 1`] = `""`;
+
+exports[`Copy/cut/paste of whole blocks can copy group onto non textual element (image, spacer) 2`] = `
+"
+
+
+
+
+
+"
+`;
+
exports[`Copy/cut/paste of whole blocks should copy and paste individual blocks 1`] = `
"
Here is a unique string so we can test copying.
@@ -56,6 +70,20 @@ exports[`Copy/cut/paste of whole blocks should cut and paste individual blocks 2
"
`;
+exports[`Copy/cut/paste of whole blocks should handle paste events once 1`] = `""`;
+
+exports[`Copy/cut/paste of whole blocks should handle paste events once 2`] = `
+"
+
+
+
+
+
+"
+`;
+
exports[`Copy/cut/paste of whole blocks should respect inline copy in places like input fields and textareas 1`] = `
"
[my-shortcode]
diff --git a/packages/e2e-tests/specs/editor/various/copy-cut-paste-whole-blocks.test.js b/packages/e2e-tests/specs/editor/various/copy-cut-paste-whole-blocks.test.js
index 4536b65266ce1..bcce036b4dea4 100644
--- a/packages/e2e-tests/specs/editor/various/copy-cut-paste-whole-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/copy-cut-paste-whole-blocks.test.js
@@ -92,4 +92,96 @@ describe( 'Copy/cut/paste of whole blocks', () => {
await pressKeyWithModifier( 'primary', 'v' );
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
+
+ it( 'should handle paste events once', async () => {
+ // Add group block with paragraph
+ await insertBlock( 'Group' );
+ await page.click( '.block-editor-button-block-appender' );
+ await page.click( '.editor-block-list-item-paragraph' );
+ await page.keyboard.type( 'P' );
+ await page.keyboard.press( 'ArrowLeft' );
+ await page.keyboard.press( 'ArrowLeft' );
+ // Cut group
+ await pressKeyWithModifier( 'primary', 'x' );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+
+ await page.keyboard.press( 'Enter' );
+
+ await page.evaluate( () => {
+ window.e2eTestPasteOnce = [];
+ let oldBlocks = wp.data.select( 'core/block-editor' ).getBlocks();
+ wp.data.subscribe( () => {
+ const blocks = wp.data
+ .select( 'core/block-editor' )
+ .getBlocks();
+ if ( blocks !== oldBlocks ) {
+ window.e2eTestPasteOnce.push(
+ blocks.map( ( { clientId, name } ) => ( {
+ clientId,
+ name,
+ } ) )
+ );
+ }
+ oldBlocks = blocks;
+ } );
+ } );
+
+ // Paste
+ await pressKeyWithModifier( 'primary', 'v' );
+
+ // Blocks should only be modified once, not twice with new clientIds on a single paste action
+ const blocksUpdated = await page.evaluate(
+ () => window.e2eTestPasteOnce
+ );
+
+ expect( blocksUpdated.length ).toEqual( 1 );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'can copy group onto non textual element (image, spacer)', async () => {
+ // Add group block with paragraph
+ await insertBlock( 'Group' );
+ await page.click( '.block-editor-button-block-appender' );
+ await page.click( '.editor-block-list-item-paragraph' );
+ await page.keyboard.type( 'P' );
+ await page.keyboard.press( 'ArrowLeft' );
+ await page.keyboard.press( 'ArrowLeft' );
+ // Cut group
+ await pressKeyWithModifier( 'primary', 'x' );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+
+ await page.keyboard.press( 'Enter' );
+
+ // Insert a non textual element (a spacer)
+ await insertBlock( 'Spacer' );
+ // Spacer is focused
+ await page.evaluate( () => {
+ window.e2eTestPasteOnce = [];
+ let oldBlocks = wp.data.select( 'core/block-editor' ).getBlocks();
+ wp.data.subscribe( () => {
+ const blocks = wp.data
+ .select( 'core/block-editor' )
+ .getBlocks();
+ if ( blocks !== oldBlocks ) {
+ window.e2eTestPasteOnce.push(
+ blocks.map( ( { clientId, name } ) => ( {
+ clientId,
+ name,
+ } ) )
+ );
+ }
+ oldBlocks = blocks;
+ } );
+ } );
+
+ await pressKeyWithModifier( 'primary', 'v' );
+
+ // Paste should be handled on non-textual elements and only handled once.
+ const blocksUpdated = await page.evaluate(
+ () => window.e2eTestPasteOnce
+ );
+
+ expect( blocksUpdated.length ).toEqual( 1 );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
} );