react-dropzone 14.3.8 → 14.4.0

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.
Binary file
@@ -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.3.8",
200
+ "version": "14.4.0",
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(2);
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(2);
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"],
@@ -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" || accepts(file, accept);
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),
@@ -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]);
@@ -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[];