A daily game
at main 167 lines 3.9 kB view raw
1"use client" 2 3import * as LabelPrimitive from "@radix-ui/react-label" 4import { Slot } from "@radix-ui/react-slot" 5import { 6 Controller, 7 FormProvider, 8 useFormContext, 9 useFormState, 10 type ControllerProps, 11 type FieldPath, 12 type FieldValues, 13} from "react-hook-form" 14 15import * as React from "react" 16 17import { Label } from "@/components/ui/label" 18 19import { cn } from "@/lib/utils" 20 21const Form = FormProvider 22 23type FormFieldContextValue< 24 TFieldValues extends FieldValues = FieldValues, 25 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 26> = { 27 name: TName 28} 29 30const FormFieldContext = React.createContext<FormFieldContextValue>( 31 {} as FormFieldContextValue, 32) 33 34function FormField< 35 TFieldValues extends FieldValues = FieldValues, 36 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 37>({ ...props }: 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("font-heading", className)} 101 htmlFor={formItemId} 102 {...props} 103 /> 104 ) 105} 106 107function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { 108 const { error, formItemId, formDescriptionId, formMessageId } = useFormField() 109 110 return ( 111 <Slot 112 data-slot="form-control" 113 id={formItemId} 114 aria-describedby={ 115 !error 116 ? `${formDescriptionId}` 117 : `${formDescriptionId} ${formMessageId}` 118 } 119 aria-invalid={!!error} 120 {...props} 121 /> 122 ) 123} 124 125function FormDescription({ className, ...props }: React.ComponentProps<"p">) { 126 const { formDescriptionId } = useFormField() 127 128 return ( 129 <p 130 data-slot="form-description" 131 id={formDescriptionId} 132 className={cn("text-sm font-base text-foreground", className)} 133 {...props} 134 /> 135 ) 136} 137 138function FormMessage({ className, ...props }: React.ComponentProps<"p">) { 139 const { error, formMessageId } = useFormField() 140 const body = error ? String(error?.message ?? "") : props.children 141 142 if (!body) { 143 return null 144 } 145 146 return ( 147 <p 148 data-slot="form-message" 149 id={formMessageId} 150 className={cn("text-sm font-base text-red-500", className)} 151 {...props} 152 > 153 {body} 154 </p> 155 ) 156} 157 158export { 159 useFormField, 160 Form, 161 FormItem, 162 FormLabel, 163 FormControl, 164 FormDescription, 165 FormMessage, 166 FormField, 167}