151 lines · 3.7 KB · TypeScript
RawDownload
import * as React from "react";import type { Label as LabelPrimitive } from "radix-ui";import { Slot } from "radix-ui";import {  Controller,  FormProvider,  useFormContext,  useFormState,  type ControllerProps,  type FieldPath,  type FieldValues,} from "react-hook-form"; import { cn } from "../lib/utils";import { Label } from "./label"; const Form = FormProvider; type FormFieldContextValue<  TFieldValues extends FieldValues = FieldValues,  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,> = {  name: TName;}; const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue); const FormField = <  TFieldValues extends FieldValues = FieldValues,  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,>({  ...props}: ControllerProps<TFieldValues, TName>) => {  return (    <FormFieldContext.Provider value={{ name: props.name }}>      <Controller {...props} />    </FormFieldContext.Provider>  );}; const useFormField = () => {  const fieldContext = React.useContext(FormFieldContext);  const itemContext = React.useContext(FormItemContext);  const { getFieldState } = useFormContext();  const formState = useFormState({ name: fieldContext.name });  const fieldState = getFieldState(fieldContext.name, formState);   if (!fieldContext) {    throw new Error("useFormField should be used within <FormField>");  }   const { id } = itemContext;   return {    id,    name: fieldContext.name,    formItemId: `${id}-form-item`,    formDescriptionId: `${id}-form-item-description`,    formMessageId: `${id}-form-item-message`,    ...fieldState,  };}; type FormItemContextValue = {  id: string;}; const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue); function FormItem({ className, ...props }: React.ComponentProps<"div">) {  const id = React.useId();   return (    <FormItemContext.Provider value={{ id }}>      <div data-slot="form-item" className={cn("grid gap-2", className)} {...props} />    </FormItemContext.Provider>  );} function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {  const { error, formItemId } = useFormField();   return (    <Label      data-slot="form-label"      data-error={!!error}      className={cn("data-[error=true]:text-destructive", className)}      htmlFor={formItemId}      {...props}    />  );} function FormControl({ ...props }: React.ComponentProps<typeof Slot.Root>) {  const { error, formItemId, formDescriptionId, formMessageId } = useFormField();   return (    <Slot.Root      data-slot="form-control"      id={formItemId}      aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}      aria-invalid={!!error}      {...props}    />  );} function FormDescription({ className, ...props }: React.ComponentProps<"p">) {  const { formDescriptionId } = useFormField();   return (    <p      data-slot="form-description"      id={formDescriptionId}      className={cn("text-sm text-muted-foreground", className)}      {...props}    />  );} function FormMessage({ className, ...props }: React.ComponentProps<"p">) {  const { error, formMessageId } = useFormField();  // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: react-hook-form field errors carry public validation text  const body = error ? String(error?.message ?? "") : props.children;   if (!body) {    return null;  }   return (    <p      data-slot="form-message"      id={formMessageId}      className={cn("text-sm text-destructive", className)}      {...props}    >      {body}    </p>  );} export {  useFormField,  Form,  FormItem,  FormLabel,  FormControl,  FormDescription,  FormMessage,  FormField,};