react-dropzone 12.1.0 → 13.0.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.
package/src/index.spec.js CHANGED
@@ -41,7 +41,9 @@ describe("useDropzone() hook", () => {
41
41
  });
42
42
 
43
43
  it("sets {accept} prop on the <input>", () => {
44
- const accept = "image/jpeg";
44
+ const accept = {
45
+ "image/jpeg": [],
46
+ };
45
47
  const { container } = render(
46
48
  <Dropzone accept={accept}>
47
49
  {({ getRootProps, getInputProps }) => (
@@ -54,12 +56,16 @@ describe("useDropzone() hook", () => {
54
56
 
55
57
  const input = container.querySelector("input");
56
58
 
57
- expect(input).toHaveAttribute("accept", accept);
59
+ expect(input).toHaveAttribute("accept", "image/jpeg");
58
60
  });
59
61
 
60
62
  it("updates {multiple} prop on the <input> when it changes", () => {
61
63
  const { container, rerender } = render(
62
- <Dropzone accept="image/jpeg">
64
+ <Dropzone
65
+ accept={{
66
+ "image/jpeg": [],
67
+ }}
68
+ >
63
69
  {({ getRootProps, getInputProps }) => (
64
70
  <div {...getRootProps()}>
65
71
  <input {...getInputProps()} />
@@ -74,7 +80,11 @@ describe("useDropzone() hook", () => {
74
80
  );
75
81
 
76
82
  rerender(
77
- <Dropzone accept="image/png">
83
+ <Dropzone
84
+ accept={{
85
+ "image/png": [],
86
+ }}
87
+ >
78
88
  {({ getRootProps, getInputProps }) => (
79
89
  <div {...getRootProps()}>
80
90
  <input {...getInputProps()} />
@@ -968,6 +978,42 @@ describe("useDropzone() hook", () => {
968
978
 
969
979
  expect(props.getFilesFromEvent).toHaveBeenCalledTimes(2);
970
980
  });
981
+
982
+ it("calls {onError} when getFilesFromEvent() rejects", async () => {
983
+ const data = createDtWithFiles(files);
984
+
985
+ const props = {
986
+ getFilesFromEvent: jest
987
+ .fn()
988
+ .mockImplementation(() => Promise.reject("oops :(")),
989
+ onDragEnter: jest.fn(),
990
+ onDrop: jest.fn(),
991
+ onError: jest.fn(),
992
+ };
993
+
994
+ const ui = (
995
+ <Dropzone {...props}>
996
+ {({ getRootProps, getInputProps }) => (
997
+ <div {...getRootProps()}>
998
+ <input {...getInputProps()} />
999
+ </div>
1000
+ )}
1001
+ </Dropzone>
1002
+ );
1003
+ const { container, rerender } = render(ui);
1004
+ const dropzone = container.querySelector("div");
1005
+
1006
+ fireDragEnter(dropzone, data);
1007
+ await flushPromises(rerender, ui);
1008
+ expect(props.onDragEnter).not.toHaveBeenCalled();
1009
+
1010
+ fireDrop(dropzone, data);
1011
+ await flushPromises(rerender, ui);
1012
+ expect(props.onDrop).not.toHaveBeenCalled();
1013
+
1014
+ expect(props.getFilesFromEvent).toHaveBeenCalledTimes(2);
1015
+ expect(props.onError).toHaveBeenCalledTimes(2);
1016
+ });
971
1017
  });
972
1018
 
973
1019
  describe("onFocus", () => {
@@ -1368,7 +1414,9 @@ describe("useDropzone() hook", () => {
1368
1414
  <Dropzone
1369
1415
  onDrop={onDropSpy}
1370
1416
  onFileDialogOpen={onFileDialogOpenSpy}
1371
- accept="application/pdf"
1417
+ accept={{
1418
+ "application/pdf": [],
1419
+ }}
1372
1420
  multiple
1373
1421
  useFsAccessApi={false}
1374
1422
  >
@@ -1425,7 +1473,9 @@ describe("useDropzone() hook", () => {
1425
1473
  <Dropzone
1426
1474
  onDrop={onDropSpy}
1427
1475
  onFileDialogOpen={onFileDialogOpenSpy}
1428
- accept="application/pdf"
1476
+ accept={{
1477
+ "application/pdf": [],
1478
+ }}
1429
1479
  multiple
1430
1480
  >
1431
1481
  {({ getRootProps, getInputProps, isFileDialogActive }) => (
@@ -1481,7 +1531,9 @@ describe("useDropzone() hook", () => {
1481
1531
  <Dropzone
1482
1532
  onDrop={onDropSpy}
1483
1533
  onFileDialogOpen={onFileDialogOpenSpy}
1484
- accept="application/pdf"
1534
+ accept={{
1535
+ "application/pdf": [],
1536
+ }}
1485
1537
  multiple
1486
1538
  useFsAccessApi
1487
1539
  >
@@ -1516,7 +1568,6 @@ describe("useDropzone() hook", () => {
1516
1568
  multiple: true,
1517
1569
  types: [
1518
1570
  {
1519
- description: "everything",
1520
1571
  accept: { "application/pdf": [] },
1521
1572
  },
1522
1573
  ],
@@ -1680,7 +1731,9 @@ describe("useDropzone() hook", () => {
1680
1731
  <Dropzone
1681
1732
  onDrop={onDropSpy}
1682
1733
  onFileDialogOpen={onFileDialogOpenSpy}
1683
- accept="application/pdf"
1734
+ accept={{
1735
+ "application/pdf": [],
1736
+ }}
1684
1737
  multiple
1685
1738
  useFsAccessApi
1686
1739
  >
@@ -1768,6 +1821,62 @@ describe("useDropzone() hook", () => {
1768
1821
 
1769
1822
  jest.useRealTimers();
1770
1823
  });
1824
+
1825
+ test("showOpenFilePicker() should call {onError} when an unexpected error occurs", async () => {
1826
+ const activeRef = createRef();
1827
+ const active = <span ref={activeRef}>I am active</span>;
1828
+
1829
+ const thenable = createThenable();
1830
+ const showOpenFilePickerMock = jest
1831
+ .fn()
1832
+ .mockReturnValue(thenable.promise);
1833
+
1834
+ window.showOpenFilePicker = showOpenFilePickerMock;
1835
+
1836
+ const onErrorSpy = jest.fn();
1837
+ const onDropSpy = jest.fn();
1838
+ const onFileDialogOpenSpy = jest.fn();
1839
+
1840
+ const ui = (
1841
+ <Dropzone
1842
+ onError={onErrorSpy}
1843
+ onDrop={onDropSpy}
1844
+ onFileDialogOpen={onFileDialogOpenSpy}
1845
+ accept={{
1846
+ "application/pdf": [],
1847
+ }}
1848
+ multiple
1849
+ useFsAccessApi
1850
+ >
1851
+ {({ getRootProps, getInputProps, isFileDialogActive }) => (
1852
+ <div {...getRootProps()}>
1853
+ <input {...getInputProps()} />
1854
+ {isFileDialogActive && active}
1855
+ </div>
1856
+ )}
1857
+ </Dropzone>
1858
+ );
1859
+
1860
+ const { container, rerender } = render(ui);
1861
+
1862
+ const dropzone = container.querySelector("div");
1863
+
1864
+ fireEvent.click(dropzone);
1865
+
1866
+ await flushPromises(rerender, ui);
1867
+
1868
+ expect(activeRef.current).not.toBeNull();
1869
+ expect(dropzone).toContainElement(activeRef.current);
1870
+ expect(onFileDialogOpenSpy).toHaveBeenCalled();
1871
+
1872
+ const err = new Error("oops :(");
1873
+ thenable.cancel(err);
1874
+ await flushPromises(rerender, ui);
1875
+
1876
+ expect(activeRef.current).not.toBeNull();
1877
+ expect(dropzone).toContainElement(activeRef.current);
1878
+ expect(onErrorSpy).toHaveBeenCalledWith(err);
1879
+ });
1771
1880
  });
1772
1881
 
1773
1882
  describe("onKeyDown", () => {
@@ -2215,7 +2324,11 @@ describe("useDropzone() hook", () => {
2215
2324
 
2216
2325
  it("sets {isDragActive} and {isDragReject} of some files are not accepted on dragenter", async () => {
2217
2326
  const ui = (
2218
- <Dropzone accept="image/*">
2327
+ <Dropzone
2328
+ accept={{
2329
+ "image/*": [],
2330
+ }}
2331
+ >
2219
2332
  {({
2220
2333
  getRootProps,
2221
2334
  getInputProps,
@@ -2269,7 +2382,12 @@ describe("useDropzone() hook", () => {
2269
2382
 
2270
2383
  it("sets {isDragActive, isDragAccept, isDragReject} if any files are rejected and {multiple} is false on dragenter", async () => {
2271
2384
  const ui = (
2272
- <Dropzone accept="image/*" multiple={false}>
2385
+ <Dropzone
2386
+ accept={{
2387
+ "image/*": [],
2388
+ }}
2389
+ multiple={false}
2390
+ >
2273
2391
  {({
2274
2392
  getRootProps,
2275
2393
  getInputProps,
@@ -2337,7 +2455,11 @@ describe("useDropzone() hook", () => {
2337
2455
 
2338
2456
  it("updates {isDragActive} if {accept} changes mid-drag", async () => {
2339
2457
  const ui = (
2340
- <Dropzone accept="image/*">
2458
+ <Dropzone
2459
+ accept={{
2460
+ "image/*": [],
2461
+ }}
2462
+ >
2341
2463
  {({
2342
2464
  getRootProps,
2343
2465
  getInputProps,
@@ -2366,7 +2488,11 @@ describe("useDropzone() hook", () => {
2366
2488
  expect(dropzone).not.toHaveTextContent("dragReject");
2367
2489
 
2368
2490
  rerender(
2369
- <Dropzone accept="text/*">
2491
+ <Dropzone
2492
+ accept={{
2493
+ "text/*": [],
2494
+ }}
2495
+ >
2370
2496
  {({
2371
2497
  getRootProps,
2372
2498
  getInputProps,
@@ -2391,7 +2517,11 @@ describe("useDropzone() hook", () => {
2391
2517
 
2392
2518
  it("resets {isDragActive, isDragAccept, isDragReject} on dragleave", async () => {
2393
2519
  const ui = (
2394
- <Dropzone accept="image/*">
2520
+ <Dropzone
2521
+ accept={{
2522
+ "image/*": [],
2523
+ }}
2524
+ >
2395
2525
  {({
2396
2526
  getRootProps,
2397
2527
  getInputProps,
@@ -2514,7 +2644,11 @@ describe("useDropzone() hook", () => {
2514
2644
  Array.from(errorList).every((item) => item.textContent === code);
2515
2645
 
2516
2646
  const ui = (
2517
- <Dropzone accept="image/*">
2647
+ <Dropzone
2648
+ accept={{
2649
+ "image/*": [],
2650
+ }}
2651
+ >
2518
2652
  {({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
2519
2653
  <div {...getRootProps()}>
2520
2654
  <input {...getInputProps()} />
@@ -2586,7 +2720,13 @@ describe("useDropzone() hook", () => {
2586
2720
  it("rejects all files if {multiple} is false and {accept} criteria is not met", async () => {
2587
2721
  const onDropSpy = jest.fn();
2588
2722
  const ui = (
2589
- <Dropzone accept="image/*" onDrop={onDropSpy} multiple={false}>
2723
+ <Dropzone
2724
+ accept={{
2725
+ "image/*": [],
2726
+ }}
2727
+ onDrop={onDropSpy}
2728
+ multiple={false}
2729
+ >
2590
2730
  {({ getRootProps, getInputProps }) => (
2591
2731
  <div {...getRootProps()}>
2592
2732
  <input {...getInputProps()} />
@@ -2619,7 +2759,13 @@ describe("useDropzone() hook", () => {
2619
2759
  it("rejects all files if {multiple} is false and {accept} criteria is met", async () => {
2620
2760
  const onDropSpy = jest.fn();
2621
2761
  const ui = (
2622
- <Dropzone accept="image/*" onDrop={onDropSpy} multiple={false}>
2762
+ <Dropzone
2763
+ accept={{
2764
+ "image/*": [],
2765
+ }}
2766
+ onDrop={onDropSpy}
2767
+ multiple={false}
2768
+ >
2623
2769
  {({ getRootProps, getInputProps }) => (
2624
2770
  <div {...getRootProps()}>
2625
2771
  <input {...getInputProps()} />
@@ -2663,7 +2809,9 @@ describe("useDropzone() hook", () => {
2663
2809
  const onDropRejectedSpy = jest.fn();
2664
2810
  const ui = (
2665
2811
  <Dropzone
2666
- accept="image/*"
2812
+ accept={{
2813
+ "image/*": [],
2814
+ }}
2667
2815
  onDrop={onDropSpy}
2668
2816
  onDropRejected={onDropRejectedSpy}
2669
2817
  multiple={true}
@@ -2718,7 +2866,9 @@ describe("useDropzone() hook", () => {
2718
2866
  const onDropRejectedSpy = jest.fn();
2719
2867
  const ui = (maxFiles) => (
2720
2868
  <Dropzone
2721
- accept="image/*"
2869
+ accept={{
2870
+ "image/*": [],
2871
+ }}
2722
2872
  onDrop={onDropSpy}
2723
2873
  multiple={true}
2724
2874
  maxFiles={maxFiles}
@@ -2758,7 +2908,9 @@ describe("useDropzone() hook", () => {
2758
2908
  const onDropRejectedSpy = jest.fn();
2759
2909
  const ui = (
2760
2910
  <Dropzone
2761
- accept="image/*"
2911
+ accept={{
2912
+ "image/*": [],
2913
+ }}
2762
2914
  onDrop={onDropSpy}
2763
2915
  multiple={true}
2764
2916
  maxFiles={3}
@@ -2783,7 +2935,13 @@ describe("useDropzone() hook", () => {
2783
2935
  it("accepts a single files if {multiple} is false and {accept} criteria is met", async () => {
2784
2936
  const onDropSpy = jest.fn();
2785
2937
  const ui = (
2786
- <Dropzone accept="image/*" onDrop={onDropSpy} multiple={false}>
2938
+ <Dropzone
2939
+ accept={{
2940
+ "image/*": [],
2941
+ }}
2942
+ onDrop={onDropSpy}
2943
+ multiple={false}
2944
+ >
2787
2945
  {({ getRootProps, getInputProps }) => (
2788
2946
  <div {...getRootProps()}>
2789
2947
  <input {...getInputProps()} />
@@ -2855,7 +3013,12 @@ describe("useDropzone() hook", () => {
2855
3013
  it("gets invoked with both accepted/rejected files", async () => {
2856
3014
  const onDropSpy = jest.fn();
2857
3015
  const ui = (
2858
- <Dropzone accept="image/*" onDrop={onDropSpy}>
3016
+ <Dropzone
3017
+ accept={{
3018
+ "image/*": [],
3019
+ }}
3020
+ onDrop={onDropSpy}
3021
+ >
2859
3022
  {({ getRootProps, getInputProps }) => (
2860
3023
  <div {...getRootProps()}>
2861
3024
  <input {...getInputProps()} />
@@ -2912,7 +3075,12 @@ describe("useDropzone() hook", () => {
2912
3075
  test("onDropAccepted callback is invoked if some files are accepted", async () => {
2913
3076
  const onDropAcceptedSpy = jest.fn();
2914
3077
  const ui = (
2915
- <Dropzone accept="image/*" onDropAccepted={onDropAcceptedSpy}>
3078
+ <Dropzone
3079
+ accept={{
3080
+ "image/*": [],
3081
+ }}
3082
+ onDropAccepted={onDropAcceptedSpy}
3083
+ >
2916
3084
  {({ getRootProps, getInputProps }) => (
2917
3085
  <div {...getRootProps()}>
2918
3086
  <input {...getInputProps()} />
@@ -2941,7 +3109,12 @@ describe("useDropzone() hook", () => {
2941
3109
  test("onDropRejected callback is invoked if some files are rejected", async () => {
2942
3110
  const onDropRejectedSpy = jest.fn();
2943
3111
  const ui = (
2944
- <Dropzone accept="image/*" onDropRejected={onDropRejectedSpy}>
3112
+ <Dropzone
3113
+ accept={{
3114
+ "image/*": [],
3115
+ }}
3116
+ onDropRejected={onDropRejectedSpy}
3117
+ >
2945
3118
  {({ getRootProps, getInputProps }) => (
2946
3119
  <div {...getRootProps()}>
2947
3120
  <input {...getInputProps()} />
@@ -2996,7 +3169,12 @@ describe("useDropzone() hook", () => {
2996
3169
  it("accepts a dropped image when Firefox provides a bogus file type", async () => {
2997
3170
  const onDropSpy = jest.fn();
2998
3171
  const ui = (
2999
- <Dropzone accept="image/*" onDrop={onDropSpy}>
3172
+ <Dropzone
3173
+ accept={{
3174
+ "image/*": [],
3175
+ }}
3176
+ onDrop={onDropSpy}
3177
+ >
3000
3178
  {({ getRootProps, getInputProps }) => (
3001
3179
  <div {...getRootProps()}>
3002
3180
  <input {...getInputProps()} />
@@ -175,33 +175,60 @@ export function canUseFileSystemAccessAPI() {
175
175
  }
176
176
 
177
177
  /**
178
- * filePickerOptionsTypes returns the {types} option for https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker
179
- * based on the accept attr (see https://github.com/react-dropzone/attr-accept)
180
- * E.g: converts ['image/*', 'text/*'] to {'image/*': [], 'text/*': []}
181
- * @param {string|string[]} accept
178
+ * Convert the `{accept}` dropzone prop to the
179
+ * `{types}` option for https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker
180
+ *
181
+ * @param {AcceptProp} accept
182
+ * @returns {{accept: string[]}[]}
182
183
  */
183
- export function filePickerOptionsTypes(accept) {
184
- accept = typeof accept === "string" ? accept.split(",") : accept;
185
- return [
186
- {
187
- description: "everything",
188
- // TODO: Need to handle filtering more elegantly than this!
189
- accept: Array.isArray(accept)
190
- ? // Accept just MIME types as per spec
191
- // NOTE: accept can be https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
192
- accept
193
- .filter(
194
- (item) =>
195
- item === "audio/*" ||
196
- item === "video/*" ||
197
- item === "image/*" ||
198
- item === "text/*" ||
199
- /\w+\/[-+.\w]+/g.test(item)
200
- )
201
- .reduce((a, b) => ({ ...a, [b]: [] }), {})
202
- : {},
203
- },
204
- ];
184
+ export function pickerOptionsFromAccept(accept) {
185
+ if (isDefined(accept)) {
186
+ return Object.entries(accept)
187
+ .filter(([mimeType, ext]) => {
188
+ let ok = true;
189
+
190
+ if (!isMIMEType(mimeType)) {
191
+ console.warn(
192
+ `Skipped "${mimeType}" because it is not a valid MIME type. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types for a list of valid MIME types.`
193
+ );
194
+ ok = false;
195
+ }
196
+
197
+ if (!Array.isArray(ext) || !ext.every(isExt)) {
198
+ console.warn(
199
+ `Skipped "${mimeType}" because an invalid file extension was provided.`
200
+ );
201
+ ok = false;
202
+ }
203
+
204
+ return ok;
205
+ })
206
+ .map(([mimeType, ext]) => ({
207
+ accept: {
208
+ [mimeType]: ext,
209
+ },
210
+ }));
211
+ }
212
+ return accept;
213
+ }
214
+
215
+ /**
216
+ * Convert the `{accept}` dropzone prop to an array of MIME types/extensions.
217
+ * @param {AcceptProp} accept
218
+ * @returns {string}
219
+ */
220
+ export function acceptPropAsAcceptAttr(accept) {
221
+ if (isDefined(accept)) {
222
+ return (
223
+ Object.entries(accept)
224
+ .reduce((a, [mimeType, ext]) => [...a, mimeType, ...ext], [])
225
+ // Silently discard invalid entries as pickerOptionsFromAccept warns about these
226
+ .filter((v) => isMIMEType(v) || isExt(v))
227
+ .join(",")
228
+ );
229
+ }
230
+
231
+ return undefined;
205
232
  }
206
233
 
207
234
  /**
@@ -231,3 +258,32 @@ export function isSecurityError(v) {
231
258
  (v.name === "SecurityError" || v.code === v.SECURITY_ERR)
232
259
  );
233
260
  }
261
+
262
+ /**
263
+ * Check if v is a MIME type string.
264
+ *
265
+ * See accepted format: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers.
266
+ *
267
+ * @param {string} v
268
+ */
269
+ export function isMIMEType(v) {
270
+ return (
271
+ v === "audio/*" ||
272
+ v === "video/*" ||
273
+ v === "image/*" ||
274
+ v === "text/*" ||
275
+ /\w+\/[-+.\w]+/g.test(v)
276
+ );
277
+ }
278
+
279
+ /**
280
+ * Check if v is a file extension.
281
+ * @param {string} v
282
+ */
283
+ export function isExt(v) {
284
+ return /^.*\.[\w]+$/.test(v);
285
+ }
286
+
287
+ /**
288
+ * @typedef {Object.<string, string[]>} AcceptProp
289
+ */