Skip to content

Commit

Permalink
fix/feat: dispatch clipboard events without active selection
Browse files Browse the repository at this point in the history
  • Loading branch information
tommie-lie committed Jan 26, 2024
1 parent d036279 commit 9cfa857
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 35 deletions.
26 changes: 15 additions & 11 deletions src/clipboard/copy.ts
Original file line number Diff line number Diff line change
@@ -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)
}

Expand Down
25 changes: 15 additions & 10 deletions src/clipboard/cut.ts
Original file line number Diff line number Diff line change
@@ -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)
}

Expand Down
37 changes: 30 additions & 7 deletions tests/clipboard/copy.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>(
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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(`<input/>`)
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 () => {
Expand All @@ -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(`<div tabindex="-1" />`, {})

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', {
Expand All @@ -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')
})
})
37 changes: 30 additions & 7 deletions tests/clipboard/cut.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>(
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -42,22 +43,22 @@ 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')

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(`<input/>`)
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 () => {
Expand All @@ -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(`<div tabindex="-1" />`, {})

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', {
Expand All @@ -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')
})
})

0 comments on commit 9cfa857

Please sign in to comment.