react-dropzone 19.3.0 → 20.1.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.tsx CHANGED
@@ -7,10 +7,10 @@ import {
7
7
  canUseFileSystemAccessAPI,
8
8
  composeEventHandlers,
9
9
  ErrorCode,
10
+ evaluateDragFiles,
10
11
  fileAccepted,
11
12
  fileMatchSize,
12
13
  flattenAccept,
13
- getDragVerdict,
14
14
  isAbort,
15
15
  isEvtWithFiles,
16
16
  isIeOrEdge,
@@ -22,7 +22,13 @@ import {
22
22
  pickerOptionsFromAccept,
23
23
  TOO_MANY_FILES_REJECTION
24
24
  } from "./utils";
25
- import type {Accept, AcceptGroup, FileError, ValidatorResult} from "./utils";
25
+ import type {
26
+ Accept,
27
+ AcceptGroup,
28
+ DragFileRejection as UtilsDragFileRejection,
29
+ FileError,
30
+ ValidatorResult
31
+ } from "./utils";
26
32
 
27
33
  export type {Accept, AcceptGroup, FileError, FileWithPath, ValidatorResult};
28
34
  export {ErrorCode};
@@ -36,6 +42,8 @@ export interface FileRejection {
36
42
  errors: readonly FileError[];
37
43
  }
38
44
 
45
+ export type DragFileRejection = UtilsDragFileRejection<FileWithPath | DataTransferItem>;
46
+
39
47
  type SharedProps = "multiple" | "onDragEnter" | "onDragOver" | "onDragLeave";
40
48
 
41
49
  export type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
@@ -116,6 +124,13 @@ export type DropzoneState = DropzoneRef & {
116
124
  isProcessing: boolean;
117
125
  acceptedFiles: readonly FileWithPath[];
118
126
  fileRejections: readonly FileRejection[];
127
+ /**
128
+ * Rejections that can be determined before drop. Standard drag events expose
129
+ * `DataTransferItem`s with a MIME type but no name or size, so extension, size, and custom
130
+ * validator failures are only available in {@link fileRejections} after drop. In practice this
131
+ * state can report MIME-type and surplus-file errors during a standard browser drag.
132
+ */
133
+ dragFileRejections: readonly DragFileRejection[];
119
134
  rootRef: React.RefObject<HTMLElement>;
120
135
  inputRef: React.RefObject<HTMLInputElement>;
121
136
  getRootProps: <T extends DropzoneRootProps>(props?: T) => T;
@@ -171,6 +186,7 @@ interface DropzoneInternalState {
171
186
  isProcessing: boolean;
172
187
  acceptedFiles: FileWithPath[];
173
188
  fileRejections: FileRejection[];
189
+ dragFileRejections: DragFileRejection[];
174
190
  }
175
191
 
176
192
  /**
@@ -196,7 +212,8 @@ const initialState: DropzoneInternalState = {
196
212
  isDragGlobal: false,
197
213
  isProcessing: false,
198
214
  acceptedFiles: [],
199
- fileRejections: []
215
+ fileRejections: [],
216
+ dragFileRejections: []
200
217
  };
201
218
 
202
219
  /**
@@ -459,28 +476,30 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
459
476
  return;
460
477
  }
461
478
 
462
- const fileCount = files.length;
463
479
  // During a drag we only have DataTransferItems (MIME type, no name/size), so the
464
480
  // custom validator can't run yet - a validator-configured dropzone is "unknown" until
465
- // drop rather than a misleading accept/reject. See getDragVerdict for the details.
466
- const verdict =
467
- fileCount > 0
468
- ? getDragVerdict({
469
- files: files as Array<File | DataTransferItem>,
481
+ // drop rather than a misleading accept/reject. The verdict and rejection details come
482
+ // from one evaluation so the two states cannot disagree.
483
+ const evaluation =
484
+ files.length > 0
485
+ ? evaluateDragFiles({
486
+ files: files as Array<FileWithPath | DataTransferItem>,
470
487
  accept: acceptAttr,
471
488
  minSize,
472
489
  maxSize,
473
490
  multiple,
474
491
  maxFiles,
475
- validator
492
+ validator,
493
+ getErrorMessage
476
494
  })
477
495
  : null;
478
496
 
479
497
  dispatch({
480
- isDragAccept: verdict === "accept",
481
- isDragReject: verdict === "reject",
482
- isDragUnknown: verdict === "unknown",
498
+ isDragAccept: evaluation?.verdict === "accept",
499
+ isDragReject: evaluation?.verdict === "reject",
500
+ isDragUnknown: evaluation?.verdict === "unknown",
483
501
  isDragActive: true,
502
+ dragFileRejections: evaluation?.rejections ?? [],
484
503
  type: "setDraggedFiles"
485
504
  });
486
505
 
@@ -501,7 +520,8 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
501
520
  maxSize,
502
521
  multiple,
503
522
  maxFiles,
504
- validator
523
+ validator,
524
+ getErrorMessage
505
525
  ]
506
526
  );
507
527
 
@@ -558,7 +578,8 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
558
578
  isDragActive: false,
559
579
  isDragAccept: false,
560
580
  isDragReject: false,
561
- isDragUnknown: false
581
+ isDragUnknown: false,
582
+ dragFileRejections: []
562
583
  });
563
584
 
564
585
  if (isEvtWithFiles(event) && onDragLeave) {
@@ -1058,7 +1079,8 @@ function reducer(state: DropzoneInternalState, action: any): DropzoneInternalSta
1058
1079
  isDragActive: action.isDragActive,
1059
1080
  isDragAccept: action.isDragAccept,
1060
1081
  isDragReject: action.isDragReject,
1061
- isDragUnknown: action.isDragUnknown
1082
+ isDragUnknown: action.isDragUnknown,
1083
+ dragFileRejections: action.dragFileRejections
1062
1084
  };
1063
1085
  case "setProcessing":
1064
1086
  return {
@@ -1070,6 +1092,7 @@ function reducer(state: DropzoneInternalState, action: any): DropzoneInternalSta
1070
1092
  ...state,
1071
1093
  acceptedFiles: action.acceptedFiles,
1072
1094
  fileRejections: action.fileRejections,
1095
+ dragFileRejections: [],
1073
1096
  isProcessing: false,
1074
1097
  isDragReject: false,
1075
1098
  isDragUnknown: false
@@ -207,6 +207,19 @@ export function allFilesAccepted({
207
207
  */
208
208
  export type DragVerdict = "accept" | "reject" | "unknown";
209
209
 
210
+ /**
211
+ * A file rejected by the checks that can run during a drag.
212
+ */
213
+ export interface DragFileRejection<T extends File | DataTransferItem = File | DataTransferItem> {
214
+ file: T;
215
+ errors: readonly FileError[];
216
+ }
217
+
218
+ export interface DragEvaluation<T extends File | DataTransferItem = File | DataTransferItem> {
219
+ verdict: DragVerdict;
220
+ rejections: DragFileRejection<T>[];
221
+ }
222
+
210
223
  /**
211
224
  * Classify a set of dragged files for the `isDragAccept`/`isDragReject`/`isDragUnknown` states.
212
225
  *
@@ -224,16 +237,17 @@ export type DragVerdict = "accept" | "reject" | "unknown";
224
237
  *
225
238
  * The full check (including the `validator`) still runs on drop in {@link fileAccepted}/`setFiles`.
226
239
  */
227
- export function getDragVerdict({
240
+ export function evaluateDragFiles<T extends File | DataTransferItem>({
228
241
  files,
229
242
  accept,
230
243
  minSize,
231
244
  maxSize,
232
245
  multiple,
233
246
  maxFiles = 0,
234
- validator
247
+ validator,
248
+ getErrorMessage
235
249
  }: {
236
- files: Array<File | DataTransferItem>;
250
+ files: T[];
237
251
  accept?: string;
238
252
  minSize?: number;
239
253
  maxSize?: number;
@@ -242,24 +256,52 @@ export function getDragVerdict({
242
256
  // The validator is never invoked here (see the note above), so its async variant is accepted
243
257
  // purely so the same `validator` prop is assignable during a drag.
244
258
  validator?: (file: File) => ValidatorResult | Promise<ValidatorResult>;
245
- }): DragVerdict {
246
- // The file count is knowable during a drag, so an over-the-limit selection is a confident reject.
247
- if ((!multiple && files.length > 1) || (multiple && maxFiles >= 1 && files.length > maxFiles)) {
248
- return "reject";
259
+ getErrorMessage?: (error: FileError, file: File) => string;
260
+ }): DragEvaluation<T> {
261
+ const acceptedFiles: T[] = [];
262
+ const rejections: DragFileRejection<T>[] = [];
263
+
264
+ const localizeError = (error: FileError, file: T): FileError =>
265
+ getErrorMessage && typeof File !== "undefined" && file instanceof File
266
+ ? {...error, message: getErrorMessage(error, file)}
267
+ : error;
268
+
269
+ files.forEach(file => {
270
+ const [accepted, acceptError] = fileAccepted(file as File, accept);
271
+ const [sizeMatch, sizeError] = fileMatchSize(file as File, minSize, maxSize);
272
+
273
+ if (accepted && sizeMatch) {
274
+ acceptedFiles.push(file);
275
+ } else {
276
+ rejections.push({
277
+ file,
278
+ errors: [acceptError, sizeError]
279
+ .filter((error): error is FileError => error != null)
280
+ .map(error => localizeError(error, file))
281
+ });
282
+ }
283
+ });
284
+
285
+ // Match the drop path: files that already failed a per-file check do not consume the limit,
286
+ // and only otherwise-valid surplus files receive a too-many-files rejection.
287
+ const acceptedFilesLimit = multiple ? (maxFiles >= 1 ? maxFiles : Number.POSITIVE_INFINITY) : 1;
288
+ if (acceptedFiles.length > acceptedFilesLimit) {
289
+ acceptedFiles.slice(acceptedFilesLimit).forEach(file => {
290
+ rejections.push({file, errors: [localizeError(TOO_MANY_FILES_REJECTION, file)]});
291
+ });
249
292
  }
250
293
 
251
- const confidentlyRejected = files.some(file => {
252
- const [accepted] = fileAccepted(file as File, accept);
253
- const [sizeMatch] = fileMatchSize(file as File, minSize, maxSize);
254
- return !accepted || !sizeMatch;
255
- });
256
- if (confidentlyRejected) {
257
- return "reject";
294
+ if (rejections.length > 0) {
295
+ return {verdict: "reject", rejections};
258
296
  }
259
297
 
260
298
  // Built-in checks pass. A custom validator can only ever add rejections on drop, never rescue
261
299
  // one - so with a validator present the drag outcome is unknown until we have real Files.
262
- return validator ? "unknown" : "accept";
300
+ return {verdict: validator ? "unknown" : "accept", rejections};
301
+ }
302
+
303
+ export function getDragVerdict(options: Parameters<typeof evaluateDragFiles>[0]): DragVerdict {
304
+ return evaluateDragFiles(options).verdict;
263
305
  }
264
306
 
265
307
  // React's synthetic events has event.isPropagationStopped,