UNPKG

zentrixui

Version:

ZentrixUI - A modern, highly customizable and accessible React file upload component library with multiple variants, JSON-based configuration, and excellent developer experience.

351 lines (350 loc) 13.6 kB
import { jsxs, jsx, Fragment } from "react/jsx-runtime"; import { useRef, useState, useCallback, useEffect } from "react"; import { useFileUpload } from "../file-upload-context.js"; import { UploadFeedback } from "../feedback/upload-feedback.js"; import { StatusIndicator } from "../progress/status-indicator.js"; import { ProgressBar } from "../progress/progress-bar.js"; const DropzoneUpload = ({ className, style, ariaLabel, ariaDescribedBy, onDragEnter, onDragLeave, onDragOver, onDrop, children, height = "200px", showBorder = true, dropzoneText, activeDropzoneText }) => { const { config, actions, state } = useFileUpload(); const inputRef = useRef(null); const dropzoneRef = useRef(null); const [isDragOver, setIsDragOver] = useState(false); const [dragCounter, setDragCounter] = useState(0); const [isFocused, setIsFocused] = useState(false); const handleFileSelect = useCallback((event) => { const files = Array.from(event.target.files || []); if (files.length > 0) { actions.selectFiles(files); } event.target.value = ""; }, [actions]); const handleClick = useCallback(() => { if (!config.defaults.disabled && !state.isUploading) { inputRef.current?.click(); } }, [config.defaults.disabled, state.isUploading]); const handleKeyDown = useCallback((event) => { if (config.defaults.disabled || state.isUploading) return; switch (event.key) { case "Enter": case " ": event.preventDefault(); handleClick(); break; case "Escape": if (isDragOver) { event.preventDefault(); setIsDragOver(false); setDragCounter(0); } break; } }, [config.defaults.disabled, state.isUploading, handleClick, isDragOver]); const handleFocus = useCallback(() => { setIsFocused(true); }, []); const handleBlur = useCallback(() => { setIsFocused(false); }, []); const handleDragEnter = useCallback((event) => { event.preventDefault(); event.stopPropagation(); if (config.defaults.disabled || state.isUploading || !config.features.dragAndDrop) { return; } setDragCounter((prev) => prev + 1); if (dragCounter === 0) { setIsDragOver(true); } onDragEnter?.(event); }, [config.defaults.disabled, state.isUploading, config.features.dragAndDrop, dragCounter, onDragEnter]); const handleDragLeave = useCallback((event) => { event.preventDefault(); event.stopPropagation(); if (config.defaults.disabled || state.isUploading || !config.features.dragAndDrop) { return; } setDragCounter((prev) => { const newCounter = prev - 1; if (newCounter === 0) { setIsDragOver(false); } return newCounter; }); onDragLeave?.(event); }, [config.defaults.disabled, state.isUploading, config.features.dragAndDrop, onDragLeave]); const handleDragOver = useCallback((event) => { event.preventDefault(); event.stopPropagation(); if (config.defaults.disabled || state.isUploading || !config.features.dragAndDrop) { return; } event.dataTransfer.dropEffect = "copy"; onDragOver?.(event); }, [config.defaults.disabled, state.isUploading, config.features.dragAndDrop, onDragOver]); const handleDrop = useCallback((event) => { event.preventDefault(); event.stopPropagation(); if (config.defaults.disabled || state.isUploading || !config.features.dragAndDrop) { return; } setIsDragOver(false); setDragCounter(0); const files = Array.from(event.dataTransfer.files); if (files.length > 0) { actions.selectFiles(files); } onDrop?.(event); }, [config.defaults.disabled, state.isUploading, config.features.dragAndDrop, actions, onDrop]); useEffect(() => { if (config.defaults.disabled || state.isUploading) { setIsDragOver(false); setDragCounter(0); } }, [config.defaults.disabled, state.isUploading]); const isDisabled = config.defaults.disabled || state.isUploading; const isActive = isDragOver && !isDisabled; const hasFocus = isFocused && !isDisabled; const dropzoneClasses = [ "file-upload-dropzone", `file-upload-dropzone--${config.defaults.size}`, `file-upload-dropzone--${config.defaults.radius}`, isDisabled ? "file-upload-dropzone--disabled" : "", state.isUploading ? "file-upload-dropzone--uploading" : "", isActive ? "file-upload-dropzone--drag-over" : "", hasFocus ? "file-upload-dropzone--focused" : "", className || "" ].filter(Boolean).join(" "); const dropzoneStyle = { border: showBorder ? `2px ${config.styling.borders.style} ${isActive ? config.styling.colors.primary : hasFocus ? config.styling.colors.primary : config.styling.colors.border}` : "none", borderRadius: config.styling.spacing.borderRadius, padding: "2rem", textAlign: "center", cursor: isDisabled ? "not-allowed" : "pointer", backgroundColor: isActive ? `${config.styling.colors.primary}15` : hasFocus ? `${config.styling.colors.primary}08` : config.styling.colors.background, color: config.styling.colors.foreground, fontSize: config.styling.typography.fontSize, transition: config.animations.enabled ? `all ${config.animations.duration}ms ${config.animations.easing}` : "none", opacity: isDisabled ? 0.6 : 1, minHeight: typeof height === "number" ? `${height}px` : height, height: typeof height === "number" ? `${height}px` : height, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: config.styling.spacing.gap, outline: hasFocus ? `2px solid ${config.styling.colors.primary}` : "none", outlineOffset: "2px", boxShadow: isActive ? config.styling.shadows.md : hasFocus ? config.styling.shadows.sm : "none", ...style }; return /* @__PURE__ */ jsxs("div", { className: "file-upload file-upload--dropzone", children: [ /* @__PURE__ */ jsx( "input", { ref: inputRef, type: "file", multiple: config.defaults.multiple, accept: config.defaults.accept, disabled: isDisabled, onChange: handleFileSelect, className: "sr-only", "aria-hidden": "true", tabIndex: -1 } ), /* @__PURE__ */ jsx( "div", { ref: dropzoneRef, className: dropzoneClasses, onClick: handleClick, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, onDragEnter: config.features.dragAndDrop ? handleDragEnter : void 0, onDragLeave: config.features.dragAndDrop ? handleDragLeave : void 0, onDragOver: config.features.dragAndDrop ? handleDragOver : void 0, onDrop: config.features.dragAndDrop ? handleDrop : void 0, role: "button", tabIndex: isDisabled ? -1 : 0, "aria-label": ariaLabel || (isActive ? activeDropzoneText || config.labels.dropText : dropzoneText || config.labels.dragText), "aria-describedby": ariaDescribedBy, "aria-disabled": isDisabled, "aria-pressed": state.files.length > 0, style: dropzoneStyle, children: children || /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("div", { className: "file-upload-dropzone-icon", style: { fontSize: "2rem", color: isDragOver ? config.styling.colors.primary : config.styling.colors.muted }, children: "📁" }), /* @__PURE__ */ jsxs("div", { className: "file-upload-dropzone-text", children: [ /* @__PURE__ */ jsx("div", { className: "file-upload-dropzone-primary-text", style: { fontWeight: "600", marginBottom: "0.5rem", color: isDragOver ? config.styling.colors.primary : config.styling.colors.foreground }, children: isDragOver ? config.labels.dropText : config.labels.dragText }), /* @__PURE__ */ jsxs("div", { className: "file-upload-dropzone-secondary-text", style: { fontSize: "0.875rem", color: config.styling.colors.muted }, children: [ "or ", /* @__PURE__ */ jsx("span", { style: { color: config.styling.colors.primary, textDecoration: "underline" }, children: config.labels.browseText }), " to choose files" ] }) ] }), state.files.length > 0 && /* @__PURE__ */ jsxs("div", { className: "file-upload-dropzone-count", style: { fontSize: "0.875rem", color: config.styling.colors.primary, fontWeight: "500" }, children: [ state.files.length, " ", state.files.length === 1 ? "file" : "files", " selected" ] }) ] }) } ), state.files.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginTop: config.styling.spacing.margin }, children: /* @__PURE__ */ jsx( UploadFeedback, { showOverallProgress: true, showIndividualProgress: false, showStatusIndicator: true, showAccessibilityAnnouncer: true, layout: "vertical", progressSize: "md", statusSize: "sm" } ) }), state.files.length > 0 && /* @__PURE__ */ jsxs("div", { className: "file-upload-files", style: { marginTop: config.styling.spacing.margin }, children: [ state.files.map((file) => /* @__PURE__ */ jsxs("div", { className: "file-upload-file-item", style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0.75rem", border: `1px solid ${config.styling.colors.border}`, borderRadius: config.styling.spacing.borderRadius, marginBottom: "0.5rem", backgroundColor: config.styling.colors.background }, children: [ /* @__PURE__ */ jsxs("div", { className: "file-upload-file-info", children: [ /* @__PURE__ */ jsx("div", { className: "file-upload-file-name", style: { fontWeight: "500", color: config.styling.colors.foreground }, children: file.name }), /* @__PURE__ */ jsxs("div", { className: "file-upload-file-size", style: { fontSize: "0.75rem", color: config.styling.colors.muted }, children: [ (file.size / 1024).toFixed(1), " KB" ] }) ] }), /* @__PURE__ */ jsxs("div", { className: "file-upload-file-actions", style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [ /* @__PURE__ */ jsx( StatusIndicator, { status: file.status, size: "sm", showText: false } ), file.status === "uploading" && /* @__PURE__ */ jsx("div", { style: { width: "60px" }, children: /* @__PURE__ */ jsx( ProgressBar, { file, size: "sm", showLabel: false, showPercentage: true } ) }), file.status === "error" && /* @__PURE__ */ jsx( "button", { type: "button", onClick: () => actions.retryUpload(file.id), className: "file-upload-retry-button", style: { background: "none", border: `1px solid ${config.styling.colors.error}`, color: config.styling.colors.error, cursor: "pointer", fontSize: "0.75rem", padding: "0.25rem 0.5rem", borderRadius: "0.25rem" }, "aria-label": `${config.labels.retryText} ${file.name}`, children: config.labels.retryText } ), file.status === "pending" && /* @__PURE__ */ jsx( "button", { type: "button", onClick: () => actions.removeFile(file.id), className: "file-upload-remove-button", style: { background: "none", border: "none", color: config.styling.colors.error, cursor: "pointer", fontSize: "1.25rem", padding: "0.25rem" }, "aria-label": `${config.labels.removeText} ${file.name}`, children: "×" } ) ] }) ] }, file.id)), state.files.some((f) => f.status === "pending") && /* @__PURE__ */ jsx( "button", { type: "button", onClick: actions.uploadFiles, disabled: state.isUploading, className: "file-upload-upload-button", style: { backgroundColor: config.styling.colors.primary, color: config.styling.colors.background, padding: "0.75rem 1.5rem", border: "none", borderRadius: config.styling.spacing.borderRadius, cursor: state.isUploading ? "not-allowed" : "pointer", opacity: state.isUploading ? 0.6 : 1, fontSize: config.styling.typography.fontSize, fontWeight: "500", width: "100%", marginTop: "1rem" }, children: state.isUploading ? config.labels.progressText : "Upload Files" } ) ] }) ] }); }; DropzoneUpload.displayName = "DropzoneUpload"; export { DropzoneUpload }; //# sourceMappingURL=dropzone-upload.js.map