Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/components/ui/input/InputRadioGroupCards.tsx
T

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