This commit is contained in:
David Arranz 2024-07-03 17:15:52 +02:00
parent a32ba80bb6
commit 1ac0533de5
15 changed files with 244 additions and 209 deletions

View File

@ -1,11 +1,17 @@
import { CancelButton, FormDatePickerField, FormTextAreaField, FormTextField } from "@/components"; import {
BackHistoryButton,
FormDatePickerField,
FormTextAreaField,
FormTextField,
} from "@/components";
import { t } from "i18next"; import { t } from "i18next";
import { ChevronLeft } from "lucide-react"; import { ChevronLeft } from "lucide-react";
import { SubmitButton } from "@/components"; import { SubmitButton } from "@/components";
import { Button, Form } from "@/ui"; import { Button, Form } from "@/ui";
import { SubmitHandler, useForm } from "react-hook-form"; import { FieldErrors, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useQuotes } from "./hooks"; import { useQuotes } from "./hooks";
type QuoteDataForm = { type QuoteDataForm = {
@ -32,14 +38,15 @@ export const QuoteCreate = () => {
//const { data: userIdentity } = useGetIdentity(); //const { data: userIdentity } = useGetIdentity();
//console.log(userIdentity); //console.log(userIdentity);
const navigate = useNavigate();
const { useMutation } = useQuotes(); const { useMutation } = useQuotes();
const { mutate } = useMutation; const { mutate } = useMutation();
const form = useForm<QuoteDataForm>({ const form = useForm<QuoteDataForm>({
defaultValues: { defaultValues: {
reference: "", date: new Date(Date.now()).toUTCString(),
date: Date.now().toLocaleString(),
customer_information: "", customer_information: "",
reference: "",
}, },
}); });
@ -48,15 +55,25 @@ export const QuoteCreate = () => {
try { try {
//setLoading(true); //setLoading(true);
mutate(formData); mutate(formData, {
onSuccess: (data) => {
navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true });
},
});
} finally { } finally {
//setLoading(false); //setLoading(false);
} }
}; };
const onErrors: SubmitErrorHandler<QuoteDataForm> = async (
errors: FieldErrors<QuoteDataForm>
) => {
console.log(errors);
};
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}> <form onSubmit={form.handleSubmit(onSubmit, onErrors)}>
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'> <div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<Button variant='outline' size='icon' className='h-7 w-7'> <Button variant='outline' size='icon' className='h-7 w-7'>
@ -70,47 +87,35 @@ export const QuoteCreate = () => {
<div className='grid max-w-lg gap-6'> <div className='grid max-w-lg gap-6'>
<FormTextField <FormTextField
className='row-span-2'
name='reference'
required
label={t("quotes.create.form_fields.reference.label")} label={t("quotes.create.form_fields.reference.label")}
description={t("quotes.create.form_fields.reference.desc")} description={t("quotes.create.form_fields.reference.desc")}
disabled={form.formState.disabled}
placeholder={t("quotes.create.form_fields.reference.placeholder")} placeholder={t("quotes.create.form_fields.reference.placeholder")}
{...form.register("reference", {
required: false,
})}
/> />
<FormDatePickerField <FormDatePickerField
required required
label={t("quotes.create.form_fields.date.label")} label={t("quotes.create.form_fields.date.label")}
description={t("quotes.create.form_fields.date.desc")} description={t("quotes.create.form_fields.date.desc")}
disabled={form.formState.disabled}
placeholder={t("quotes.create.form_fields.date.placeholder")} placeholder={t("quotes.create.form_fields.date.placeholder")}
{...form.register("date", { name='date'
required: true,
})}
/> />
<div className='grid grid-cols-1 grid-rows-2 gap-6'> <FormTextAreaField
<FormTextAreaField rows={4}
className='row-span-2' className='row-span-2'
required name='customer_information'
label={t("quotes.create.form_fields.customer_information.label")} required
description={t("quotes.create.form_fields.customer_information.desc")} label={t("quotes.create.form_fields.customer_information.label")}
disabled={form.formState.disabled} description={t("quotes.create.form_fields.customer_information.desc")}
placeholder={t("quotes.create.form_fields.customer_information.placeholder")} placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
{...form.register("customer_information", { />
required: true,
})}
errors={form.formState.errors}
/>
</div>
</div> </div>
<div className='flex items-center justify-center gap-2'> <div className='flex items-center justify-start gap-2'>
<CancelButton <BackHistoryButton size='sm' label={t("quotes.create.buttons.discard")} url='/quotes' />
variant='outline'
size='sm'
label={t("quotes.create.buttons.discard")}
></CancelButton>
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton> <SubmitButton size='sm' label={t("common.continue")}></SubmitButton>
</div> </div>

View File

@ -19,32 +19,36 @@ export const useQuotes = (params?: UseQuotesGetParamsType) => {
const keys = useQueryKey(); const keys = useQueryKey();
return { return {
useQuery: useOne<IGetQuote_Response_DTO>({ useQuery: () =>
queryKey: keys().data().resource("quotes").action("one").id("").params().get(), useOne<IGetQuote_Response_DTO>({
queryFn: () => queryKey: keys().data().resource("quotes").action("one").id("").params().get(),
dataSource.getOne({ queryFn: () =>
resource: "quotes", dataSource.getOne({
id: "", resource: "quotes",
}), id: "",
...params, }),
}), ...params,
useMutation: useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({ }),
mutationKey: keys().data().resource("quotes").action("one").id("").params().get(), useMutation: () =>
mutationFn: (data) => { useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({
let { id } = data; mutationKey: keys().data().resource("quotes").action("one").id("").params().get(),
mutationFn: (data) => {
let { id, status } = data;
if (!id) { if (!id) {
id = UniqueID.generateNewID().object.toString(); id = UniqueID.generateNewID().object.toString();
} status = "draft";
}
return dataSource.createOne({ return dataSource.createOne({
resource: "quotes", resource: "quotes",
data: { data: {
...data, ...data,
id, status,
}, id,
}); },
}, });
}), },
}),
}; };
}; };

View File

@ -4,12 +4,9 @@ export interface CancelButtonProps extends ButtonProps {
label?: string; label?: string;
} }
export const CancelButton = ({ export const CancelButton = ({ label = "Cancelar", ...props }: CancelButtonProps): JSX.Element => {
label = "Cancelar",
...props
}: CancelButtonProps): JSX.Element => {
return ( return (
<Button type="button" variant="secondary" {...props}> <Button type='button' variant='secondary' {...props}>
{label} {label}
</Button> </Button>
); );

View File

@ -18,9 +18,7 @@ const customButtonVariants = cva("", {
}, },
}); });
export interface CustomButtonProps export interface CustomButtonProps extends ButtonProps, VariantProps<typeof customButtonVariants> {
extends ButtonProps,
VariantProps<typeof customButtonVariants> {
icon: LucideIcon; // Propiedad para proporcionar el icono personalizado icon: LucideIcon; // Propiedad para proporcionar el icono personalizado
label?: string; label?: string;
} }

View File

@ -1,6 +1,7 @@
import { Button, ButtonProps } from "@/ui"; import { Button, ButtonProps } from "@/ui";
import { ChevronLeft } from "lucide-react"; import { ChevronLeft } from "lucide-react";
import { To, useNavigate } from "react-router-dom"; import { To, useNavigate } from "react-router-dom";
import { CustomButton } from "../Buttons/CustomButton";
export interface BackHistoryButtonProps extends ButtonProps { export interface BackHistoryButtonProps extends ButtonProps {
label?: string; label?: string;
@ -15,16 +16,29 @@ export const BackHistoryButton = ({
}: BackHistoryButtonProps): JSX.Element => { }: BackHistoryButtonProps): JSX.Element => {
const navigate = useNavigate(); const navigate = useNavigate();
return (
<CustomButton
type='button'
label={label}
icon={ChevronLeft}
variant='ghost'
onClick={() => {
url ? navigate(url) : navigate(-1);
}}
{...props}
/>
);
return ( return (
<Button <Button
variant="ghost" variant='ghost'
onClick={() => { onClick={() => {
url ? navigate(url) : navigate(-1); url ? navigate(url) : navigate(-1);
}} }}
size={size} size={size}
{...props} {...props}
> >
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className='w-4 h-4' />
<span className={size === "icon" ? "sr-only" : "ml-2"}>{label}</span> <span className={size === "icon" ? "sr-only" : "ml-2"}>{label}</span>
</Button> </Button>
); );

View File

@ -12,7 +12,6 @@ import {
FormDescription, FormDescription,
FormField, FormField,
FormItem, FormItem,
FormMessage,
InputProps, InputProps,
Popover, Popover,
PopoverContent, PopoverContent,
@ -28,14 +27,12 @@ import { CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { t } from "i18next"; import { t } from "i18next";
import { FormErrorMessage } from "./FormErrorMessage";
type FormDatePickerFieldProps< type FormDatePickerFieldProps<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = InputProps & > = InputProps & FormInputProps & Partial<FormLabelProps> & UseControllerProps<TFieldValues, TName>;
FormInputProps &
Partial<FormLabelProps> &
UseControllerProps<TFieldValues, TName> & {};
/*const loadDateFnsLocale = async (locale: Locale) => { /*const loadDateFnsLocale = async (locale: Locale) => {
return await import(`date-fns/locale/${locale.code}/index.js`); return await import(`date-fns/locale/${locale.code}/index.js`);
@ -45,19 +42,7 @@ export const FormDatePickerField = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormDatePickerFieldProps React.HTMLAttributes<HTMLDivElement> & FormDatePickerFieldProps
>((props: FormDatePickerFieldProps, ref) => { >((props: FormDatePickerFieldProps, ref) => {
const { const { label, placeholder, hint, description, required, className, name } = props;
label,
placeholder,
hint,
description,
required,
className,
disabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
errors,
name,
type,
} = props;
const { control } = useFormContext(); const { control } = useFormContext();
//const { locale } = loadDateFnsLocale(); //const { locale } = loadDateFnsLocale();
@ -72,11 +57,10 @@ export const FormDatePickerField = React.forwardRef<
control={control} control={control}
name={name} name={name}
rules={{ required }} rules={{ required }}
disabled={disabled}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => ( render={({ field, fieldState, formState }) => (
<FormItem ref={ref} className={cn(className, "flex flex-col")}> <FormItem ref={ref} className={cn(className, "flex flex-col")}>
{label && <FormLabel label={label} hint={hint} />} {label && <FormLabel label={label} hint={hint} required={required} />}
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -90,6 +74,8 @@ export const FormDatePickerField = React.forwardRef<
> >
{field.value ? ( {field.value ? (
new Date(field.value).toLocaleDateString() //"en-US", DATE_OPTIONS) new Date(field.value).toLocaleDateString() //"en-US", DATE_OPTIONS)
) : placeholder ? (
placeholder
) : ( ) : (
<span>{t("common.pick_date")}</span> <span>{t("common.pick_date")}</span>
)} )}
@ -117,8 +103,9 @@ export const FormDatePickerField = React.forwardRef<
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{description && <FormDescription>{description}</FormDescription>} {description && <FormDescription>{description}</FormDescription>}
<FormMessage /> <FormErrorMessage />
</FormItem> </FormItem>
)} )}
/> />

View File

@ -0,0 +1,30 @@
import { FormMessage, useFormField } from "@/ui";
import { t } from "i18next";
import * as React from "react";
export const FormErrorMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ children, ...props }, ref) => {
const { error } = useFormField();
let message = children;
if (error) {
if (error.message) {
message = String(error?.message || error.root?.message);
} else {
if (error.type === "required") {
message = t("common.required_field");
}
}
}
console.log(message);
return (
<FormMessage ref={ref} {...props}>
{message}
</FormMessage>
);
});
FormErrorMessage.displayName = "FormErrorMessage";

View File

@ -14,12 +14,14 @@ export const FormLabel = React.forwardRef<
FormLabelProps & FormLabelProps &
Pick<FormInputProps, "required"> Pick<FormInputProps, "required">
>(({ label, hint, required, ...props }, ref) => { >(({ label, hint, required, ...props }, ref) => {
const { error } = UI.useFormField();
const _hint = hint ? hint : required ? "obligatorio" : undefined; const _hint = hint ? hint : required ? "obligatorio" : undefined;
const _hintClassName = required ? "text-destructive" : ""; const _hintClassName = error ? "text-destructive font-semibold" : "";
return ( return (
<UI.FormLabel ref={ref} className='flex justify-between text-sm' {...props}> <UI.FormLabel ref={ref} className='flex justify-between text-sm' {...props}>
<span className='block font-semibold'>{label}</span> <span className={`block font-semibold ${_hintClassName}`}>{label}</span>
{_hint && <span className={`text-sm font-medium ${_hintClassName}`}>{_hint}</span>} {_hint && <span className={`text-sm font-medium ${_hintClassName} `}>{_hint}</span>}
</UI.FormLabel> </UI.FormLabel>
); );
}); });

View File

@ -5,7 +5,6 @@ import {
FormDescription, FormDescription,
FormField, FormField,
FormItem, FormItem,
FormMessage,
Textarea, Textarea,
} from "@/ui"; } from "@/ui";
import * as React from "react"; import * as React from "react";
@ -17,6 +16,7 @@ import {
UseControllerProps, UseControllerProps,
useFormContext, useFormContext,
} from "react-hook-form"; } from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel"; import { FormLabel, FormLabelProps } from "./FormLabel";
export type FormTextAreaFieldProps< export type FormTextAreaFieldProps<
@ -41,12 +41,14 @@ export const FormTextAreaField = React.forwardRef<
name, name,
label, label,
hint, hint,
description,
placeholder, placeholder,
description,
required, required,
disabled,
autoSize,
className, className,
autoSize,
...props ...props
}, },
ref ref
@ -57,7 +59,6 @@ export const FormTextAreaField = React.forwardRef<
control={control} control={control}
name={name} name={name}
rules={{ required }} rules={{ required }}
disabled={disabled}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => ( render={({ field, fieldState, formState }) => (
<FormItem ref={ref} className={cn(className, "flex flex-col space-y-3")}> <FormItem ref={ref} className={cn(className, "flex flex-col space-y-3")}>
@ -65,17 +66,29 @@ export const FormTextAreaField = React.forwardRef<
<FormControl className='grow'> <FormControl className='grow'>
{autoSize ? ( {autoSize ? (
<AutosizeTextarea <AutosizeTextarea
{...field}
placeholder={placeholder} placeholder={placeholder}
className='resize-y' className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : "",
"resize-y"
)}
{...props} {...props}
{...field}
/> />
) : ( ) : (
<Textarea {...field} placeholder={placeholder} className='resize-y' {...props} /> <Textarea
placeholder={placeholder}
className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : "",
"resize-y"
)}
{...props}
{...field}
/>
)} )}
</FormControl> </FormControl>
{description && <FormDescription>{description}</FormDescription>} {description && <FormDescription>{description}</FormDescription>}
<FormMessage /> <FormErrorMessage />
</FormItem> </FormItem>
)} )}
/> />

View File

@ -1,23 +1,10 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import { FormControl, FormDescription, FormField, FormItem, Input, InputProps } from "@/ui";
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
Input,
InputProps,
} from "@/ui";
import * as React from "react"; import * as React from "react";
import { createElement } from "react"; import { createElement } from "react";
import { import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
FieldErrors, import { FormErrorMessage } from "./FormErrorMessage";
FieldPath,
FieldValues,
UseControllerProps,
useFormContext,
} from "react-hook-form";
import { FormLabel, FormLabelProps } from "./FormLabel"; import { FormLabel, FormLabelProps } from "./FormLabel";
import { FormInputProps, FormInputWithIconProps } from "./FormProps"; import { FormInputProps, FormInputWithIconProps } from "./FormProps";
@ -30,28 +17,25 @@ export type FormTextFieldProps<
FormInputProps & FormInputProps &
Partial<FormLabelProps> & Partial<FormLabelProps> &
FormInputWithIconProps & FormInputWithIconProps &
UseControllerProps<TFieldValues, TName> & { UseControllerProps<TFieldValues, TName>;
errors?: FieldErrors<TFieldValues>;
};
export const FormTextField = React.forwardRef< export const FormTextField = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormTextFieldProps React.HTMLAttributes<HTMLDivElement> & FormTextFieldProps
>((props, ref) => { >((props, ref) => {
const { const {
name,
label, label,
placeholder,
hint, hint,
placeholder,
description, description,
required, required,
className, className,
leadIcon, leadIcon,
trailIcon, trailIcon,
button, button,
disabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
errors,
name,
type, type,
} = props; } = props;
@ -62,55 +46,64 @@ export const FormTextField = React.forwardRef<
control={control} control={control}
name={name} name={name}
rules={{ required }} rules={{ required }}
disabled={disabled}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => ( render={({ field, fieldState, formState }) => {
<FormItem ref={ref} className={cn(className, "space-y-3")}> return (
{label && <FormLabel label={label} hint={hint} required={required} />} <FormItem ref={ref} className={cn(className, "space-y-3")}>
<div className={cn(button ? "flex" : null)}> {label && <FormLabel label={label} hint={hint} required={required} />}
<div <div className={cn(button ? "flex" : null)}>
className={cn( <div
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : "" className={cn(
)} leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
> )}
{leadIcon && (
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
{React.createElement(
leadIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
<FormControl
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
> >
<Input type={type} placeholder={placeholder} disabled={disabled} {...field} /> {leadIcon && (
</FormControl> <div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
{React.createElement(
leadIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
{trailIcon && ( <FormControl
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'> className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
{createElement( >
trailIcon, <Input
{ type={type}
className: "h-5 w-5 text-muted-foreground", placeholder={placeholder}
"aria-hidden": true, className={cn(
}, fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
null )}
)} {...field}
</div> />
)} </FormControl>
{trailIcon && (
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
{createElement(
trailIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
</div>
{button && <>{createElement(button)}</>}
</div> </div>
{button && <>{createElement(button)}</>}
</div> {description && <FormDescription>{description}</FormDescription>}
{description && <FormDescription>{description}</FormDescription>} <FormErrorMessage />
<FormMessage /> </FormItem>
</FormItem> );
)} }}
/> />
); );
}); });

View File

@ -1,4 +1,5 @@
export * from "./FormDatePickerField"; export * from "./FormDatePickerField";
export * from "./FormErrorMessage";
export * from "./FormGroup"; export * from "./FormGroup";
export * from "./FormLabel"; export * from "./FormLabel";
export * from "./FormMoneyField"; export * from "./FormMoneyField";

View File

@ -21,8 +21,8 @@
--secondary-foreground: 25 18% 30%; --secondary-foreground: 25 18% 30%;
--accent: 25 23% 83%; --accent: 25 23% 83%;
--accent-foreground: 25 23% 23%; --accent-foreground: 25 23% 23%;
--destructive: 13 96% 20%; --destructive: 0 72.2% 50.6%; /* 13 96% 20%; */
--destructive-foreground: 13 96% 80%; --destructive-foreground: 0 85.7% 97.3%; /* 13 96% 80%; */
--ring: 25 31% 75%; --ring: 25 31% 75%;
--radius: 0.5rem; --radius: 0.5rem;
} }

View File

@ -29,7 +29,8 @@
"open_menu": "Abrir el menú", "open_menu": "Abrir el menú",
"duplicate_rows": "Duplicar", "duplicate_rows": "Duplicar",
"duplicate_rows_tooltip": "Duplica las fila(s) seleccionadas(s)", "duplicate_rows_tooltip": "Duplica las fila(s) seleccionadas(s)",
"pick_date": "Elige una fecha" "pick_date": "Elige una fecha",
"required_field": "Este campo es obligatorio"
}, },
"main_menu": { "main_menu": {
"home": "Inicio", "home": "Inicio",

View File

@ -22,9 +22,7 @@ type FormFieldContextValue<
name: TName; name: TName;
}; };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
{} as FormFieldContextValue
);
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@ -66,22 +64,19 @@ type FormItemContextValue = {
id: string; id: string;
}; };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
{} as FormItemContextValue
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
}
); );
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem"; FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
@ -105,18 +100,13 @@ const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>, React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot> React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
useFormField();
return ( return (
<Slot <Slot
ref={ref} ref={ref}
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
@ -146,7 +136,8 @@ const FormMessage = React.forwardRef<
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField(); const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
const body = error && error.message ? String(error?.message || error.root?.message) : children;
if (!body) { if (!body) {
return null; return null;

View File

@ -1,9 +1,8 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
export interface InputProps export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
@ -17,9 +16,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} }
) );
Input.displayName = "Input" Input.displayName = "Input";
export { Input } export { Input };