gitscope
← marzneshin / v0.5.5 / dashboard/src/components/ui/form.tsx
https://dev.kronoz.win/r/marzneshin.git
dashboard/src/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/utils"
 14import { Label } from "@marzneshin/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}