Uecko_ERP/packages/rdx-ui/src/components/form/radio-group-field.tsx
2026-04-03 18:15:25 +02:00

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>
)}
/>
);
};