Openstatus
www.openstatus.dev
1"use client";
2
3import type * as LabelPrimitive from "@radix-ui/react-label";
4import { Slot } from "@radix-ui/react-slot";
5import * as React from "react";
6import {
7 Controller,
8 type ControllerProps,
9 type FieldPath,
10 type FieldValues,
11 FormProvider,
12 useFormContext,
13 useFormState,
14} from "react-hook-form";
15
16import { Label } from "@/components/ui/label";
17import { cn } from "@/lib/utils";
18
19const Form = FormProvider;
20
21type FormFieldContextValue<
22 TFieldValues extends FieldValues = FieldValues,
23 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
24> = {
25 name: TName;
26};
27
28const FormFieldContext = React.createContext<FormFieldContextValue>(
29 {} as FormFieldContextValue,
30);
31
32const FormField = <
33 TFieldValues extends FieldValues = FieldValues,
34 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
35>({
36 ...props
37}: ControllerProps<TFieldValues, TName>) => {
38 return (
39 <FormFieldContext.Provider value={{ name: props.name }}>
40 <Controller {...props} />
41 </FormFieldContext.Provider>
42 );
43};
44
45const useFormField = () => {
46 const fieldContext = React.useContext(FormFieldContext);
47 const itemContext = React.useContext(FormItemContext);
48 const { getFieldState } = useFormContext();
49 const formState = useFormState({ name: fieldContext.name });
50 const fieldState = getFieldState(fieldContext.name, formState);
51
52 if (!fieldContext) {
53 throw new Error("useFormField should be used within <FormField>");
54 }
55
56 const { id } = itemContext;
57
58 return {
59 id,
60 name: fieldContext.name,
61 formItemId: `${id}-form-item`,
62 formDescriptionId: `${id}-form-item-description`,
63 formMessageId: `${id}-form-item-message`,
64 ...fieldState,
65 };
66};
67
68type FormItemContextValue = {
69 id: string;
70};
71
72const FormItemContext = React.createContext<FormItemContextValue>(
73 {} as FormItemContextValue,
74);
75
76function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 const id = React.useId();
78
79 return (
80 <FormItemContext.Provider value={{ id }}>
81 <div
82 data-slot="form-item"
83 className={cn("grid gap-2", className)}
84 {...props}
85 />
86 </FormItemContext.Provider>
87 );
88}
89
90function FormLabel({
91 className,
92 ...props
93}: React.ComponentProps<typeof LabelPrimitive.Root>) {
94 const { error, formItemId } = useFormField();
95
96 return (
97 <Label
98 data-slot="form-label"
99 data-error={!!error}
100 className={cn("data-[error=true]:text-destructive", className)}
101 htmlFor={formItemId}
102 {...props}
103 />
104 );
105}
106
107function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
108 const { error, formItemId, formDescriptionId, formMessageId } =
109 useFormField();
110
111 return (
112 <Slot
113 data-slot="form-control"
114 id={formItemId}
115 aria-describedby={
116 !error
117 ? `${formDescriptionId}`
118 : `${formDescriptionId} ${formMessageId}`
119 }
120 aria-invalid={!!error}
121 {...props}
122 />
123 );
124}
125
126function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
127 const { formDescriptionId } = useFormField();
128
129 return (
130 <p
131 data-slot="form-description"
132 id={formDescriptionId}
133 className={cn("text-muted-foreground text-sm", className)}
134 {...props}
135 />
136 );
137}
138
139function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
140 const { error, formMessageId } = useFormField();
141 const body = error ? String(error?.message ?? "") : props.children;
142
143 if (!body) {
144 return null;
145 }
146
147 return (
148 <p
149 data-slot="form-message"
150 id={formMessageId}
151 className={cn("text-destructive text-sm", className)}
152 {...props}
153 >
154 {body}
155 </p>
156 );
157}
158
159export {
160 useFormField,
161 Form,
162 FormItem,
163 FormLabel,
164 FormControl,
165 FormDescription,
166 FormMessage,
167 FormField,
168};