Files
tailwind-ui-library/src/form/Checkbox.tsx
T
tbenandClaude Opus 5.5 66c2f6850e Clamp Checkbox labels to one line, add wrap prop
Labels no longer overflow their container: by default they are clamped
to a single line with an ellipsis and the full label is shown as the
wrapper's title on hover. Pass `wrap` to let the label flow onto
multiple lines. Adds the `twui-checkbox-label` class.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:04:03 +01:00

173 lines
5.3 KiB
TypeScript

import React, {
type ComponentProps,
type DetailedHTMLProps,
type HTMLAttributes,
type ReactNode,
} from "react";
import { twMerge } from "tailwind-merge";
import CheckMarkSVG from "../svgs/CheckMarkSVG";
import Stack from "../layout/Stack";
import Row from "../layout/Row";
import { Info } from "lucide-react";
import Span from "../layout/Span";
export type CheckboxProps = Omit<
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>,
"title"
> & {
title?: string | ReactNode;
wrapperProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
label?: string | ReactNode;
labelProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
defaultChecked?: boolean;
wrapperClassName?: string;
setChecked?: React.Dispatch<React.SetStateAction<boolean>>;
checked?: boolean;
readOnly?: boolean;
noLabel?: boolean;
size?: number;
changeHandler?: (value: boolean) => void;
clickDispatchHandler?: (value: boolean) => void;
info?: string | ReactNode;
wrapperWrapperProps?: ComponentProps<typeof Stack>;
/**
* Let the label wrap onto multiple lines. By default the label is
* clamped to a single line with an ellipsis, and the full text is
* shown as the wrapper's `title` on hover.
*/
wrap?: boolean;
};
/**
* # Checkbox Component
* @className twui-checkbox
* @className twui-checkbox-checked
* @className twui-checkbox-unchecked
* @className twui-checkbox-label
*/
export default function Checkbox({
wrapperProps,
label,
labelProps,
size,
wrapperClassName,
defaultChecked,
setChecked: externalSetChecked,
readOnly,
checked: externalChecked,
changeHandler,
info,
wrapperWrapperProps,
noLabel,
title,
clickDispatchHandler,
wrap,
...props
}: CheckboxProps) {
const finalSize = size || 20;
const [checked, setChecked] = React.useState(
defaultChecked || externalChecked || false,
);
const finalTitle = title;
const labelContent = label || finalTitle;
const hoverTitle =
wrapperProps?.title ??
(typeof labelContent == "string" || typeof labelContent == "number"
? String(labelContent)
: typeof finalTitle == "string"
? finalTitle
: undefined);
React.useEffect(() => {
if (typeof externalChecked == "undefined") return;
setChecked(externalChecked);
}, [externalChecked]);
React.useEffect(() => {
changeHandler?.(checked);
}, [checked]);
return (
<Stack
{...wrapperWrapperProps}
className={twMerge(
"gap-1.5 min-w-0 max-w-full",
wrapperWrapperProps?.className,
)}
>
<div
{...wrapperProps}
title={hoverTitle}
className={twMerge(
"flex gap-2 flex-nowrap min-w-0 max-w-full",
wrap ? "items-start" : "items-center",
readOnly ? "opacity-70 pointer-events-none" : "",
wrapperClassName,
wrapperProps?.className,
)}
onClick={(e) => {
setChecked(!checked);
externalSetChecked?.(!checked);
clickDispatchHandler?.(!checked);
}}
>
<div
{...props}
className={twMerge(
"flex items-center justify-center p-[3px] rounded-default shrink-0",
checked
? "bg-primary twui-checkbox-checked text-white outline-slate-400"
: "dark:outline-white/50 outline-2 -outline-offset-2 twui-checkbox-unchecked",
"twui-checkbox",
props.className,
)}
style={{
minWidth: finalSize + "px",
width: finalSize + "px",
height: finalSize + "px",
...props.style,
}}
>
{checked && <CheckMarkSVG />}
</div>
{!noLabel && (
<Stack className="gap-0.5 min-w-0 max-w-full">
<div
{...labelProps}
className={twMerge(
"select-none max-w-full twui-checkbox-label",
wrap
? "whitespace-normal break-words"
: "line-clamp-1 break-all",
labelProps?.className,
)}
>
{labelContent}
</div>
</Stack>
)}
</div>
{info && (
<Row className="gap-1 flex-nowrap" title={info.toString()}>
<Info size={13} className="opacity-40 min-w-[20px]" />
<Span size="smaller" className="opacity-70">
{info}
</Span>
</Row>
)}
</Stack>
);
}