react-dropzone 19.0.2 → 19.1.1
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.cjs +125 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +125 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +236 -51
- package/src/utils/index.ts +18 -1
package/src/index.tsx
CHANGED
|
@@ -14,15 +14,16 @@ import {
|
|
|
14
14
|
isEvtWithFiles,
|
|
15
15
|
isIeOrEdge,
|
|
16
16
|
isNotAllowedError,
|
|
17
|
+
isThenable,
|
|
17
18
|
isPropagationStopped,
|
|
18
19
|
isSecurityError,
|
|
19
20
|
onDocumentDragOver,
|
|
20
21
|
pickerOptionsFromAccept,
|
|
21
22
|
TOO_MANY_FILES_REJECTION
|
|
22
23
|
} from "./utils";
|
|
23
|
-
import type {Accept, FileError} from "./utils";
|
|
24
|
+
import type {Accept, FileError, ValidatorResult} from "./utils";
|
|
24
25
|
|
|
25
|
-
export type {Accept, FileError, FileWithPath};
|
|
26
|
+
export type {Accept, FileError, FileWithPath, ValidatorResult};
|
|
26
27
|
export {ErrorCode};
|
|
27
28
|
|
|
28
29
|
export interface DropzoneProps extends DropzoneOptions {
|
|
@@ -54,7 +55,18 @@ export type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> &
|
|
|
54
55
|
onFileDialogCancel?: () => void;
|
|
55
56
|
onFileDialogOpen?: () => void;
|
|
56
57
|
onError?: (err: Error) => void;
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Custom validation, run once per file on drop/selection. Return `null` to accept the file, or a
|
|
60
|
+
* {@link FileError} (or array of them) to reject it. May be `async` (return a `Promise`) to support
|
|
61
|
+
* checks that can't run synchronously - e.g. reading image dimensions, inspecting file contents,
|
|
62
|
+
* or calling an external service. While an async validator is pending, {@link DropzoneState.isProcessing}
|
|
63
|
+
* is `true`, and `onDrop`/`onDropAccepted`/`onDropRejected` fire only once it settles. If the
|
|
64
|
+
* validator throws or rejects, `onError` is called and the drop is discarded.
|
|
65
|
+
*
|
|
66
|
+
* Note: the validator never runs during a drag (a `DataTransferItem` has no name/size), so a
|
|
67
|
+
* validator-configured dropzone is `isDragUnknown` until drop.
|
|
68
|
+
*/
|
|
69
|
+
validator?: <T extends File>(file: T) => ValidatorResult | Promise<ValidatorResult>;
|
|
58
70
|
/**
|
|
59
71
|
* Override the message of any rejection error (built-in or custom). Called once per error;
|
|
60
72
|
* receives the error and the file it belongs to and returns the message to use. Return
|
|
@@ -79,6 +91,14 @@ export type DropzoneState = DropzoneRef & {
|
|
|
79
91
|
isDragUnknown: boolean;
|
|
80
92
|
isDragGlobal: boolean;
|
|
81
93
|
isFileDialogActive: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* `true` while a drop/selection is being processed asynchronously - i.e. while `getFilesFromEvent`
|
|
96
|
+
* reads the files and/or an async {@link DropzoneOptions.validator} runs. Spans the whole pipeline,
|
|
97
|
+
* from when files start being read until validation settles. When both are synchronous (the default
|
|
98
|
+
* `getFilesFromEvent` with no/async-free validator) the work resolves within a microtask, so it's
|
|
99
|
+
* only observable for genuinely async work. Use it to show a spinner or disable UI while processing.
|
|
100
|
+
*/
|
|
101
|
+
isProcessing: boolean;
|
|
82
102
|
acceptedFiles: readonly FileWithPath[];
|
|
83
103
|
fileRejections: readonly FileRejection[];
|
|
84
104
|
rootRef: React.RefObject<HTMLElement>;
|
|
@@ -133,10 +153,24 @@ interface DropzoneInternalState {
|
|
|
133
153
|
isDragReject: boolean;
|
|
134
154
|
isDragUnknown: boolean;
|
|
135
155
|
isDragGlobal: boolean;
|
|
156
|
+
isProcessing: boolean;
|
|
136
157
|
acceptedFiles: FileWithPath[];
|
|
137
158
|
fileRejections: FileRejection[];
|
|
138
159
|
}
|
|
139
160
|
|
|
161
|
+
/**
|
|
162
|
+
* The per-file outcome of the built-in checks plus the (resolved) custom validator, assembled in
|
|
163
|
+
* setFiles before the accepted/rejected split.
|
|
164
|
+
*/
|
|
165
|
+
interface PerFileResult {
|
|
166
|
+
file: FileWithPath;
|
|
167
|
+
accepted: boolean;
|
|
168
|
+
acceptError: FileError | null;
|
|
169
|
+
sizeMatch: boolean;
|
|
170
|
+
sizeError: FileError | null;
|
|
171
|
+
customErrors: ValidatorResult;
|
|
172
|
+
}
|
|
173
|
+
|
|
140
174
|
const initialState: DropzoneInternalState = {
|
|
141
175
|
isFocused: false,
|
|
142
176
|
isFileDialogActive: false,
|
|
@@ -145,6 +179,7 @@ const initialState: DropzoneInternalState = {
|
|
|
145
179
|
isDragReject: false,
|
|
146
180
|
isDragUnknown: false,
|
|
147
181
|
isDragGlobal: false,
|
|
182
|
+
isProcessing: false,
|
|
148
183
|
acceptedFiles: [],
|
|
149
184
|
fileRejections: []
|
|
150
185
|
};
|
|
@@ -228,6 +263,34 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
228
263
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
229
264
|
const {isFocused, isFileDialogActive} = state;
|
|
230
265
|
|
|
266
|
+
// Mirror {isFileDialogActive} into a ref so the memoized drag handlers can read the current value
|
|
267
|
+
// without being recreated (and churning getRootProps/getInputProps) every time the dialog toggles.
|
|
268
|
+
const isFileDialogActiveRef = useRef(isFileDialogActive);
|
|
269
|
+
isFileDialogActiveRef.current = isFileDialogActive;
|
|
270
|
+
|
|
271
|
+
// Tracks the in-flight processing run - reading files (getFilesFromEvent) plus running an async
|
|
272
|
+
// validator. A newer drop/selection aborts the previous run so slow async work can't resolve late
|
|
273
|
+
// and clobber the state with stale results.
|
|
274
|
+
const processingAbortRef = useRef<AbortController | null>(null);
|
|
275
|
+
|
|
276
|
+
// Begin a processing run: supersede any run still in flight and flip {isProcessing} on. Returns
|
|
277
|
+
// the run's AbortSignal, which downstream async steps check to bail if a newer run took over.
|
|
278
|
+
const beginProcessing = useCallback(() => {
|
|
279
|
+
processingAbortRef.current?.abort();
|
|
280
|
+
const controller = new AbortController();
|
|
281
|
+
processingAbortRef.current = controller;
|
|
282
|
+
dispatch({type: "setProcessing", isProcessing: true});
|
|
283
|
+
return controller.signal;
|
|
284
|
+
}, []);
|
|
285
|
+
|
|
286
|
+
// End a processing run by clearing {isProcessing} - but only if this run is still the active one.
|
|
287
|
+
// A superseded run (signal aborted) leaves the flag to the run that replaced it.
|
|
288
|
+
const endProcessing = useCallback((signal: AbortSignal) => {
|
|
289
|
+
if (!signal.aborted) {
|
|
290
|
+
dispatch({type: "setProcessing", isProcessing: false});
|
|
291
|
+
}
|
|
292
|
+
}, []);
|
|
293
|
+
|
|
231
294
|
const fsAccessApiWorksRef = useRef(
|
|
232
295
|
typeof window !== "undefined" && window.isSecureContext && useFsAccessApi && canUseFileSystemAccessAPI()
|
|
233
296
|
);
|
|
@@ -360,6 +423,13 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
360
423
|
event.persist?.();
|
|
361
424
|
stopPropagation(event);
|
|
362
425
|
|
|
426
|
+
// Ignore drags onto the dropzone while the file picker dialog is open: the page underneath a
|
|
427
|
+
// live picker shouldn't react to (or accept) dropped files. See #1455. preventDefault() above
|
|
428
|
+
// still runs so the browser doesn't try to open/navigate to a dropped file.
|
|
429
|
+
if (isFileDialogActiveRef.current) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
363
433
|
dragTargetsRef.current = [...dragTargetsRef.current, event.target];
|
|
364
434
|
|
|
365
435
|
if (isEvtWithFiles(event)) {
|
|
@@ -421,6 +491,11 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
421
491
|
event.persist?.();
|
|
422
492
|
stopPropagation(event);
|
|
423
493
|
|
|
494
|
+
// Ignore drags over the dropzone while the file picker dialog is open. See #1455.
|
|
495
|
+
if (isFileDialogActiveRef.current) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
424
499
|
const hasFiles = isEvtWithFiles(event);
|
|
425
500
|
if (hasFiles && event.dataTransfer) {
|
|
426
501
|
try {
|
|
@@ -474,65 +549,114 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
474
549
|
);
|
|
475
550
|
|
|
476
551
|
const setFiles = useCallback(
|
|
477
|
-
(files: FileWithPath[], event: any) => {
|
|
478
|
-
const acceptedFiles: FileWithPath[] = [];
|
|
479
|
-
const fileRejections: FileRejection[] = [];
|
|
480
|
-
|
|
552
|
+
async (files: FileWithPath[], event: any, signal: AbortSignal) => {
|
|
481
553
|
const localizeError = (error: FileError, file: File): FileError =>
|
|
482
554
|
getErrorMessage ? {...error, message: getErrorMessage(error, file)} : error;
|
|
483
555
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
556
|
+
// Commit the per-file verdicts: split accepted/rejected, cap the surplus, update state and
|
|
557
|
+
// fire the onDrop callbacks. Runs synchronously so nested-dropzone ordering is preserved on
|
|
558
|
+
// the fast path (an onDrop handler calling stopPropagation must do so before a parent's onDrop
|
|
559
|
+
// check - see the noDragEventsBubbling tests).
|
|
560
|
+
const commit = (results: Array<PerFileResult>) => {
|
|
561
|
+
const acceptedFiles: FileWithPath[] = [];
|
|
562
|
+
const fileRejections: FileRejection[] = [];
|
|
563
|
+
|
|
564
|
+
results.forEach(({file, accepted, acceptError, sizeMatch, sizeError, customErrors}) => {
|
|
565
|
+
if (accepted && sizeMatch && !customErrors) {
|
|
566
|
+
acceptedFiles.push(file);
|
|
567
|
+
} else {
|
|
568
|
+
let errors: Array<FileError | null> = [acceptError, sizeError];
|
|
488
569
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
let errors: Array<FileError | null> = [acceptError, sizeError];
|
|
570
|
+
if (customErrors) {
|
|
571
|
+
errors = errors.concat(customErrors);
|
|
572
|
+
}
|
|
493
573
|
|
|
494
|
-
|
|
495
|
-
|
|
574
|
+
fileRejections.push({
|
|
575
|
+
file,
|
|
576
|
+
errors: errors.filter((e): e is FileError => e != null).map(error => localizeError(error, file))
|
|
577
|
+
});
|
|
496
578
|
}
|
|
579
|
+
});
|
|
497
580
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
581
|
+
// Cap the accepted files at the configured limit and reject only the surplus (the files past
|
|
582
|
+
// the limit) with a too-many-files error, instead of rejecting the whole batch. The limit is 1
|
|
583
|
+
// when {multiple} is false, and {maxFiles} when {multiple} is true (0 means no limit). Files
|
|
584
|
+
// that already failed the per-file checks above are in {fileRejections} and don't count here.
|
|
585
|
+
// See https://github.com/react-dropzone/react-dropzone/issues/1355
|
|
586
|
+
// and https://github.com/react-dropzone/react-dropzone/issues/1358
|
|
587
|
+
const acceptedFilesLimit = multiple ? (maxFiles >= 1 ? maxFiles : Number.POSITIVE_INFINITY) : 1;
|
|
588
|
+
if (acceptedFiles.length > acceptedFilesLimit) {
|
|
589
|
+
const surplusFiles = acceptedFiles.splice(acceptedFilesLimit);
|
|
590
|
+
surplusFiles.forEach(file => {
|
|
591
|
+
fileRejections.push({file, errors: [localizeError(TOO_MANY_FILES_REJECTION, file)]});
|
|
501
592
|
});
|
|
502
593
|
}
|
|
503
|
-
});
|
|
504
594
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
// and https://github.com/react-dropzone/react-dropzone/issues/1358
|
|
511
|
-
const acceptedFilesLimit = multiple ? (maxFiles >= 1 ? maxFiles : Number.POSITIVE_INFINITY) : 1;
|
|
512
|
-
if (acceptedFiles.length > acceptedFilesLimit) {
|
|
513
|
-
const surplusFiles = acceptedFiles.splice(acceptedFilesLimit);
|
|
514
|
-
surplusFiles.forEach(file => {
|
|
515
|
-
fileRejections.push({file, errors: [localizeError(TOO_MANY_FILES_REJECTION, file)]});
|
|
595
|
+
// Clears isProcessing back to false (see the reducer) in the same update that sets the files.
|
|
596
|
+
dispatch({
|
|
597
|
+
acceptedFiles,
|
|
598
|
+
fileRejections,
|
|
599
|
+
type: "setFiles"
|
|
516
600
|
});
|
|
517
|
-
}
|
|
518
601
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
602
|
+
if (onDrop) {
|
|
603
|
+
onDrop(acceptedFiles, fileRejections, event);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (fileRejections.length > 0 && onDropRejected) {
|
|
607
|
+
onDropRejected(fileRejections, event);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (acceptedFiles.length > 0 && onDropAccepted) {
|
|
611
|
+
onDropAccepted(acceptedFiles, event);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Run the built-in checks synchronously and invoke the validator (which may return a value or
|
|
616
|
+
// a Promise). customErrors is left as-is here so we can tell sync from async below.
|
|
617
|
+
const pending = files.map(file => {
|
|
618
|
+
const [accepted, acceptError] = fileAccepted(file, inputAcceptAttr);
|
|
619
|
+
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize);
|
|
620
|
+
const customErrors = validator ? validator(file) : null;
|
|
621
|
+
return {file, accepted, acceptError, sizeMatch, sizeError, customErrors};
|
|
523
622
|
});
|
|
524
623
|
|
|
525
|
-
|
|
526
|
-
|
|
624
|
+
// Callers check signal.aborted right before invoking setFiles (synchronously, no await in
|
|
625
|
+
// between), so this run is guaranteed live here - the supersession guards below only matter
|
|
626
|
+
// after we await the validator.
|
|
627
|
+
|
|
628
|
+
// Fast path: no validator, or a synchronous one. Commit synchronously - no extra microtask
|
|
629
|
+
// hop, so nested-dropzone ordering is preserved (an onDrop handler calling stopPropagation
|
|
630
|
+
// must run before a parent's onDrop check - see the noDragEventsBubbling tests). commit's
|
|
631
|
+
// dispatch also clears isProcessing.
|
|
632
|
+
if (!pending.some(({customErrors}) => isThenable(customErrors))) {
|
|
633
|
+
commit(pending as Array<PerFileResult>);
|
|
634
|
+
return;
|
|
527
635
|
}
|
|
528
636
|
|
|
529
|
-
|
|
530
|
-
|
|
637
|
+
// Async path: at least one validator returned a Promise. isProcessing is already on (set when
|
|
638
|
+
// the run began, before getFilesFromEvent); keep guarding against supersession while we await.
|
|
639
|
+
let results: Array<PerFileResult>;
|
|
640
|
+
try {
|
|
641
|
+
results = await Promise.all(
|
|
642
|
+
pending.map(async ({customErrors, ...rest}) => ({...rest, customErrors: await customErrors}))
|
|
643
|
+
);
|
|
644
|
+
} catch (e) {
|
|
645
|
+
// A validator threw/rejected. If a newer run already superseded this one, let it own the
|
|
646
|
+
// state; otherwise clear the processing flag and report the error via onError.
|
|
647
|
+
if (!signal.aborted) {
|
|
648
|
+
endProcessing(signal);
|
|
649
|
+
onErrCb(e as Error);
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
531
652
|
}
|
|
532
653
|
|
|
533
|
-
|
|
534
|
-
|
|
654
|
+
// A newer drop landed while we were validating - discard these stale results.
|
|
655
|
+
if (signal.aborted) {
|
|
656
|
+
return;
|
|
535
657
|
}
|
|
658
|
+
|
|
659
|
+
commit(results);
|
|
536
660
|
},
|
|
537
661
|
[
|
|
538
662
|
dispatch,
|
|
@@ -545,7 +669,9 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
545
669
|
onDropAccepted,
|
|
546
670
|
onDropRejected,
|
|
547
671
|
validator,
|
|
548
|
-
getErrorMessage
|
|
672
|
+
getErrorMessage,
|
|
673
|
+
onErrCb,
|
|
674
|
+
endProcessing
|
|
549
675
|
]
|
|
550
676
|
);
|
|
551
677
|
|
|
@@ -558,19 +684,44 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
558
684
|
|
|
559
685
|
dragTargetsRef.current = [];
|
|
560
686
|
|
|
687
|
+
// Ignore a drop landing on the dropzone while the file picker dialog is open (see #1455).
|
|
688
|
+
// Guard on {event.dataTransfer} so only real drag-drops are suppressed: the input's change
|
|
689
|
+
// event (a file picked from the dialog) also runs through here while the dialog is still
|
|
690
|
+
// flagged active, and that path must keep working. Returning before the reset below leaves
|
|
691
|
+
// the dialog flag intact.
|
|
692
|
+
if (isFileDialogActiveRef.current && event.dataTransfer) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Clear drag state before we begin processing so beginProcessing's isProcessing isn't reset.
|
|
697
|
+
dispatch({type: "reset"});
|
|
698
|
+
|
|
561
699
|
if (isEvtWithFiles(event)) {
|
|
700
|
+
// Processing spans reading the files (getFilesFromEvent) and running the validator.
|
|
701
|
+
const signal = beginProcessing();
|
|
562
702
|
Promise.resolve(getFilesFromEvent(event))
|
|
563
703
|
.then(files => {
|
|
704
|
+
// A newer drop superseded this one while reading files - it owns isProcessing now.
|
|
705
|
+
if (signal.aborted) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
564
708
|
if (isPropagationStopped(event) && !noDragEventsBubbling) {
|
|
709
|
+
endProcessing(signal);
|
|
565
710
|
return;
|
|
566
711
|
}
|
|
567
|
-
setFiles(
|
|
712
|
+
// setFiles handles validator errors internally (routing them to onError); the outer
|
|
713
|
+
// catch here only fires for a getFilesFromEvent failure.
|
|
714
|
+
return setFiles(files as FileWithPath[], event, signal);
|
|
568
715
|
})
|
|
569
|
-
.catch(e =>
|
|
716
|
+
.catch(e => {
|
|
717
|
+
if (!signal.aborted) {
|
|
718
|
+
endProcessing(signal);
|
|
719
|
+
onErrCb(e);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
570
722
|
}
|
|
571
|
-
dispatch({type: "reset"});
|
|
572
723
|
},
|
|
573
|
-
[getFilesFromEvent, setFiles, onErrCb, noDragEventsBubbling]
|
|
724
|
+
[getFilesFromEvent, setFiles, onErrCb, noDragEventsBubbling, beginProcessing, endProcessing]
|
|
574
725
|
);
|
|
575
726
|
|
|
576
727
|
// Fn for opening the file dialog programmatically
|
|
@@ -585,14 +736,31 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
585
736
|
multiple,
|
|
586
737
|
types: pickerTypes
|
|
587
738
|
};
|
|
739
|
+
// Set once the user has picked file(s); processing then spans reading them (getFilesFromEvent)
|
|
740
|
+
// and running the validator. Picker rejections (cancel, security) happen before this, so it
|
|
741
|
+
// stays undefined there and no processing state is touched.
|
|
742
|
+
let signal: AbortSignal | undefined;
|
|
588
743
|
(window as any)
|
|
589
744
|
.showOpenFilePicker(opts)
|
|
590
|
-
.then((handles: any) =>
|
|
745
|
+
.then((handles: any) => {
|
|
746
|
+
signal = beginProcessing();
|
|
747
|
+
return getFilesFromEvent(handles);
|
|
748
|
+
})
|
|
591
749
|
.then((files: Array<File | DataTransferItem>) => {
|
|
592
|
-
|
|
750
|
+
// Close the dialog as soon as we have the selection; validation runs afterwards and is
|
|
751
|
+
// reflected by isProcessing rather than by keeping the dialog "active".
|
|
593
752
|
dispatch({type: "closeDialog"});
|
|
753
|
+
// A drop elsewhere superseded this selection while reading files - it owns isProcessing.
|
|
754
|
+
if (signal!.aborted) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
return setFiles(files as FileWithPath[], null, signal!);
|
|
594
758
|
})
|
|
595
759
|
.catch((e: any) => {
|
|
760
|
+
// Clear any processing this run started (e.g. a getFilesFromEvent failure).
|
|
761
|
+
if (signal) {
|
|
762
|
+
endProcessing(signal);
|
|
763
|
+
}
|
|
596
764
|
// AbortError means the user canceled
|
|
597
765
|
if (isAbort(e)) {
|
|
598
766
|
onFileDialogCancelCb(e);
|
|
@@ -626,7 +794,18 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
626
794
|
inputRef.current.value = "";
|
|
627
795
|
inputRef.current.click();
|
|
628
796
|
}
|
|
629
|
-
}, [
|
|
797
|
+
}, [
|
|
798
|
+
dispatch,
|
|
799
|
+
onFileDialogOpenCb,
|
|
800
|
+
onFileDialogCancelCb,
|
|
801
|
+
useFsAccessApi,
|
|
802
|
+
setFiles,
|
|
803
|
+
onErrCb,
|
|
804
|
+
pickerTypes,
|
|
805
|
+
multiple,
|
|
806
|
+
beginProcessing,
|
|
807
|
+
endProcessing
|
|
808
|
+
]);
|
|
630
809
|
|
|
631
810
|
// Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
|
|
632
811
|
const onKeyDownCb = useCallback(
|
|
@@ -814,11 +993,17 @@ function reducer(state: DropzoneInternalState, action: any): DropzoneInternalSta
|
|
|
814
993
|
isDragReject: action.isDragReject,
|
|
815
994
|
isDragUnknown: action.isDragUnknown
|
|
816
995
|
};
|
|
996
|
+
case "setProcessing":
|
|
997
|
+
return {
|
|
998
|
+
...state,
|
|
999
|
+
isProcessing: action.isProcessing
|
|
1000
|
+
};
|
|
817
1001
|
case "setFiles":
|
|
818
1002
|
return {
|
|
819
1003
|
...state,
|
|
820
1004
|
acceptedFiles: action.acceptedFiles,
|
|
821
1005
|
fileRejections: action.fileRejections,
|
|
1006
|
+
isProcessing: false,
|
|
822
1007
|
isDragReject: false,
|
|
823
1008
|
isDragUnknown: false
|
|
824
1009
|
};
|
package/src/utils/index.ts
CHANGED
|
@@ -21,6 +21,13 @@ export interface FileError {
|
|
|
21
21
|
code: ErrorCode | string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* What a custom `validator` returns: a single error, a list of errors, or `null` when the file
|
|
26
|
+
* passes. A validator may return the result directly (synchronous) or wrapped in a `Promise`
|
|
27
|
+
* (asynchronous, e.g. reading image dimensions or calling an external service).
|
|
28
|
+
*/
|
|
29
|
+
export type ValidatorResult = FileError | readonly FileError[] | null;
|
|
30
|
+
|
|
24
31
|
// Error codes
|
|
25
32
|
export const FILE_INVALID_TYPE = "file-invalid-type";
|
|
26
33
|
export const FILE_TOO_LARGE = "file-too-large";
|
|
@@ -133,6 +140,14 @@ function isDefined<T>(value: T): value is NonNullable<T> {
|
|
|
133
140
|
return value !== undefined && value !== null;
|
|
134
141
|
}
|
|
135
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Check if a value is thenable (Promise-like), used to tell a synchronous validator result from an
|
|
145
|
+
* asynchronous one.
|
|
146
|
+
*/
|
|
147
|
+
export function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
148
|
+
return value != null && typeof (value as {then?: unknown}).then === "function";
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
export function allFilesAccepted({
|
|
137
152
|
files,
|
|
138
153
|
accept,
|
|
@@ -205,7 +220,9 @@ export function getDragVerdict({
|
|
|
205
220
|
maxSize?: number;
|
|
206
221
|
multiple?: boolean;
|
|
207
222
|
maxFiles?: number;
|
|
208
|
-
validator
|
|
223
|
+
// The validator is never invoked here (see the note above), so its async variant is accepted
|
|
224
|
+
// purely so the same `validator` prop is assignable during a drag.
|
|
225
|
+
validator?: (file: File) => ValidatorResult | Promise<ValidatorResult>;
|
|
209
226
|
}): DragVerdict {
|
|
210
227
|
// The file count is knowable during a drag, so an over-the-limit selection is a confident reject.
|
|
211
228
|
if ((!multiple && files.length > 1) || (multiple && maxFiles >= 1 && files.length > maxFiles)) {
|