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