74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
import { ReactNode, useEffect, useState } from "react";
|
|
import clsx from "~/utils/shared/ClassesUtils";
|
|
import { RadioGroup } from "~/ui/components/RadioGroup";
|
|
|
|
// Subframe migration (strangler shim):
|
|
// Keeps the original InputRadioGroup API (options/value/setValue/...) but renders the Subframe
|
|
// design-system `RadioGroup` (horizontal layout). The public props are unchanged.
|
|
//
|
|
// value/setValue keep the original `string | number | undefined` type. Subframe/Radix
|
|
// RadioGroup speaks strings only, so values are stringified at the boundary and mapped back to
|
|
// their original typed value (via the options lookup) before calling setValue.
|
|
//
|
|
// controlled + uncontrolled: Subframe's Root is internally controlled. The legacy field was
|
|
// usually controlled via value/setValue, but setValue is optional, so the shim holds its own
|
|
// state seeded from `value`. When the parent controls `value` it stays in sync; when it
|
|
// doesn't, internal state keeps the selection from sticking.
|
|
|
|
interface Props {
|
|
name?: string;
|
|
title?: string;
|
|
options: { name: string | ReactNode; value: string | number | undefined; disabled?: boolean }[];
|
|
value?: string | number | undefined;
|
|
setValue?: React.Dispatch<React.SetStateAction<string | number | undefined>>;
|
|
className?: string;
|
|
required?: boolean;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
const toKey = (value: string | number | undefined): string => (value === undefined ? "" : String(value));
|
|
|
|
export default function InputRadioGroup({ name, title, value, options, setValue, className, required, disabled }: Props) {
|
|
const [actualValue, setActualValue] = useState<string | number | undefined>(value);
|
|
|
|
useEffect(() => {
|
|
setActualValue(value);
|
|
}, [value]);
|
|
|
|
function onValueChange(nextKey: string) {
|
|
// Map the stringified key back to the original typed option value.
|
|
const matched = options.find((option) => toKey(option.value) === nextKey);
|
|
const next = matched ? matched.value : nextKey;
|
|
setActualValue(next);
|
|
if (setValue) {
|
|
setValue(next);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={clsx(className, "w-full grow")}>
|
|
<div className="flex items-center justify-between">
|
|
<label className="flex justify-between space-x-2 truncate text-xs font-medium">
|
|
{title}
|
|
{required && <span className="ml-1 text-red-600">*</span>}
|
|
</label>
|
|
</div>
|
|
|
|
<input type="hidden" name={name} value={toKey(actualValue)} hidden readOnly />
|
|
|
|
<RadioGroup
|
|
className="mt-1 w-full max-w-none"
|
|
horizontal={true}
|
|
disabled={disabled}
|
|
value={toKey(actualValue)}
|
|
onValueChange={onValueChange}
|
|
aria-label={title}
|
|
>
|
|
{options.map((option, idx) => (
|
|
<RadioGroup.Option key={idx} value={toKey(option.value)} disabled={option.disabled || disabled} label={option.name} className="flex-1" />
|
|
))}
|
|
</RadioGroup>
|
|
</div>
|
|
);
|
|
}
|