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