react-dropzone 14.3.8 → 14.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -1
- package/.releaserc.json +27 -0
- package/CHANGELOG.md +6 -0
- package/dist/es/index.js +66 -1
- package/dist/es/utils/index.js +25 -1
- package/dist/index.js +2 -2
- package/examples/drag-overlay/README.md +132 -0
- package/package.json +7 -1
- package/src/index.js +58 -0
- package/src/index.spec.js +141 -4
- package/src/utils/index.js +27 -1
- package/src/utils/index.spec.js +25 -0
- package/styleguide.config.js +4 -0
- package/typings/react-dropzone.d.ts +1 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# isDragGlobal Example
|
|
2
|
+
|
|
3
|
+
The `isDragGlobal` state is `true` when files are being dragged anywhere on the document, before they reach the dropzone. This allows you to show visual feedback (like a full-page overlay) to indicate where files can be dropped.
|
|
4
|
+
|
|
5
|
+
## Simple Example
|
|
6
|
+
|
|
7
|
+
```jsx harmony
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import {useDropzone} from 'react-dropzone';
|
|
10
|
+
|
|
11
|
+
function DragOverlay() {
|
|
12
|
+
const {
|
|
13
|
+
getRootProps,
|
|
14
|
+
getInputProps,
|
|
15
|
+
isDragGlobal,
|
|
16
|
+
isDragActive,
|
|
17
|
+
isDragAccept,
|
|
18
|
+
isDragReject,
|
|
19
|
+
acceptedFiles
|
|
20
|
+
} = useDropzone({
|
|
21
|
+
accept: {
|
|
22
|
+
'image/*': ['.png', '.jpg', '.jpeg', '.gif']
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const files = acceptedFiles.map(file => (
|
|
27
|
+
<li key={file.path}>
|
|
28
|
+
{file.path} - {file.size} bytes
|
|
29
|
+
</li>
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div style={{ minHeight: '100vh', padding: '20px' }}>
|
|
34
|
+
{/* Global overlay shown when dragging anywhere on the page */}
|
|
35
|
+
{isDragGlobal && !isDragActive && (
|
|
36
|
+
<div style={{
|
|
37
|
+
position: 'fixed',
|
|
38
|
+
top: 0,
|
|
39
|
+
left: 0,
|
|
40
|
+
right: 0,
|
|
41
|
+
bottom: 0,
|
|
42
|
+
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
43
|
+
border: '3px dashed #007bff',
|
|
44
|
+
pointerEvents: 'none',
|
|
45
|
+
display: 'flex',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
zIndex: 1000,
|
|
49
|
+
}}>
|
|
50
|
+
<h2 style={{ color: '#007bff' }}>Drop files anywhere on this page...</h2>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
<section className="container">
|
|
55
|
+
<div {...getRootProps({
|
|
56
|
+
className: 'dropzone',
|
|
57
|
+
style: {
|
|
58
|
+
border: '2px dashed #ccc',
|
|
59
|
+
borderRadius: '8px',
|
|
60
|
+
padding: '40px',
|
|
61
|
+
textAlign: 'center',
|
|
62
|
+
backgroundColor: isDragAccept ? '#d4edda' : isDragReject ? '#f8d7da' : 'white',
|
|
63
|
+
transition: 'all 0.2s',
|
|
64
|
+
}
|
|
65
|
+
})}>
|
|
66
|
+
<input {...getInputProps()} />
|
|
67
|
+
|
|
68
|
+
{/* Status indicators */}
|
|
69
|
+
{isDragGlobal && !isDragActive && (
|
|
70
|
+
<p style={{ color: '#007bff', fontWeight: 'bold' }}>
|
|
71
|
+
🌐 Drag detected on page!
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{isDragActive && !isDragAccept && !isDragReject && (
|
|
76
|
+
<p style={{ color: '#6c757d' }}>Drop files here...</p>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{isDragAccept && (
|
|
80
|
+
<p style={{ color: '#28a745', fontWeight: 'bold' }}>
|
|
81
|
+
✅ Drop to upload these files
|
|
82
|
+
</p>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{isDragReject && (
|
|
86
|
+
<p style={{ color: '#dc3545', fontWeight: 'bold' }}>
|
|
87
|
+
❌ Some files will be rejected
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{!isDragGlobal && !isDragActive && (
|
|
92
|
+
<p>Drag 'n' drop images here, or click to select files</p>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<aside>
|
|
97
|
+
<h4>Accepted files</h4>
|
|
98
|
+
<ul>{files}</ul>
|
|
99
|
+
</aside>
|
|
100
|
+
</section>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
<DragOverlay />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## State Transitions
|
|
109
|
+
|
|
110
|
+
The `isDragGlobal` state provides early feedback about drag operations:
|
|
111
|
+
|
|
112
|
+
1. **`isDragGlobal: false`** - No drag operation detected
|
|
113
|
+
2. **`isDragGlobal: true`** - Files are being dragged anywhere on the document
|
|
114
|
+
- This is set when `dragenter` fires on the document with files
|
|
115
|
+
3. **`isDragActive: true`** - Files are being dragged over the dropzone
|
|
116
|
+
- Takes precedence when you want to show different feedback
|
|
117
|
+
4. **`isDragAccept: true`** / **`isDragReject: true`** - Files are validated
|
|
118
|
+
- Indicates whether the dragged files meet the dropzone criteria
|
|
119
|
+
|
|
120
|
+
## Use Cases
|
|
121
|
+
|
|
122
|
+
- **Full-page overlays**: Show a visual indicator across the entire page when drag starts
|
|
123
|
+
- **Multi-dropzone highlighting**: Highlight all available dropzones when files are detected
|
|
124
|
+
- **Early user feedback**: Provide immediate visual feedback before users reach the target dropzone
|
|
125
|
+
- **Improved UX**: Make it clear that the application accepts drag and drop
|
|
126
|
+
|
|
127
|
+
## Events
|
|
128
|
+
|
|
129
|
+
`isDragGlobal` is reset to `false` when:
|
|
130
|
+
- Drag leaves the document (`dragleave` on all elements)
|
|
131
|
+
- Files are dropped anywhere (`drop` event)
|
|
132
|
+
- Drag operation is cancelled (`dragend` event, e.g., user presses ESC)
|
package/package.json
CHANGED
|
@@ -144,6 +144,11 @@
|
|
|
144
144
|
"@rollup/plugin-babel": "^5.3.0",
|
|
145
145
|
"@rollup/plugin-commonjs": "^21.0.1",
|
|
146
146
|
"@rollup/plugin-node-resolve": "^13.1.3",
|
|
147
|
+
"@semantic-release/changelog": "^6.0.0",
|
|
148
|
+
"@semantic-release/commit-analyzer": "^12.0.0",
|
|
149
|
+
"@semantic-release/github": "^10.0.0",
|
|
150
|
+
"@semantic-release/npm": "^13.1.3",
|
|
151
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
147
152
|
"@size-limit/preset-small-lib": "^7.0.5",
|
|
148
153
|
"@size-limit/webpack": "^7.0.5",
|
|
149
154
|
"@size-limit/webpack-why": "^7.0.5",
|
|
@@ -183,6 +188,7 @@
|
|
|
183
188
|
"rimraf": "^3.0.2",
|
|
184
189
|
"rollup": "^2.66.1",
|
|
185
190
|
"rollup-plugin-terser": "^7.0.2",
|
|
191
|
+
"semantic-release": "^25.0.2",
|
|
186
192
|
"size-limit": "^7.0.5",
|
|
187
193
|
"style-loader": "^3.3.1",
|
|
188
194
|
"styled-components": "^5.3.3",
|
|
@@ -191,7 +197,7 @@
|
|
|
191
197
|
"webpack-blocks": "^2.1.0"
|
|
192
198
|
},
|
|
193
199
|
"typings": "typings/react-dropzone.d.ts",
|
|
194
|
-
"version": "14.
|
|
200
|
+
"version": "14.4.1",
|
|
195
201
|
"engines": {
|
|
196
202
|
"node": ">= 10.13"
|
|
197
203
|
},
|
package/src/index.js
CHANGED
|
@@ -86,6 +86,7 @@ Dropzone.propTypes = {
|
|
|
86
86
|
* @param {boolean} params.isDragActive Active drag is in progress
|
|
87
87
|
* @param {boolean} params.isDragAccept Dragged files are accepted
|
|
88
88
|
* @param {boolean} params.isDragReject Some dragged files are rejected
|
|
89
|
+
* @param {boolean} params.isDragGlobal Files are being dragged anywhere on the document
|
|
89
90
|
* @param {File[]} params.acceptedFiles Accepted files
|
|
90
91
|
* @param {FileRejection[]} params.fileRejections Rejected files and why they were rejected
|
|
91
92
|
*/
|
|
@@ -323,6 +324,7 @@ export default Dropzone;
|
|
|
323
324
|
* @property {boolean} isDragActive Active drag is in progress
|
|
324
325
|
* @property {boolean} isDragAccept Dragged files are accepted
|
|
325
326
|
* @property {boolean} isDragReject Some dragged files are rejected
|
|
327
|
+
* @property {boolean} isDragGlobal Files are being dragged anywhere on the document
|
|
326
328
|
* @property {File[]} acceptedFiles Accepted files
|
|
327
329
|
* @property {FileRejection[]} fileRejections Rejected files and why they were rejected
|
|
328
330
|
*/
|
|
@@ -342,6 +344,7 @@ const initialState = {
|
|
|
342
344
|
isDragActive: false,
|
|
343
345
|
isDragAccept: false,
|
|
344
346
|
isDragReject: false,
|
|
347
|
+
isDragGlobal: false,
|
|
345
348
|
acceptedFiles: [],
|
|
346
349
|
fileRejections: [],
|
|
347
350
|
};
|
|
@@ -508,6 +511,7 @@ export function useDropzone(props = {}) {
|
|
|
508
511
|
}, [inputRef, isFileDialogActive, onFileDialogCancelCb, fsAccessApiWorksRef]);
|
|
509
512
|
|
|
510
513
|
const dragTargetsRef = useRef([]);
|
|
514
|
+
const globalDragTargetsRef = useRef([]);
|
|
511
515
|
const onDocumentDrop = (event) => {
|
|
512
516
|
if (rootRef.current && rootRef.current.contains(event.target)) {
|
|
513
517
|
// If we intercepted an event for our instance, let it propagate down to the instance's onDrop handler
|
|
@@ -531,6 +535,55 @@ export function useDropzone(props = {}) {
|
|
|
531
535
|
};
|
|
532
536
|
}, [rootRef, preventDropOnDocument]);
|
|
533
537
|
|
|
538
|
+
// Track global drag state for document-level drag events
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
const onDocumentDragEnter = (event) => {
|
|
541
|
+
globalDragTargetsRef.current = [
|
|
542
|
+
...globalDragTargetsRef.current,
|
|
543
|
+
event.target,
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
if (isEvtWithFiles(event)) {
|
|
547
|
+
dispatch({ isDragGlobal: true, type: "setDragGlobal" });
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const onDocumentDragLeave = (event) => {
|
|
552
|
+
// Only deactivate once we've left all children
|
|
553
|
+
globalDragTargetsRef.current = globalDragTargetsRef.current.filter(
|
|
554
|
+
(el) => el !== event.target && el !== null
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
if (globalDragTargetsRef.current.length > 0) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
dispatch({ isDragGlobal: false, type: "setDragGlobal" });
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const onDocumentDragEnd = () => {
|
|
565
|
+
globalDragTargetsRef.current = [];
|
|
566
|
+
dispatch({ isDragGlobal: false, type: "setDragGlobal" });
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const onDocumentDropGlobal = () => {
|
|
570
|
+
globalDragTargetsRef.current = [];
|
|
571
|
+
dispatch({ isDragGlobal: false, type: "setDragGlobal" });
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
document.addEventListener("dragenter", onDocumentDragEnter, false);
|
|
575
|
+
document.addEventListener("dragleave", onDocumentDragLeave, false);
|
|
576
|
+
document.addEventListener("dragend", onDocumentDragEnd, false);
|
|
577
|
+
document.addEventListener("drop", onDocumentDropGlobal, false);
|
|
578
|
+
|
|
579
|
+
return () => {
|
|
580
|
+
document.removeEventListener("dragenter", onDocumentDragEnter);
|
|
581
|
+
document.removeEventListener("dragleave", onDocumentDragLeave);
|
|
582
|
+
document.removeEventListener("dragend", onDocumentDragEnd);
|
|
583
|
+
document.removeEventListener("drop", onDocumentDropGlobal);
|
|
584
|
+
};
|
|
585
|
+
}, [rootRef]);
|
|
586
|
+
|
|
534
587
|
// Auto focus the root when autoFocus is true
|
|
535
588
|
useEffect(() => {
|
|
536
589
|
if (!disabled && autoFocus && rootRef.current) {
|
|
@@ -1028,6 +1081,11 @@ function reducer(state, action) {
|
|
|
1028
1081
|
fileRejections: action.fileRejections,
|
|
1029
1082
|
isDragReject: action.isDragReject,
|
|
1030
1083
|
};
|
|
1084
|
+
case "setDragGlobal":
|
|
1085
|
+
return {
|
|
1086
|
+
...state,
|
|
1087
|
+
isDragGlobal: action.isDragGlobal,
|
|
1088
|
+
};
|
|
1031
1089
|
case "reset":
|
|
1032
1090
|
return {
|
|
1033
1091
|
...initialState,
|
package/src/index.spec.js
CHANGED
|
@@ -579,13 +579,16 @@ describe("useDropzone() hook", () => {
|
|
|
579
579
|
</Dropzone>
|
|
580
580
|
);
|
|
581
581
|
|
|
582
|
-
expect(addEventListenerSpy).toHaveBeenCalledTimes(
|
|
582
|
+
expect(addEventListenerSpy).toHaveBeenCalledTimes(6);
|
|
583
583
|
|
|
584
584
|
const addEventCalls = collectEventListenerCalls(addEventListenerSpy);
|
|
585
585
|
const events = Object.keys(addEventCalls);
|
|
586
586
|
|
|
587
587
|
expect(events).toContain("dragover");
|
|
588
588
|
expect(events).toContain("drop");
|
|
589
|
+
expect(events).toContain("dragenter");
|
|
590
|
+
expect(events).toContain("dragleave");
|
|
591
|
+
expect(events).toContain("dragend");
|
|
589
592
|
|
|
590
593
|
events.forEach((eventName) => {
|
|
591
594
|
const [fn, options] = addEventCalls[eventName];
|
|
@@ -607,7 +610,7 @@ describe("useDropzone() hook", () => {
|
|
|
607
610
|
|
|
608
611
|
unmount();
|
|
609
612
|
|
|
610
|
-
expect(removeEventListenerSpy).toHaveBeenCalledTimes(
|
|
613
|
+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(6);
|
|
611
614
|
|
|
612
615
|
const addEventCalls = collectEventListenerCalls(addEventListenerSpy);
|
|
613
616
|
const removeEventCalls = collectEventListenerCalls(
|
|
@@ -617,6 +620,9 @@ describe("useDropzone() hook", () => {
|
|
|
617
620
|
|
|
618
621
|
expect(events).toContain("dragover");
|
|
619
622
|
expect(events).toContain("drop");
|
|
623
|
+
expect(events).toContain("dragenter");
|
|
624
|
+
expect(events).toContain("dragleave");
|
|
625
|
+
expect(events).toContain("dragend");
|
|
620
626
|
|
|
621
627
|
events.forEach((eventName) => {
|
|
622
628
|
const [a] = addEventCalls[eventName];
|
|
@@ -2360,6 +2366,46 @@ describe("useDropzone() hook", () => {
|
|
|
2360
2366
|
expect(dropzone).toHaveTextContent("dragReject");
|
|
2361
2367
|
});
|
|
2362
2368
|
|
|
2369
|
+
it("accepts files with empty type during dragenter (Chrome .md file issue)", async () => {
|
|
2370
|
+
const markdownFiles = [createFile("README.md", 1234, "text/markdown")];
|
|
2371
|
+
|
|
2372
|
+
const { container } = render(
|
|
2373
|
+
<Dropzone
|
|
2374
|
+
accept={{
|
|
2375
|
+
"text/markdown": [".md"],
|
|
2376
|
+
}}
|
|
2377
|
+
>
|
|
2378
|
+
{({
|
|
2379
|
+
getRootProps,
|
|
2380
|
+
getInputProps,
|
|
2381
|
+
isDragActive,
|
|
2382
|
+
isDragAccept,
|
|
2383
|
+
isDragReject,
|
|
2384
|
+
}) => (
|
|
2385
|
+
<div {...getRootProps()}>
|
|
2386
|
+
<input {...getInputProps()} />
|
|
2387
|
+
{isDragActive && "dragActive"}
|
|
2388
|
+
{isDragAccept && "dragAccept"}
|
|
2389
|
+
{isDragReject && "dragReject"}
|
|
2390
|
+
</div>
|
|
2391
|
+
)}
|
|
2392
|
+
</Dropzone>
|
|
2393
|
+
);
|
|
2394
|
+
const dropzone = container.querySelector("div");
|
|
2395
|
+
|
|
2396
|
+
// Simulate Chrome's behavior: during drag, .md files have empty type
|
|
2397
|
+
await act(() =>
|
|
2398
|
+
fireEvent.dragEnter(
|
|
2399
|
+
dropzone,
|
|
2400
|
+
createDtWithFiles(markdownFiles, { emptyTypes: true })
|
|
2401
|
+
)
|
|
2402
|
+
);
|
|
2403
|
+
|
|
2404
|
+
expect(dropzone).toHaveTextContent("dragActive");
|
|
2405
|
+
expect(dropzone).toHaveTextContent("dragAccept");
|
|
2406
|
+
expect(dropzone).not.toHaveTextContent("dragReject");
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2363
2409
|
it("sets {isDragActive, isDragAccept, isDragReject} if any files are rejected and {multiple} is false on dragenter", async () => {
|
|
2364
2410
|
const { container } = render(
|
|
2365
2411
|
<Dropzone
|
|
@@ -2632,6 +2678,94 @@ describe("useDropzone() hook", () => {
|
|
|
2632
2678
|
expect(dropzone).not.toHaveTextContent("dragReject");
|
|
2633
2679
|
});
|
|
2634
2680
|
|
|
2681
|
+
it("sets {isDragGlobal} to true when drag event is detected on document", async () => {
|
|
2682
|
+
const { container } = render(
|
|
2683
|
+
<Dropzone>
|
|
2684
|
+
{({ getRootProps, getInputProps, isDragGlobal }) => (
|
|
2685
|
+
<div {...getRootProps()}>
|
|
2686
|
+
<input {...getInputProps()} />
|
|
2687
|
+
{isDragGlobal && "dragGlobal"}
|
|
2688
|
+
</div>
|
|
2689
|
+
)}
|
|
2690
|
+
</Dropzone>
|
|
2691
|
+
);
|
|
2692
|
+
const dropzone = container.querySelector("div");
|
|
2693
|
+
|
|
2694
|
+
await act(() =>
|
|
2695
|
+
fireEvent.dragEnter(document.body, createDtWithFiles(files))
|
|
2696
|
+
);
|
|
2697
|
+
|
|
2698
|
+
expect(dropzone).toHaveTextContent("dragGlobal");
|
|
2699
|
+
});
|
|
2700
|
+
|
|
2701
|
+
it("sets {isDragGlobal} to false when drag leaves document", async () => {
|
|
2702
|
+
const { container } = render(
|
|
2703
|
+
<Dropzone>
|
|
2704
|
+
{({ getRootProps, getInputProps, isDragGlobal }) => (
|
|
2705
|
+
<div {...getRootProps()}>
|
|
2706
|
+
<input {...getInputProps()} />
|
|
2707
|
+
{isDragGlobal && "dragGlobal"}
|
|
2708
|
+
</div>
|
|
2709
|
+
)}
|
|
2710
|
+
</Dropzone>
|
|
2711
|
+
);
|
|
2712
|
+
const dropzone = container.querySelector("div");
|
|
2713
|
+
|
|
2714
|
+
await act(() =>
|
|
2715
|
+
fireEvent.dragEnter(document.body, createDtWithFiles(files))
|
|
2716
|
+
);
|
|
2717
|
+
expect(dropzone).toHaveTextContent("dragGlobal");
|
|
2718
|
+
|
|
2719
|
+
await act(() =>
|
|
2720
|
+
fireEvent.dragLeave(document.body, createDtWithFiles(files))
|
|
2721
|
+
);
|
|
2722
|
+
expect(dropzone).not.toHaveTextContent("dragGlobal");
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2725
|
+
it("sets {isDragGlobal} to false when drop occurs on document", async () => {
|
|
2726
|
+
const { container } = render(
|
|
2727
|
+
<Dropzone>
|
|
2728
|
+
{({ getRootProps, getInputProps, isDragGlobal }) => (
|
|
2729
|
+
<div {...getRootProps()}>
|
|
2730
|
+
<input {...getInputProps()} />
|
|
2731
|
+
{isDragGlobal && "dragGlobal"}
|
|
2732
|
+
</div>
|
|
2733
|
+
)}
|
|
2734
|
+
</Dropzone>
|
|
2735
|
+
);
|
|
2736
|
+
const dropzone = container.querySelector("div");
|
|
2737
|
+
|
|
2738
|
+
await act(() =>
|
|
2739
|
+
fireEvent.dragEnter(document.body, createDtWithFiles(files))
|
|
2740
|
+
);
|
|
2741
|
+
expect(dropzone).toHaveTextContent("dragGlobal");
|
|
2742
|
+
|
|
2743
|
+
await act(() => fireEvent.drop(document.body, createDtWithFiles(files)));
|
|
2744
|
+
expect(dropzone).not.toHaveTextContent("dragGlobal");
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
it("sets {isDragGlobal} to false when dragend occurs on document", async () => {
|
|
2748
|
+
const { container } = render(
|
|
2749
|
+
<Dropzone>
|
|
2750
|
+
{({ getRootProps, getInputProps, isDragGlobal }) => (
|
|
2751
|
+
<div {...getRootProps()}>
|
|
2752
|
+
<input {...getInputProps()} />
|
|
2753
|
+
{isDragGlobal && "dragGlobal"}
|
|
2754
|
+
</div>
|
|
2755
|
+
)}
|
|
2756
|
+
</Dropzone>
|
|
2757
|
+
);
|
|
2758
|
+
const dropzone = container.querySelector("div");
|
|
2759
|
+
|
|
2760
|
+
await act(() =>
|
|
2761
|
+
fireEvent.dragEnter(document.body, createDtWithFiles(files))
|
|
2762
|
+
);
|
|
2763
|
+
expect(dropzone).toHaveTextContent("dragGlobal");
|
|
2764
|
+
|
|
2765
|
+
fireEvent.dragEnd(document.body, createDtWithFiles(files));
|
|
2766
|
+
expect(dropzone).not.toHaveTextContent("dragGlobal");
|
|
2767
|
+
});
|
|
2768
|
+
|
|
2635
2769
|
it("rejects all files if {multiple} is false and {accept} criteria is not met", async () => {
|
|
2636
2770
|
const onDropSpy = jest.fn();
|
|
2637
2771
|
|
|
@@ -3581,15 +3715,18 @@ function focusWindow() {
|
|
|
3581
3715
|
/**
|
|
3582
3716
|
* createDtWithFiles creates a mock data transfer object that can be used for drop events
|
|
3583
3717
|
* @param {File[]} files
|
|
3718
|
+
* @param {object} options
|
|
3719
|
+
* @param {boolean} options.emptyTypes - If true, sets item types to empty string (simulates Chrome drag behavior)
|
|
3584
3720
|
*/
|
|
3585
|
-
function createDtWithFiles(files = []) {
|
|
3721
|
+
function createDtWithFiles(files = [], options = {}) {
|
|
3722
|
+
const { emptyTypes = false } = options;
|
|
3586
3723
|
return {
|
|
3587
3724
|
dataTransfer: {
|
|
3588
3725
|
files,
|
|
3589
3726
|
items: files.map((file) => ({
|
|
3590
3727
|
kind: "file",
|
|
3591
3728
|
size: file.size,
|
|
3592
|
-
type: file.type,
|
|
3729
|
+
type: emptyTypes ? "" : file.type,
|
|
3593
3730
|
getAsFile: () => file,
|
|
3594
3731
|
})),
|
|
3595
3732
|
types: ["Files"],
|
package/src/utils/index.js
CHANGED
|
@@ -53,19 +53,45 @@ export const TOO_MANY_FILES_REJECTION = {
|
|
|
53
53
|
message: "Too many files",
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Check if the given file is a DataTransferItem with an empty type.
|
|
58
|
+
*
|
|
59
|
+
* During drag events, browsers may return DataTransferItem objects instead of File objects.
|
|
60
|
+
* Some browsers (e.g., Chrome) return an empty MIME type for certain file types (like .md files)
|
|
61
|
+
* on DataTransferItem during drag events, even though the type is correctly set during drop.
|
|
62
|
+
*
|
|
63
|
+
* This function detects such cases by checking for:
|
|
64
|
+
* 1. Empty type string
|
|
65
|
+
* 2. Presence of getAsFile method (indicates it's a DataTransferItem, not a File)
|
|
66
|
+
*
|
|
67
|
+
* We accept these during drag to provide proper UI feedback, while maintaining
|
|
68
|
+
* strict validation during drop when real File objects are available.
|
|
69
|
+
*
|
|
70
|
+
* @param {File | DataTransferItem} file
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function isDataTransferItemWithEmptyType(file) {
|
|
74
|
+
return file.type === "" && typeof file.getAsFile === "function";
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
/**
|
|
57
78
|
* Check if file is accepted.
|
|
58
79
|
*
|
|
59
80
|
* Firefox versions prior to 53 return a bogus MIME type for every file drag,
|
|
60
81
|
* so dragovers with that MIME type will always be accepted.
|
|
61
82
|
*
|
|
83
|
+
* Chrome/other browsers may return an empty MIME type for files during drag events,
|
|
84
|
+
* so we accept those as well (we'll validate properly on drop).
|
|
85
|
+
*
|
|
62
86
|
* @param {File} file
|
|
63
87
|
* @param {string} accept
|
|
64
88
|
* @returns
|
|
65
89
|
*/
|
|
66
90
|
export function fileAccepted(file, accept) {
|
|
67
91
|
const isAcceptable =
|
|
68
|
-
file.type === "application/x-moz-file" ||
|
|
92
|
+
file.type === "application/x-moz-file" ||
|
|
93
|
+
accepts(file, accept) ||
|
|
94
|
+
isDataTransferItemWithEmptyType(file);
|
|
69
95
|
return [
|
|
70
96
|
isAcceptable,
|
|
71
97
|
isAcceptable ? null : getInvalidTypeRejectionErr(accept),
|
package/src/utils/index.spec.js
CHANGED
|
@@ -262,6 +262,31 @@ describe("fileAccepted()", () => {
|
|
|
262
262
|
expect(utils.fileAccepted(file, ".pdf")).toEqual([true, null]);
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
+
it("accepts DataTransferItem with empty type during drag (Chrome .md file issue)", () => {
|
|
266
|
+
// Simulate Chrome's DataTransferItem during drag with empty type
|
|
267
|
+
const dataTransferItem = {
|
|
268
|
+
type: "",
|
|
269
|
+
kind: "file",
|
|
270
|
+
getAsFile: () => createFile("README.md", 100, ""),
|
|
271
|
+
};
|
|
272
|
+
expect(utils.fileAccepted(dataTransferItem, "text/markdown")).toEqual([
|
|
273
|
+
true,
|
|
274
|
+
null,
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("rejects regular File with empty type (not DataTransferItem)", () => {
|
|
279
|
+
// Regular File objects (on drop) with empty type should still be rejected
|
|
280
|
+
const file = createFile("unknown.xyz", 100, "");
|
|
281
|
+
expect(utils.fileAccepted(file, "text/markdown")).toEqual([
|
|
282
|
+
false,
|
|
283
|
+
{
|
|
284
|
+
code: "file-invalid-type",
|
|
285
|
+
message: "File type must be text/markdown",
|
|
286
|
+
},
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
265
290
|
it("accepts file when single accept criteria", () => {
|
|
266
291
|
const file = createFile("hamster.pdf", 100, "application/pdf");
|
|
267
292
|
expect(utils.fileAccepted(file, ".pdf")).toEqual([true, null]);
|
package/styleguide.config.js
CHANGED
|
@@ -56,6 +56,10 @@ module.exports = {
|
|
|
56
56
|
name: "Styling Dropzone",
|
|
57
57
|
content: "examples/styling/README.md",
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: "Drag Overlay",
|
|
61
|
+
content: "examples/drag-overlay/README.md",
|
|
62
|
+
},
|
|
59
63
|
{
|
|
60
64
|
name: "Accepting specific file types",
|
|
61
65
|
content: "examples/accept/README.md",
|
|
@@ -71,6 +71,7 @@ export type DropzoneState = DropzoneRef & {
|
|
|
71
71
|
isDragActive: boolean;
|
|
72
72
|
isDragAccept: boolean;
|
|
73
73
|
isDragReject: boolean;
|
|
74
|
+
isDragGlobal: boolean;
|
|
74
75
|
isFileDialogActive: boolean;
|
|
75
76
|
acceptedFiles: readonly FileWithPath[];
|
|
76
77
|
fileRejections: readonly FileRejection[];
|