Openstatus www.openstatus.dev
at main 168 lines 3.8 kB view raw
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};