@spaced-out/ui-design-system
Version:
Sense UI components library
213 lines (188 loc) • 5.71 kB
Flow
// @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);