Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions e2e/drag-drop.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ test("drops a single file and returns it", async ({page}) => {
expect(files[0].hasHandle).toBe(true);
});

test("the returned File stays readable after the drop (issue #1411)", async ({page}) => {
// The handle branch now hands back the File captured from getAsFile() rather than
// handle.getFile(). Reading its contents *after* the drop event resolved proves that File is a
// durable, readable snapshot — the property Electron's webUtils.getPathForFile() relies on, and
// what regressed when v1.x started returning the handle's File instead.
// https://github.com/react-dropzone/react-dropzone/issues/1411
await cdpDrop(page, [fixture("files/hello.json")]);

const files = await readResult(page);
expect(files[0].hasHandle).toBe(true);
expect(files[0].content).toBe('{"hello": true}\n');
});

test("drops a directory and flattens it recursively", async ({page}) => {
await cdpDrop(page, [fixture("tree")]);

Expand Down
23 changes: 14 additions & 9 deletions e2e/fixture.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@
async function handle(evt) {
try {
const files = await fromEvent(evt);
window.__result = files.map(file => ({
name: file.name,
type: file.type,
size: file.size,
path: file.path,
relativePath: file.relativePath,
isFile: file instanceof File,
hasHandle: typeof file.handle?.getFile === "function"
}));
// Read contents *after* fromEvent resolved (i.e. after the drop event and its awaits), to
// prove the returned File is still a durable, readable snapshot.
window.__result = await Promise.all(
files.map(async file => ({
name: file.name,
type: file.type,
size: file.size,
path: file.path,
relativePath: file.relativePath,
isFile: file instanceof File,
hasHandle: typeof file.handle?.getFile === "function",
content: await file.text()
}))
);
} catch (err) {
window.__error = String((err && err.message) || err);
}
Expand Down
1 change: 1 addition & 0 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface SelectedFile {
relativePath?: string;
isFile: boolean;
hasHandle: boolean;
content: string;
}

const MIME: Record<string, string> = {
Expand Down
47 changes: 47 additions & 0 deletions src/file-selector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,53 @@ it("reads getAsFile() synchronously so awaiting getAsFileSystemHandle can't neut
expect((files[0] as FileWithPath).name).toBe(name);
});

it("returns the original getAsFile() File (not handle.getFile()) so Electron's webUtils.getPathForFile keeps resolving a path (issue #1411)", async () => {
// In Electron, webUtils.getPathForFile() only resolves an on-disk path from the *original* File
// the drop produced (what DataTransferItem.getAsFile() returns). A File from
// FileSystemFileHandle.getFile() loses that path (electron/electron#33647), so when a handle is
// available we must still hand back the original File, with the handle attached.
// See https://github.com/react-dropzone/react-dropzone/issues/1411
const name = "dropped.json";
const originalFile = createFile(name, {ping: true}, {type: "application/json"});
const handleFile = createFile(name, {ping: true}, {type: "application/json"});
const h = {
getFile() {
return Promise.resolve(handleFile);
}
} as any;
const evt = dragEvtFromItems([dataTransferItemWithFsHandle(originalFile, h)]);

const files = await fromEvent(evt);
expect(files).toHaveLength(1);

const [file] = files as FileWithPath[];
// Object identity is preserved: the returned File is the one from getAsFile(), not the handle's.
expect(file).toBe(originalFile);
expect(file).not.toBe(handleFile);
// The durable handle is still attached.
expect((file as any).handle).toBe(h);
});

it("falls back to handle.getFile() when getAsFile() returns null (e.g. a cross-window drop)", async () => {
// No original File to preserve, so the handle's File is the only source. It still carries the
// durable handle.
const name = "cross-window.json";
const handleFile = createFile(name, {ping: true}, {type: "application/json"});
const h = {
getFile() {
return Promise.resolve(handleFile);
}
} as any;
const evt = dragEvtFromItems([dataTransferItemWithFsHandle(null, h)]);

const files = await fromEvent(evt);
expect(files).toHaveLength(1);

const [file] = files as FileWithPath[];
expect(file).toBe(handleFile);
expect((file as any).handle).toBe(h);
});

it("guesses the {type} from the extension using the built-in defaults", async () => {
const mockFile = createFile("test.png", {ping: true}); // typeless File
const evt = inputEvtFromFiles(mockFile);
Expand Down
12 changes: 9 additions & 3 deletions src/file-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,16 @@ async function fromDataTransferItem(item: DataTransferItem, entry?: FileSystemEn
const syncFile = item.getAsFile();

const h = await getFsHandle(item);
// Prefer the File System Access handle when we got one: it carries a durable handle (and, for
// cross-window drops, the freshest File).
if (h !== null && h !== undefined) {
const file = await h.getFile();
// Hand back the File we captured synchronously from the DataTransferItem when we have it,
// rather than `handle.getFile()`. Both wrap the same dropped file, but only the original
// preserves the File object identity that Electron's `webUtils.getPathForFile()` needs to
// resolve an on-disk path; a File from `FileSystemFileHandle.getFile()` loses it
// (electron/electron#33647), which broke drag-and-drop path access in Electron.
// See https://github.com/react-dropzone/react-dropzone/issues/1411
// `syncFile` is only null in rare cases (e.g. a cross-window drop where getAsFile() returned
// null), so fall back to the handle's File there. Either way, attach the durable handle.
const file = syncFile ?? (await h.getFile());
file.handle = h;
return toFileWithPath(file);
}
Expand Down