149 lines
5.0 KiB
TypeScript
149 lines
5.0 KiB
TypeScript
import clsx from "clsx";
|
|
import HintTooltip from "../tooltips/HintTooltip";
|
|
import { useState, useEffect, ReactNode } from "react";
|
|
import { Colors } from "~/application/enums/shared/Colors";
|
|
import { RadioCardGroup } from "~/ui/components/RadioCardGroup";
|
|
|
|
// Subframe migration (strangler shim):
|
|
// Keeps the original InputRadioGroupCards API but renders the Subframe design-system
|
|
// `RadioCardGroup`. The public props (title/options/value/onChange/columns/display/...) are
|
|
// unchanged; call-sites are untouched.
|
|
//
|
|
// The Subframe Root provides only the radio behaviour + selected styling (no label, no grid),
|
|
// so the surrounding label (title/required/help/hint) and the columns grid stay in this shim.
|
|
// The grid classes are passed via `className` (applied last by Subframe's twClassNames) to
|
|
// override the Root's default `flex` layout.
|
|
//
|
|
// controlled + uncontrolled: Subframe/Radix Root is internally controlled. The legacy field
|
|
// already held its own `actualValue` state (seeded from value || defaultValue) and called
|
|
// `onChange` on change — that behaviour is preserved, so selection works whether or not the
|
|
// parent supplies value/onChange.
|
|
|
|
type ItemDto = {
|
|
value: string | number;
|
|
name: string;
|
|
color?: Colors;
|
|
icon?: ReactNode;
|
|
disabled?: boolean;
|
|
renderName?: React.ReactNode;
|
|
};
|
|
export default function InputRadioGroupCards({
|
|
title,
|
|
name,
|
|
options,
|
|
defaultValue,
|
|
value,
|
|
onChange,
|
|
required,
|
|
disabled,
|
|
help,
|
|
hint,
|
|
columns,
|
|
className,
|
|
display,
|
|
}: {
|
|
title?: string;
|
|
name?: string;
|
|
options: ItemDto[];
|
|
defaultValue?: string;
|
|
value?: string;
|
|
onChange?: (value: string) => void;
|
|
required?: boolean;
|
|
disabled?: boolean;
|
|
help?: string;
|
|
hint?: React.ReactNode;
|
|
columns?: number;
|
|
className?: string;
|
|
display?: "name" | "value" | "nameAndValue";
|
|
}) {
|
|
const [displayType, setDisplayType] = useState<"name" | "value" | "nameAndValue">("name");
|
|
|
|
const [actualValue, setActualValue] = useState<string | undefined>(value || defaultValue);
|
|
|
|
useEffect(() => {
|
|
if (actualValue && onChange && value !== actualValue) {
|
|
onChange(actualValue);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [actualValue]);
|
|
|
|
useEffect(() => {
|
|
if (display) {
|
|
setDisplayType(display);
|
|
} else {
|
|
const hasNames = options.some((item) => item.name);
|
|
if (!hasNames) {
|
|
setDisplayType("value");
|
|
} else {
|
|
const namesAreDifferent = options.some((item) => item.name !== item.value.toString());
|
|
if (namesAreDifferent) {
|
|
setDisplayType("nameAndValue");
|
|
} else {
|
|
setDisplayType("name");
|
|
}
|
|
}
|
|
}
|
|
}, [display, options]);
|
|
|
|
return (
|
|
<div className={className}>
|
|
{title && (
|
|
<div className="mb-1 flex justify-between space-x-2 text-xs font-medium">
|
|
<div className="flex items-center space-x-1">
|
|
<div className="truncate">
|
|
{title}
|
|
{required && <span className="ml-1 text-red-600">*</span>}
|
|
</div>
|
|
|
|
{help && <HintTooltip text={help} />}
|
|
</div>
|
|
{hint}
|
|
</div>
|
|
)}
|
|
<input type="hidden" name={name} value={actualValue} hidden readOnly />
|
|
<RadioCardGroup
|
|
value={actualValue}
|
|
onValueChange={(e) => setActualValue(e)}
|
|
aria-label={title}
|
|
className={clsx(
|
|
"grid w-full max-w-none grid-cols-1 gap-3",
|
|
columns === undefined && "md:grid-cols-3",
|
|
columns === 1 && "md:grid-cols-1",
|
|
columns === 2 && "md:grid-cols-2",
|
|
columns === 3 && "md:grid-cols-3",
|
|
columns === 4 && "md:grid-cols-4",
|
|
columns === 5 && "md:grid-cols-5",
|
|
columns === 6 && "md:grid-cols-6",
|
|
columns === 7 && "md:grid-cols-7",
|
|
columns === 8 && "md:grid-cols-8",
|
|
columns === 9 && "md:grid-cols-9",
|
|
columns === 10 && "md:grid-cols-10",
|
|
columns === 11 && "md:grid-cols-11",
|
|
columns === 12 && "md:grid-cols-12"
|
|
)}
|
|
>
|
|
{options.map((item) => (
|
|
<RadioCardGroup.RadioCard
|
|
key={item.value}
|
|
value={item.value.toString()}
|
|
disabled={disabled || item.disabled}
|
|
hideRadio={Boolean(item.icon)}
|
|
className="w-full justify-center"
|
|
>
|
|
{item.icon ? (
|
|
<div className="flex w-full justify-center truncate">{item.icon}</div>
|
|
) : (
|
|
<span className="flex w-full flex-col select-none">
|
|
<span className={clsx("block truncate text-sm", !disabled ? "font-medium" : "")}>
|
|
{["name", "nameAndValue"].includes(displayType) ? <span>{item.renderName ? item.renderName : item.name}</span> : item.value}
|
|
</span>
|
|
{["nameAndValue", "value"].includes(displayType) && <span className="mt-1 flex items-center truncate text-sm">{item.value}</span>}
|
|
</span>
|
|
)}
|
|
</RadioCardGroup.RadioCard>
|
|
))}
|
|
</RadioCardGroup>
|
|
</div>
|
|
);
|
|
}
|