fix: normalize DataTransfer format aliases - #1326
Conversation
snowystinger
left a comment
There was a problem hiding this comment.
Ok, went down a bit of a rabbit hole looking into this.
setData and getData appear to work consistently across the browsers. I learned during this process though that add, which has a similar spec, works completely differently FF/Safari/Chrome https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransferitemlist-add
This does mean that this combination wouldn't work anymore
dt.items.add('foo', 'TEXT/PLAIN')
dt.getData('TEXT/PLAIN')
I am a little torn if we should match the spec for the add function just in case. Or not, because all the browsers disagree.
Will think on it some more tonight. If you have any thoughts, let me know.
The spec lowercases the type in DataTransferItemList.add() but, unlike setData(), does not replace the text and url shorthands.
|
The spec is unambiguous on the half your example needs: Worth knowing before you decide: main isn't a working baseline here either. It works for exactly one spelling, and this branch moved which one. Both store and look up verbatim, so the case has to agree on the two sides by luck. Lowercasing in Pushed that as eb51950, kept as its own commit so it's one revert if you'd rather leave |
|
Recording output for tests for Real browser tests are unaffected, UserEvent's stub only applies if For the tested items.add() cases, Safari matches the current HTML Standard. I think that's enough reason for us to make the code in User Event spec compliant. If it breaks people, then likely their code is only working in one browser anyways. This change should continue to match more browsers as they converge on the spec. If someone needs to jsdom test a specific browser with different behaviour, they can define their own DataTransfer on the window, we'll defer to that.
|
| Input | item.type |
DataTransfer.types |
getData(original) |
getData(lowercase) |
|---|---|---|---|---|
TEXT/PLAIN |
TEXT/PLAIN |
["TEXT/PLAIN"] |
"" |
"" |
TEXT |
TEXT |
["TEXT"] |
"" |
"" |
URL |
URL |
["URL"] |
"" |
"" |
MiXeD/Type |
MiXeD/Type |
["MiXeD/Type"] |
"" |
"" |
Safari
| Input | item.type |
DataTransfer.types |
getData(original) |
getData(lowercase) |
|---|---|---|---|---|
TEXT/PLAIN |
text/plain |
["text/plain"] |
value |
value |
TEXT |
text |
["text"] |
"" |
"" |
URL |
url |
["url"] |
"" |
"" |
MiXeD/Type |
mixed/type |
["mixed/type"] |
value |
value |
Firefox
| Input | item.type |
DataTransfer.types |
getData(original) |
getData(lowercase) |
|---|---|---|---|---|
TEXT/PLAIN |
text/plain |
["text/plain"] |
value |
value |
TEXT |
text/plain |
["text/plain"] |
value |
value |
URL |
text/uri-list |
["text/uri-list"] |
value |
value |
MiXeD/Type |
mixed/type |
["mixed/type"] |
value |
value |
Code used to test
(() => {
const test = type => {
const dt = new DataTransfer()
try {
const item = dt.items.add('value', type)
return {
input: type,
itemType: item?.type,
dataTransferTypes: [...dt.types],
getOriginal: dt.getData(type),
getLowercase: dt.getData(type.toLowerCase()),
}
} catch (error) {
return {
input: type,
error: `${error.name}: ${error.message}`,
}
}
}
const results = [
'TEXT/PLAIN',
'TEXT',
'URL',
'MiXeD/Type',
].map(test)
console.log(navigator.userAgent)
console.table(results)
return results
})()setData
Chrome
| Input | item.type |
DataTransfer.types |
getData(original) |
getData(lowercase) |
|---|---|---|---|---|
TEXT/PLAIN |
text/plain |
["text/plain"] |
value |
value |
TEXT |
text/plain |
["text/plain"] |
value |
value |
URL |
text/uri-list |
["text/uri-list"] |
https://example.com/path |
https://example.com/path |
MiXeD/Type |
mixed/type |
["mixed/type"] |
value |
value |
Safari
| Input | item.type |
DataTransfer.types |
getData(original) |
getData(lowercase) |
|---|---|---|---|---|
TEXT/PLAIN |
text/plain |
["text/plain"] |
value |
value |
TEXT |
text/plain |
["text/plain"] |
value |
value |
URL |
text/uri-list |
["text/uri-list"] |
https://example.com/path |
https://example.com/path |
MiXeD/Type |
mixed/type |
["mixed/type"] |
value |
value |
Firefox
| Input | item.type |
DataTransfer.types |
getData(original) |
getData(lowercase) |
|---|---|---|---|---|
TEXT/PLAIN |
text/plain |
["text/plain"] |
value |
value |
TEXT |
text/plain |
["text/plain"] |
value |
value |
URL |
text/uri-list |
["text/uri-list"] |
https://example.com/path |
https://example.com/path |
MiXeD/Type |
mixed/type |
["mixed/type"] |
value |
value |
|
🎉 This PR is included in version 14.6.7 🎉 The release is available on: Your semantic-release bot 📦🚀 |
What
DataTransfer.getData(),setData()andclearData()now normalize theirformatargument the way the HTML spec requires. The format is converted to ASCII lowercase, and the shorthandstextandurlare replaced withtext/plainandtext/uri-list.Fixes #1269
Why
userEvent.paste('foo')builds itsDataTransferwithsetData('text', ...), and the stub stored that string verbatim as the item type. A paste handler readingevent.clipboardData.typestherefore saw['text'], so the check reported in #1269,types.includes('text/plain'), was false. No browser produces atexttype on a clipboard event, so handlers written against real clipboard data did not match.Two related cases were wrong in the same way on main.
setData('url', ...)stored the typeurl, which leftgetData('text/uri-list')returning an empty string. Separately,clearData('text')failed to remove an item stored astext/plain, and a format containing uppercase letters was stored and looked up as a type distinct from its lowercase spelling.The spec puts this mapping inside each of the three methods.
setData()converts the format to ASCII lowercase, then changestexttotext/plainandurltotext/uri-list, at https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-setdata.getData()repeats both steps before looking the item up, at https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-getdata, andclearData()does the same before removing one, at https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-cleardata.How
The mapping lives in a
normalizeFormat()helper insrc/utils/dataTransfer/DataTransfer.ts, and all three methods run theirformatargument through it.It belongs there rather than in
src/clipboard/paste.tsbecause the type is decided bysetData(), not by its callers, so fixing it at the method covers every caller at once.copySelection()and anyDataTransfera user builds and hands touserEvent.paste()reach the same methods, and a change confined topaste.tswould have left theurlshorthand and the case handling broken for all of them.paste.tsis untouched and now yieldstext/plainon its own, which is what the added end to end test asserts.One part of the spec algorithm is deliberately left out.
getData('url')is also supposed to parse atext/uri-listbody down to its first URL, which is a separate step from format normalization and is not included here.tests/utils/dataTransfer/DataTransfer.tsgains asetDataandgetDataround trip through both shorthands, the same round trip written in mixed case, an overwrite where an item declared under one spelling is replaced by writing the other, and aclearData()call that removes an item by its shorthand.tests/clipboard/paste.tsgains the reported scenario, asserting that a paste event built from a string exposestext/plainintypesand returns the string fromgetData('text/plain'). Every added test fails on main and passes with this change.The whole Jest suite passes at 514 tests in 54 files,
npm run validatereports no type errors, andeslintreports no new problems on the changed files.Checklist