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.

245 lines (244 loc) 9.16 kB
import { jsxs, jsx, Fragment } from "react/jsx-runtime"; import { useRef, useCallback } from "react"; import { Root as Slot } from "../../../node_modules/@radix-ui/react-slot/dist/index.js"; import { useFileUpload } from "../file-upload-context.js"; import { validateFiles } from "../../../utils/file-validation.js"; import { cn, generateThemeClasses } from "../../../utils/theme.js"; import { UploadFeedback } from "../feedback/upload-feedback.js"; import { StatusIndicator } from "../progress/status-indicator.js"; import { ProgressBar } from "../progress/progress-bar.js"; import { LoadingSpinner } from "../progress/loading-spinner.js"; const ButtonUpload = ({ className = "", style, ariaLabel, ariaDescribedBy, children, buttonText, icon, iconPosition = "left", asChild = false, disabled: propDisabled, multiple: propMultiple, accept: propAccept, maxSize: propMaxSize, maxFiles: propMaxFiles, onFileSelect, onError, ...props }) => { const { config, actions, state } = useFileUpload(); const inputRef = useRef(null); const disabled = propDisabled ?? config.defaults.disabled ?? false; const multiple = propMultiple ?? config.defaults.multiple ?? false; const accept = propAccept ?? config.defaults.accept ?? "*"; const maxSize = propMaxSize ?? config.validation.maxSize; const maxFiles = propMaxFiles ?? config.validation.maxFiles; const handleFileSelect = useCallback(async (event) => { const files = Array.from(event.target.files || []); if (files.length === 0) return; try { const validationResult = await validateFiles(files, { ...config, validation: { ...config.validation, maxSize, maxFiles, allowedTypes: accept === "*" ? ["*"] : accept.split(",").map((t) => t.trim()) } }, state.files.length); if (validationResult.validFiles.length > 0) { actions.selectFiles(validationResult.validFiles); onFileSelect?.(validationResult.validFiles); } if (validationResult.rejectedFiles.length > 0) { const errorMessage = validationResult.rejectedFiles.map((rf) => `${rf.file.name}: ${rf.errors.map((e) => e.message).join(", ")}`).join("\n"); onError?.(errorMessage); } } catch (error) { const errorMessage = error instanceof Error ? error.message : "File validation failed"; onError?.(errorMessage); } event.target.value = ""; }, [config, maxSize, maxFiles, accept, state.files.length, actions, onFileSelect, onError]); const handleButtonClick = useCallback(() => { if (disabled || state.isUploading) return; inputRef.current?.click(); }, [disabled, state.isUploading]); const handleKeyDown = useCallback((event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); handleButtonClick(); } }, [handleButtonClick]); const getButtonClasses = () => { const themeClasses = generateThemeClasses( "button", config.defaults.size, config.defaults.radius, config, ["file-upload--primary"] ); const additionalClasses = [ "inline-flex items-center justify-center gap-2", disabled || state.isUploading ? "file-upload--disabled" : "", state.isUploading ? "file-upload--loading" : "" ]; return cn(themeClasses, ...additionalClasses, className); }; const renderButtonContent = () => { const text = buttonText || children || config.labels.selectFilesText; const showIcon = icon && !state.isUploading; const showCount = state.files.length > 0; if (state.isUploading) { return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(LoadingSpinner, { size: "sm", color: "primary" }), config.labels.progressText ] }); } return /* @__PURE__ */ jsxs(Fragment, { children: [ showIcon && iconPosition === "left" && icon, /* @__PURE__ */ jsxs("span", { children: [ text, showCount && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-xs opacity-75", children: [ "(", state.files.length, " ", state.files.length === 1 ? "file" : "files", ")" ] }) ] }), showIcon && iconPosition === "right" && icon ] }); }; const buttonProps = { type: "button", className: getButtonClasses(), onClick: handleButtonClick, onKeyDown: handleKeyDown, disabled: disabled || state.isUploading, "aria-label": ariaLabel || config.labels.selectFilesText, "aria-describedby": ariaDescribedBy, "aria-pressed": state.files.length > 0, style, ...props }; const Comp = asChild ? Slot : "button"; return /* @__PURE__ */ jsxs("div", { className: "file-upload-button-container", children: [ /* @__PURE__ */ jsx( "input", { ref: inputRef, type: "file", multiple, accept, disabled: disabled || state.isUploading, onChange: handleFileSelect, className: "sr-only", "aria-hidden": "true", tabIndex: -1 } ), /* @__PURE__ */ jsx(Comp, { ...buttonProps, children: renderButtonContent() }), state.files.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx( UploadFeedback, { showOverallProgress: true, showIndividualProgress: false, showStatusIndicator: true, showAccessibilityAnnouncer: true, layout: "compact", progressSize: "sm", statusSize: "sm" } ) }), state.files.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", role: "list", "aria-label": "Selected files", children: [ state.files.map((file) => /* @__PURE__ */ jsxs( "div", { className: cn( "flex items-center justify-between", "file-upload--sm", "file-upload--radius-md", "border border-[var(--file-upload-border)]", "bg-[var(--file-upload-background)]", "text-[var(--file-upload-foreground)]" ), role: "listitem", children: [ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [ /* @__PURE__ */ jsx("p", { className: "font-medium truncate", style: { fontSize: "var(--file-upload-font-size-sm)" }, children: file.name }), /* @__PURE__ */ jsxs("p", { className: "text-[var(--file-upload-muted)]", style: { fontSize: "var(--file-upload-font-size-sm)" }, children: [ (file.size / 1024 / 1024).toFixed(2), " MB" ] }) ] }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 ml-4", children: [ /* @__PURE__ */ jsx( StatusIndicator, { status: file.status, size: "sm", showText: false } ), file.status === "uploading" && /* @__PURE__ */ jsx("div", { className: "w-16", children: /* @__PURE__ */ jsx( ProgressBar, { file, size: "sm", showLabel: false, showPercentage: false } ) }), file.status === "pending" && /* @__PURE__ */ jsx( "button", { type: "button", onClick: () => actions.removeFile(file.id), className: cn( generateThemeClasses("button", "sm", "sm", config, ["file-upload--error", "file-upload--outline"]) ), "aria-label": `Remove ${file.name}`, children: config.labels.removeText } ), file.status === "error" && /* @__PURE__ */ jsx( "button", { type: "button", onClick: () => actions.retryUpload(file.id), className: cn( generateThemeClasses("button", "sm", "sm", config, ["file-upload--error", "file-upload--outline"]) ), "aria-label": `Retry upload for ${file.name}`, children: config.labels.retryText } ) ] }) ] }, file.id )), state.files.some((f) => f.status === "pending") && !config.features.autoUpload && /* @__PURE__ */ jsx( "button", { type: "button", onClick: actions.uploadFiles, disabled: state.isUploading, className: cn( generateThemeClasses("button", "md", config.defaults.radius, config, ["file-upload--success"]), "w-full mt-3", state.isUploading ? "file-upload--disabled" : "" ), children: state.isUploading ? config.labels.progressText : "Upload Files" } ) ] }) ] }); }; ButtonUpload.displayName = "ButtonUpload"; export { ButtonUpload }; //# sourceMappingURL=button-upload.js.map