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`] = ` +" +

+ + + +
+

P

+
+" +`; + 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`] = ` +" +

+ + + +
+

P

+
+" +`; + 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(); + } ); } );