react-dropzone 19.2.0 → 19.3.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.
@@ -8,9 +8,28 @@ const accepts =
8
8
 
9
9
  /**
10
10
  * A map of accepted MIME types to file extensions, as passed to the `accept` prop.
11
+ *
12
+ * An extension value may be a single extension string or an array of them - both are
13
+ * accepted, mirroring the shape of `window.showOpenFilePicker`'s `accept`.
11
14
  */
12
15
  export interface Accept {
13
- [key: string]: readonly string[];
16
+ [key: string]: string | readonly string[];
17
+ }
18
+
19
+ /**
20
+ * A labeled group of accepted types, mirroring one entry of `window.showOpenFilePicker`'s
21
+ * `types` option. Passing the `accept` prop as an array of these lets the File System Access
22
+ * picker present multiple named filter rows instead of a single one (see
23
+ * {@link pickerOptionsFromAccept}). The optional `description` labels the row; when omitted it
24
+ * is derived from the group's extensions.
25
+ *
26
+ * Grouping only surfaces when the FS Access picker is actually used (`useFsAccessApi` +
27
+ * a secure context + browser support). The native `<input>` fallback has no concept of groups
28
+ * or descriptions, so every group is flattened into one accept attribute there.
29
+ */
30
+ export interface AcceptGroup {
31
+ description?: string;
32
+ accept: Accept;
14
33
  }
15
34
 
16
35
  /**
@@ -323,41 +342,142 @@ export function canUseFileSystemAccessAPI(): boolean {
323
342
  }
324
343
 
325
344
  /**
326
- * Convert the `{accept}` dropzone prop to the `{types}` option for showOpenFilePicker.
345
+ * Coerce an `accept` extension value to an array. `window.showOpenFilePicker` allows a single
346
+ * extension string as well as an array, so we accept both and normalize to an array. Anything
347
+ * else (a malformed value) collapses to an empty list.
327
348
  */
328
- export function pickerOptionsFromAccept(accept?: Accept): Array<{description: string; accept: Accept}> | undefined {
329
- if (isDefined(accept)) {
330
- const acceptForPicker = Object.entries(accept)
331
- .filter(([mimeType, ext]) => {
332
- let ok = true;
333
-
334
- if (!isMIMEType(mimeType)) {
335
- console.warn(
336
- `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.`
337
- );
338
- ok = false;
339
- }
349
+ function toExtensions(ext: string | readonly string[] | undefined): readonly string[] {
350
+ if (Array.isArray(ext)) {
351
+ return ext;
352
+ }
353
+ if (typeof ext === "string") {
354
+ return [ext];
355
+ }
356
+ return [];
357
+ }
340
358
 
341
- if (!Array.isArray(ext) || !ext.every(isExt)) {
342
- console.warn(`Skipped "${mimeType}" because an invalid file extension was provided.`);
343
- ok = false;
344
- }
359
+ /**
360
+ * Normalize either `accept` form to the internal array of `{description, accept}` groups.
361
+ *
362
+ * - The object form (`{mime: ext}`) becomes a single group; its `description` (which the object
363
+ * form can't express) is derived from the extensions like any other group.
364
+ * - The array form is passed through; entries missing an `accept` map are dropped.
365
+ *
366
+ * In both cases a missing `description` is filled in later from the group's extensions (see
367
+ * {@link describeAccept}), so the picker always gets the non-empty description it requires
368
+ * (https://crbug.com/1264708). Returns `undefined` when no `accept` is provided.
369
+ */
370
+ function normalizeAcceptGroups(accept?: Accept | readonly AcceptGroup[]): AcceptGroup[] | undefined {
371
+ if (!isDefined(accept)) {
372
+ return undefined;
373
+ }
374
+ if (Array.isArray(accept)) {
375
+ return (accept as readonly AcceptGroup[]).filter(group => isDefined(group) && isDefined(group.accept));
376
+ }
377
+ return [{accept: accept as Accept}];
378
+ }
345
379
 
346
- return ok;
347
- })
348
- .reduce<Accept>((agg, [mimeType, ext]) => {
349
- agg[mimeType] = ext;
350
- return agg;
351
- }, {});
352
- return [
353
- {
354
- // description is required due to https://crbug.com/1264708
355
- description: "Files",
356
- accept: acceptForPicker
380
+ /**
381
+ * Build a human-readable label for a picker group from its (validated) accept map, used when the
382
+ * caller doesn't supply a `description`. Prefer the file extensions, fall back to the MIME types,
383
+ * then to a generic label - `showOpenFilePicker` requires a non-empty description (crbug 1264708).
384
+ */
385
+ function describeAccept(accept: Accept): string {
386
+ const extensions: string[] = [];
387
+ const mimeTypes = Object.keys(accept);
388
+ for (const ext of Object.values(accept)) {
389
+ for (const e of toExtensions(ext)) {
390
+ if (!extensions.includes(e)) {
391
+ extensions.push(e);
357
392
  }
358
- ];
393
+ }
359
394
  }
360
- return undefined;
395
+ if (extensions.length > 0) {
396
+ return extensions.join(", ");
397
+ }
398
+ if (mimeTypes.length > 0) {
399
+ return mimeTypes.join(", ");
400
+ }
401
+ return "Files";
402
+ }
403
+
404
+ /**
405
+ * Merge every `accept` group into a single MIME-type -> extensions map. Duplicate MIME keys union
406
+ * their extensions. This flattened map drives the native `<input accept>` attribute and the
407
+ * drag/drop validators, which have no concept of groups or descriptions.
408
+ */
409
+ export function flattenAccept(accept?: Accept | readonly AcceptGroup[]): Accept | undefined {
410
+ const groups = normalizeAcceptGroups(accept);
411
+ if (!isDefined(groups)) {
412
+ return undefined;
413
+ }
414
+ const merged: Record<string, string[]> = {};
415
+ for (const group of groups) {
416
+ for (const [mimeType, ext] of Object.entries(group.accept)) {
417
+ const existing = merged[mimeType] ?? (merged[mimeType] = []);
418
+ for (const e of toExtensions(ext)) {
419
+ if (!existing.includes(e)) {
420
+ existing.push(e);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ return merged;
426
+ }
427
+
428
+ /**
429
+ * Convert the `{accept}` dropzone prop to the `{types}` option for showOpenFilePicker.
430
+ *
431
+ * Each group becomes one filter row in the picker. Invalid MIME types / extensions are dropped
432
+ * with a warning; a group left with no valid entries is omitted entirely (showOpenFilePicker
433
+ * rejects an empty `accept`). Returns `undefined` when nothing valid remains.
434
+ */
435
+ export function pickerOptionsFromAccept(
436
+ accept?: Accept | readonly AcceptGroup[]
437
+ ): Array<{description: string; accept: Accept}> | undefined {
438
+ const groups = normalizeAcceptGroups(accept);
439
+ if (!isDefined(groups)) {
440
+ return undefined;
441
+ }
442
+
443
+ const options = groups
444
+ .map(group => {
445
+ const acceptForPicker = Object.entries(group.accept)
446
+ .filter(([mimeType, ext]) => {
447
+ let ok = true;
448
+
449
+ if (!isMIMEType(mimeType)) {
450
+ console.warn(
451
+ `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.`
452
+ );
453
+ ok = false;
454
+ }
455
+
456
+ const isExtInput = Array.isArray(ext) || typeof ext === "string";
457
+ if (!isExtInput || !toExtensions(ext).every(isExt)) {
458
+ console.warn(`Skipped "${mimeType}" because an invalid file extension was provided.`);
459
+ ok = false;
460
+ }
461
+
462
+ return ok;
463
+ })
464
+ .reduce<Accept>((agg, [mimeType, ext]) => {
465
+ agg[mimeType] = toExtensions(ext);
466
+ return agg;
467
+ }, {});
468
+
469
+ return {
470
+ description:
471
+ isDefined(group.description) && group.description !== ""
472
+ ? group.description
473
+ : describeAccept(acceptForPicker),
474
+ accept: acceptForPicker
475
+ };
476
+ })
477
+ // Drop groups with no valid entries - showOpenFilePicker rejects an empty accept map.
478
+ .filter(option => Object.keys(option.accept).length > 0);
479
+
480
+ return options.length > 0 ? options : undefined;
361
481
  }
362
482
 
363
483
  /**
@@ -379,7 +499,8 @@ export function acceptPropAsAcceptAttr(
379
499
  if (isDefined(accept)) {
380
500
  return (
381
501
  Object.entries(accept)
382
- .reduce<string[]>((a, [mimeType, ext]) => {
502
+ .reduce<string[]>((a, [mimeType, extVal]) => {
503
+ const ext = toExtensions(extVal);
383
504
  if (omitWildcardMimeTypesWithExtensions && isMIMETypeWildcard(mimeType) && ext.some(isExt)) {
384
505
  a.push(...ext);
385
506
  } else {