blob: c6a08f6380015eb77c02ddf4b3e9780e31a49e78 [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import * as React from "react";
2import * as LabelPrimitive from "@radix-ui/react-label";
3import { Slot } from "@radix-ui/react-slot";
4import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
gio5f2f1002025-03-20 18:38:48 +04005
giod0026612025-05-08 13:00:36 +00006import { cn } from "@/lib/utils";
7import { Label } from "@/components/ui/label";
gio5f2f1002025-03-20 18:38:48 +04008
giod0026612025-05-08 13:00:36 +00009const Form = FormProvider;
gio5f2f1002025-03-20 18:38:48 +040010
11type FormFieldContextValue<
giod0026612025-05-08 13:00:36 +000012 TFieldValues extends FieldValues = FieldValues,
13 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
gio5f2f1002025-03-20 18:38:48 +040014> = {
giod0026612025-05-08 13:00:36 +000015 name: TName;
16};
gio5f2f1002025-03-20 18:38:48 +040017
giod0026612025-05-08 13:00:36 +000018const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
gio5f2f1002025-03-20 18:38:48 +040019
20const FormField = <
giod0026612025-05-08 13:00:36 +000021 TFieldValues extends FieldValues = FieldValues,
22 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
gio5f2f1002025-03-20 18:38:48 +040023>({
giod0026612025-05-08 13:00:36 +000024 ...props
gio5f2f1002025-03-20 18:38:48 +040025}: ControllerProps<TFieldValues, TName>) => {
giod0026612025-05-08 13:00:36 +000026 return (
27 <FormFieldContext.Provider value={{ name: props.name }}>
28 <Controller {...props} />
29 </FormFieldContext.Provider>
30 );
31};
gio5f2f1002025-03-20 18:38:48 +040032
33const useFormField = () => {
giod0026612025-05-08 13:00:36 +000034 const fieldContext = React.useContext(FormFieldContext);
35 const itemContext = React.useContext(FormItemContext);
36 const { getFieldState, formState } = useFormContext();
gio5f2f1002025-03-20 18:38:48 +040037
giod0026612025-05-08 13:00:36 +000038 const fieldState = getFieldState(fieldContext.name, formState);
gio5f2f1002025-03-20 18:38:48 +040039
giod0026612025-05-08 13:00:36 +000040 if (!fieldContext) {
41 throw new Error("useFormField should be used within <FormField>");
42 }
gio5f2f1002025-03-20 18:38:48 +040043
giod0026612025-05-08 13:00:36 +000044 const { id } = itemContext;
gio5f2f1002025-03-20 18:38:48 +040045
giod0026612025-05-08 13:00:36 +000046 return {
47 id,
48 name: fieldContext.name,
49 formItemId: `${id}-form-item`,
50 formDescriptionId: `${id}-form-item-description`,
51 formMessageId: `${id}-form-item-message`,
52 ...fieldState,
53 };
54};
gio5f2f1002025-03-20 18:38:48 +040055
56type FormItemContextValue = {
giod0026612025-05-08 13:00:36 +000057 id: string;
58};
gio5f2f1002025-03-20 18:38:48 +040059
giod0026612025-05-08 13:00:36 +000060const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
gio5f2f1002025-03-20 18:38:48 +040061
giod0026612025-05-08 13:00:36 +000062const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
63 ({ className, ...props }, ref) => {
64 const id = React.useId();
gio5f2f1002025-03-20 18:38:48 +040065
giod0026612025-05-08 13:00:36 +000066 return (
67 <FormItemContext.Provider value={{ id }}>
68 <div ref={ref} className={cn("space-y-2", className)} {...props} />
69 </FormItemContext.Provider>
70 );
71 },
72);
73FormItem.displayName = "FormItem";
gio5f2f1002025-03-20 18:38:48 +040074
75const FormLabel = React.forwardRef<
giod0026612025-05-08 13:00:36 +000076 React.ElementRef<typeof LabelPrimitive.Root>,
77 React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
gio5f2f1002025-03-20 18:38:48 +040078>(({ className, ...props }, ref) => {
giod0026612025-05-08 13:00:36 +000079 const { error, formItemId } = useFormField();
gio5f2f1002025-03-20 18:38:48 +040080
giod0026612025-05-08 13:00:36 +000081 return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
82});
83FormLabel.displayName = "FormLabel";
gio5f2f1002025-03-20 18:38:48 +040084
giod0026612025-05-08 13:00:36 +000085const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
86 ({ ...props }, ref) => {
87 const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
gio5f2f1002025-03-20 18:38:48 +040088
giod0026612025-05-08 13:00:36 +000089 return (
90 <Slot
91 ref={ref}
92 id={formItemId}
93 aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
94 aria-invalid={!!error}
95 {...props}
96 />
97 );
98 },
99);
100FormControl.displayName = "FormControl";
gio5f2f1002025-03-20 18:38:48 +0400101
giod0026612025-05-08 13:00:36 +0000102const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
103 ({ className, ...props }, ref) => {
104 const { formDescriptionId } = useFormField();
gio5f2f1002025-03-20 18:38:48 +0400105
giod0026612025-05-08 13:00:36 +0000106 return (
107 <p
108 ref={ref}
109 id={formDescriptionId}
110 className={cn("text-[0.8rem] text-muted-foreground", className)}
111 {...props}
112 />
113 );
114 },
115);
116FormDescription.displayName = "FormDescription";
gio5f2f1002025-03-20 18:38:48 +0400117
giod0026612025-05-08 13:00:36 +0000118const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
119 ({ className, children, ...props }, ref) => {
120 const { error, formMessageId } = useFormField();
121 const body = error ? String(error?.message) : children;
gio5f2f1002025-03-20 18:38:48 +0400122
giod0026612025-05-08 13:00:36 +0000123 if (!body) {
124 return null;
125 }
gio5f2f1002025-03-20 18:38:48 +0400126
giod0026612025-05-08 13:00:36 +0000127 return (
128 <p
129 ref={ref}
130 id={formMessageId}
131 className={cn("text-[0.8rem] font-medium text-destructive", className)}
132 {...props}
133 >
134 {body}
135 </p>
136 );
137 },
138);
139FormMessage.displayName = "FormMessage";
gio5f2f1002025-03-20 18:38:48 +0400140
giod0026612025-05-08 13:00:36 +0000141export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };