diff --git a/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts index e85d8c39..fd26b9ba 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts @@ -9,34 +9,19 @@ import { z } from "zod/v4"; import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared"; -export const CreateProformaItemRequestSchema = z - .object({ - position: ItemPositionSchema, +export const CreateProformaItemRequestSchema = z.object({ + position: ItemPositionSchema, - is_valued: z.boolean(), - description: z.string().nullable(), + is_valued: z.boolean(), + description: z.string().nullable(), - quantity: NumericStringSchema.nullable(), - unit_amount: NumericStringSchema.nullable(), + quantity: NumericStringSchema.nullable(), + unit_amount: NumericStringSchema.nullable(), - item_discount_percentage: PercentageSchema.nullable(), + item_discount_percentage: PercentageSchema.nullable(), - taxes: TaxCombinationCodeSchema, - }) - .refine( - (item) => { - if (!item.is_valued) { - return item.quantity === null && item.unit_amount === null; - } - - return item.quantity !== null && item.unit_amount !== null; - }, - { - message: - "quantity and unit_amount must be null when is_valued is false and non-null when is_valued is true", - path: ["is_valued"], - } - ); + taxes: TaxCombinationCodeSchema, +}); export type CreateProformaItemRequestDTO = z.infer; diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx index be44b406..2d61f346 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx @@ -1,4 +1,4 @@ -import { DateHelper } from "@erp/core"; +import { DateHelper } from "@erp/rdx-utils"; import { ReactQRCode } from "@lglab/react-qr-code"; import { DataTableColumnHeader } from "@repo/rdx-ui/components"; import { diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx index db712060..580a129b 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx @@ -1,4 +1,4 @@ -import { DateHelper } from "@erp/core"; +import { DateHelper } from "@repo/rdx-utils"; import { Button, Tooltip, diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.schema.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.schema.ts index 18678692..d50b47c5 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.schema.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.schema.ts @@ -14,32 +14,17 @@ import { z } from "zod/v4"; * - sin detalles impuestos por el widget */ -export const ProformaItemUpdateFormSchema = z - .object({ - id: z.uuid(), - position: z.number().int().nonnegative(), - isValued: z.boolean(), +export const ProformaItemUpdateFormSchema = z.object({ + id: z.uuid(), + position: z.number().int().nonnegative(), + isValued: z.boolean(), - description: z.string().nullable(), + description: z.string().nullable(), - quantity: z.number().nullable(), - unitAmount: z.number().nonnegative().nullable(), + quantity: z.number().nullable(), + unitAmount: z.number().nonnegative().nullable(), - itemDiscountPercentage: z.number().min(0).max(100).nullable(), - }) - .refine( - (item) => { - if (!item.isValued) { - return item.quantity === null && item.unitAmount === null; - } - - return item.quantity !== null && item.unitAmount !== null; - }, - { - message: - "quantity and unitAmount must be null when isValued is false and non-null when isValued is true", - path: ["isValued"], - } - ); + itemDiscountPercentage: z.number().min(0).max(100).nullable(), +}); export type ProformaItemUpdateFormSchemaType = z.infer; diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx index e2593796..524fd92f 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx @@ -18,7 +18,7 @@ import { ChevronDown, ChevronUp, Copy, - MoreHorizontal, + MoreHorizontalIcon, Plus, PlusCircle, Trash2, @@ -120,15 +120,20 @@ export const LineEditor = ({ - # + # {columns.map((column) => ( - + {column.header} ))} - + + + @@ -147,28 +152,28 @@ export const LineEditor = ({ hasLineError && "bg-destructive/5 hover:bg-destructive/5" )} > - + {index + 1} {columns.map((column) => ( - + {column.cell({ line, index })} ))} - + - + } /> diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx index d17798ab..dc6224cd 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx @@ -1,4 +1,9 @@ -import { AmountField, PercentageField, QuantityField, TextField } from "@repo/rdx-ui/components"; +import { + AmountField, + LineDescriptionField, + PercentageField, + QuantityField, +} from "@repo/rdx-ui/components"; import { MoneyHelper } from "@repo/rdx-utils"; import { useTranslation } from "../../../../i18n"; @@ -57,9 +62,14 @@ export const ProformaLineEditor = ({ { id: "description", header: t("form_fields.items.description.label", "Descripción"), - headClassName: "min-w-[200px]", + headClassName: "min-w-[260px]", + className: "min-w-[260px] align-top", cell: ({ index }) => ( - + ), }, { @@ -67,7 +77,12 @@ export const ProformaLineEditor = ({ header: t("form_fields.items.quantity.label", "Cantidad"), headClassName: "w-[100px] text-right", cell: ({ index }) => ( - + ), }, { @@ -75,7 +90,12 @@ export const ProformaLineEditor = ({ header: t("form_fields.items.unit_amount.label", "Importe unitario"), headClassName: "w-[120px] text-right", cell: ({ index }) => ( - + ), }, { @@ -85,6 +105,8 @@ export const ProformaLineEditor = ({ cell: ({ index }) => ( ), @@ -93,7 +115,7 @@ export const ProformaLineEditor = ({ id: "total", header: t("form_fields.items.total.label", "Total"), headClassName: "w-[120px] text-right", - className: "text-right font-medium tabular-nums", + className: "text-right font-medium tabular-nums pt-4", cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency), }, ]; diff --git a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx index 0a9833ed..8eff3a78 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx @@ -1,6 +1,7 @@ // modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx import type { CustomerSelectionOption } from "@erp/customers"; +import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers"; import { Button } from "@repo/shadcn-ui/components"; import { ProformaUpdateRecipientEditor } from "."; @@ -37,7 +38,13 @@ export const ProformaUpdateEditorForm = ({ const { t } = useTranslation(); return ( -
+ { return ( - +
diff --git a/modules/customers/src/web/update/ui/editor/customer-edit-form.tsx b/modules/customers/src/web/update/ui/editor/customer-edit-form.tsx index ebd1919f..1d004ee7 100644 --- a/modules/customers/src/web/update/ui/editor/customer-edit-form.tsx +++ b/modules/customers/src/web/update/ui/editor/customer-edit-form.tsx @@ -1,3 +1,5 @@ +import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers"; + import { CustomerAdditionalConfigEditor } from "./customer-additional-config-fields"; import { CustomerAddressEditor } from "./customer-address-editor"; import { CustomerBasicInfoEditor } from "./customer-basic-info-editor"; @@ -17,7 +19,13 @@ export const CustomerUpdateEditorForm = ({ onReset, }: CustomerUpdateEditorFormProps) => { return ( - + diff --git a/packages/rdx-ddd/src/value-objects/url-address.ts b/packages/rdx-ddd/src/value-objects/url-address.ts index 03893513..e32e5d52 100644 --- a/packages/rdx-ddd/src/value-objects/url-address.ts +++ b/packages/rdx-ddd/src/value-objects/url-address.ts @@ -1,6 +1,8 @@ import { Result } from "@repo/rdx-utils"; import { z } from "zod/v4"; + import { translateZodValidationError } from "../helpers"; + import { ValueObject } from "./value-object"; interface URLAddressProps { diff --git a/packages/rdx-ui/src/components/form/decimal-field/decimal-field.tsx b/packages/rdx-ui/src/components/form/decimal-field/decimal-field.tsx index e4fd4518..c3e0c252 100644 --- a/packages/rdx-ui/src/components/form/decimal-field/decimal-field.tsx +++ b/packages/rdx-ui/src/components/form/decimal-field/decimal-field.tsx @@ -16,7 +16,9 @@ import type { DecimalFieldBaseProps } from "./decimal-field.types.ts"; import { DECIMAL_INPUT_PATTERN, clampNumber, - formatDecimalValue, + formatDecimalDisplayValue, + formatDecimalEditingValue, + normalizeEditingDecimalInput, parseDecimalOrNull, trimToScale, } from "./decimal-field.utils.ts"; @@ -46,12 +48,25 @@ export const DecimalField = ({ rightAddon, scale = 4, + minFractionDigits, + maxFractionDigits, + useGrouping = true, min, max, ...inputRest }: DecimalFieldProps) => { const { control, formState, getFieldState } = useFormContext(); + const [isFocused, setIsFocused] = React.useState(false); + const formatOptions = React.useMemo( + () => ({ + scale, + minFractionDigits, + maxFractionDigits, + useGrouping, + }), + [scale, minFractionDigits, maxFractionDigits, useGrouping] + ); const { field } = useController({ name, @@ -59,23 +74,36 @@ export const DecimalField = ({ defaultValue: null as any, }); + const getDisplayValue = React.useCallback( + (value: number | null | undefined): string => { + return formatDecimalDisplayValue(value, formatOptions); + }, + [formatOptions] + ); + + const getEditingValue = React.useCallback( + (value: number | null | undefined): string => { + return formatDecimalEditingValue(value, scale); + }, + [scale] + ); + const inputId = React.useId(); const disabled = formState.isSubmitting || inputRest.disabled; const fieldError = getFieldState(name, formState).error; const [inputValue, setInputValue] = React.useState(() => - formatDecimalValue(field.value as number | null | undefined, scale) + getDisplayValue(field.value as number | null | undefined) ); React.useEffect(() => { - const nextFormattedValue = formatDecimalValue(field.value as number | null | undefined, scale); + const value = field.value as number | null | undefined; + const nextValue = isFocused ? getEditingValue(value) : getDisplayValue(value); - setInputValue((currentValue) => - currentValue === nextFormattedValue ? currentValue : nextFormattedValue - ); - }, [field.value, scale]); + setInputValue((currentValue) => (currentValue === nextValue ? currentValue : nextValue)); + }, [field.value, isFocused, getDisplayValue, getEditingValue]); const handleChange = (event: React.ChangeEvent) => { - const rawValue = event.target.value; + const rawValue = normalizeEditingDecimalInput(event.target.value); if (rawValue === "") { setInputValue(""); @@ -88,20 +116,22 @@ export const DecimalField = ({ } const trimmedValue = trimToScale(rawValue, scale); + setInputValue(trimmedValue); const parsedValue = parseDecimalOrNull(trimmedValue); - if (parsedValue === null) { - field.onChange(null); - return; - } + field.onChange(parsedValue === null ? null : clampNumber(parsedValue, min, max)); + }; - field.onChange(clampNumber(parsedValue, min, max)); + const handleFocus = () => { + setIsFocused(true); + setInputValue(getEditingValue(field.value as number | null | undefined)); }; const handleBlur = (event: React.FocusEvent) => { field.onBlur(); + setIsFocused(false); const parsedValue = parseDecimalOrNull(event.target.value); @@ -112,10 +142,9 @@ export const DecimalField = ({ } const clampedValue = clampNumber(parsedValue, min, max); - const formattedValue = formatDecimalValue(clampedValue, scale); - setInputValue(formattedValue); field.onChange(clampedValue); + setInputValue(getDisplayValue(clampedValue)); }; const renderedLeftAddon = leftAddon ?? leftIcon; @@ -156,6 +185,7 @@ export const DecimalField = ({ name={field.name} onBlur={handleBlur} onChange={handleChange} + onFocus={handleFocus} readOnly={readOnly} ref={field.ref} required={required} diff --git a/packages/rdx-ui/src/components/form/decimal-field/decimal-field.types.ts b/packages/rdx-ui/src/components/form/decimal-field/decimal-field.types.ts index a69762cb..c3b612ef 100644 --- a/packages/rdx-ui/src/components/form/decimal-field/decimal-field.types.ts +++ b/packages/rdx-ui/src/components/form/decimal-field/decimal-field.types.ts @@ -27,6 +27,10 @@ export type DecimalFieldBaseProps = Omit< rightAddon?: React.ReactNode; scale?: number; + minFractionDigits?: number; + maxFractionDigits?: number; + useGrouping?: boolean; + min?: number; max?: number; }; diff --git a/packages/rdx-ui/src/components/form/decimal-field/decimal-field.utils.ts b/packages/rdx-ui/src/components/form/decimal-field/decimal-field.utils.ts index 6bc7da50..85323da9 100644 --- a/packages/rdx-ui/src/components/form/decimal-field/decimal-field.utils.ts +++ b/packages/rdx-ui/src/components/form/decimal-field/decimal-field.utils.ts @@ -1,25 +1,59 @@ -import { NumberHelper } from "@repo/rdx-utils"; +export interface FormatDecimalDisplayValueOptions { + scale: number; + minFractionDigits?: number; + maxFractionDigits?: number; + useGrouping?: boolean; +} -export const DECIMAL_INPUT_PATTERN = /^-?\d*([.,]\d*)?$/; +/** + * Patrón permitido durante la edición de un decimal. + * + * Reglas: + * - permite signo negativo inicial + * - permite dígitos + * - permite una única coma decimal + * - no permite separadores de miles + */ +export const DECIMAL_INPUT_PATTERN = /^-?\d*(,\d*)?$/; -export const normalizeDecimalInput = (value: string): string => { - return value.replace(",", "."); +/** + * Normaliza el valor introducido mientras el usuario edita. + * + * Convierte el punto del teclado numérico en coma decimal, + * siguiendo el comportamiento habitual en España. + * + * @param value - Valor crudo del input. + * @returns Valor normalizado para edición. + */ +export const normalizeEditingDecimalInput = (value: string): string => { + return value.replace(".", ","); }; +/** + * Recorta la parte decimal al número máximo de decimales permitido. + * + * @param value - Valor en formato de edición, usando coma decimal. + * @param scale - Número máximo de decimales permitidos. + * @returns Valor recortado manteniendo formato de edición. + */ export const trimToScale = (value: string, scale: number): string => { - const normalized = normalizeDecimalInput(value); - - if (!normalized.includes(".")) { - return normalized; + if (!value.includes(",")) { + return value; } - const [integerPart, decimalPart = ""] = normalized.split("."); + const [integerPart, decimalPart = ""] = value.split(","); - return `${integerPart}.${decimalPart.slice(0, scale)}`; + return `${integerPart},${decimalPart.slice(0, scale)}`; }; +/** + * Convierte un valor de edición a número o null. + * + * @param value - Valor en formato de edición, usando coma decimal. + * @returns Número parseado o null si el valor no representa un número completo. + */ export const parseDecimalOrNull = (value: string): number | null => { - const normalized = normalizeDecimalInput(value).trim(); + const normalized = value.trim().replace(",", "."); if (normalized === "") { return null; @@ -31,13 +65,17 @@ export const parseDecimalOrNull = (value: string): number | null => { const parsed = Number(normalized); - if (!Number.isFinite(parsed)) { - return null; - } - - return parsed; + return Number.isFinite(parsed) ? parsed : null; }; +/** + * Limita un número dentro del rango indicado. + * + * @param value - Valor numérico. + * @param min - Valor mínimo permitido. + * @param max - Valor máximo permitido. + * @returns Valor ajustado al rango. + */ export const clampNumber = (value: number, min?: number, max?: number): number => { if (typeof min === "number" && value < min) { return min; @@ -50,20 +88,59 @@ export const clampNumber = (value: number, min?: number, max?: number): number = return value; }; -export const formatDecimalValue = (value: number | null | undefined, scale: number): string => { - return NumberHelper.formatNumber(value, scale); - - /*if (value === null || value === undefined || Number.isNaN(value)); - return ""; - - const asString = String(value); - - if (!asString.includes(".")) { - return asString; +/** + * Formatea un número para edición. + * + * No añade separadores de miles ni rellena decimales. + * Usa coma decimal para mantener una experiencia natural en locale español. + * + * @param value - Valor numérico del formulario. + * @param scale - Número máximo de decimales a mostrar. + * @returns Valor textual editable. + */ +export const formatDecimalEditingValue = ( + value: number | null | undefined, + scale: number +): string => { + if (value === null || value === undefined || Number.isNaN(value)) { + return ""; } - const [integerPart, decimalPart = ""] = asString.split("."); - const trimmedDecimalPart = decimalPart.slice(0, scale).replace(/0+$/, ""); + const rawValue = String(value); - return trimmedDecimalPart.length > 0 ? `${integerPart}.${trimmedDecimalPart}` : `${integerPart}`;*/ + if (!rawValue.includes(".")) { + return rawValue; + } + + const [integerPart, decimalPart = ""] = rawValue.split("."); + const trimmedDecimalPart = decimalPart.slice(0, scale); + + return `${integerPart},${trimmedDecimalPart}`; +}; + +/** + * Formatea un número para visualización en reposo. + * + * Puede añadir separadores de miles y controlar el número mínimo + * y máximo de decimales visibles. + * + * @param value - Valor numérico del formulario. + * @param options - Opciones de formato. + * @returns Valor textual formateado para visualización. + */ +export const formatDecimalDisplayValue = ( + value: number | null | undefined, + options: FormatDecimalDisplayValueOptions +): string => { + if (value === null || value === undefined || Number.isNaN(value)) { + return ""; + } + + const { scale, minFractionDigits = 0, maxFractionDigits = scale, useGrouping = true } = options; + + return new Intl.NumberFormat("es-ES", { + minimumFractionDigits: minFractionDigits, + maximumFractionDigits: maxFractionDigits, + useGrouping, + }).format(value); }; diff --git a/packages/rdx-ui/src/components/form/form-section-card.tsx b/packages/rdx-ui/src/components/form/form-section-card.tsx index 2eebca7a..de28fe13 100644 --- a/packages/rdx-ui/src/components/form/form-section-card.tsx +++ b/packages/rdx-ui/src/components/form/form-section-card.tsx @@ -33,7 +33,7 @@ export const FormSectionCard = ({
{hasHeader ? ( - +
{title ? ( diff --git a/packages/rdx-ui/src/components/form/index.ts b/packages/rdx-ui/src/components/form/index.ts index 38b62557..81b12648 100644 --- a/packages/rdx-ui/src/components/form/index.ts +++ b/packages/rdx-ui/src/components/form/index.ts @@ -8,6 +8,6 @@ export * from "./form-section-grid.tsx"; export * from "./multi-select-field.tsx"; export * from "./radio-group-field.tsx"; export * from "./select-field.tsx"; -export * from "./semantic-numeric-fields/index.ts"; +export * from "./semantic-fields/index.ts"; export * from "./text-area-field.tsx"; export * from "./text-field.tsx"; diff --git a/packages/rdx-ui/src/components/form/semantic-numeric-fields/amount-field.tsx b/packages/rdx-ui/src/components/form/semantic-fields/amount-field.tsx similarity index 61% rename from packages/rdx-ui/src/components/form/semantic-numeric-fields/amount-field.tsx rename to packages/rdx-ui/src/components/form/semantic-fields/amount-field.tsx index 3fbe58e2..a83faa27 100644 --- a/packages/rdx-ui/src/components/form/semantic-numeric-fields/amount-field.tsx +++ b/packages/rdx-ui/src/components/form/semantic-fields/amount-field.tsx @@ -7,8 +7,19 @@ import type { SemanticNumericFieldProps } from "./semantic-numeric-fields.types. export const AmountField = ({ scale = 4, + minFractionDigits = 2, + maxFractionDigits = 4, rightAddon = "€", ...props }: SemanticNumericFieldProps) => { - return ; + return ( + + ); }; diff --git a/packages/rdx-ui/src/components/form/semantic-numeric-fields/index.ts b/packages/rdx-ui/src/components/form/semantic-fields/index.ts similarity index 53% rename from packages/rdx-ui/src/components/form/semantic-numeric-fields/index.ts rename to packages/rdx-ui/src/components/form/semantic-fields/index.ts index 146a4ace..970e0673 100644 --- a/packages/rdx-ui/src/components/form/semantic-numeric-fields/index.ts +++ b/packages/rdx-ui/src/components/form/semantic-fields/index.ts @@ -1,3 +1,5 @@ export * from "./amount-field.tsx"; +export * from "./line-description-field.tsx"; export * from "./percentage-field.tsx"; export * from "./quantity-field.tsx"; +export * from "./semantic-numeric-fields.types.ts"; diff --git a/packages/rdx-ui/src/components/form/semantic-fields/line-description-field.tsx b/packages/rdx-ui/src/components/form/semantic-fields/line-description-field.tsx new file mode 100644 index 00000000..5845ce9b --- /dev/null +++ b/packages/rdx-ui/src/components/form/semantic-fields/line-description-field.tsx @@ -0,0 +1,125 @@ +import { Field, FieldDescription, FieldError, Textarea } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import * as React from "react"; +import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form"; + +import { FormFieldLabel } from "../form-field-label.tsx"; +import type { NativeTextareaProps } from "../types.ts"; + +type LineDescriptionFieldProps = Omit< + NativeTextareaProps, + "name" +> & { + name: FieldPath; + + label?: string; + description?: string; + reserveDescriptionSpace?: boolean; + + required?: boolean; + readOnly?: boolean; + + orientation?: "vertical" | "horizontal" | "responsive"; + + inputClassName?: string; + + minRows?: number; + maxRows?: number; +}; + +const DEFAULT_LINE_HEIGHT = 20; + +export const LineDescriptionField = ({ + name, + + label, + description, + reserveDescriptionSpace = false, + + required = false, + readOnly = false, + + orientation = "vertical", + + className, + inputClassName, + + minRows = 1, + maxRows = 99, + + ...textareaRest +}: LineDescriptionFieldProps) => { + const { register, formState, getFieldState } = useFormContext(); + + const textareaId = React.useId(); + const textareaRef = React.useRef(null); + + const disabled = formState.isSubmitting || textareaRest.disabled; + const fieldError = getFieldState(name, formState).error; + + const registeredField = register(name); + + const resizeTextarea = React.useCallback(() => { + const textarea = textareaRef.current; + + if (!textarea) return; + + const minHeight = minRows * DEFAULT_LINE_HEIGHT; + const maxHeight = maxRows * DEFAULT_LINE_HEIGHT; + + textarea.style.height = "auto"; + + const nextHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight); + + textarea.style.height = `${nextHeight}px`; + textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden"; + }, [maxRows, minRows]); + + React.useEffect(() => { + resizeTextarea(); + }, [resizeTextarea]); + + return ( + + {label ? ( + + {label} + + ) : null} + +