126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
import { Switch } from "@headlessui/react";
|
|
import clsx from "clsx";
|
|
import { ReactNode, useEffect, useState } from "react";
|
|
import EntityIcon from "~/components/layouts/icons/EntityIcon";
|
|
import HintTooltip from "~/components/ui/tooltips/HintTooltip";
|
|
import { Checkbox } from "~/ui/components/Checkbox";
|
|
|
|
interface Props {
|
|
name?: string;
|
|
title?: string;
|
|
withLabel?: boolean;
|
|
value?: boolean | undefined;
|
|
setValue?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
className?: string;
|
|
required?: boolean;
|
|
disabled?: boolean;
|
|
asToggle?: boolean;
|
|
readOnly?: boolean;
|
|
hint?: ReactNode;
|
|
help?: string;
|
|
icon?: string;
|
|
autoFocus?: boolean;
|
|
}
|
|
|
|
export default function InputCheckbox({
|
|
name,
|
|
title,
|
|
withLabel = true,
|
|
value,
|
|
setValue,
|
|
className,
|
|
required,
|
|
disabled,
|
|
asToggle,
|
|
readOnly,
|
|
hint,
|
|
help,
|
|
icon,
|
|
autoFocus,
|
|
}: Props) {
|
|
// useImperativeHandle(ref, () => ({ input }));
|
|
// const input = useRef<HTMLInputElement>(null);
|
|
|
|
// Subframe's Checkbox is always controlled (no uncontrolled/defaultChecked path), so the
|
|
// shim owns the checked state and mirrors it back through setValue — preserving the
|
|
// original controlled+uncontrolled public API.
|
|
const [actualValue, setActualValue] = useState(value ?? false);
|
|
|
|
useEffect(() => {
|
|
setActualValue(value ?? false);
|
|
}, [value]);
|
|
|
|
useEffect(() => {
|
|
if (setValue && actualValue !== value) {
|
|
setValue(actualValue);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [actualValue]);
|
|
|
|
const accessibleName = title || name || undefined;
|
|
|
|
return (
|
|
<div className={clsx(className, "")}>
|
|
{withLabel && title && (
|
|
<label htmlFor={name} 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>
|
|
<div className="">{help && <HintTooltip text={help} />}</div>
|
|
</div>
|
|
{hint}
|
|
</label>
|
|
)}
|
|
<div className={clsx("relative flex w-full rounded-md")}>
|
|
{icon && (
|
|
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<EntityIcon className="text-muted-foreground h-5 w-5" icon={icon} />
|
|
</div>
|
|
)}
|
|
<input type="hidden" name={name} value={actualValue === true ? "true" : "false"} hidden readOnly />
|
|
{asToggle ? (
|
|
// asToggle renders a switch (not a checkbox) — left on the legacy headless-ui Switch;
|
|
// switch migration is tracked separately (Subframe Switch).
|
|
<Switch
|
|
checked={actualValue}
|
|
onChange={setActualValue}
|
|
disabled={disabled || readOnly}
|
|
autoFocus={autoFocus}
|
|
aria-label={accessibleName}
|
|
className={clsx(
|
|
actualValue ? "bg-primary" : " bg-secondary ",
|
|
"focus:ring-primary relative inline-flex h-5 w-8 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden focus:ring-2 focus:ring-offset-2",
|
|
icon && "pl-10",
|
|
(disabled || readOnly) && "cursor-not-allowed opacity-60"
|
|
)}
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className={clsx(
|
|
actualValue ? "translate-x-3" : "translate-x-0",
|
|
"bg-background pointer-events-none inline-block h-4 w-4 transform rounded-full shadow-xs ring-0 transition duration-200 ease-in-out"
|
|
)}
|
|
/>
|
|
</Switch>
|
|
) : (
|
|
<Checkbox
|
|
id={name}
|
|
name={name}
|
|
// The visible <label> above already labels this field; Subframe's own label slot is
|
|
// hidden to avoid a duplicate. aria-label is the accessible name when no label shows.
|
|
hideLabel={true}
|
|
aria-label={!withLabel || !title ? accessibleName : undefined}
|
|
onCheckedChange={(checked) => setActualValue(Boolean(checked))}
|
|
disabled={disabled || readOnly}
|
|
autoFocus={autoFocus}
|
|
checked={actualValue}
|
|
className={clsx((disabled || readOnly) && "cursor-not-allowed", "mt-1", icon && "pl-10")}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|