Release v0.1.3
This commit is contained in:
113
webui/src/components/FileDropZone.tsx
Normal file
113
webui/src/components/FileDropZone.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useRef, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { UploadCloud } from "lucide-react";
|
||||
|
||||
export type FileDropZoneProps = {
|
||||
accept?: string;
|
||||
multiple?: boolean;
|
||||
disabled?: boolean;
|
||||
busy?: boolean;
|
||||
progress?: number | null;
|
||||
label?: ReactNode;
|
||||
actionLabel?: ReactNode;
|
||||
busyLabel?: ReactNode;
|
||||
progressLabel?: ReactNode;
|
||||
note?: ReactNode;
|
||||
className?: string;
|
||||
inputLabel?: string;
|
||||
onFiles: (files: File[]) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export default function FileDropZone({
|
||||
accept,
|
||||
multiple = true,
|
||||
disabled = false,
|
||||
busy = false,
|
||||
progress = null,
|
||||
label = "Drop files here",
|
||||
actionLabel = "or click to select files",
|
||||
busyLabel = "Uploading files",
|
||||
progressLabel,
|
||||
note,
|
||||
className = "",
|
||||
inputLabel = "Drop files here or click to select files",
|
||||
onFiles
|
||||
}: FileDropZoneProps) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const progressValue = typeof progress === "number" && Number.isFinite(progress) ? Math.max(0, Math.min(100, progress)) : null;
|
||||
const showProgress = busy || progressValue !== null;
|
||||
const interactionDisabled = disabled || busy;
|
||||
const zoneClassName = `file-drop-zone ${dragActive ? "is-active" : ""} ${showProgress ? "is-busy" : ""} ${className}`.trim();
|
||||
const roundedProgress = progressValue === null ? null : Math.round(progressValue);
|
||||
const progressStyle = progressValue === null ? undefined : { "--file-drop-progress": `${progressValue}%` } as CSSProperties;
|
||||
|
||||
async function handleFiles(fileList: FileList | File[]) {
|
||||
if (interactionDisabled) return;
|
||||
const files = Array.from(fileList);
|
||||
if (files.length === 0) return;
|
||||
try {
|
||||
await onFiles(files);
|
||||
} finally {
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={zoneClassName}
|
||||
role="button"
|
||||
tabIndex={interactionDisabled ? -1 : 0}
|
||||
aria-label={inputLabel}
|
||||
aria-disabled={interactionDisabled}
|
||||
aria-busy={showProgress || undefined}
|
||||
onClick={() => {
|
||||
if (!interactionDisabled) inputRef.current?.click();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if ((event.key === "Enter" || event.key === " ") && !interactionDisabled) {
|
||||
event.preventDefault();
|
||||
inputRef.current?.click();
|
||||
}
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
if (!interactionDisabled) setDragActive(true);
|
||||
}}
|
||||
onDragLeave={() => setDragActive(false)}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
setDragActive(false);
|
||||
if (!interactionDisabled) void handleFiles(event.dataTransfer.files);
|
||||
}}
|
||||
>
|
||||
{showProgress ? (
|
||||
<span
|
||||
className={`file-drop-progress ${progressValue === null ? "is-indeterminate" : ""}`.trim()}
|
||||
role="progressbar"
|
||||
aria-label="Upload progress"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={roundedProgress ?? undefined}
|
||||
style={progressStyle}
|
||||
>
|
||||
<span>{roundedProgress === null ? "" : `${roundedProgress}%`}</span>
|
||||
</span>
|
||||
) : (
|
||||
<UploadCloud size={28} aria-hidden="true" />
|
||||
)}
|
||||
<strong>{showProgress ? busyLabel : label}</strong>
|
||||
<span>{showProgress ? progressLabel ?? (roundedProgress === null ? "Uploading…" : `${roundedProgress}% uploaded`) : actionLabel}</span>
|
||||
{note && <span className="muted small-text">{note}</span>}
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
hidden
|
||||
onChange={(event) => event.target.files && void handleFiles(event.target.files)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user