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
JavaScript
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