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