import type { HTMLAttributes, ReactNode } from 'react' import React, { forwardRef, useCallback } from 'react' import { useDropzone, type Accept, type FileRejection } from 'react-dropzone' /** * Merges multiple refs (object refs and callback refs) into a single callback ref. */ function mergeRefs( ...refs: Array | undefined> ): React.RefCallback { return (instance: T | null) => { for (const ref of refs) { if (typeof ref === 'function') { ref(instance) } else if (ref && typeof ref === 'object') { ;(ref as React.MutableRefObject).current = instance } } } } export interface DropzoneProps extends HTMLAttributes { /** * ID to find this component in testing tools (e.g.: cypress, * testing-library, and jest). */ testId?: string /** * A React component that will be rendered as an icon. */ icon?: ReactNode /** * Callback function called when files are accepted. */ onFilesAccepted?: (files: File[]) => void /** * Callback function called when files are rejected. */ onFilesRejected?: (fileRejections: FileRejection[]) => void /** * Maximum number of files that can be selected. */ maxFiles?: number /** * Maximum file size in bytes. */ maxSize?: number /** * Minimum file size in bytes. */ minSize?: number /** * Accepted file types. */ accept?: Accept /** * Whether multiple files can be selected. */ multiple?: boolean /** * Whether the dropzone is disabled. */ disabled?: boolean /** * Custom text to display when dragging files over the dropzone. */ dragActiveText?: string /** * Custom text to display in the dropzone. */ text?: string /** * Whether to disable opening the file dialog on click. */ noClick?: boolean /** * Whether to disable opening the file dialog via keyboard. */ noKeyboard?: boolean /** * Whether to disable drag-and-drop on the drop zone. */ noDrag?: boolean /** * Custom content for the select files button. */ selectFilesButton?: ReactNode /** * Accessible label for the dropzone root element. * When disabled, `aria-disabled` is also set so assistive tech * can still identify the dropzone and its state. */ ariaLabel?: string } export interface DropzoneState { acceptedFiles: File[] fileRejections: FileRejection[] } export interface DropzoneState { acceptedFiles: File[] fileRejections: FileRejection[] } const Dropzone = forwardRef(function Dropzone( { icon, testId = 'fs-dropzone', children, onFilesAccepted, onFilesRejected, maxFiles = 0, maxSize, minSize, accept, multiple = true, disabled = false, dragActiveText, text, noClick = false, noKeyboard = false, noDrag = false, selectFilesButton = null, ariaLabel, ...otherProps }, ref ) { const onDrop = useCallback( (acceptedFiles: File[]) => { if (onFilesAccepted && acceptedFiles.length > 0) { onFilesAccepted(acceptedFiles) } }, [onFilesAccepted] ) const onDropRejected = useCallback( (fileRejections: FileRejection[]) => { if (onFilesRejected && fileRejections.length > 0) { onFilesRejected(fileRejections) } }, [onFilesRejected] ) const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject, acceptedFiles, fileRejections, } = useDropzone({ onDrop, onDropRejected, accept, maxFiles, maxSize, minSize, multiple, disabled, noClick, noKeyboard, noDrag, }) const rootProps = getRootProps() const mergedRef = mergeRefs(ref, rootProps.ref as React.Ref) const accessibilityProps = ariaLabel ? disabled ? { role: 'button' as const, 'aria-label': ariaLabel, 'aria-disabled': true as const, } : { role: 'button' as const, 'aria-label': ariaLabel, } : {} return ( {icon && {icon}} {isDragActive ? ( {dragActiveText} ) : ( <>{children || {text}}> )} {selectFilesButton && !noClick && !disabled && selectFilesButton} {acceptedFiles.length > 0 && ( {acceptedFiles.map((file: File, index: number) => ( {file.name} - {file.size} bytes ))} )} {fileRejections.length > 0 && ( {fileRejections.map( (fileRejection: FileRejection, index: number) => { const { file, errors } = fileRejection return ( {file.name} -{' '} {errors .map((e: { message: string }) => e.message) .join(', ')} ) } )} )} ) }) export default Dropzone
{dragActiveText}
{text}