diff --git a/src/clipboard/copy.ts b/src/clipboard/copy.ts index fac54bb9..813233bf 100644 --- a/src/clipboard/copy.ts +++ b/src/clipboard/copy.ts @@ -1,23 +1,27 @@ import {copySelection} from '../document' import {type Instance} from '../setup' -import {writeDataTransferToClipboard} from '../utils' +import { + createDataTransfer, + getWindow, + writeDataTransferToClipboard, +} from '../utils' export async function copy(this: Instance) { const doc = this.config.document const target = doc.activeElement ?? /* istanbul ignore next */ doc.body - const clipboardData = copySelection(target) - - if (clipboardData.items.length === 0) { - return + const clipboardData = createDataTransfer(getWindow(target)) + const shouldDoDefault = this.dispatchUIEvent(target, 'copy', { + clipboardData, + }) + if (shouldDoDefault) { + const defaultClipboardData = copySelection(target) + defaultClipboardData.types.forEach(type => { + clipboardData.setData(type, defaultClipboardData.getData(type)) + }) } - if ( - this.dispatchUIEvent(target, 'copy', { - clipboardData, - }) && - this.config.writeToClipboard - ) { + if (clipboardData.items.length > 0 && this.config.writeToClipboard) { await writeDataTransferToClipboard(doc, clipboardData) } diff --git a/src/clipboard/cut.ts b/src/clipboard/cut.ts index 7e75e2ee..9e8b5e0d 100644 --- a/src/clipboard/cut.ts +++ b/src/clipboard/cut.ts @@ -1,23 +1,28 @@ import {copySelection} from '../document' import {type Instance} from '../setup' -import {writeDataTransferToClipboard} from '../utils' +import { + createDataTransfer, + getWindow, + writeDataTransferToClipboard, +} from '../utils' export async function cut(this: Instance) { const doc = this.config.document const target = doc.activeElement ?? /* istanbul ignore next */ doc.body - const clipboardData = copySelection(target) + const defaultClipboardData = copySelection(target) - if (clipboardData.items.length === 0) { - return + const clipboardData = createDataTransfer(getWindow(target)) + const shouldDoDefault = this.dispatchUIEvent(target, 'cut', { + clipboardData, + }) + if (shouldDoDefault) { + defaultClipboardData.types.forEach(type => { + clipboardData.setData(type, defaultClipboardData.getData(type)) + }) } - if ( - this.dispatchUIEvent(target, 'cut', { - clipboardData, - }) && - this.config.writeToClipboard - ) { + if (clipboardData.items.length > 0 && this.config.writeToClipboard) { await writeDataTransferToClipboard(target.ownerDocument, clipboardData) } diff --git a/tests/clipboard/copy.ts b/tests/clipboard/copy.ts index 1b0e3b11..1dfc26c7 100644 --- a/tests/clipboard/copy.ts +++ b/tests/clipboard/copy.ts @@ -1,5 +1,6 @@ import userEvent from '#src' import {render, setup} from '#testHelpers' +import {readDataTransferFromClipboard} from '#src/utils' test('copy selected value', async () => { const {getEvents, user} = setup( @@ -11,7 +12,7 @@ test('copy selected value', async () => { const dt = await user.copy() - expect(dt?.getData('text')).toBe('bar') + expect(dt.getData('text')).toBe('bar') expect(getEvents('copy')).toHaveLength(1) await expect(window.navigator.clipboard.readText()).resolves.toBe('bar') @@ -24,7 +25,7 @@ test('copy selected text outside of editable', async () => { const dt = await user.copy() - expect(dt?.getData('text')).toBe('oo b') + expect(dt.getData('text')).toBe('oo b') expect(getEvents('copy')).toHaveLength(1) await expect(window.navigator.clipboard.readText()).resolves.toBe('oo b') @@ -37,20 +38,20 @@ test('copy selected text in contenteditable', async () => { const dt = await user.copy() - expect(dt?.getData('text')).toBe('oo b') + expect(dt.getData('text')).toBe('oo b') expect(getEvents('copy')).toHaveLength(1) await expect(window.navigator.clipboard.readText()).resolves.toBe('oo b') }) -test('copy on empty selection does nothing', async () => { +test('copy on empty selection does not change clipboard', async () => { const {getEvents, user} = setup(``) await window.navigator.clipboard.writeText('foo') await user.copy() await expect(window.navigator.clipboard.readText()).resolves.toBe('foo') - expect(getEvents()).toHaveLength(0) + expect(getEvents('copy')).toHaveLength(1) }) test('prevent default behavior per event handler', async () => { @@ -65,10 +66,32 @@ test('prevent default behavior per event handler', async () => { await user.copy() expect(eventWasFired('copy')).toBe(true) - expect(getEvents('copy')[0].clipboardData?.getData('text')).toBe('bar') + expect(getEvents('copy')[0].clipboardData?.getData('text')).toBe('') await expect(window.navigator.clipboard.readText()).resolves.toBe('foo') }) +test('copies all items added in event handler', async () => { + const {element, user} = setup(`
`, {}) + + element.addEventListener('copy', e => { + e.clipboardData?.setData('text/plain', 'a = 42') + e.clipboardData?.setData('application/json', '{"a": 42}') + e.preventDefault() + }) + + await user.copy() + + const receivedClipboardData = await readDataTransferFromClipboard( + element.ownerDocument, + ) + expect(receivedClipboardData.types).toEqual([ + 'text/plain', + 'application/json', + ]) + expect(receivedClipboardData.getData('text/plain')).toBe('a = 42') + expect(receivedClipboardData.getData('application/json')).toBe('{"a": 42}') +}) + describe('without Clipboard API', () => { beforeEach(() => { Object.defineProperty(window.navigator, 'clipboard', { @@ -95,6 +118,6 @@ describe('without Clipboard API', () => { }) const dt = await userEvent.copy() - expect(dt?.getData('text/plain')).toBe('bar') + expect(dt.getData('text/plain')).toBe('bar') }) }) diff --git a/tests/clipboard/cut.ts b/tests/clipboard/cut.ts index b8c2fc45..e3871e1d 100644 --- a/tests/clipboard/cut.ts +++ b/tests/clipboard/cut.ts @@ -1,5 +1,6 @@ import userEvent from '#src' import {render, setup} from '#testHelpers' +import {readDataTransferFromClipboard} from '#src/utils' test('cut selected value', async () => { const {getEvents, user} = setup( @@ -11,7 +12,7 @@ test('cut selected value', async () => { const dt = await user.cut() - expect(dt?.getData('text')).toBe('bar') + expect(dt.getData('text')).toBe('bar') expect(getEvents('cut')).toHaveLength(1) expect(getEvents('input')).toHaveLength(1) @@ -25,7 +26,7 @@ test('cut selected text outside of editable', async () => { const dt = await user.cut() - expect(dt?.getData('text')).toBe('oo b') + expect(dt.getData('text')).toBe('oo b') expect(getEvents('cut')).toHaveLength(1) expect(getEvents('input')).toHaveLength(0) @@ -42,7 +43,7 @@ test('cut selected text in contenteditable', async () => { const dt = await user.cut() - expect(dt?.getData('text')).toBe('oo b') + expect(dt.getData('text')).toBe('oo b') expect(getEvents('cut')).toHaveLength(1) expect(getEvents('input')).toHaveLength(1) expect(element).toHaveTextContent('far baz') @@ -50,14 +51,14 @@ test('cut selected text in contenteditable', async () => { await expect(window.navigator.clipboard.readText()).resolves.toBe('oo b') }) -test('cut on empty selection does nothing', async () => { +test('cut on empty selection does not change clipboard', async () => { const {getEvents, user} = setup(``) await window.navigator.clipboard.writeText('foo') await user.cut() await expect(window.navigator.clipboard.readText()).resolves.toBe('foo') - expect(getEvents()).toHaveLength(0) + expect(getEvents('cut')).toHaveLength(1) }) test('prevent default behavior per event handler', async () => { @@ -72,11 +73,33 @@ test('prevent default behavior per event handler', async () => { await user.cut() expect(eventWasFired('cut')).toBe(true) - expect(getEvents('cut')[0].clipboardData?.getData('text')).toBe('bar') + expect(getEvents('cut')[0].clipboardData?.getData('text')).toBe('') expect(eventWasFired('input')).toBe(false) await expect(window.navigator.clipboard.readText()).resolves.toBe('foo') }) +test('cuts all items added in event handler', async () => { + const {element, user} = setup(`
`, {}) + + element.addEventListener('cut', e => { + e.clipboardData?.setData('text/plain', 'a = 42') + e.clipboardData?.setData('application/json', '{"a": 42}') + e.preventDefault() + }) + + await user.cut() + + const receivedClipboardData = await readDataTransferFromClipboard( + element.ownerDocument, + ) + expect(receivedClipboardData.types).toEqual([ + 'text/plain', + 'application/json', + ]) + expect(receivedClipboardData.getData('text/plain')).toBe('a = 42') + expect(receivedClipboardData.getData('application/json')).toBe('{"a": 42}') +}) + describe('without Clipboard API', () => { beforeEach(() => { Object.defineProperty(window.navigator, 'clipboard', { @@ -103,6 +126,6 @@ describe('without Clipboard API', () => { }) const dt = await userEvent.cut() - expect(dt?.getData('text/plain')).toBe('bar') + expect(dt.getData('text/plain')).toBe('bar') }) })