UNPKG

@nish1896/rhf-mui-components

Version:

A suite of 25+ production-ready react-hook-form components built with material-ui. Fully typed, tree-shakable, and optimized for enterprise-grade forms.

240 lines (239 loc) 10 kB
"use client"; import { RHFMuiConfigContext } from "../../config/ConfigProvider.js"; import { keepLabelAboveFormField, mergeRefs } from "../../utils/control.js"; import FormControl from "../../common/FormControl.js"; import FormHelperText from "../../common/FormHelperText.js"; import FormLabel from "../../common/FormLabel.js"; import HiddenInput from "./components/HiddenInput.js"; import UploadButton from "./components/UploadButton.js"; import { validateFileList } from "../../utils/file.js"; import { fieldNameToLabel } from "../../utils/text-transform.js"; import { useFieldIds } from "../../utils/useFieldIds.js"; import { Fragment, forwardRef, useContext, useState } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; import { Controller } from "react-hook-form"; import Box from "@mui/material/Box"; //#region src/mui/file-uploader/index.tsx let FileUploadError = /* @__PURE__ */ function(FileUploadError) { FileUploadError["sizeExceeded"] = "FILE_SIZE_EXCEEDED"; FileUploadError["invalidExtension"] = "FILE_TYPE_NOT_ALLOWED"; FileUploadError["limitExceeded"] = "FILE_LIMIT_EXCEEDED"; return FileUploadError; }({}); const RHFFileUploader = forwardRef(function RHFFileUploader({ fieldName, control, registerOptions, accept, multiple, maxFiles, maxSize, existingFiles = [], renderExistingFileItem, existingFileListProps, uploadedFileListProps, renderUploadButton, renderFileItem, customOnChange, onValueChange, disabled: muiDisabled, onUploadError, label, showLabelAboveFormField, formLabelProps, hideLabel, required, helperText, hideErrorMessage, formHelperTextProps, fullWidth = false, disableDragAndDrop = false, dropZoneProps, customIds }, ref) { const [isDragging, setIsDragging] = useState(false); const { fieldId, labelId, helperTextId, errorId } = useFieldIds(fieldName, customIds); const { allLabelsAboveFields } = useContext(RHFMuiConfigContext); const defaultFieldLabel = fieldNameToLabel(fieldName); const fieldLabel = label ?? defaultFieldLabel; const accessibleFieldLabel = typeof fieldLabel === "string" ? fieldLabel : defaultFieldLabel; const isLabelAboveFormField = keepLabelAboveFormField(showLabelAboveFormField, allLabelsAboveFields); const serverFileCount = existingFiles.length; const { required: registerRequired, validate: registerValidate, ...controllerRulesBase } = registerOptions ?? {}; const isRegisterRequired = typeof registerRequired === "object" ? registerRequired.value : !!registerRequired; const isFieldRequired = required || isRegisterRequired; const requiredMessage = typeof registerRequired === "string" ? registerRequired : typeof registerRequired === "object" ? registerRequired.message : "This field is required"; const validateRequiredFiles = (value) => { if (!isFieldRequired || serverFileCount > 0) return true; if (Array.isArray(value)) return value.length > 0 || requiredMessage; return value instanceof File || requiredMessage; }; return /* @__PURE__ */ jsx(Controller, { name: fieldName, control, rules: { ...controllerRulesBase, validate: typeof registerValidate === "function" ? async (value, formValues) => { const requiredValidation = validateRequiredFiles(value); if (requiredValidation !== true) return requiredValidation; return registerValidate(value, formValues); } : { ...registerValidate, requiredFiles: (value) => validateRequiredFiles(value) } }, render: ({ field: { name: rhfFieldName, value: rhfValue, onChange: rhfOnChange, onBlur: rhfOnBlur, ref: rhfRef, disabled: rhfDisabled }, fieldState: { error: fieldStateError } }) => { const isDisabled = muiDisabled || rhfDisabled; const fieldErrorMessage = fieldStateError?.message?.toString(); const isError = !!fieldErrorMessage; const showHelperTextElement = !!(helperText || isError && !hideErrorMessage); const updateFieldValue = (newValue, event) => { if (customOnChange) { customOnChange({ rhfOnChange, newValue, event }); return; } rhfOnChange(newValue); onValueChange?.({ newValue, event }); }; const processFiles = (files, event) => { if (!files || files.length === 0) { updateFieldValue(null, event); return; } const incomingFiles = Array.from(files); const previousFiles = multiple ? Array.isArray(rhfValue) ? rhfValue : rhfValue instanceof File ? [rhfValue] : [] : []; const remainingFileSlots = maxFiles !== void 0 ? Math.max(0, maxFiles - serverFileCount - previousFiles.length) : void 0; const { acceptedFiles, rejectedFiles } = validateFileList(incomingFiles, { accept, maxSize }); const fileErrors = [...rejectedFiles]; let acceptedIncomingFiles = acceptedFiles; if (remainingFileSlots !== void 0 && acceptedIncomingFiles.length > remainingFileSlots) { const excessFiles = acceptedIncomingFiles.slice(remainingFileSlots); acceptedIncomingFiles = acceptedIncomingFiles.slice(0, remainingFileSlots); fileErrors.push(...excessFiles.map((file) => ({ file, errors: ["FILE_LIMIT_EXCEEDED"] }))); } if (fileErrors.length > 0) onUploadError?.(fileErrors); const finalAcceptedFiles = multiple ? [...previousFiles, ...acceptedIncomingFiles] : acceptedIncomingFiles; updateFieldValue(multiple ? finalAcceptedFiles.length > 0 ? finalAcceptedFiles : null : finalAcceptedFiles[0] ?? null, event); }; const handleFileChange = (event) => { processFiles(event.target.files, event); /** * Reset so the same file(s) can be selected again in * a subsequent pick */ event.target.value = ""; }; const handleDrop = (event) => { event.preventDefault(); setIsDragging(false); if (isDisabled) return; if (event.dataTransfer.files.length === 0) return; processFiles(event.dataTransfer.files, event); }; const handleDragOver = (event) => { event.preventDefault(); if (!isDisabled) setIsDragging(true); }; const handleDragLeave = (event) => { if (event.currentTarget.contains(event.relatedTarget)) return; setIsDragging(false); }; const handleRemoveFile = (index, event) => { let newValue; if (multiple && Array.isArray(rhfValue)) { const newFiles = rhfValue.filter((_, i) => i !== index); newValue = newFiles.length > 0 ? newFiles : null; } else newValue = null; updateFieldValue(newValue, event); }; const InputComponent = /* @__PURE__ */ jsx(HiddenInput, { id: fieldId, name: rhfFieldName, type: "file", ref: mergeRefs(rhfRef, ref), accept, multiple, onChange: handleFileChange, onBlur: rhfOnBlur, disabled: isDisabled, "aria-labelledby": !hideLabel && isLabelAboveFormField ? labelId : void 0, "aria-label": hideLabel ? accessibleFieldLabel : void 0, "aria-describedby": showHelperTextElement ? isError ? errorId : helperTextId : void 0, "aria-invalid": isError, "aria-required": isFieldRequired }); const uploadAreaContent = renderUploadButton ? renderUploadButton(InputComponent) : /* @__PURE__ */ jsx(UploadButton, { label: typeof fieldLabel === "string" ? fieldLabel : `Upload ${defaultFieldLabel}`, fieldName, disabled: isDisabled, children: InputComponent }); const { sx: dropZoneSx, onDragEnter: dropZoneOnDragEnter, onDragOver: dropZoneOnDragOver, onDragLeave: dropZoneOnDragLeave, onDrop: dropZoneOnDrop, ...restDropZoneProps } = typeof dropZoneProps === "function" ? dropZoneProps({ isDragging, disabled: !!isDisabled, error: isError }) : dropZoneProps ?? {}; const dropZoneContent = !disableDragAndDrop ? /* @__PURE__ */ jsx(Box, { ...restDropZoneProps, onDragEnter: (event) => { handleDragOver(event); dropZoneOnDragEnter?.(event); }, onDragOver: (event) => { handleDragOver(event); dropZoneOnDragOver?.(event); }, onDragLeave: (event) => { handleDragLeave(event); dropZoneOnDragLeave?.(event); }, onDrop: (event) => { handleDrop(event); dropZoneOnDrop?.(event); }, sx: [{ border: "2px dashed", borderColor: isDragging ? "primary.main" : "grey.400", borderRadius: 2, p: 2, textAlign: "center", transition: "border-color 0.2s ease-in-out", mb: 2, cursor: isDisabled ? "not-allowed" : "pointer" }, ...Array.isArray(dropZoneSx) ? dropZoneSx : dropZoneSx ? [dropZoneSx] : []], children: uploadAreaContent }) : uploadAreaContent; return /* @__PURE__ */ jsxs(FormControl, { fullWidth, error: isError, disabled: isDisabled, children: [ !hideLabel && /* @__PURE__ */ jsx(FormLabel, { label: fieldLabel, isVisible: isLabelAboveFormField, required: isFieldRequired, error: isError, disabled: isDisabled, formLabelProps: { ...formLabelProps, id: labelId, htmlFor: fieldId } }), dropZoneContent, /* @__PURE__ */ jsx(FormHelperText, { error: isError, errorMessage: fieldErrorMessage, hideErrorMessage, helperText, showHelperTextElement, formHelperTextProps: { ...formHelperTextProps, id: isError ? errorId : helperTextId } }), existingFiles.length > 0 && renderExistingFileItem && /* @__PURE__ */ jsx(Box, { ...existingFileListProps, children: existingFiles.map((file, index) => /* @__PURE__ */ jsx(Fragment, { children: renderExistingFileItem({ file, index }) }, `existing-${file.name}-${index}`)) }), rhfValue && renderFileItem && /* @__PURE__ */ jsx(Box, { ...uploadedFileListProps, children: (Array.isArray(rhfValue) ? rhfValue : [rhfValue]).map((file, index) => /* @__PURE__ */ jsx(Fragment, { children: renderFileItem({ file, index, removeFile: (event) => handleRemoveFile(index, event) }) }, `${file.name}-${file.lastModified}-${index}`)) }) ] }); } }); }); //#endregion export { FileUploadError, RHFFileUploader as default };