Skip to content

Commit 1e0020b

Browse files
fix: normalize DataTransfer format aliases (#1326)
* fix: normalize DataTransfer format aliases --------- Co-authored-by: Dylan Pulver <dylanpulver@users.noreply.github.com> Co-authored-by: Rob Snow <snowystinger@gmail.com>
1 parent d4b0593 commit 1e0020b

3 files changed

Lines changed: 105 additions & 6 deletions

File tree

‎src/utils/dataTransfer/DataTransfer.ts‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,23 @@ class DataTransferItemStub implements DataTransferItem {
4141
}
4242
}
4343

44+
function toAsciiLowercase(value: string) {
45+
return value.replace(/[A-Z]/g, char => char.toLowerCase())
46+
}
47+
4448
class DataTransferItemListStub
4549
extends Array<DataTransferItem>
4650
implements DataTransferItemList {
4751
add(data: string, type: string): DataTransferItem
4852
add(file: File): DataTransferItem
4953
add(...args: never[]) {
50-
const item = new DataTransferItemStub(args[0], args[1])
54+
// The spec converts the type to ASCII lowercase here, but - unlike
55+
// `setData()` - does not replace the `text` and `url` shorthands.
56+
// https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransferitemlist-add
57+
const item =
58+
typeof args[0] === 'string'
59+
? new DataTransferItemStub(args[0], toAsciiLowercase(args[1]))
60+
: new DataTransferItemStub(args[0])
5161
this.push(item)
5262
return item
5363
}
@@ -61,6 +71,18 @@ class DataTransferItemListStub
6171
}
6272
}
6373

74+
// The spec requires the format to be converted to ASCII lowercase
75+
// and the shorthands `text` and `url` to be replaced with their MIME types.
76+
// https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-setdata
77+
function normalizeFormat(format: string) {
78+
const type = toAsciiLowercase(format)
79+
return type === 'text'
80+
? 'text/plain'
81+
: type === 'url'
82+
? 'text/uri-list'
83+
: type
84+
}
85+
6486
function getTypeMatcher(type: string, exact: boolean) {
6587
const [group, sub] = type.split('/')
6688
const isGroup = !sub || sub === '*'
@@ -76,9 +98,10 @@ function getTypeMatcher(type: string, exact: boolean) {
7698
function createDataTransferStub(window: Window & typeof globalThis) {
7799
return new (class DataTransferStub implements DataTransfer {
78100
getData(format: string) {
101+
const type = normalizeFormat(format)
79102
const match =
80-
this.items.find(getTypeMatcher(format, true)) ??
81-
this.items.find(getTypeMatcher(format, false))
103+
this.items.find(getTypeMatcher(type, true)) ??
104+
this.items.find(getTypeMatcher(type, false))
82105

83106
let text = ''
84107
match?.getAsString(t => {
@@ -89,9 +112,10 @@ function createDataTransferStub(window: Window & typeof globalThis) {
89112
}
90113

91114
setData(format: string, data: string) {
92-
const matchIndex = this.items.findIndex(getTypeMatcher(format, true))
115+
const type = normalizeFormat(format)
116+
const matchIndex = this.items.findIndex(getTypeMatcher(type, true))
93117

94-
const item = new DataTransferItemStub(data, format) as DataTransferItem
118+
const item = new DataTransferItemStub(data, type) as DataTransferItem
95119
if (matchIndex >= 0) {
96120
this.items.splice(matchIndex, 1, item)
97121
} else {
@@ -101,7 +125,8 @@ function createDataTransferStub(window: Window & typeof globalThis) {
101125

102126
clearData(format?: string) {
103127
if (format) {
104-
const matchIndex = this.items.findIndex(getTypeMatcher(format, true))
128+
const type = normalizeFormat(format)
129+
const matchIndex = this.items.findIndex(getTypeMatcher(type, true))
105130

106131
if (matchIndex >= 0) {
107132
this.items.remove(matchIndex)

‎tests/clipboard/paste.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ test('prevent input per paste event handler', async () => {
8686
expect(eventWasFired('input')).toBe(false)
8787
})
8888

89+
test('paste string provides clipboard data as text/plain', async () => {
90+
const {getEvents, user} = setup(`<input/>`)
91+
92+
await user.paste('foo')
93+
94+
const clipboardData = getEvents('paste')[0].clipboardData
95+
expect(clipboardData?.types).toEqual(['text/plain'])
96+
expect(clipboardData?.getData('text/plain')).toBe('foo')
97+
})
98+
8999
test.each(['input', 'textarea'])(
90100
'should paste text in <%s> up to maxLength if provided',
91101
async type => {

‎tests/utils/dataTransfer/DataTransfer.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,70 @@ describe('create DataTransfer', () => {
3030
expect(dt.getData('text')).toBe('baz')
3131
})
3232

33+
test('normalize format shorthands', async () => {
34+
const dt = createDataTransfer(window)
35+
dt.setData('text', 'foo')
36+
dt.setData('url', 'https://example.com')
37+
38+
expect(dt.types).toEqual(['text/plain', 'text/uri-list'])
39+
40+
expect(dt.getData('text/plain')).toBe('foo')
41+
expect(dt.getData('text')).toBe('foo')
42+
expect(dt.getData('text/uri-list')).toBe('https://example.com')
43+
expect(dt.getData('url')).toBe('https://example.com')
44+
})
45+
46+
test('normalize format case', async () => {
47+
const dt = createDataTransfer(window)
48+
dt.setData('TEXT/PLAIN', 'foo')
49+
dt.setData('Url', 'https://example.com')
50+
51+
expect(dt.types).toEqual(['text/plain', 'text/uri-list'])
52+
53+
expect(dt.getData('text/plain')).toBe('foo')
54+
expect(dt.getData('Text')).toBe('foo')
55+
expect(dt.getData('text/uri-list')).toBe('https://example.com')
56+
})
57+
58+
test('items.add follows the standard', () => {
59+
const dt = createDataTransfer(window)
60+
dt.items.add('foo', 'TEXT/PLAIN')
61+
dt.items.add('bar', 'TEXT')
62+
dt.items.add('baz', 'URL')
63+
64+
// Chrome currently preserves casing. Keep this spec assertion so the
65+
// Toolbox result becomes green when Chrome converges with the standard.
66+
expect(dt.types).toEqual(['text/plain', 'text', 'url'])
67+
expect(dt.getData('TEXT/PLAIN')).toBe('foo')
68+
expect(dt.getData('text/plain')).toBe('foo')
69+
expect(dt.getData('text')).toBe('foo')
70+
})
71+
72+
test('overwrite item declared per shorthand', async () => {
73+
const dt = createDataTransfer(window)
74+
dt.setData('text/plain', 'foo')
75+
dt.setData('text', 'bar')
76+
77+
expect(dt.types).toEqual(['text/plain'])
78+
expect(dt.getData('text/plain')).toBe('bar')
79+
})
80+
81+
test('clear data per shorthand', async () => {
82+
const dt = createDataTransfer(window)
83+
dt.setData('text/plain', 'foo')
84+
dt.setData('text/uri-list', 'https://example.com')
85+
86+
dt.clearData('text')
87+
88+
expect(dt.types).toEqual(['text/uri-list'])
89+
expect(dt.getData('text/plain')).toBe('')
90+
91+
dt.clearData('URL')
92+
93+
expect(dt.types).toEqual([])
94+
expect(dt.getData('text/uri-list')).toBe('')
95+
})
96+
3397
test('overwrite item', async () => {
3498
const dt = createDataTransfer(window)
3599
dt.setData('text/plain', 'foo')

0 commit comments

Comments
 (0)