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