diff --git a/src/components/FileInput.tsx b/src/components/FileInput.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5c0f9f3168dfd8627d42b66757ccc755801752ed --- /dev/null +++ b/src/components/FileInput.tsx @@ -0,0 +1,66 @@ +import { + ChangeEvent, + Dispatch, + SetStateAction, + useEffect, + useRef, + InputHTMLAttributes, + useId, +} from "react"; +import Typography from "src/components/Typography"; + +interface Props extends InputHTMLAttributes<HTMLInputElement> { + label: string; + files: File[]; + onFilesChange: Dispatch<SetStateAction<File[]>>; +} + +/** + * Controlled File Input component + * @param props + * @constructor + */ +const FileInput = (props: Props) => { + const inputRef = useRef<HTMLInputElement>(null); + const inputId = useId(); + + const { files, onFilesChange, label, ...rest } = props; + + /** + * Workaround for not being able to set the value directly on <input type="file"> + * Using the https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer + * API to construct a valid file list; not available in IE! + */ + useEffect(() => { + const dataTransfer = new DataTransfer(); + files.forEach((file: File) => dataTransfer.items.add(file)); + if (inputRef.current) inputRef.current.files = dataTransfer.files; + }, [files]); + + const handleChange = (evt: ChangeEvent<HTMLInputElement>) => { + const fileList = evt.currentTarget.files; + if (!fileList) { + onFilesChange([]); + } else { + onFilesChange(Array.from(fileList)); + } + }; + + return ( + <div className="flex flex-col"> + <label htmlFor={inputId} className="-mt-3.5"> + <Typography text={label} variant="paragraph" /> + </label> + <input + className="mt-3" + id={inputId} + ref={inputRef} + type="file" + onChange={handleChange} + {...rest} + /> + </div> + ); +}; + +export default FileInput; diff --git a/src/index.ts b/src/index.ts index 5bcf613893aa80f072c055b200db36164169839b..db265d61e63be01ee8688ce762889a702c05834b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export { default as Button } from "./components/Button"; export { default as Checkbox } from "./components/Checkbox"; export { default as DateInput } from "./components/DateInput"; export { default as Dropdown } from "./components/Dropdown"; +export { default as FileInput } from "./components/FileInput"; export { default as Icon } from "./components/Icon"; export { default as Modal } from "./components/Modal"; export { default as ProgressBar } from "./components/ProgressBar"; diff --git a/src/stories/FileInput.stories.tsx b/src/stories/FileInput.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b98f2999df13128c920d1e984d004f9da8ee1633 --- /dev/null +++ b/src/stories/FileInput.stories.tsx @@ -0,0 +1,75 @@ +import { Meta } from "@storybook/react"; +import { useState } from "react"; +import FileInput from "src/components/FileInput"; + +const meta = { + title: "Components/File Input", + component: FileInput, +} satisfies Meta; + +export default meta; + +export const SimpleFileInput = () => { + const [files, setFiles] = useState<File[]>([]); + + return ( + <FileInput label="My File Input" files={files} onFilesChange={setFiles} /> + ); +}; + +export const UseSelectedFile = () => { + const [files, setFiles] = useState<File[]>([]); + + return ( + <div> + <FileInput label="Managed file" files={files} onFilesChange={setFiles} /> + <div> + Selected file: + <br /> + {files.length === 0 ? "No file selected." : files[0].name} + </div> + </div> + ); +}; + +export const MultipleFilesInput = () => { + const [files, setFiles] = useState<File[]>([]); + + return ( + <div> + <FileInput + label="Show me them files!" + files={files} + onFilesChange={setFiles} + multiple + /> + <ul> + <li>Selected files:</li> + {files.length === 0 && <li>No files selected.</li>} + {files.map((f) => ( + <li key={f.name}>{f.name}</li> + ))} + </ul> + </div> + ); +}; + +export const AcceptOnlyTxtFiles = () => { + const [files, setFiles] = useState<File[]>([]); + + return ( + <div> + <FileInput + label="Text Only!" + files={files} + onFilesChange={setFiles} + accept="text/plain" + /> + <div> + Selected file: + <br /> + {files.length === 0 ? "No file selected." : files[0].name} + </div> + </div> + ); +};