main
tsx 129 lines 3.92 KB
Raw
1 import * as React from "react";
2 import * as LabelPrimitive from "@radix-ui/react-label";
3 import { Slot } from "@radix-ui/react-slot";
4 import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
5
6 import { cn } from "@/lib/utils";
7 import { Label } from "@/components/ui/label";
8
9 const Form = FormProvider;
10
11 type FormFieldContextValue<
12 TFieldValues extends FieldValues = FieldValues,
13 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
14 > = {
15 name: TName;
16 };
17
18 const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
19
20 const FormField = <
21 TFieldValues extends FieldValues = FieldValues,
22 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
23 >({
24 ...props
25 }: ControllerProps<TFieldValues, TName>) => {
26 return (
27 <FormFieldContext.Provider value={{ name: props.name }}>
28 <Controller {...props} />
29 </FormFieldContext.Provider>
30 );
31 };
32
33 const useFormField = () => {
34 const fieldContext = React.useContext(FormFieldContext);
35 const itemContext = React.useContext(FormItemContext);
36 const { getFieldState, formState } = useFormContext();
37
38 const fieldState = getFieldState(fieldContext.name, formState);
39
40 if (!fieldContext) {
41 throw new Error("useFormField should be used within <FormField>");
42 }
43
44 const { id } = itemContext;
45
46 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 };
55
56 type FormItemContextValue = {
57 id: string;
58 };
59
60 const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
61
62 const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
63 ({ className, ...props }, ref) => {
64 const id = React.useId();
65
66 return (
67 <FormItemContext.Provider value={{ id }}>
68 <div ref={ref} className={cn("space-y-2", className)} {...props} />
69 </FormItemContext.Provider>
70 );
71 },
72 );
73 FormItem.displayName = "FormItem";
74
75 const FormLabel = React.forwardRef<
76 React.ElementRef<typeof LabelPrimitive.Root>,
77 React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
78 >(({ className, ...props }, ref) => {
79 const { error, formItemId } = useFormField();
80
81 return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
82 });
83 FormLabel.displayName = "FormLabel";
84
85 const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
86 ({ ...props }, ref) => {
87 const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
88
89 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 );
100 FormControl.displayName = "FormControl";
101
102 const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
103 ({ className, ...props }, ref) => {
104 const { formDescriptionId } = useFormField();
105
106 return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
107 },
108 );
109 FormDescription.displayName = "FormDescription";
110
111 const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
112 ({ className, children, ...props }, ref) => {
113 const { error, formMessageId } = useFormField();
114 const body = error ? String(error?.message) : children;
115
116 if (!body) {
117 return null;
118 }
119
120 return (
121 <p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
122 {body}
123 </p>
124 );
125 },
126 );
127 FormMessage.displayName = "FormMessage";
128
129 export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };