140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
import {
|
|
Field,
|
|
FieldDescription,
|
|
FieldError,
|
|
FormControl,
|
|
RadioGroup,
|
|
RadioGroupItem,
|
|
} from "@repo/shadcn-ui/components";
|
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
import * as React from "react";
|
|
import { Controller, type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
|
|
|
|
import { FormFieldLabel } from "./form-field-label.tsx";
|
|
|
|
export interface RadioGroupFieldItem {
|
|
value: string;
|
|
label: string;
|
|
description?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
type RadioGroupFieldProps<TFormValues extends FieldValues> = {
|
|
name: FieldPath<TFormValues>;
|
|
items: RadioGroupFieldItem[];
|
|
|
|
label?: string;
|
|
description?: string;
|
|
|
|
disabled?: boolean;
|
|
required?: boolean;
|
|
readOnly?: boolean;
|
|
|
|
orientation?: "vertical" | "horizontal" | "responsive";
|
|
|
|
className?: string;
|
|
inputClassName?: string;
|
|
itemClassName?: string;
|
|
};
|
|
|
|
export const RadioGroupField = <TFormValues extends FieldValues>({
|
|
name,
|
|
items,
|
|
label,
|
|
description,
|
|
disabled = false,
|
|
required = false,
|
|
readOnly = false,
|
|
orientation = "vertical",
|
|
className,
|
|
inputClassName,
|
|
itemClassName,
|
|
}: RadioGroupFieldProps<TFormValues>) => {
|
|
const baseId = React.useId();
|
|
const { control, formState } = useFormContext<TFormValues>();
|
|
|
|
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
|
|
|
|
return (
|
|
<Controller
|
|
control={control}
|
|
name={name}
|
|
render={({ field, fieldState }) => (
|
|
<Field
|
|
className={cn("gap-1", className)}
|
|
data-invalid={fieldState.invalid}
|
|
orientation={orientation}
|
|
>
|
|
{label ? (
|
|
<FormFieldLabel
|
|
htmlFor={`${baseId}-${items[0]?.value ?? "option"}`}
|
|
required={required}
|
|
>
|
|
{label}
|
|
</FormFieldLabel>
|
|
) : null}
|
|
|
|
<FormControl>
|
|
<RadioGroup
|
|
className={cn(
|
|
"gap-3",
|
|
orientation === "horizontal" && "flex flex-row flex-wrap items-center gap-6",
|
|
orientation === "responsive" &&
|
|
"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-6",
|
|
inputClassName
|
|
)}
|
|
disabled={isDisabled}
|
|
name={field.name}
|
|
onValueChange={field.onChange}
|
|
value={field.value ?? ""}
|
|
>
|
|
{items.map((item, index) => {
|
|
const itemId = `${baseId}-${index}-${item.value}`;
|
|
const itemDisabled = Boolean(isDisabled || item.disabled);
|
|
|
|
return (
|
|
<div className={cn("flex items-start gap-2", itemClassName)} key={item.value}>
|
|
<RadioGroupItem
|
|
aria-invalid={fieldState.invalid}
|
|
aria-required={required || undefined}
|
|
className="mt-0.5"
|
|
disabled={itemDisabled}
|
|
id={itemId}
|
|
onBlur={field.onBlur}
|
|
value={item.value}
|
|
/>
|
|
|
|
<div className="grid gap-0.5">
|
|
<label
|
|
className={cn(
|
|
"cursor-pointer text-sm font-medium leading-none",
|
|
itemDisabled && "cursor-not-allowed opacity-70"
|
|
)}
|
|
htmlFor={itemId}
|
|
>
|
|
{item.label}
|
|
</label>
|
|
|
|
{item.description ? (
|
|
<p className="text-muted-foreground text-sm">{item.description}</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</RadioGroup>
|
|
</FormControl>
|
|
|
|
{description ? (
|
|
<FieldDescription>{description}</FieldDescription>
|
|
) : (
|
|
<div aria-hidden="true" className="min-h-5" />
|
|
)}
|
|
|
|
<FieldError errors={[fieldState.error]} />
|
|
</Field>
|
|
)}
|
|
/>
|
|
);
|
|
};
|