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