diff --git a/src/utils/dataTransfer/DataTransfer.ts b/src/utils/dataTransfer/DataTransfer.ts index 87241e2a..86f7b269 100644 --- a/src/utils/dataTransfer/DataTransfer.ts +++ b/src/utils/dataTransfer/DataTransfer.ts @@ -61,6 +61,18 @@ class DataTransferItemListStub } } +// The spec requires the format to be converted to ASCII lowercase +// and the shorthands `text` and `url` to be replaced with their MIME types. +// https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-setdata +function normalizeFormat(format: string) { + const type = format.replace(/[A-Z]/g, char => char.toLowerCase()) + return type === 'text' + ? 'text/plain' + : type === 'url' + ? 'text/uri-list' + : type +} + function getTypeMatcher(type: string, exact: boolean) { const [group, sub] = type.split('/') const isGroup = !sub || sub === '*' @@ -76,9 +88,10 @@ function getTypeMatcher(type: string, exact: boolean) { function createDataTransferStub(window: Window & typeof globalThis) { return new (class DataTransferStub implements DataTransfer { getData(format: string) { + const type = normalizeFormat(format) const match = - this.items.find(getTypeMatcher(format, true)) ?? - this.items.find(getTypeMatcher(format, false)) + this.items.find(getTypeMatcher(type, true)) ?? + this.items.find(getTypeMatcher(type, false)) let text = '' match?.getAsString(t => { @@ -89,9 +102,10 @@ function createDataTransferStub(window: Window & typeof globalThis) { } setData(format: string, data: string) { - const matchIndex = this.items.findIndex(getTypeMatcher(format, true)) + const type = normalizeFormat(format) + const matchIndex = this.items.findIndex(getTypeMatcher(type, true)) - const item = new DataTransferItemStub(data, format) as DataTransferItem + const item = new DataTransferItemStub(data, type) as DataTransferItem if (matchIndex >= 0) { this.items.splice(matchIndex, 1, item) } else { @@ -101,7 +115,8 @@ function createDataTransferStub(window: Window & typeof globalThis) { clearData(format?: string) { if (format) { - const matchIndex = this.items.findIndex(getTypeMatcher(format, true)) + const type = normalizeFormat(format) + const matchIndex = this.items.findIndex(getTypeMatcher(type, true)) if (matchIndex >= 0) { this.items.remove(matchIndex) diff --git a/tests/clipboard/paste.ts b/tests/clipboard/paste.ts index cd5eb967..9e163921 100644 --- a/tests/clipboard/paste.ts +++ b/tests/clipboard/paste.ts @@ -86,6 +86,16 @@ test('prevent input per paste event handler', async () => { expect(eventWasFired('input')).toBe(false) }) +test('paste string provides clipboard data as text/plain', async () => { + const {getEvents, user} = setup(``) + + await user.paste('foo') + + const clipboardData = getEvents('paste')[0].clipboardData + expect(clipboardData?.types).toEqual(['text/plain']) + expect(clipboardData?.getData('text/plain')).toBe('foo') +}) + test.each(['input', 'textarea'])( 'should paste text in <%s> up to maxLength if provided', async type => { diff --git a/tests/utils/dataTransfer/DataTransfer.ts b/tests/utils/dataTransfer/DataTransfer.ts index 94352d9f..10e8bdb1 100644 --- a/tests/utils/dataTransfer/DataTransfer.ts +++ b/tests/utils/dataTransfer/DataTransfer.ts @@ -30,6 +30,56 @@ describe('create DataTransfer', () => { expect(dt.getData('text')).toBe('baz') }) + test('normalize format shorthands', async () => { + const dt = createDataTransfer(window) + dt.setData('text', 'foo') + dt.setData('url', 'https://example.com') + + expect(dt.types).toEqual(['text/plain', 'text/uri-list']) + + expect(dt.getData('text/plain')).toBe('foo') + expect(dt.getData('text')).toBe('foo') + expect(dt.getData('text/uri-list')).toBe('https://example.com') + expect(dt.getData('url')).toBe('https://example.com') + }) + + test('normalize format case', async () => { + const dt = createDataTransfer(window) + dt.setData('TEXT/PLAIN', 'foo') + dt.setData('Url', 'https://example.com') + + expect(dt.types).toEqual(['text/plain', 'text/uri-list']) + + expect(dt.getData('text/plain')).toBe('foo') + expect(dt.getData('Text')).toBe('foo') + expect(dt.getData('text/uri-list')).toBe('https://example.com') + }) + + test('overwrite item declared per shorthand', async () => { + const dt = createDataTransfer(window) + dt.setData('text/plain', 'foo') + dt.setData('text', 'bar') + + expect(dt.types).toEqual(['text/plain']) + expect(dt.getData('text/plain')).toBe('bar') + }) + + test('clear data per shorthand', async () => { + const dt = createDataTransfer(window) + dt.setData('text/plain', 'foo') + dt.setData('text/uri-list', 'https://example.com') + + dt.clearData('text') + + expect(dt.types).toEqual(['text/uri-list']) + expect(dt.getData('text/plain')).toBe('') + + dt.clearData('URL') + + expect(dt.types).toEqual([]) + expect(dt.getData('text/uri-list')).toBe('') + }) + test('overwrite item', async () => { const dt = createDataTransfer(window) dt.setData('text/plain', 'foo')