react-dropzone 19.0.2 → 19.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/dist/index.cjs +120 -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 +120 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +210 -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,29 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
228
263
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
229
264
|
const {isFocused, isFileDialogActive} = state;
|
|
230
265
|
|
|
266
|
+
// Tracks the in-flight processing run - reading files (getFilesFromEvent) plus running an async
|
|
267
|
+
// validator. A newer drop/selection aborts the previous run so slow async work can't resolve late
|
|
268
|
+
// and clobber the state with stale results.
|
|
269
|
+
const processingAbortRef = useRef<AbortController | null>(null);
|
|
270
|
+
|
|
271
|
+
// Begin a processing run: supersede any run still in flight and flip {isProcessing} on. Returns
|
|
272
|
+
// the run's AbortSignal, which downstream async steps check to bail if a newer run took over.
|
|
273
|
+
const beginProcessing = useCallback(() => {
|
|
274
|
+
processingAbortRef.current?.abort();
|
|
275
|
+
const controller = new AbortController();
|
|
276
|
+
processingAbortRef.current = controller;
|
|
277
|
+
dispatch({type: "setProcessing", isProcessing: true});
|
|
278
|
+
return controller.signal;
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
// End a processing run by clearing {isProcessing} - but only if this run is still the active one.
|
|
282
|
+
// A superseded run (signal aborted) leaves the flag to the run that replaced it.
|
|
283
|
+
const endProcessing = useCallback((signal: AbortSignal) => {
|
|
284
|
+
if (!signal.aborted) {
|
|
285
|
+
dispatch({type: "setProcessing", isProcessing: false});
|
|
286
|
+
}
|
|
287
|
+
}, []);
|
|
288
|
+
|
|
231
289
|
const fsAccessApiWorksRef = useRef(
|
|
232
290
|
typeof window !== "undefined" && window.isSecureContext && useFsAccessApi && canUseFileSystemAccessAPI()
|
|
233
291
|
);
|
|
@@ -474,65 +532,114 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
474
532
|
);
|
|
475
533
|
|
|
476
534
|
const setFiles = useCallback(
|
|
477
|
-
(files: FileWithPath[], event: any) => {
|
|
478
|
-
const acceptedFiles: FileWithPath[] = [];
|
|
479
|
-
const fileRejections: FileRejection[] = [];
|
|
480
|
-
|
|
535
|
+
async (files: FileWithPath[], event: any, signal: AbortSignal) => {
|
|
481
536
|
const localizeError = (error: FileError, file: File): FileError =>
|
|
482
537
|
getErrorMessage ? {...error, message: getErrorMessage(error, file)} : error;
|
|
483
538
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
539
|
+
// Commit the per-file verdicts: split accepted/rejected, cap the surplus, update state and
|
|
540
|
+
// fire the onDrop callbacks. Runs synchronously so nested-dropzone ordering is preserved on
|
|
541
|
+
// the fast path (an onDrop handler calling stopPropagation must do so before a parent's onDrop
|
|
542
|
+
// check - see the noDragEventsBubbling tests).
|
|
543
|
+
const commit = (results: Array<PerFileResult>) => {
|
|
544
|
+
const acceptedFiles: FileWithPath[] = [];
|
|
545
|
+
const fileRejections: FileRejection[] = [];
|
|
546
|
+
|
|
547
|
+
results.forEach(({file, accepted, acceptError, sizeMatch, sizeError, customErrors}) => {
|
|
548
|
+
if (accepted && sizeMatch && !customErrors) {
|
|
549
|
+
acceptedFiles.push(file);
|
|
550
|
+
} else {
|
|
551
|
+
let errors: Array<FileError | null> = [acceptError, sizeError];
|
|
488
552
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
let errors: Array<FileError | null> = [acceptError, sizeError];
|
|
553
|
+
if (customErrors) {
|
|
554
|
+
errors = errors.concat(customErrors);
|
|
555
|
+
}
|
|
493
556
|
|
|
494
|
-
|
|
495
|
-
|
|
557
|
+
fileRejections.push({
|
|
558
|
+
file,
|
|
559
|
+
errors: errors.filter((e): e is FileError => e != null).map(error => localizeError(error, file))
|
|
560
|
+
});
|
|
496
561
|
}
|
|
562
|
+
});
|
|
497
563
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
564
|
+
// Cap the accepted files at the configured limit and reject only the surplus (the files past
|
|
565
|
+
// the limit) with a too-many-files error, instead of rejecting the whole batch. The limit is 1
|
|
566
|
+
// when {multiple} is false, and {maxFiles} when {multiple} is true (0 means no limit). Files
|
|
567
|
+
// that already failed the per-file checks above are in {fileRejections} and don't count here.
|
|
568
|
+
// See https://github.com/react-dropzone/react-dropzone/issues/1355
|
|
569
|
+
// and https://github.com/react-dropzone/react-dropzone/issues/1358
|
|
570
|
+
const acceptedFilesLimit = multiple ? (maxFiles >= 1 ? maxFiles : Number.POSITIVE_INFINITY) : 1;
|
|
571
|
+
if (acceptedFiles.length > acceptedFilesLimit) {
|
|
572
|
+
const surplusFiles = acceptedFiles.splice(acceptedFilesLimit);
|
|
573
|
+
surplusFiles.forEach(file => {
|
|
574
|
+
fileRejections.push({file, errors: [localizeError(TOO_MANY_FILES_REJECTION, file)]});
|
|
501
575
|
});
|
|
502
576
|
}
|
|
503
|
-
});
|
|
504
577
|
|
|
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)]});
|
|
578
|
+
// Clears isProcessing back to false (see the reducer) in the same update that sets the files.
|
|
579
|
+
dispatch({
|
|
580
|
+
acceptedFiles,
|
|
581
|
+
fileRejections,
|
|
582
|
+
type: "setFiles"
|
|
516
583
|
});
|
|
517
|
-
}
|
|
518
584
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
585
|
+
if (onDrop) {
|
|
586
|
+
onDrop(acceptedFiles, fileRejections, event);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (fileRejections.length > 0 && onDropRejected) {
|
|
590
|
+
onDropRejected(fileRejections, event);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (acceptedFiles.length > 0 && onDropAccepted) {
|
|
594
|
+
onDropAccepted(acceptedFiles, event);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// Run the built-in checks synchronously and invoke the validator (which may return a value or
|
|
599
|
+
// a Promise). customErrors is left as-is here so we can tell sync from async below.
|
|
600
|
+
const pending = files.map(file => {
|
|
601
|
+
const [accepted, acceptError] = fileAccepted(file, inputAcceptAttr);
|
|
602
|
+
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize);
|
|
603
|
+
const customErrors = validator ? validator(file) : null;
|
|
604
|
+
return {file, accepted, acceptError, sizeMatch, sizeError, customErrors};
|
|
523
605
|
});
|
|
524
606
|
|
|
525
|
-
|
|
526
|
-
|
|
607
|
+
// Callers check signal.aborted right before invoking setFiles (synchronously, no await in
|
|
608
|
+
// between), so this run is guaranteed live here - the supersession guards below only matter
|
|
609
|
+
// after we await the validator.
|
|
610
|
+
|
|
611
|
+
// Fast path: no validator, or a synchronous one. Commit synchronously - no extra microtask
|
|
612
|
+
// hop, so nested-dropzone ordering is preserved (an onDrop handler calling stopPropagation
|
|
613
|
+
// must run before a parent's onDrop check - see the noDragEventsBubbling tests). commit's
|
|
614
|
+
// dispatch also clears isProcessing.
|
|
615
|
+
if (!pending.some(({customErrors}) => isThenable(customErrors))) {
|
|
616
|
+
commit(pending as Array<PerFileResult>);
|
|
617
|
+
return;
|
|
527
618
|
}
|
|
528
619
|
|
|
529
|
-
|
|
530
|
-
|
|
620
|
+
// Async path: at least one validator returned a Promise. isProcessing is already on (set when
|
|
621
|
+
// the run began, before getFilesFromEvent); keep guarding against supersession while we await.
|
|
622
|
+
let results: Array<PerFileResult>;
|
|
623
|
+
try {
|
|
624
|
+
results = await Promise.all(
|
|
625
|
+
pending.map(async ({customErrors, ...rest}) => ({...rest, customErrors: await customErrors}))
|
|
626
|
+
);
|
|
627
|
+
} catch (e) {
|
|
628
|
+
// A validator threw/rejected. If a newer run already superseded this one, let it own the
|
|
629
|
+
// state; otherwise clear the processing flag and report the error via onError.
|
|
630
|
+
if (!signal.aborted) {
|
|
631
|
+
endProcessing(signal);
|
|
632
|
+
onErrCb(e as Error);
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
531
635
|
}
|
|
532
636
|
|
|
533
|
-
|
|
534
|
-
|
|
637
|
+
// A newer drop landed while we were validating - discard these stale results.
|
|
638
|
+
if (signal.aborted) {
|
|
639
|
+
return;
|
|
535
640
|
}
|
|
641
|
+
|
|
642
|
+
commit(results);
|
|
536
643
|
},
|
|
537
644
|
[
|
|
538
645
|
dispatch,
|
|
@@ -545,7 +652,9 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
545
652
|
onDropAccepted,
|
|
546
653
|
onDropRejected,
|
|
547
654
|
validator,
|
|
548
|
-
getErrorMessage
|
|
655
|
+
getErrorMessage,
|
|
656
|
+
onErrCb,
|
|
657
|
+
endProcessing
|
|
549
658
|
]
|
|
550
659
|
);
|
|
551
660
|
|
|
@@ -558,19 +667,35 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
558
667
|
|
|
559
668
|
dragTargetsRef.current = [];
|
|
560
669
|
|
|
670
|
+
// Clear drag state before we begin processing so beginProcessing's isProcessing isn't reset.
|
|
671
|
+
dispatch({type: "reset"});
|
|
672
|
+
|
|
561
673
|
if (isEvtWithFiles(event)) {
|
|
674
|
+
// Processing spans reading the files (getFilesFromEvent) and running the validator.
|
|
675
|
+
const signal = beginProcessing();
|
|
562
676
|
Promise.resolve(getFilesFromEvent(event))
|
|
563
677
|
.then(files => {
|
|
678
|
+
// A newer drop superseded this one while reading files - it owns isProcessing now.
|
|
679
|
+
if (signal.aborted) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
564
682
|
if (isPropagationStopped(event) && !noDragEventsBubbling) {
|
|
683
|
+
endProcessing(signal);
|
|
565
684
|
return;
|
|
566
685
|
}
|
|
567
|
-
setFiles(
|
|
686
|
+
// setFiles handles validator errors internally (routing them to onError); the outer
|
|
687
|
+
// catch here only fires for a getFilesFromEvent failure.
|
|
688
|
+
return setFiles(files as FileWithPath[], event, signal);
|
|
568
689
|
})
|
|
569
|
-
.catch(e =>
|
|
690
|
+
.catch(e => {
|
|
691
|
+
if (!signal.aborted) {
|
|
692
|
+
endProcessing(signal);
|
|
693
|
+
onErrCb(e);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
570
696
|
}
|
|
571
|
-
dispatch({type: "reset"});
|
|
572
697
|
},
|
|
573
|
-
[getFilesFromEvent, setFiles, onErrCb, noDragEventsBubbling]
|
|
698
|
+
[getFilesFromEvent, setFiles, onErrCb, noDragEventsBubbling, beginProcessing, endProcessing]
|
|
574
699
|
);
|
|
575
700
|
|
|
576
701
|
// Fn for opening the file dialog programmatically
|
|
@@ -585,14 +710,31 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
585
710
|
multiple,
|
|
586
711
|
types: pickerTypes
|
|
587
712
|
};
|
|
713
|
+
// Set once the user has picked file(s); processing then spans reading them (getFilesFromEvent)
|
|
714
|
+
// and running the validator. Picker rejections (cancel, security) happen before this, so it
|
|
715
|
+
// stays undefined there and no processing state is touched.
|
|
716
|
+
let signal: AbortSignal | undefined;
|
|
588
717
|
(window as any)
|
|
589
718
|
.showOpenFilePicker(opts)
|
|
590
|
-
.then((handles: any) =>
|
|
719
|
+
.then((handles: any) => {
|
|
720
|
+
signal = beginProcessing();
|
|
721
|
+
return getFilesFromEvent(handles);
|
|
722
|
+
})
|
|
591
723
|
.then((files: Array<File | DataTransferItem>) => {
|
|
592
|
-
|
|
724
|
+
// Close the dialog as soon as we have the selection; validation runs afterwards and is
|
|
725
|
+
// reflected by isProcessing rather than by keeping the dialog "active".
|
|
593
726
|
dispatch({type: "closeDialog"});
|
|
727
|
+
// A drop elsewhere superseded this selection while reading files - it owns isProcessing.
|
|
728
|
+
if (signal!.aborted) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
return setFiles(files as FileWithPath[], null, signal!);
|
|
594
732
|
})
|
|
595
733
|
.catch((e: any) => {
|
|
734
|
+
// Clear any processing this run started (e.g. a getFilesFromEvent failure).
|
|
735
|
+
if (signal) {
|
|
736
|
+
endProcessing(signal);
|
|
737
|
+
}
|
|
596
738
|
// AbortError means the user canceled
|
|
597
739
|
if (isAbort(e)) {
|
|
598
740
|
onFileDialogCancelCb(e);
|
|
@@ -626,7 +768,18 @@ export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
|
626
768
|
inputRef.current.value = "";
|
|
627
769
|
inputRef.current.click();
|
|
628
770
|
}
|
|
629
|
-
}, [
|
|
771
|
+
}, [
|
|
772
|
+
dispatch,
|
|
773
|
+
onFileDialogOpenCb,
|
|
774
|
+
onFileDialogCancelCb,
|
|
775
|
+
useFsAccessApi,
|
|
776
|
+
setFiles,
|
|
777
|
+
onErrCb,
|
|
778
|
+
pickerTypes,
|
|
779
|
+
multiple,
|
|
780
|
+
beginProcessing,
|
|
781
|
+
endProcessing
|
|
782
|
+
]);
|
|
630
783
|
|
|
631
784
|
// Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
|
|
632
785
|
const onKeyDownCb = useCallback(
|
|
@@ -814,11 +967,17 @@ function reducer(state: DropzoneInternalState, action: any): DropzoneInternalSta
|
|
|
814
967
|
isDragReject: action.isDragReject,
|
|
815
968
|
isDragUnknown: action.isDragUnknown
|
|
816
969
|
};
|
|
970
|
+
case "setProcessing":
|
|
971
|
+
return {
|
|
972
|
+
...state,
|
|
973
|
+
isProcessing: action.isProcessing
|
|
974
|
+
};
|
|
817
975
|
case "setFiles":
|
|
818
976
|
return {
|
|
819
977
|
...state,
|
|
820
978
|
acceptedFiles: action.acceptedFiles,
|
|
821
979
|
fileRejections: action.fileRejections,
|
|
980
|
+
isProcessing: false,
|
|
822
981
|
isDragReject: false,
|
|
823
982
|
isDragUnknown: false
|
|
824
983
|
};
|
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)) {
|