UNPKG

@spaced-out/ui-design-system

Version:
213 lines (188 loc) 5.71 kB
// @flow strict import * as React from 'react'; // $FlowFixMe[untyped-import] -- this should be fixed soon import { type UseFileUploadReturnProps, useFileUpload, } from '../../hooks/useFileUpload'; import classify from '../../utils/classify'; import {UnstyledButton} from '../Button'; import {Truncate} from '../Truncate'; import {FileBlock} from './FileBlock'; import css from './FileUpload.module.css'; type ClassNames = $ReadOnly<{ wrapper?: string, instruction?: string, secondaryInstruction?: string, dropZone?: string, files?: string, }>; export type FileProgress = number | 'indeterminate'; export type FileObject = { file: File, id: string, reject?: boolean, rejectReason?: string, progress?: FileProgress, success?: boolean, successMessage?: string, // This is a flag that is used to show/hide re-upload button showReUpload?: boolean, }; // This is a file error object that is passed to onRejectedFilesDrop callback in useFileUpload hook export type FileError = { code: string, }; // This is a file rejection object that is passed to handleDropRejected function in useFileUpload hook export type FileRejection = { errors: Array<FileError>, file: File, ... }; // This is a ref object that is passed to FileUpload component for managing state of a single file export type FileUploadRef = { moveFileToProgress: (id: string, progress: FileProgress) => mixed, moveFileToSuccess: (id: string, successMessage?: string) => mixed, moveFileToReject: (id: string, rejectReason?: string) => mixed, setShowReUpload: (id: string, showReUpload?: boolean) => mixed, handleFileClear: (id: string) => mixed, validFiles: Array<FileObject>, rejectedFiles: Array<FileObject>, files: Array<FileObject>, }; // These props are shared between FileUpload component and useFileUpload hook export type FileUploadBaseProps = { maxFiles?: number, maxSize?: number, accept?: {[string]: string[]}, disabled?: boolean, // File drop callbacks onValidFilesDrop?: (acceptedFiles: Array<FileObject>) => mixed, onRejectedFilesDrop?: (fileRejections: Array<FileObject>) => mixed, // File clear callbacks onFileClear?: (id: string) => mixed, onClear?: () => mixed, }; export type FileUploadProps = { ...FileUploadBaseProps, classNames?: ClassNames, label?: React.Node, instruction?: React.Node, draggingInstruction?: React.Node, secondaryInstruction?: React.Node, required?: boolean, handleFileDeletionExternally?: boolean, // File refresh callback onFileRefreshClick?: (file: FileObject) => mixed, }; const FileUploadBase = (props: FileUploadProps, ref) => { const { classNames, label = 'Upload File', disabled = false, instruction = 'Drag and drop or click to upload', draggingInstruction = 'Drop here to start uploading..', error = false, required = false, secondaryInstruction = '', maxSize, accept, onValidFilesDrop, onRejectedFilesDrop, onFileClear, onFileRefreshClick, maxFiles = 1, handleFileDeletionExternally, } = props; // Get file upload state from useFileUpload hook const { validFiles, rejectedFiles, isDragActive, getRootProps, shouldAcceptFiles, getInputProps, handleFileClear, moveFileToProgress, moveFileToSuccess, moveFileToReject, setShowReUpload, }: UseFileUploadReturnProps = useFileUpload({ maxFiles, maxSize, accept, disabled, onValidFilesDrop, onRejectedFilesDrop, onFileClear, }); // Expose file upload actions to parent component React.useImperativeHandle(ref, () => ({ moveFileToProgress, moveFileToSuccess, moveFileToReject, handleFileClear, setShowReUpload, validFiles, rejectedFiles, files: [...validFiles, ...rejectedFiles], })); // Merge valid and rejected files const files = [...validFiles, ...rejectedFiles]; return ( <div className={classify(css.wrapper, classNames?.wrapper)}> {Boolean(label) && ( <div className={css.label}> <Truncate>{label}</Truncate> {required && <span className={css.required}>{'*'}</span>} </div> )} <UnstyledButton disabled={disabled || !shouldAcceptFiles} {...getRootProps()} className={classify( css.dropzone, { [css.disabled]: disabled || !shouldAcceptFiles, [css.dragActive]: isDragActive, [css.error]: error, }, classNames?.dropZone, )} > <input {...getInputProps()} /> <div className={classify(css.instruction, classNames?.instruction)}> {isDragActive ? draggingInstruction : instruction} </div> <div className={classify( css.secondaryInstruction, classNames?.secondaryInstruction, )} > {secondaryInstruction} </div> </UnstyledButton> {files.length > 0 && ( <div className={css.files}> {files.map((fileObject: FileObject) => ( <React.Fragment key={fileObject.id}> <FileBlock fileObject={fileObject} onFileRefreshClick={onFileRefreshClick} handleFileClear={ handleFileDeletionExternally ? onFileClear : handleFileClear } classNames={{wrapper: classNames?.files}} /> </React.Fragment> ))} </div> )} </div> ); }; export const FileUpload: React.AbstractComponent< FileUploadProps, FileUploadRef, > = React.forwardRef<FileUploadProps, FileUploadRef>(FileUploadBase);