Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix/feat: dispatch clipboard events without active selection #1190

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 15 additions & 11 deletions 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)
}

Expand Down
25 changes: 15 additions & 10 deletions 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)
}

Expand Down
37 changes: 30 additions & 7 deletions 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<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
@@ -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')
})
})