react-dropzone 12.0.6 → 14.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.
@@ -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
+ */
@@ -3,6 +3,10 @@ beforeEach(() => {
3
3
  });
4
4
 
5
5
  describe("fileMatchSize()", () => {
6
+ /**
7
+ * @constant
8
+ * @type {import('./index')}
9
+ */
6
10
  let utils;
7
11
  beforeEach(async () => {
8
12
  utils = await import("./index");
@@ -63,6 +67,10 @@ describe("fileMatchSize()", () => {
63
67
  });
64
68
 
65
69
  describe("isIeOrEdge", () => {
70
+ /**
71
+ * @constant
72
+ * @type {import('./index')}
73
+ */
66
74
  let utils;
67
75
  beforeEach(async () => {
68
76
  utils = await import("./index");
@@ -98,6 +106,10 @@ describe("isIeOrEdge", () => {
98
106
 
99
107
  describe("isKindFile()", () => {
100
108
  it('should return true for DataTransferItem of kind "file"', async () => {
109
+ /**
110
+ * @constant
111
+ * @type {import('./index')}
112
+ */
101
113
  const utils = await import("./index");
102
114
  expect(utils.isKindFile({ kind: "file" })).toBe(true);
103
115
  expect(utils.isKindFile({ kind: "text/html" })).toBe(false);
@@ -109,6 +121,10 @@ describe("isKindFile()", () => {
109
121
  describe("isPropagationStopped()", () => {
110
122
  const trueFn = jest.fn(() => true);
111
123
 
124
+ /**
125
+ * @constant
126
+ * @type {import('./index')}
127
+ */
112
128
  let utils;
113
129
  beforeEach(async () => {
114
130
  utils = await import("./index");
@@ -130,6 +146,10 @@ describe("isPropagationStopped()", () => {
130
146
  });
131
147
 
132
148
  describe("isEvtWithFiles()", () => {
149
+ /**
150
+ * @constant
151
+ * @type {import('./index')}
152
+ */
133
153
  let utils;
134
154
  beforeEach(async () => {
135
155
  utils = await import("./index");
@@ -176,7 +196,11 @@ describe("isEvtWithFiles()", () => {
176
196
  });
177
197
  });
178
198
 
179
- describe("composeEventHandlers", () => {
199
+ describe("composeEventHandlers()", () => {
200
+ /**
201
+ * @constant
202
+ * @type {import('./index')}
203
+ */
180
204
  let utils;
181
205
  beforeEach(async () => {
182
206
  utils = await import("./index");
@@ -223,7 +247,11 @@ describe("composeEventHandlers", () => {
223
247
  });
224
248
  });
225
249
 
226
- describe("fileAccepted", () => {
250
+ describe("fileAccepted()", () => {
251
+ /**
252
+ * @constant
253
+ * @type {import('./index')}
254
+ */
227
255
  let utils;
228
256
  beforeEach(async () => {
229
257
  utils = await import("./index");
@@ -273,6 +301,10 @@ describe("fileAccepted", () => {
273
301
  });
274
302
 
275
303
  describe("allFilesAccepted()", () => {
304
+ /**
305
+ * @constant
306
+ * @type {import('./index')}
307
+ */
276
308
  let utils;
277
309
  beforeEach(async () => {
278
310
  utils = await import("./index");
@@ -316,6 +348,10 @@ describe("allFilesAccepted()", () => {
316
348
  });
317
349
 
318
350
  describe("ErrorCode", () => {
351
+ /**
352
+ * @constant
353
+ * @type {import('./index')}
354
+ */
319
355
  let utils;
320
356
  beforeEach(async () => {
321
357
  utils = await import("./index");
@@ -330,6 +366,10 @@ describe("ErrorCode", () => {
330
366
  });
331
367
 
332
368
  describe("canUseFileSystemAccessAPI()", () => {
369
+ /**
370
+ * @constant
371
+ * @type {import('./index')}
372
+ */
333
373
  let utils;
334
374
  beforeEach(async () => {
335
375
  utils = await import("./index");
@@ -346,87 +386,109 @@ describe("canUseFileSystemAccessAPI()", () => {
346
386
  });
347
387
  });
348
388
 
349
- describe("filePickerOptionsTypes()", () => {
389
+ describe("pickerOptionsFromAccept()", () => {
390
+ /**
391
+ * @constant
392
+ * @type {import('./index')}
393
+ */
350
394
  let utils;
351
395
  beforeEach(async () => {
352
396
  utils = await import("./index");
353
397
  });
354
398
 
355
- it("should return proper types when the arg is a MIME type", () => {
356
- expect(utils.filePickerOptionsTypes("application/zip")).toEqual([
357
- {
358
- description: "everything",
359
- accept: { "application/zip": [] },
360
- },
361
- ]);
362
- });
363
-
364
- it("should return proper types when the arg is an array of MIME types", () => {
399
+ it("converts the {accept} prop to file picker options", () => {
365
400
  expect(
366
- utils.filePickerOptionsTypes(["application/zip", "application/json"])
401
+ utils.pickerOptionsFromAccept({
402
+ "image/*": [".png", ".jpg"], // ok
403
+ "text/*": [".txt", ".pdf"], // ok
404
+ "audio/*": ["mp3"], // not ok
405
+ "*": [".p12"], // not ok
406
+ })
367
407
  ).toEqual([
368
408
  {
369
- description: "everything",
370
409
  accept: {
371
- "application/zip": [],
372
- "application/json": [],
410
+ "image/*": [".png", ".jpg"],
373
411
  },
374
412
  },
375
- ]);
376
- });
377
-
378
- it("should exclude anything that's not a MIME type", () => {
379
- expect(
380
- utils.filePickerOptionsTypes([
381
- "audio/*",
382
- "video/*",
383
- "image/*",
384
- ".txt",
385
- "text/*",
386
- ])
387
- ).toEqual([
388
413
  {
389
- description: "everything",
390
414
  accept: {
391
- "audio/*": [],
392
- "video/*": [],
393
- "image/*": [],
394
- "text/*": [],
415
+ "text/*": [".txt", ".pdf"],
395
416
  },
396
417
  },
397
418
  ]);
398
419
  });
420
+ });
421
+
422
+ describe("acceptPropAsAcceptAttr()", () => {
423
+ /**
424
+ * @constant
425
+ * @type {import('./index')}
426
+ */
427
+ let utils;
428
+ beforeEach(async () => {
429
+ utils = await import("./index");
430
+ });
399
431
 
400
- it("should work with comma separated string of MIME types", () => {
432
+ it("converts {accept} to an array of strings", () => {
401
433
  expect(
402
- utils.filePickerOptionsTypes(
403
- "audio/*,video/*,image/*,.txt,text/*,application/zip"
404
- )
405
- ).toEqual([
406
- {
407
- description: "everything",
408
- accept: {
409
- "audio/*": [],
410
- "video/*": [],
411
- "image/*": [],
412
- "text/*": [],
413
- "application/zip": [],
414
- },
415
- },
416
- ]);
434
+ utils.acceptPropAsAcceptAttr({
435
+ "image/*": [".png", ".jpg"],
436
+ "text/*": [".txt", ".pdf"],
437
+ "audio/*": ["mp3"], // `mp3` not ok
438
+ "*": [".p12"], // `*` not ok
439
+ })
440
+ ).toEqual("image/*,.png,.jpg,text/*,.txt,.pdf,audio/*,.p12");
417
441
  });
442
+ });
418
443
 
419
- it("should return empty otherwise", () => {
420
- expect(utils.filePickerOptionsTypes("")).toEqual([
421
- {
422
- description: "everything",
423
- accept: {},
424
- },
425
- ]);
444
+ describe("isMIMEType()", () => {
445
+ /**
446
+ * @constant
447
+ * @type {import('./index')}
448
+ */
449
+ let utils;
450
+ beforeEach(async () => {
451
+ utils = await import("./index");
452
+ });
453
+
454
+ it("checks that the value is a valid MIME type string", () => {
455
+ expect(utils.isMIMEType("text/html")).toBe(true);
456
+ expect(utils.isMIMEType("text/*")).toBe(true);
457
+ expect(utils.isMIMEType("image/*")).toBe(true);
458
+ expect(utils.isMIMEType("video/*")).toBe(true);
459
+ expect(utils.isMIMEType("audio/*")).toBe(true);
460
+ expect(utils.isMIMEType("test/*")).toBe(false);
461
+ expect(utils.isMIMEType("text")).toBe(false);
462
+ expect(utils.isMIMEType("")).toBe(false);
463
+ expect(utils.isMIMEType(undefined)).toBe(false);
464
+ });
465
+ });
466
+
467
+ describe("isExt()", () => {
468
+ /**
469
+ * @constant
470
+ * @type {import('./index')}
471
+ */
472
+ let utils;
473
+ beforeEach(async () => {
474
+ utils = await import("./index");
475
+ });
476
+
477
+ it("checks that the value is a valid file extension", () => {
478
+ expect(utils.isExt(".jpg")).toBe(true);
479
+ expect(utils.isExt("me.jpg")).toBe(true);
480
+ expect(utils.isExt("me.prev.png")).toBe(true);
481
+ expect(utils.isExt("")).toBe(false);
482
+ expect(utils.isExt("text")).toBe(false);
483
+ expect(utils.isExt(undefined)).toBe(false);
426
484
  });
427
485
  });
428
486
 
429
487
  describe("isAbort()", () => {
488
+ /**
489
+ * @constant
490
+ * @type {import('./index')}
491
+ */
430
492
  let utils;
431
493
  beforeEach(async () => {
432
494
  utils = await import("./index");
@@ -453,6 +515,10 @@ describe("isAbort()", () => {
453
515
  });
454
516
 
455
517
  describe("isSecurityError()", () => {
518
+ /**
519
+ * @constant
520
+ * @type {import('./index')}
521
+ */
456
522
  let utils;
457
523
  beforeEach(async () => {
458
524
  utils = await import("./index");
@@ -28,7 +28,7 @@ export interface FileRejection {
28
28
  }
29
29
 
30
30
  export type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, PropTypes> & {
31
- accept?: string | string[];
31
+ accept?: Accept;
32
32
  minSize?: number;
33
33
  maxSize?: number;
34
34
  maxFiles?: number;
@@ -66,7 +66,6 @@ export type DropzoneState = DropzoneRef & {
66
66
  isDragAccept: boolean;
67
67
  isDragReject: boolean;
68
68
  isFileDialogActive: boolean;
69
- draggedFiles: File[];
70
69
  acceptedFiles: File[];
71
70
  fileRejections: FileRejection[];
72
71
  rootRef: React.RefObject<HTMLElement>;
@@ -90,3 +89,7 @@ export interface DropzoneInputProps
90
89
  }
91
90
 
92
91
  type PropTypes = "multiple" | "onDragEnter" | "onDragOver" | "onDragLeave";
92
+
93
+ export interface Accept {
94
+ [key: string]: string[];
95
+ }
@@ -12,7 +12,9 @@ export default class Accept extends React.Component {
12
12
  <section>
13
13
  <div className="dropzone">
14
14
  <Dropzone
15
- accept="image/jpeg, image/png"
15
+ accept={{
16
+ "image/*": [".jpeg", ".png"],
17
+ }}
16
18
  onDrop={(accepted, rejected) => {
17
19
  this.setState({ accepted, rejected });
18
20
  }}
@@ -50,39 +52,3 @@ export default class Accept extends React.Component {
50
52
  );
51
53
  }
52
54
  }
53
-
54
- export const acceptExt = (
55
- <Dropzone accept=".jpeg,.png">
56
- {({ getRootProps, isDragActive, isDragAccept, isDragReject }) => (
57
- <div {...getRootProps()}>
58
- {isDragAccept && "All files will be accepted"}
59
- {isDragReject && "Some files will be rejected"}
60
- {isDragActive && "Drop some files here ..."}
61
- </div>
62
- )}
63
- </Dropzone>
64
- );
65
-
66
- export const acceptMime = (
67
- <Dropzone accept="image/jpeg, image/png">
68
- {({ getRootProps, isDragActive, isDragAccept, isDragReject }) => (
69
- <div {...getRootProps()}>
70
- {isDragAccept && "All files will be accepted"}
71
- {isDragReject && "Some files will be rejected"}
72
- {isDragActive && "Drop some files here ..."}
73
- </div>
74
- )}
75
- </Dropzone>
76
- );
77
-
78
- export const acceptArray = (
79
- <Dropzone accept={["image/jpeg", "image/png"]}>
80
- {({ getRootProps, isDragActive, isDragAccept, isDragReject }) => (
81
- <div {...getRootProps()}>
82
- {isDragAccept && "All files will be accepted"}
83
- {isDragReject && "Some files will be rejected"}
84
- {isDragActive && "Drop some files here ..."}
85
- </div>
86
- )}
87
- </Dropzone>
88
- );
@@ -25,7 +25,9 @@ export default class Test extends React.Component {
25
25
  noDragEventsBubbling={false}
26
26
  disabled
27
27
  multiple={false}
28
- accept="*.png"
28
+ accept={{
29
+ "image/*": [".png"],
30
+ }}
29
31
  useFsAccessApi={false}
30
32
  >
31
33
  {({ getRootProps, getInputProps }) => (