react-dropzone 19.1.1 → 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.
package/dist/index.d.cts CHANGED
@@ -3,9 +3,27 @@ import * as React from "react";
3
3
  //#region src/utils/index.d.ts
4
4
  /**
5
5
  * A map of accepted MIME types to file extensions, as passed to the `accept` prop.
6
+ *
7
+ * An extension value may be a single extension string or an array of them - both are
8
+ * accepted, mirroring the shape of `window.showOpenFilePicker`'s `accept`.
6
9
  */
7
10
  interface Accept {
8
- [key: string]: readonly string[];
11
+ [key: string]: string | readonly string[];
12
+ }
13
+ /**
14
+ * A labeled group of accepted types, mirroring one entry of `window.showOpenFilePicker`'s
15
+ * `types` option. Passing the `accept` prop as an array of these lets the File System Access
16
+ * picker present multiple named filter rows instead of a single one (see
17
+ * {@link pickerOptionsFromAccept}). The optional `description` labels the row; when omitted it
18
+ * is derived from the group's extensions.
19
+ *
20
+ * Grouping only surfaces when the FS Access picker is actually used (`useFsAccessApi` +
21
+ * a secure context + browser support). The native `<input>` fallback has no concept of groups
22
+ * or descriptions, so every group is flattened into one accept attribute there.
23
+ */
24
+ interface AcceptGroup {
25
+ description?: string;
26
+ accept: Accept;
9
27
  }
10
28
  /**
11
29
  * A file rejection error.
@@ -37,7 +55,7 @@ interface FileRejection {
37
55
  }
38
56
  type SharedProps = "multiple" | "onDragEnter" | "onDragOver" | "onDragLeave";
39
57
  type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
40
- accept?: Accept;
58
+ accept?: Accept | AcceptGroup[];
41
59
  minSize?: number;
42
60
  maxSize?: number;
43
61
  maxFiles?: number;
@@ -46,6 +64,14 @@ type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
46
64
  noKeyboard?: boolean;
47
65
  noDrag?: boolean;
48
66
  noDragEventsBubbling?: boolean;
67
+ /**
68
+ * If true, disables paste-to-upload. By default, when the dropzone (or a focused child) receives a
69
+ * paste that carries files - e.g. a screenshot pasted with Ctrl/Cmd+V - those files go through the
70
+ * same `accept`/size/`validator` checks and `onDrop` callbacks as a drop. Pastes with no files
71
+ * (plain text, etc.) are ignored and left untouched. See
72
+ * https://github.com/react-dropzone/react-dropzone/issues/1210
73
+ */
74
+ noPaste?: boolean;
49
75
  disabled?: boolean;
50
76
  onDrop?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
51
77
  onDropAccepted?: <T extends File>(files: T[], event: DropEvent) => void;
@@ -75,7 +101,7 @@ type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
75
101
  useFsAccessApi?: boolean;
76
102
  autoFocus?: boolean;
77
103
  };
78
- type DropEvent = React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement> | DragEvent | Event;
104
+ type DropEvent = React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement> | React.ClipboardEvent<HTMLElement> | DragEvent | ClipboardEvent | Event;
79
105
  interface DropzoneRef {
80
106
  open: () => void;
81
107
  }
@@ -145,5 +171,5 @@ declare const Dropzone: React.ForwardRefExoticComponent<DropzoneProps & React.Re
145
171
  */
146
172
  declare function useDropzone(props?: DropzoneOptions): DropzoneState;
147
173
  //#endregion
148
- export { type Accept, DropEvent, DropzoneInputProps, DropzoneOptions, DropzoneProps, DropzoneRef, DropzoneRootProps, DropzoneState, ErrorCode, type FileError, FileRejection, type FileWithPath, type ValidatorResult, Dropzone as default, useDropzone };
174
+ export { type Accept, type AcceptGroup, DropEvent, DropzoneInputProps, DropzoneOptions, DropzoneProps, DropzoneRef, DropzoneRootProps, DropzoneState, ErrorCode, type FileError, FileRejection, type FileWithPath, type ValidatorResult, Dropzone as default, useDropzone };
149
175
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -3,9 +3,27 @@ import * as React from "react";
3
3
  //#region src/utils/index.d.ts
4
4
  /**
5
5
  * A map of accepted MIME types to file extensions, as passed to the `accept` prop.
6
+ *
7
+ * An extension value may be a single extension string or an array of them - both are
8
+ * accepted, mirroring the shape of `window.showOpenFilePicker`'s `accept`.
6
9
  */
7
10
  interface Accept {
8
- [key: string]: readonly string[];
11
+ [key: string]: string | readonly string[];
12
+ }
13
+ /**
14
+ * A labeled group of accepted types, mirroring one entry of `window.showOpenFilePicker`'s
15
+ * `types` option. Passing the `accept` prop as an array of these lets the File System Access
16
+ * picker present multiple named filter rows instead of a single one (see
17
+ * {@link pickerOptionsFromAccept}). The optional `description` labels the row; when omitted it
18
+ * is derived from the group's extensions.
19
+ *
20
+ * Grouping only surfaces when the FS Access picker is actually used (`useFsAccessApi` +
21
+ * a secure context + browser support). The native `<input>` fallback has no concept of groups
22
+ * or descriptions, so every group is flattened into one accept attribute there.
23
+ */
24
+ interface AcceptGroup {
25
+ description?: string;
26
+ accept: Accept;
9
27
  }
10
28
  /**
11
29
  * A file rejection error.
@@ -37,7 +55,7 @@ interface FileRejection {
37
55
  }
38
56
  type SharedProps = "multiple" | "onDragEnter" | "onDragOver" | "onDragLeave";
39
57
  type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
40
- accept?: Accept;
58
+ accept?: Accept | AcceptGroup[];
41
59
  minSize?: number;
42
60
  maxSize?: number;
43
61
  maxFiles?: number;
@@ -46,6 +64,14 @@ type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
46
64
  noKeyboard?: boolean;
47
65
  noDrag?: boolean;
48
66
  noDragEventsBubbling?: boolean;
67
+ /**
68
+ * If true, disables paste-to-upload. By default, when the dropzone (or a focused child) receives a
69
+ * paste that carries files - e.g. a screenshot pasted with Ctrl/Cmd+V - those files go through the
70
+ * same `accept`/size/`validator` checks and `onDrop` callbacks as a drop. Pastes with no files
71
+ * (plain text, etc.) are ignored and left untouched. See
72
+ * https://github.com/react-dropzone/react-dropzone/issues/1210
73
+ */
74
+ noPaste?: boolean;
49
75
  disabled?: boolean;
50
76
  onDrop?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
51
77
  onDropAccepted?: <T extends File>(files: T[], event: DropEvent) => void;
@@ -75,7 +101,7 @@ type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
75
101
  useFsAccessApi?: boolean;
76
102
  autoFocus?: boolean;
77
103
  };
78
- type DropEvent = React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement> | DragEvent | Event;
104
+ type DropEvent = React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement> | React.ClipboardEvent<HTMLElement> | DragEvent | ClipboardEvent | Event;
79
105
  interface DropzoneRef {
80
106
  open: () => void;
81
107
  }
@@ -145,5 +171,5 @@ declare const Dropzone: React.ForwardRefExoticComponent<DropzoneProps & React.Re
145
171
  */
146
172
  declare function useDropzone(props?: DropzoneOptions): DropzoneState;
147
173
  //#endregion
148
- export { type Accept, DropEvent, DropzoneInputProps, DropzoneOptions, DropzoneProps, DropzoneRef, DropzoneRootProps, DropzoneState, ErrorCode, type FileError, FileRejection, type FileWithPath, type ValidatorResult, Dropzone as default, useDropzone };
174
+ export { type Accept, type AcceptGroup, DropEvent, DropzoneInputProps, DropzoneOptions, DropzoneProps, DropzoneRef, DropzoneRootProps, DropzoneState, ErrorCode, type FileError, FileRejection, type FileWithPath, type ValidatorResult, Dropzone as default, useDropzone };
149
175
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -135,8 +135,9 @@ function isPropagationStopped(event) {
135
135
  return false;
136
136
  }
137
137
  function isEvtWithFiles(event) {
138
- if (!event.dataTransfer) return !!event.target && !!event.target.files;
139
- return Array.prototype.some.call(event.dataTransfer.types, (type) => type === "Files" || type === "application/x-moz-file") || Array.prototype.some.call(event.dataTransfer.items ?? [], isKindFile);
138
+ const dataTransfer = event.dataTransfer ?? event.clipboardData;
139
+ if (!dataTransfer) return !!event.target && !!event.target.files;
140
+ return Array.prototype.some.call(dataTransfer.types, (type) => type === "Files" || type === "application/x-moz-file") || Array.prototype.some.call(dataTransfer.items ?? [], isKindFile);
140
141
  }
141
142
  function isKindFile(item) {
142
143
  return typeof item === "object" && item !== null && item.kind === "file";
@@ -173,27 +174,91 @@ function canUseFileSystemAccessAPI() {
173
174
  return "showOpenFilePicker" in window;
174
175
  }
175
176
  /**
177
+ * Coerce an `accept` extension value to an array. `window.showOpenFilePicker` allows a single
178
+ * extension string as well as an array, so we accept both and normalize to an array. Anything
179
+ * else (a malformed value) collapses to an empty list.
180
+ */
181
+ function toExtensions(ext) {
182
+ if (Array.isArray(ext)) return ext;
183
+ if (typeof ext === "string") return [ext];
184
+ return [];
185
+ }
186
+ /**
187
+ * Normalize either `accept` form to the internal array of `{description, accept}` groups.
188
+ *
189
+ * - The object form (`{mime: ext}`) becomes a single group; its `description` (which the object
190
+ * form can't express) is derived from the extensions like any other group.
191
+ * - The array form is passed through; entries missing an `accept` map are dropped.
192
+ *
193
+ * In both cases a missing `description` is filled in later from the group's extensions (see
194
+ * {@link describeAccept}), so the picker always gets the non-empty description it requires
195
+ * (https://crbug.com/1264708). Returns `undefined` when no `accept` is provided.
196
+ */
197
+ function normalizeAcceptGroups(accept) {
198
+ if (!isDefined(accept)) return;
199
+ if (Array.isArray(accept)) return accept.filter((group) => isDefined(group) && isDefined(group.accept));
200
+ return [{ accept }];
201
+ }
202
+ /**
203
+ * Build a human-readable label for a picker group from its (validated) accept map, used when the
204
+ * caller doesn't supply a `description`. Prefer the file extensions, fall back to the MIME types,
205
+ * then to a generic label - `showOpenFilePicker` requires a non-empty description (crbug 1264708).
206
+ */
207
+ function describeAccept(accept) {
208
+ const extensions = [];
209
+ const mimeTypes = Object.keys(accept);
210
+ for (const ext of Object.values(accept)) for (const e of toExtensions(ext)) if (!extensions.includes(e)) extensions.push(e);
211
+ if (extensions.length > 0) return extensions.join(", ");
212
+ if (mimeTypes.length > 0) return mimeTypes.join(", ");
213
+ return "Files";
214
+ }
215
+ /**
216
+ * Merge every `accept` group into a single MIME-type -> extensions map. Duplicate MIME keys union
217
+ * their extensions. This flattened map drives the native `<input accept>` attribute and the
218
+ * drag/drop validators, which have no concept of groups or descriptions.
219
+ */
220
+ function flattenAccept(accept) {
221
+ const groups = normalizeAcceptGroups(accept);
222
+ if (!isDefined(groups)) return;
223
+ const merged = {};
224
+ for (const group of groups) for (const [mimeType, ext] of Object.entries(group.accept)) {
225
+ const existing = merged[mimeType] ?? (merged[mimeType] = []);
226
+ for (const e of toExtensions(ext)) if (!existing.includes(e)) existing.push(e);
227
+ }
228
+ return merged;
229
+ }
230
+ /**
176
231
  * Convert the `{accept}` dropzone prop to the `{types}` option for showOpenFilePicker.
232
+ *
233
+ * Each group becomes one filter row in the picker. Invalid MIME types / extensions are dropped
234
+ * with a warning; a group left with no valid entries is omitted entirely (showOpenFilePicker
235
+ * rejects an empty `accept`). Returns `undefined` when nothing valid remains.
177
236
  */
178
237
  function pickerOptionsFromAccept(accept) {
179
- if (isDefined(accept)) return [{
180
- description: "Files",
181
- accept: Object.entries(accept).filter(([mimeType, ext]) => {
238
+ const groups = normalizeAcceptGroups(accept);
239
+ if (!isDefined(groups)) return;
240
+ const options = groups.map((group) => {
241
+ const acceptForPicker = Object.entries(group.accept).filter(([mimeType, ext]) => {
182
242
  let ok = true;
183
243
  if (!isMIMEType(mimeType)) {
184
244
  console.warn(`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.`);
185
245
  ok = false;
186
246
  }
187
- if (!Array.isArray(ext) || !ext.every(isExt)) {
247
+ if (!(Array.isArray(ext) || typeof ext === "string") || !toExtensions(ext).every(isExt)) {
188
248
  console.warn(`Skipped "${mimeType}" because an invalid file extension was provided.`);
189
249
  ok = false;
190
250
  }
191
251
  return ok;
192
252
  }).reduce((agg, [mimeType, ext]) => {
193
- agg[mimeType] = ext;
253
+ agg[mimeType] = toExtensions(ext);
194
254
  return agg;
195
- }, {})
196
- }];
255
+ }, {});
256
+ return {
257
+ description: isDefined(group.description) && group.description !== "" ? group.description : describeAccept(acceptForPicker),
258
+ accept: acceptForPicker
259
+ };
260
+ }).filter((option) => Object.keys(option.accept).length > 0);
261
+ return options.length > 0 ? options : void 0;
197
262
  }
198
263
  /**
199
264
  * Convert the `{accept}` dropzone prop to a comma-separated accept attribute string.
@@ -208,7 +273,8 @@ function pickerOptionsFromAccept(accept) {
208
273
  * See https://github.com/react-dropzone/react-dropzone/issues/1220
209
274
  */
210
275
  function acceptPropAsAcceptAttr(accept, { omitWildcardMimeTypesWithExtensions = false } = {}) {
211
- if (isDefined(accept)) return Object.entries(accept).reduce((a, [mimeType, ext]) => {
276
+ if (isDefined(accept)) return Object.entries(accept).reduce((a, [mimeType, extVal]) => {
277
+ const ext = toExtensions(extVal);
212
278
  if (omitWildcardMimeTypesWithExtensions && isMIMETypeWildcard(mimeType) && ext.some(isExt)) a.push(...ext);
213
279
  else a.push(mimeType, ...ext);
214
280
  return a;
@@ -312,9 +378,10 @@ const initialState = {
312
378
  * ```
313
379
  */
314
380
  function useDropzone(props = {}) {
315
- const { accept, disabled = false, getFilesFromEvent = fromEvent, maxSize = Number.POSITIVE_INFINITY, minSize = 0, multiple = true, maxFiles = 0, onDragEnter, onDragLeave, onDragOver, onDrop, onDropAccepted, onDropRejected, onFileDialogCancel, onFileDialogOpen, useFsAccessApi = false, autoFocus = false, preventDropOnDocument = true, noClick = false, noKeyboard = false, noDrag = false, noDragEventsBubbling = false, onError, validator, getErrorMessage } = props;
316
- const acceptAttr = useMemo(() => acceptPropAsAcceptAttr(accept), [accept]);
317
- const inputAcceptAttr = useMemo(() => acceptPropAsAcceptAttr(accept, { omitWildcardMimeTypesWithExtensions: true }), [accept]);
381
+ const { accept, disabled = false, getFilesFromEvent = fromEvent, maxSize = Number.POSITIVE_INFINITY, minSize = 0, multiple = true, maxFiles = 0, onDragEnter, onDragLeave, onDragOver, onDrop, onDropAccepted, onDropRejected, onFileDialogCancel, onFileDialogOpen, useFsAccessApi = false, autoFocus = false, preventDropOnDocument = true, noClick = false, noKeyboard = false, noDrag = false, noDragEventsBubbling = false, noPaste = false, onError, validator, getErrorMessage } = props;
382
+ const flatAccept = useMemo(() => flattenAccept(accept), [accept]);
383
+ const acceptAttr = useMemo(() => acceptPropAsAcceptAttr(flatAccept), [flatAccept]);
384
+ const inputAcceptAttr = useMemo(() => acceptPropAsAcceptAttr(flatAccept, { omitWildcardMimeTypesWithExtensions: true }), [flatAccept]);
318
385
  const pickerTypes = useMemo(() => pickerOptionsFromAccept(accept), [accept]);
319
386
  const onFileDialogOpenCb = useMemo(() => typeof onFileDialogOpen === "function" ? onFileDialogOpen : noop, [onFileDialogOpen]);
320
387
  const onFileDialogCancelCb = useMemo(() => typeof onFileDialogCancel === "function" ? onFileDialogCancel : noop, [onFileDialogCancel]);
@@ -620,6 +687,33 @@ function useDropzone(props = {}) {
620
687
  beginProcessing,
621
688
  endProcessing
622
689
  ]);
690
+ const onPasteCb = useCallback((event) => {
691
+ if (!isEvtWithFiles(event)) return;
692
+ event.preventDefault();
693
+ event.persist?.();
694
+ stopPropagation(event);
695
+ const signal = beginProcessing();
696
+ Promise.resolve(getFilesFromEvent(event)).then((files) => {
697
+ if (signal.aborted) return;
698
+ if (isPropagationStopped(event) && !noDragEventsBubbling) {
699
+ endProcessing(signal);
700
+ return;
701
+ }
702
+ return setFiles(files, event, signal);
703
+ }).catch((e) => {
704
+ if (!signal.aborted) {
705
+ endProcessing(signal);
706
+ onErrCb(e);
707
+ }
708
+ });
709
+ }, [
710
+ getFilesFromEvent,
711
+ setFiles,
712
+ onErrCb,
713
+ noDragEventsBubbling,
714
+ beginProcessing,
715
+ endProcessing
716
+ ]);
623
717
  const openFileDialog = useCallback(() => {
624
718
  if (fsAccessApiWorksRef.current) {
625
719
  dispatch({ type: "openDialog" });
@@ -696,10 +790,13 @@ function useDropzone(props = {}) {
696
790
  const composeDragHandler = (fn) => {
697
791
  return noDrag ? null : composeHandler(fn);
698
792
  };
793
+ const composePasteHandler = (fn) => {
794
+ return noPaste ? null : composeHandler(fn);
795
+ };
699
796
  const stopPropagation = (event) => {
700
797
  if (noDragEventsBubbling) event.stopPropagation();
701
798
  };
702
- const getRootProps = useMemo(() => ({ refKey = "ref", role, onKeyDown, onFocus, onBlur, onClick, onDragEnter, onDragOver, onDragLeave, onDrop, ...rest } = {}) => ({
799
+ const getRootProps = useMemo(() => ({ refKey = "ref", role, onKeyDown, onFocus, onBlur, onClick, onDragEnter, onDragOver, onDragLeave, onDrop, onPaste, ...rest } = {}) => ({
703
800
  onKeyDown: composeKeyboardHandler(composeEventHandlers(onKeyDown, onKeyDownCb)),
704
801
  onFocus: composeKeyboardHandler(composeEventHandlers(onFocus, onFocusCb)),
705
802
  onBlur: composeKeyboardHandler(composeEventHandlers(onBlur, onBlurCb)),
@@ -708,6 +805,7 @@ function useDropzone(props = {}) {
708
805
  onDragOver: composeDragHandler(composeEventHandlers(onDragOver, onDragOverCb)),
709
806
  onDragLeave: composeDragHandler(composeEventHandlers(onDragLeave, onDragLeaveCb)),
710
807
  onDrop: composeDragHandler(composeEventHandlers(onDrop, onDropCb)),
808
+ onPaste: composePasteHandler(composeEventHandlers(onPaste, onPasteCb)),
711
809
  role: typeof role === "string" && role !== "" ? role : "presentation",
712
810
  [refKey]: rootRef,
713
811
  ...!disabled && !noKeyboard ? { tabIndex: 0 } : {},
@@ -723,8 +821,10 @@ function useDropzone(props = {}) {
723
821
  onDragOverCb,
724
822
  onDragLeaveCb,
725
823
  onDropCb,
824
+ onPasteCb,
726
825
  noKeyboard,
727
826
  noDrag,
827
+ noPaste,
728
828
  disabled
729
829
  ]);
730
830
  const onInputElementClick = useCallback((event) => {