react-dropzone
Version:
Simple HTML5 drag-drop zone with React.js
1 lines • 67.5 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","names":["attrAccept","fromEvent"],"sources":["../src/utils/index.ts","../src/index.tsx"],"sourcesContent":["import attrAccept from \"attr-accept\";\n\n// attr-accept ships as a CommonJS module (`module.exports = { __esModule: true, default: fn }`).\n// Bundler interop surfaces its default export inconsistently — as the function under Node/Vitest,\n// but as `{ default: fn }` in some browser bundles. Normalize to the function.\nconst accepts =\n typeof attrAccept === \"function\" ? attrAccept : (attrAccept as unknown as {default: typeof attrAccept}).default;\n\n/**\n * A map of accepted MIME types to file extensions, as passed to the `accept` prop.\n */\nexport interface Accept {\n [key: string]: readonly string[];\n}\n\n/**\n * A file rejection error.\n */\nexport interface FileError {\n message: string;\n code: ErrorCode | string;\n}\n\n/**\n * What a custom `validator` returns: a single error, a list of errors, or `null` when the file\n * passes. A validator may return the result directly (synchronous) or wrapped in a `Promise`\n * (asynchronous, e.g. reading image dimensions or calling an external service).\n */\nexport type ValidatorResult = FileError | readonly FileError[] | null;\n\n// Error codes\nexport const FILE_INVALID_TYPE = \"file-invalid-type\";\nexport const FILE_TOO_LARGE = \"file-too-large\";\nexport const FILE_TOO_SMALL = \"file-too-small\";\nexport const TOO_MANY_FILES = \"too-many-files\";\n\nexport enum ErrorCode {\n FileInvalidType = \"file-invalid-type\",\n FileTooLarge = \"file-too-large\",\n FileTooSmall = \"file-too-small\",\n TooManyFiles = \"too-many-files\"\n}\n\nexport function getInvalidTypeRejectionErr(accept: string = \"\"): FileError {\n const acceptArr = accept.split(\",\");\n const msg = acceptArr.length > 1 ? `one of ${acceptArr.join(\", \")}` : acceptArr[0];\n\n return {\n code: FILE_INVALID_TYPE,\n message: `File type must be ${msg}`\n };\n}\n\nconst FILE_SIZE_UNITS = [\"KB\", \"MB\", \"GB\", \"TB\", \"PB\"];\n\n/**\n * Format a byte count into a human-readable string, e.g. `1111` -> `1.08 KB`.\n * Values below 1 KB are kept in bytes to preserve the singular/plural wording.\n */\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) {\n return `${bytes} ${bytes === 1 ? \"byte\" : \"bytes\"}`;\n }\n\n let size = bytes / 1024;\n let unitIndex = 0;\n while (size >= 1024 && unitIndex < FILE_SIZE_UNITS.length - 1) {\n size /= 1024;\n unitIndex++;\n }\n\n // Round to 2 decimals, then drop trailing zeros (1.00 -> 1, 1.50 -> 1.5).\n return `${Number(size.toFixed(2))} ${FILE_SIZE_UNITS[unitIndex]}`;\n}\n\nexport function getTooLargeRejectionErr(maxSize: number): FileError {\n return {\n code: FILE_TOO_LARGE,\n message: `File is larger than ${formatBytes(maxSize)}`\n };\n}\n\nexport function getTooSmallRejectionErr(minSize: number): FileError {\n return {\n code: FILE_TOO_SMALL,\n message: `File is smaller than ${formatBytes(minSize)}`\n };\n}\n\nexport const TOO_MANY_FILES_REJECTION: FileError = {\n code: TOO_MANY_FILES,\n message: \"Too many files\"\n};\n\n/**\n * Check if the given file is a DataTransferItem with an empty type.\n *\n * During drag events, browsers may return DataTransferItem objects instead of File objects.\n * Some browsers (e.g., Chrome) return an empty MIME type for certain file types (like .md files)\n * on DataTransferItem during drag events, even though the type is correctly set during drop.\n */\nexport function isDataTransferItemWithEmptyType(file: File | DataTransferItem): boolean {\n return file.type === \"\" && typeof (file as DataTransferItem).getAsFile === \"function\";\n}\n\n/**\n * Check if file is accepted.\n *\n * Firefox versions prior to 53 return a bogus MIME type for every file drag,\n * so dragovers with that MIME type will always be accepted.\n *\n * Chrome/other browsers may return an empty MIME type for files during drag events,\n * so we accept those as well (we'll validate properly on drop).\n */\nexport function fileAccepted(file: File, accept?: string): [boolean, FileError | null] {\n const isAcceptable =\n file.type === \"application/x-moz-file\" || accepts(file, accept ?? \"\") || isDataTransferItemWithEmptyType(file);\n return [isAcceptable, isAcceptable ? null : getInvalidTypeRejectionErr(accept)];\n}\n\nexport function fileMatchSize(\n file: {size?: number | null},\n minSize?: number,\n maxSize?: number\n): [boolean, FileError | null] {\n if (isDefined(file.size)) {\n if (isDefined(minSize) && isDefined(maxSize)) {\n if (file.size > maxSize) return [false, getTooLargeRejectionErr(maxSize)];\n if (file.size < minSize) return [false, getTooSmallRejectionErr(minSize)];\n } else if (isDefined(minSize) && file.size < minSize) {\n return [false, getTooSmallRejectionErr(minSize)];\n } else if (isDefined(maxSize) && file.size > maxSize) {\n return [false, getTooLargeRejectionErr(maxSize)];\n }\n }\n return [true, null];\n}\n\nfunction isDefined<T>(value: T): value is NonNullable<T> {\n return value !== undefined && value !== null;\n}\n\n/**\n * Check if a value is thenable (Promise-like), used to tell a synchronous validator result from an\n * asynchronous one.\n */\nexport function isThenable(value: unknown): value is PromiseLike<unknown> {\n return value != null && typeof (value as {then?: unknown}).then === \"function\";\n}\n\nexport function allFilesAccepted({\n files,\n accept,\n minSize,\n maxSize,\n multiple,\n maxFiles = 0,\n validator\n}: {\n files: File[];\n accept?: string;\n minSize?: number;\n maxSize?: number;\n multiple?: boolean;\n maxFiles?: number;\n validator?: (file: File) => FileError | readonly FileError[] | null;\n}): boolean {\n if ((!multiple && files.length > 1) || (multiple && maxFiles >= 1 && files.length > maxFiles)) {\n return false;\n }\n\n return files.every(file => {\n const [accepted] = fileAccepted(file, accept);\n const [sizeMatch] = fileMatchSize(file, minSize, maxSize);\n const customErrors = validator ? validator(file) : null;\n return accepted && sizeMatch && !customErrors;\n });\n}\n\n/**\n * The outcome of the drag-time acceptance check.\n *\n * - `accept` - every file passes the checks we can evaluate during a drag.\n * - `reject` - at least one file confidently fails a check we can fully evaluate during a\n * drag (the file count, or a non-empty MIME type that doesn't match `accept`).\n * - `unknown` - nothing confidently fails, but the outcome can't be confirmed until drop,\n * because a custom `validator` is configured and can't be evaluated yet.\n */\nexport type DragVerdict = \"accept\" | \"reject\" | \"unknown\";\n\n/**\n * Classify a set of dragged files for the `isDragAccept`/`isDragReject`/`isDragUnknown` states.\n *\n * During `dragenter`/`dragover` the browser only exposes `DataTransferItem`s, which carry a MIME\n * `type` but no file name, extension or size (see https://html.spec.whatwg.org/multipage/dnd.html#dndevents).\n * So the drag-time check is deliberately optimistic about anything it can't see:\n *\n * - The custom `validator` is **never** run here. It is typed `(file: File) => ...` and users\n * routinely read `file.name`/`file.size`, which are `undefined` on a `DataTransferItem` - running\n * it would throw and abort the whole drag handler (leaving `isDragActive` stuck at `false`).\n * See https://github.com/react-dropzone/react-dropzone/issues/1408\n * - When a `validator` is configured we therefore can't promise the files are acceptable, so the\n * verdict is `unknown` rather than a misleading `reject` (or a premature `accept`).\n * See https://github.com/react-dropzone/react-dropzone/issues/1244\n *\n * The full check (including the `validator`) still runs on drop in {@link fileAccepted}/`setFiles`.\n */\nexport function getDragVerdict({\n files,\n accept,\n minSize,\n maxSize,\n multiple,\n maxFiles = 0,\n validator\n}: {\n files: Array<File | DataTransferItem>;\n accept?: string;\n minSize?: number;\n maxSize?: number;\n multiple?: boolean;\n maxFiles?: number;\n // The validator is never invoked here (see the note above), so its async variant is accepted\n // purely so the same `validator` prop is assignable during a drag.\n validator?: (file: File) => ValidatorResult | Promise<ValidatorResult>;\n}): DragVerdict {\n // The file count is knowable during a drag, so an over-the-limit selection is a confident reject.\n if ((!multiple && files.length > 1) || (multiple && maxFiles >= 1 && files.length > maxFiles)) {\n return \"reject\";\n }\n\n const confidentlyRejected = files.some(file => {\n const [accepted] = fileAccepted(file as File, accept);\n const [sizeMatch] = fileMatchSize(file as File, minSize, maxSize);\n return !accepted || !sizeMatch;\n });\n if (confidentlyRejected) {\n return \"reject\";\n }\n\n // Built-in checks pass. A custom validator can only ever add rejections on drop, never rescue\n // one - so with a validator present the drag outcome is unknown until we have real Files.\n return validator ? \"unknown\" : \"accept\";\n}\n\n// React's synthetic events has event.isPropagationStopped,\n// but to remain compatibility with other libs (Preact) fall back\n// to check event.cancelBubble\nexport function isPropagationStopped(event: any): boolean {\n if (typeof event.isPropagationStopped === \"function\") {\n return event.isPropagationStopped();\n } else if (typeof event.cancelBubble !== \"undefined\") {\n return event.cancelBubble;\n }\n return false;\n}\n\nexport function isEvtWithFiles(event: any): boolean {\n if (!event.dataTransfer) {\n return !!event.target && !!event.target.files;\n }\n // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types\n // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file\n // Some Chromium drags omit \"Files\" from types (e.g. reporting only [\"text/plain\"])\n // while still exposing a kind: \"file\" entry in items - the same signal file-selector\n // uses to extract the files. Accept either so detection stays consistent. See #1409.\n return (\n Array.prototype.some.call(\n event.dataTransfer.types,\n (type: string) => type === \"Files\" || type === \"application/x-moz-file\"\n ) || Array.prototype.some.call(event.dataTransfer.items ?? [], isKindFile)\n );\n}\n\nexport function isKindFile(item: any): boolean {\n return typeof item === \"object\" && item !== null && item.kind === \"file\";\n}\n\n// allow the entire document to be a drag target\nexport function onDocumentDragOver(event: Event): void {\n event.preventDefault();\n}\n\nfunction isIe(userAgent: string): boolean {\n return userAgent.indexOf(\"MSIE\") !== -1 || userAgent.indexOf(\"Trident/\") !== -1;\n}\n\nfunction isEdge(userAgent: string): boolean {\n return userAgent.indexOf(\"Edge/\") !== -1;\n}\n\nexport function isIeOrEdge(userAgent: string = window.navigator.userAgent): boolean {\n return isIe(userAgent) || isEdge(userAgent);\n}\n\n/**\n * This is intended to be used to compose event handlers.\n * They are executed in order until one of them calls `event.isPropagationStopped()`.\n * Note that the check is done on the first invoke too,\n * meaning that if propagation was stopped before invoking the fns,\n * no handlers will be executed.\n */\nexport function composeEventHandlers(\n ...fns: Array<((event: any, ...args: any[]) => void) | null | undefined>\n): (event: any, ...args: any[]) => boolean {\n return (event: any, ...args: any[]) =>\n fns.some(fn => {\n if (!isPropagationStopped(event) && fn) {\n fn(event, ...args);\n }\n return isPropagationStopped(event);\n });\n}\n\n/**\n * canUseFileSystemAccessAPI checks if the File System Access API is supported by the browser.\n */\nexport function canUseFileSystemAccessAPI(): boolean {\n return \"showOpenFilePicker\" in window;\n}\n\n/**\n * Convert the `{accept}` dropzone prop to the `{types}` option for showOpenFilePicker.\n */\nexport function pickerOptionsFromAccept(accept?: Accept): Array<{description: string; accept: Accept}> | undefined {\n if (isDefined(accept)) {\n const acceptForPicker = Object.entries(accept)\n .filter(([mimeType, ext]) => {\n let ok = true;\n\n if (!isMIMEType(mimeType)) {\n console.warn(\n `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.`\n );\n ok = false;\n }\n\n if (!Array.isArray(ext) || !ext.every(isExt)) {\n console.warn(`Skipped \"${mimeType}\" because an invalid file extension was provided.`);\n ok = false;\n }\n\n return ok;\n })\n .reduce<Accept>((agg, [mimeType, ext]) => {\n agg[mimeType] = ext;\n return agg;\n }, {});\n return [\n {\n // description is required due to https://crbug.com/1264708\n description: \"Files\",\n accept: acceptForPicker\n }\n ];\n }\n return undefined;\n}\n\n/**\n * Convert the `{accept}` dropzone prop to a comma-separated accept attribute string.\n *\n * When `omitWildcardMimeTypesWithExtensions` is set, a wildcard MIME type (e.g. `image/*`)\n * that is paired with explicit extensions is dropped in favour of those extensions. The\n * accept attribute is an OR list, so leaving `image/*` in would make both the native file\n * picker and the drop-time validator accept ANY file of that type, ignoring the extension\n * restriction. The drag-time `isDragAccept` check keeps the wildcard because file names\n * (and therefore extensions) aren't readable during a drag.\n *\n * See https://github.com/react-dropzone/react-dropzone/issues/1220\n */\nexport function acceptPropAsAcceptAttr(\n accept?: Accept,\n {omitWildcardMimeTypesWithExtensions = false}: {omitWildcardMimeTypesWithExtensions?: boolean} = {}\n): string | undefined {\n if (isDefined(accept)) {\n return (\n Object.entries(accept)\n .reduce<string[]>((a, [mimeType, ext]) => {\n if (omitWildcardMimeTypesWithExtensions && isMIMETypeWildcard(mimeType) && ext.some(isExt)) {\n a.push(...ext);\n } else {\n a.push(mimeType, ...ext);\n }\n return a;\n }, [])\n // Silently discard invalid entries as pickerOptionsFromAccept warns about these\n .filter(v => isMIMEType(v) || isExt(v))\n .join(\",\")\n );\n }\n\n return undefined;\n}\n\n/**\n * Check if v is an exception caused by aborting a request (e.g window.showOpenFilePicker()).\n */\nexport function isAbort(v: any): boolean {\n return v instanceof DOMException && (v.name === \"AbortError\" || v.code === v.ABORT_ERR);\n}\n\n/**\n * Check if v is a security error.\n */\nexport function isSecurityError(v: any): boolean {\n return v instanceof DOMException && (v.name === \"SecurityError\" || v.code === v.SECURITY_ERR);\n}\n\n/**\n * Check if v is a \"not allowed\" error.\n *\n * Some browsers/configurations block `window.showOpenFilePicker()` outright and reject with a\n * `NotAllowedError` instead of showing the picker (e.g. Microsoft Edge for Business, or other\n * restrictive enterprise/security policies). We treat this like a security error and fall back\n * to the native `<input>`. See https://github.com/react-dropzone/react-dropzone/issues/1429\n */\nexport function isNotAllowedError(v: any): boolean {\n return v instanceof DOMException && v.name === \"NotAllowedError\";\n}\n\n/**\n * Check if v is a MIME type string.\n */\nexport function isMIMEType(v: string): boolean {\n return (\n v === \"audio/*\" ||\n v === \"video/*\" ||\n v === \"image/*\" ||\n v === \"text/*\" ||\n v === \"application/*\" ||\n /\\w+\\/[-+.\\w]+/g.test(v)\n );\n}\n\n/**\n * Check if v is a wildcard MIME type (e.g. `image/*`).\n */\nexport function isMIMETypeWildcard(v: string): boolean {\n return v.endsWith(\"/*\");\n}\n\n/**\n * Check if v is a file extension.\n */\nexport function isExt(v: string): boolean {\n return /^.*\\.[\\w]+$/.test(v);\n}\n","import {fromEvent} from \"file-selector\";\nimport type {FileWithPath} from \"file-selector\";\nimport type * as React from \"react\";\nimport {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useReducer, useRef} from \"react\";\nimport {\n acceptPropAsAcceptAttr,\n canUseFileSystemAccessAPI,\n composeEventHandlers,\n ErrorCode,\n fileAccepted,\n fileMatchSize,\n getDragVerdict,\n isAbort,\n isEvtWithFiles,\n isIeOrEdge,\n isNotAllowedError,\n isThenable,\n isPropagationStopped,\n isSecurityError,\n onDocumentDragOver,\n pickerOptionsFromAccept,\n TOO_MANY_FILES_REJECTION\n} from \"./utils\";\nimport type {Accept, FileError, ValidatorResult} from \"./utils\";\n\nexport type {Accept, FileError, FileWithPath, ValidatorResult};\nexport {ErrorCode};\n\nexport interface DropzoneProps extends DropzoneOptions {\n children?: (state: DropzoneState) => React.ReactElement;\n}\n\nexport interface FileRejection {\n file: FileWithPath;\n errors: readonly FileError[];\n}\n\ntype SharedProps = \"multiple\" | \"onDragEnter\" | \"onDragOver\" | \"onDragLeave\";\n\nexport type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {\n accept?: Accept;\n minSize?: number;\n maxSize?: number;\n maxFiles?: number;\n preventDropOnDocument?: boolean;\n noClick?: boolean;\n noKeyboard?: boolean;\n noDrag?: boolean;\n noDragEventsBubbling?: boolean;\n disabled?: boolean;\n onDrop?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;\n onDropAccepted?: <T extends File>(files: T[], event: DropEvent) => void;\n onDropRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;\n getFilesFromEvent?: (event: DropEvent | Array<FileSystemFileHandle>) => Promise<Array<File | DataTransferItem>>;\n onFileDialogCancel?: () => void;\n onFileDialogOpen?: () => void;\n onError?: (err: Error) => void;\n /**\n * Custom validation, run once per file on drop/selection. Return `null` to accept the file, or a\n * {@link FileError} (or array of them) to reject it. May be `async` (return a `Promise`) to support\n * checks that can't run synchronously - e.g. reading image dimensions, inspecting file contents,\n * or calling an external service. While an async validator is pending, {@link DropzoneState.isProcessing}\n * is `true`, and `onDrop`/`onDropAccepted`/`onDropRejected` fire only once it settles. If the\n * validator throws or rejects, `onError` is called and the drop is discarded.\n *\n * Note: the validator never runs during a drag (a `DataTransferItem` has no name/size), so a\n * validator-configured dropzone is `isDragUnknown` until drop.\n */\n validator?: <T extends File>(file: T) => ValidatorResult | Promise<ValidatorResult>;\n /**\n * Override the message of any rejection error (built-in or custom). Called once per error;\n * receives the error and the file it belongs to and returns the message to use. Return\n * `error.message` for codes you don't want to change. Useful for localizing error messages.\n */\n getErrorMessage?: (error: FileError, file: File) => string;\n useFsAccessApi?: boolean;\n autoFocus?: boolean;\n};\n\nexport type DropEvent = React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement> | DragEvent | Event;\n\nexport interface DropzoneRef {\n open: () => void;\n}\n\nexport type DropzoneState = DropzoneRef & {\n isFocused: boolean;\n isDragActive: boolean;\n isDragAccept: boolean;\n isDragReject: boolean;\n isDragUnknown: boolean;\n isDragGlobal: boolean;\n isFileDialogActive: boolean;\n /**\n * `true` while a drop/selection is being processed asynchronously - i.e. while `getFilesFromEvent`\n * reads the files and/or an async {@link DropzoneOptions.validator} runs. Spans the whole pipeline,\n * from when files start being read until validation settles. When both are synchronous (the default\n * `getFilesFromEvent` with no/async-free validator) the work resolves within a microtask, so it's\n * only observable for genuinely async work. Use it to show a spinner or disable UI while processing.\n */\n isProcessing: boolean;\n acceptedFiles: readonly FileWithPath[];\n fileRejections: readonly FileRejection[];\n rootRef: React.RefObject<HTMLElement>;\n inputRef: React.RefObject<HTMLInputElement>;\n getRootProps: <T extends DropzoneRootProps>(props?: T) => T;\n getInputProps: <T extends DropzoneInputProps>(props?: T) => T;\n};\n\nexport interface DropzoneRootProps extends React.HTMLAttributes<HTMLElement> {\n refKey?: string;\n [key: string]: any;\n}\n\nexport interface DropzoneInputProps extends React.InputHTMLAttributes<HTMLInputElement> {\n refKey?: string;\n}\n\n/**\n * Convenience wrapper component for the `useDropzone` hook\n *\n * ```jsx\n * <Dropzone>\n * {({getRootProps, getInputProps}) => (\n * <div {...getRootProps()}>\n * <input {...getInputProps()} />\n * <p>Drag 'n' drop some files here, or click to select files</p>\n * </div>\n * )}\n * </Dropzone>\n * ```\n */\nconst Dropzone: React.ForwardRefExoticComponent<DropzoneProps & React.RefAttributes<DropzoneRef>> = forwardRef<\n DropzoneRef,\n DropzoneProps\n>(({children, ...params}, ref) => {\n const {open, ...props} = useDropzone(params);\n\n useImperativeHandle(ref, () => ({open}), [open]);\n\n return <>{children?.({...props, open})}</>;\n});\n\nDropzone.displayName = \"Dropzone\";\n\nexport default Dropzone;\n\ninterface DropzoneInternalState {\n isFocused: boolean;\n isFileDialogActive: boolean;\n isDragActive: boolean;\n isDragAccept: boolean;\n isDragReject: boolean;\n isDragUnknown: boolean;\n isDragGlobal: boolean;\n isProcessing: boolean;\n acceptedFiles: FileWithPath[];\n fileRejections: FileRejection[];\n}\n\n/**\n * The per-file outcome of the built-in checks plus the (resolved) custom validator, assembled in\n * setFiles before the accepted/rejected split.\n */\ninterface PerFileResult {\n file: FileWithPath;\n accepted: boolean;\n acceptError: FileError | null;\n sizeMatch: boolean;\n sizeError: FileError | null;\n customErrors: ValidatorResult;\n}\n\nconst initialState: DropzoneInternalState = {\n isFocused: false,\n isFileDialogActive: false,\n isDragActive: false,\n isDragAccept: false,\n isDragReject: false,\n isDragUnknown: false,\n isDragGlobal: false,\n isProcessing: false,\n acceptedFiles: [],\n fileRejections: []\n};\n\n/**\n * A React hook that creates a drag 'n' drop area.\n *\n * ```jsx\n * function MyDropzone(props) {\n * const {getRootProps, getInputProps} = useDropzone({\n * onDrop: acceptedFiles => {\n * // do something with the File objects, e.g. upload to some server\n * }\n * });\n * return (\n * <div {...getRootProps()}>\n * <input {...getInputProps()} />\n * <p>Drag and drop some files here, or click to select files</p>\n * </div>\n * )\n * }\n * ```\n */\nexport function useDropzone(props: DropzoneOptions = {}): DropzoneState {\n const {\n accept,\n disabled = false,\n getFilesFromEvent = fromEvent,\n maxSize = Number.POSITIVE_INFINITY,\n minSize = 0,\n multiple = true,\n maxFiles = 0,\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n onDropAccepted,\n onDropRejected,\n onFileDialogCancel,\n onFileDialogOpen,\n useFsAccessApi = false,\n autoFocus = false,\n preventDropOnDocument = true,\n noClick = false,\n noKeyboard = false,\n noDrag = false,\n noDragEventsBubbling = false,\n onError,\n validator,\n getErrorMessage\n } = props;\n\n // `acceptAttr` keeps wildcard MIME types (e.g. `image/*`) so the drag-time\n // `isDragAccept`/`isDragReject` check can react to a file's MIME type - file names\n // (hence extensions) aren't readable during a drag.\n const acceptAttr = useMemo(() => acceptPropAsAcceptAttr(accept), [accept]);\n // `inputAcceptAttr` drops a wildcard MIME type when it is paired with extensions, so the\n // native picker and drop-time validation enforce the extensions instead of accepting any\n // file of that type. See https://github.com/react-dropzone/react-dropzone/issues/1220\n const inputAcceptAttr = useMemo(\n () =>\n acceptPropAsAcceptAttr(accept, {\n omitWildcardMimeTypesWithExtensions: true\n }),\n [accept]\n );\n const pickerTypes = useMemo(() => pickerOptionsFromAccept(accept), [accept]);\n\n const onFileDialogOpenCb = useMemo<(...args: any[]) => void>(\n () => (typeof onFileDialogOpen === \"function\" ? onFileDialogOpen : noop),\n [onFileDialogOpen]\n );\n const onFileDialogCancelCb = useMemo<(...args: any[]) => void>(\n () => (typeof onFileDialogCancel === \"function\" ? onFileDialogCancel : noop),\n [onFileDialogCancel]\n );\n\n const rootRef = useRef<HTMLElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const {isFocused, isFileDialogActive} = state;\n\n // Mirror {isFileDialogActive} into a ref so the memoized drag handlers can read the current value\n // without being recreated (and churning getRootProps/getInputProps) every time the dialog toggles.\n const isFileDialogActiveRef = useRef(isFileDialogActive);\n isFileDialogActiveRef.current = isFileDialogActive;\n\n // Tracks the in-flight processing run - reading files (getFilesFromEvent) plus running an async\n // validator. A newer drop/selection aborts the previous run so slow async work can't resolve late\n // and clobber the state with stale results.\n const processingAbortRef = useRef<AbortController | null>(null);\n\n // Begin a processing run: supersede any run still in flight and flip {isProcessing} on. Returns\n // the run's AbortSignal, which downstream async steps check to bail if a newer run took over.\n const beginProcessing = useCallback(() => {\n processingAbortRef.current?.abort();\n const controller = new AbortController();\n processingAbortRef.current = controller;\n dispatch({type: \"setProcessing\", isProcessing: true});\n return controller.signal;\n }, []);\n\n // End a processing run by clearing {isProcessing} - but only if this run is still the active one.\n // A superseded run (signal aborted) leaves the flag to the run that replaced it.\n const endProcessing = useCallback((signal: AbortSignal) => {\n if (!signal.aborted) {\n dispatch({type: \"setProcessing\", isProcessing: false});\n }\n }, []);\n\n const fsAccessApiWorksRef = useRef(\n typeof window !== \"undefined\" && window.isSecureContext && useFsAccessApi && canUseFileSystemAccessAPI()\n );\n\n // Update file dialog active state when the window is focused on\n const onWindowFocus = () => {\n // Execute the timeout only if the file dialog is opened in the browser\n if (!fsAccessApiWorksRef.current && isFileDialogActive) {\n setTimeout(() => {\n if (inputRef.current) {\n const {files} = inputRef.current;\n\n if (!files?.length) {\n dispatch({type: \"closeDialog\"});\n onFileDialogCancelCb();\n }\n }\n }, 300);\n }\n };\n useEffect(() => {\n window.addEventListener(\"focus\", onWindowFocus, false);\n return () => {\n window.removeEventListener(\"focus\", onWindowFocus, false);\n };\n }, [inputRef, isFileDialogActive, onFileDialogCancelCb, fsAccessApiWorksRef]);\n\n const dragTargetsRef = useRef<EventTarget[]>([]);\n const globalDragTargetsRef = useRef<EventTarget[]>([]);\n const onDocumentDrop = (event: DragEvent) => {\n // This is a document-level, bubble-phase listener, so it runs *after* the event has already\n // bubbled through the dropzone root. If the drop landed inside the root and the instance's own\n // onDrop handler already prevented the default, there's nothing left to do.\n //\n // We must NOT bail out on `contains()` alone: when the dropzone is `disabled` or has `noDrag`,\n // the root has no onDrop handler, so nothing prevents the browser's default action and the file\n // is opened in the tab. Falling through to `preventDefault()` here keeps that from happening.\n // See https://github.com/react-dropzone/react-dropzone/issues/1362\n if (rootRef.current && event.target && rootRef.current.contains(event.target as Node) && event.defaultPrevented) {\n return;\n }\n event.preventDefault();\n dragTargetsRef.current = [];\n };\n\n useEffect(() => {\n if (preventDropOnDocument) {\n document.addEventListener(\"dragover\", onDocumentDragOver, false);\n document.addEventListener(\"drop\", onDocumentDrop, false);\n }\n\n return () => {\n if (preventDropOnDocument) {\n document.removeEventListener(\"dragover\", onDocumentDragOver);\n document.removeEventListener(\"drop\", onDocumentDrop);\n }\n };\n }, [rootRef, preventDropOnDocument]);\n\n // Track global drag state for document-level drag events\n useEffect(() => {\n const onDocumentDragEnter = (event: DragEvent) => {\n if (event.target) {\n globalDragTargetsRef.current = [...globalDragTargetsRef.current, event.target];\n }\n\n if (isEvtWithFiles(event)) {\n dispatch({isDragGlobal: true, type: \"setDragGlobal\"});\n }\n };\n\n const onDocumentDragLeave = (event: DragEvent) => {\n // Only deactivate once we've left all children\n globalDragTargetsRef.current = globalDragTargetsRef.current.filter(el => el !== event.target && el !== null);\n\n if (globalDragTargetsRef.current.length > 0) {\n return;\n }\n\n dispatch({isDragGlobal: false, type: \"setDragGlobal\"});\n };\n\n const onDocumentDragEnd = () => {\n globalDragTargetsRef.current = [];\n dispatch({isDragGlobal: false, type: \"setDragGlobal\"});\n };\n\n const onDocumentDropGlobal = () => {\n globalDragTargetsRef.current = [];\n dispatch({isDragGlobal: false, type: \"setDragGlobal\"});\n };\n\n document.addEventListener(\"dragenter\", onDocumentDragEnter, false);\n document.addEventListener(\"dragleave\", onDocumentDragLeave, false);\n document.addEventListener(\"dragend\", onDocumentDragEnd, false);\n document.addEventListener(\"drop\", onDocumentDropGlobal, false);\n\n return () => {\n document.removeEventListener(\"dragenter\", onDocumentDragEnter);\n document.removeEventListener(\"dragleave\", onDocumentDragLeave);\n document.removeEventListener(\"dragend\", onDocumentDragEnd);\n document.removeEventListener(\"drop\", onDocumentDropGlobal);\n };\n }, [rootRef]);\n\n // Auto focus the root when autoFocus is true\n useEffect(() => {\n if (!disabled && autoFocus && rootRef.current) {\n rootRef.current.focus();\n }\n return () => {};\n }, [rootRef, autoFocus, disabled]);\n\n const onErrCb = useCallback(\n (e: Error) => {\n if (onError) {\n onError(e);\n } else {\n // Let the user know something's gone wrong if they haven't provided the onError cb.\n console.error(e);\n }\n },\n [onError]\n );\n\n const onDragEnterCb = useCallback(\n (event: any) => {\n event.preventDefault();\n // Persist here because we need the event later after getFilesFromEvent() is done\n event.persist?.();\n stopPropagation(event);\n\n // Ignore drags onto the dropzone while the file picker dialog is open: the page underneath a\n // live picker shouldn't react to (or accept) dropped files. See #1455. preventDefault() above\n // still runs so the browser doesn't try to open/navigate to a dropped file.\n if (isFileDialogActiveRef.current) {\n return;\n }\n\n dragTargetsRef.current = [...dragTargetsRef.current, event.target];\n\n if (isEvtWithFiles(event)) {\n Promise.resolve(getFilesFromEvent(event))\n .then(files => {\n if (isPropagationStopped(event) && !noDragEventsBubbling) {\n return;\n }\n\n const fileCount = files.length;\n // During a drag we only have DataTransferItems (MIME type, no name/size), so the\n // custom validator can't run yet - a validator-configured dropzone is \"unknown\" until\n // drop rather than a misleading accept/reject. See getDragVerdict for the details.\n const verdict =\n fileCount > 0\n ? getDragVerdict({\n files: files as Array<File | DataTransferItem>,\n accept: acceptAttr,\n minSize,\n maxSize,\n multiple,\n maxFiles,\n validator\n })\n : null;\n\n dispatch({\n isDragAccept: verdict === \"accept\",\n isDragReject: verdict === \"reject\",\n isDragUnknown: verdict === \"unknown\",\n isDragActive: true,\n type: \"setDraggedFiles\"\n });\n\n if (onDragEnter) {\n onDragEnter(event);\n }\n })\n .catch(e => onErrCb(e));\n }\n },\n [\n getFilesFromEvent,\n onDragEnter,\n onErrCb,\n noDragEventsBubbling,\n acceptAttr,\n minSize,\n maxSize,\n multiple,\n maxFiles,\n validator\n ]\n );\n\n const onDragOverCb = useCallback(\n (event: any) => {\n event.preventDefault();\n event.persist?.();\n stopPropagation(event);\n\n // Ignore drags over the dropzone while the file picker dialog is open. See #1455.\n if (isFileDialogActiveRef.current) {\n return false;\n }\n\n const hasFiles = isEvtWithFiles(event);\n if (hasFiles && event.dataTransfer) {\n try {\n event.dataTransfer.dropEffect = \"copy\";\n } catch {\n /* no-op */\n }\n }\n\n if (hasFiles && onDragOver) {\n onDragOver(event);\n }\n\n return false;\n },\n [onDragOver, noDragEventsBubbling]\n );\n\n const onDragLeaveCb = useCallback(\n (event: any) => {\n event.preventDefault();\n event.persist?.();\n stopPropagation(event);\n\n // Only deactivate once the dropzone and all children have been left\n const targets = dragTargetsRef.current.filter(target => rootRef.current?.contains(target as Node));\n // Make sure to remove a target present multiple times only once\n // (Firefox may fire dragenter/dragleave multiple times on the same element)\n const targetIdx = targets.indexOf(event.target);\n if (targetIdx !== -1) {\n targets.splice(targetIdx, 1);\n }\n dragTargetsRef.current = targets;\n if (targets.length > 0) {\n return;\n }\n\n dispatch({\n type: \"setDraggedFiles\",\n isDragActive: false,\n isDragAccept: false,\n isDragReject: false,\n isDragUnknown: false\n });\n\n if (isEvtWithFiles(event) && onDragLeave) {\n onDragLeave(event);\n }\n },\n [rootRef, onDragLeave, noDragEventsBubbling]\n );\n\n const setFiles = useCallback(\n async (files: FileWithPath[], event: any, signal: AbortSignal) => {\n const localizeError = (error: FileError, file: File): FileError =>\n getErrorMessage ? {...error, message: getErrorMessage(error, file)} : error;\n\n // Commit the per-file verdicts: split accepted/rejected, cap the surplus, update state and\n // fire the onDrop callbacks. Runs synchronously so nested-dropzone ordering is preserved on\n // the fast path (an onDrop handler calling stopPropagation must do so before a parent's onDrop\n // check - see the noDragEventsBubbling tests).\n const commit = (results: Array<PerFileResult>) => {\n const acceptedFiles: FileWithPath[] = [];\n const fileRejections: FileRejection[] = [];\n\n results.forEach(({file, accepted, acceptError, sizeMatch, sizeError, customErrors}) => {\n if (accepted && sizeMatch && !customErrors) {\n acceptedFiles.push(file);\n } else {\n let errors: Array<FileError | null> = [acceptError, sizeError];\n\n if (customErrors) {\n errors = errors.concat(customErrors);\n }\n\n fileRejections.push({\n file,\n errors: errors.filter((e): e is FileError => e != null).map(error => localizeError(error, file))\n });\n }\n });\n\n // Cap the accepted files at the configured limit and reject only the surplus (the files past\n // the limit) with a too-many-files error, instead of rejecting the whole batch. The limit is 1\n // when {multiple} is false, and {maxFiles} when {multiple} is true (0 means no limit). Files\n // that already failed the per-file checks above are in {fileRejections} and don't count here.\n // See https://github.com/react-dropzone/react-dropzone/issues/1355\n // and https://github.com/react-dropzone/react-dropzone/issues/1358\n const acceptedFilesLimit = multiple ? (maxFiles >= 1 ? maxFiles : Number.POSITIVE_INFINITY) : 1;\n if (acceptedFiles.length > acceptedFilesLimit) {\n const surplusFiles = acceptedFiles.splice(acceptedFilesLimit);\n surplusFiles.forEach(file => {\n fileRejections.push({file, errors: [localizeError(TOO_MANY_FILES_REJECTION, file)]});\n });\n }\n\n // Clears isProcessing back to false (see the reducer) in the same update that sets the files.\n dispatch({\n acceptedFiles,\n fileRejections,\n type: \"setFiles\"\n });\n\n if (onDrop) {\n onDrop(acceptedFiles, fileRejections, event);\n }\n\n if (fileRejections.length > 0 && onDropRejected) {\n onDropRejected(fileRejections, event);\n }\n\n if (acceptedFiles.length > 0 && onDropAccepted) {\n onDropAccepted(acceptedFiles, event);\n }\n };\n\n // Run the built-in checks synchronously and invoke the validator (which may return a value or\n // a Promise). customErrors is left as-is here so we can tell sync from async below.\n const pending = files.map(file => {\n const [accepted, acceptError] = fileAccepted(file, inputAcceptAttr);\n const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize);\n const customErrors = validator ? validator(file) : null;\n return {file, accepted, acceptError, sizeMatch, sizeError, customErrors};\n });\n\n // Callers check signal.aborted right before invoking setFiles (synchronously, no await in\n // between), so this run is guaranteed live here - the supersession guards below only matter\n // after we await the validator.\n\n // Fast path: no validator, or a synchronous one. Commit synchronously - no extra microtask\n // hop, so nested-dropzone ordering is preserved (an onDrop handler calling stopPropagation\n // must run before a parent's onDrop check - see the noDragEventsBubbling tests). commit's\n // dispatch also clears isProcessing.\n if (!pending.some(({customErrors}) => isThenable(customErrors))) {\n commit(pending as Array<PerFileResult>);\n return;\n }\n\n // Async path: at least one validator returned a Promise. isProcessing is already on (set when\n // the run began, before getFilesFromEvent); keep guarding against supersession while we await.\n let results: Array<PerFileResult>;\n try {\n results = await Promise.all(\n pending.map(async ({customErrors, ...rest}) => ({...rest, customErrors: await customErrors}))\n );\n } catch (e) {\n // A validator threw/rejected. If a newer run already superseded this one, let it own the\n // state; otherwise clear the processing flag and report the error via onError.\n if (!signal.aborted) {\n endProcessing(signal);\n onErrCb(e as Error);\n }\n return;\n }\n\n // A newer drop landed while we were validating - discard these stale results.\n if (signal.aborted) {\n return;\n }\n\n commit(results);\n },\n [\n dispatch,\n multiple,\n inputAcceptAttr,\n minSize,\n maxSize,\n maxFiles,\n onDrop,\n onDropAccepted,\n onDropRejected,\n validator,\n getErrorMessage,\n onErrCb,\n endProcessing\n ]\n );\n\n const onDropCb = useCallback(\n (event: any) => {\n event.preventDefault();\n // Persist here because we need the event later after getFilesFromEvent() is done\n event.persist?.();\n stopPropagation(event);\n\n dragTargetsRef.current = [];\n\n // Ignore a drop landing on the dropzone while the file picker dialog is open (see #1455).\n // Guard on {event.dataTransfer} so only real drag-drops are suppressed: the input's change\n // event (a file picked from the dialog) also runs through here while the dialog is still\n // flagged active, and that path must keep working. Returning before the reset below leaves\n // the dialog flag intact.\n if (isFileDialogActiveRef.current && event.dataTransfer) {\n return;\n }\n\n // Clear drag state before we begin processing so beginProcessing's isProcessing isn't reset.\n dispatch({type: \"reset\"});\n\n if (isEvtWithFiles(event)) {\n // Processing spans reading the files (getFilesFromEvent) and running the validator.\n const signal = beginProcessing();\n Promise.resolve(getFilesFromEvent(event))\n .then(files => {\n // A newer drop superseded this one while reading files - it owns isProcessing now.\n if (signal.aborted) {\n return;\n }\n if (isPropagationStopped(event) && !noDragEventsBubbling) {\n endProcessing(signal);\n return;\n }\n // setFiles handles validator errors internally (routing them to onError); the outer\n // catch here only fires for a getFilesFromEvent failure.\n return setFiles(files as FileWithPath[], event, signal);\n })\n .catch(e => {\n if (!signal.aborted) {\n endProcessing(signal);\n onErrCb(e);\n }\n });\n }\n },\n [getFilesFromEvent, setFiles, onErrCb, noDragEventsBubbling, beginProcessing, endProcessing]\n );\n\n // Fn for opening the file dialog programmatically\n const openFileDialog = useCallback(() => {\n // No point to use FS access APIs if context is not secure\n // https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#feature_detection\n if (fsAccessApiWorksRef.current) {\n dispatch({type: \"openDialog\"});\n onFileDialogOpenCb();\n // https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker\n const opts = {\n multiple,\n types: pickerTypes\n };\n // Set once the user has picked file(s); processing then spans reading them (getFilesFromEvent)\n // and running the validator. Picker rejections (cancel, security) happen before this, so it\n // stays undefined there and no processing state is touched.\n let signal: AbortSignal | undefined;\n (window as any)\n .showOpenFilePicker(opts)\n .then((handles: any) => {\n signal = beginProcessing();\n return getFilesFromEvent(handles);\n })\n .then((files: Array<File | DataTransferItem>) => {\n // Close the dialog as soon as we have the selection; validation runs afterwards and is\n // reflected by isProcessing rather than by keeping the dialog \"active\".\n dispatch({type: \"closeDialog\"});\n // A drop elsewhere superseded this selection while reading files - it owns isProcessing.\n if (signal!.aborted) {\n return;\n }\n return setFiles(files as FileWithPath[], null, signal!);\n })\n .catch((e: any) => {\n // Clear any processing this run started (e.g. a getFilesFromEvent failure).\n if (signal) {\n endProcessing(signal);\n }\n // AbortError means the user canceled\n if (isAbort(e)) {\n onFileDialogCancelCb(e);\n dispatch({type: \"closeDialog\"});\n } else if (isSecurityError(e) || isNotAllowedError(e)) {\n // The FS access API is unusable here: either the context forbids it (SecurityError,\n // e.g. CORS) or the browser/platform blocked the picker (NotAllowedError, e.g. Edge\n // for Business - see https://github.com/react-dropzone/react-dropzone/issues/1429).\n // Stop using it and fall back to the native <input>.\n fsAccessApiWorksRef.current = false;\n if (inputRef.current) {\n inputRef.current.value = \"\";\n inputRef.current.click();\n } else {\n onErrCb(\n new Error(\n \"Cannot open the file picker because the https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API is not supported and no <input> was provided.\"\n )\n );\n }\n } else {\n onErrCb(e);\n }\n });\n return;\n }\n\n if (inputRef.current) {\n dispatch({type: \"openDialog\"});\n onFileDialogOpenCb();\n inputRef.current.value = \"\";\n inputRef.current.click();\n }\n }, [\n dispatch,\n onFileDialogOpenCb,\n onFileDialogCancelCb,\n useFsAccessApi,\n setFiles,\n onErrCb,\n pickerTypes,\n multiple,\n beginProcessing,\n endProcessing\n ]);\n\n // Cb to open the file dialog when SPACE/ENTER occurs on the dropzone\n const onKeyDownCb = useCallback(\n (event: any) => {\n // Ignore keyboard events bubbling up the DOM tree\n if (!rootRef.current?.isEqualNode(event.target)) {\n return;\n }\n\n if (event.key === \" \" || event.key === \"Enter\" || event.keyCode === 32 || event.keyCode === 13) {\n event.preventDefault();\n openFileDialog();\n }\n },\n [rootRef, openFileDialog]\n );\n\n // Update focus state for the dropzone\n const onFocusCb = useCallback(() => {\n dispatch({type: \"focus\"});\n }, []);\n const onBlurCb = useCallback(() => {\n dispatch({type: \"blur\"});\n }, []);\n\n // Cb to open the file dialog when click occurs on the dropzone\n const onClickCb = useCallback(() => {\n if (noClick) {\n return;\n }\n\n // In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout()\n // to ensure React can handle state changes\n // See: https://github.com/react-dropzone/react-dropzone/issues/450\n if (isIeOrEdge()) {\n setTimeout(openFileDialog, 0);\n } else {\n openFileDialog();\n }\n }, [noClick, openFileDialog]);\n\n const composeHandler = (fn: any) => {\n return disabled ? null : fn;\n };\n\n const composeKeyboardHandler = (fn: any) => {\n return noKeyboard ? null : composeHandler(fn);\n };\n\n const composeDragHandler = (fn: any) => {\n return noDrag ? null : composeHandler(fn);\n };\n\n const stopPropagation = (event: any) => {\n if (noDragEventsBubbling) {\n event.stopPropagation();\n }\n };\n\n const getRootProps = useMemo(\n () =>\n ({\n refKey = \"ref\",\n role,\n onKeyDown,\n onFocus,\n onBlur,\n onClick,\n onDragEnter,\n onDragOver,\n onDragLeave,\n onDrop,\n ...rest\n }: DropzoneRootProps = {}) => ({\n onKeyDown: composeKeyboardHandler(composeEventHandlers(onKeyDown, onKeyDownCb)),\n onFocus: composeKeyboardHandler(composeEventHandlers(onFocus, onFocusCb)),\n onBlur: composeKeyboardHandler(composeEventHandlers(onBlur, onBlurCb)),\n onClick: composeHandler(composeEventHandlers(onClick, onClickCb)),\n onDragEnter: composeDragHandler(composeEventHandlers(onDragEnter, onDragEnterCb)),\n onDragOver: composeDragHandler(composeEventHandlers(onDragOver, onDragOverCb)),\n onDragLeave: composeDragHandler(composeEventHandlers(onDragLeave, onDragLeaveCb)),\n onDrop: composeDragHandler(composeEventHandlers(onDrop, onDropCb)),\n role: typeof role === \"string\" && role !== \"\" ? role : \"presentation\",\n [refKey]: rootRef,\n ...(!disabled && !noKeyboard ? {tabIndex: 0} : {}),\n ...(disabled ? {\"aria-disabled\": true} : {}),\n ...rest\n }),\n [\n rootRef,\n onKeyDownCb,\n onFocusCb,\n onBlurCb,\n onClickCb,\n onDragEnterCb,\n onDragOverCb,\n onDragLeaveCb,\n onDropCb,\n noKeyboard,\n noDrag,\n disabled\n ]\n );\n\n const onInputElementClick = useCallback((event: any) => {\n event.stopPropagation();\n }, []);\n\n const getInputProps = useMemo(\n () =>\n ({refKey = \"ref\", onChange, onClick, ...rest}: DropzoneInputProps = {}) => {\n const inputProps = {\n accept: inputAcceptAttr,\n multiple,\n type: \"file\",\n \"aria-label\": \"file upload\",\n // Hide the input without taking it out of flow. A visually-hidden input with\n // {position: absol