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