156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
// components/ui/Checkbox.tsx
|
|
'use client';
|
|
|
|
import * as React from 'react';
|
|
import clsx from 'clsx';
|
|
|
|
export type CheckboxProps = Omit<
|
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
'type'
|
|
> & {
|
|
/**
|
|
* Label neben der Checkbox
|
|
*/
|
|
label?: React.ReactNode;
|
|
/**
|
|
* Beschreibung unter/neben dem Label
|
|
*/
|
|
description?: React.ReactNode;
|
|
/**
|
|
* Visueller indeterminate-Zustand (Strich statt Haken)
|
|
*/
|
|
indeterminate?: boolean;
|
|
/**
|
|
* Zusätzliche Klassen für das Wrapper-Element (Label+Beschreibung+Checkbox)
|
|
*/
|
|
wrapperClassName?: string;
|
|
/**
|
|
* Zusätzliche Klassen für das Label
|
|
*/
|
|
labelClassName?: string;
|
|
/**
|
|
* Zusätzliche Klassen für die Beschreibung
|
|
*/
|
|
descriptionClassName?: string;
|
|
};
|
|
|
|
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|
function Checkbox(
|
|
{
|
|
label,
|
|
description,
|
|
className,
|
|
wrapperClassName,
|
|
labelClassName,
|
|
descriptionClassName,
|
|
indeterminate,
|
|
id,
|
|
...inputProps
|
|
},
|
|
ref,
|
|
) {
|
|
const innerRef = React.useRef<HTMLInputElement | null>(null);
|
|
|
|
// externe + interne Ref zusammenführen
|
|
React.useImperativeHandle(ref, () => innerRef.current as HTMLInputElement);
|
|
|
|
React.useEffect(() => {
|
|
if (innerRef.current) {
|
|
innerRef.current.indeterminate = Boolean(indeterminate);
|
|
}
|
|
}, [indeterminate]);
|
|
|
|
// Fallback-ID, falls keine übergeben wurde
|
|
const inputId =
|
|
id ??
|
|
(typeof label === 'string'
|
|
? label.toLowerCase().replace(/\s+/g, '-')
|
|
: undefined);
|
|
|
|
const descriptionId =
|
|
description && inputId ? `${inputId}-description` : undefined;
|
|
|
|
return (
|
|
<div className={clsx('flex gap-3', wrapperClassName)}>
|
|
{/* Checkbox-Icon */}
|
|
<div className="flex h-6 shrink-0 items-center">
|
|
<div className="group grid size-4 grid-cols-1">
|
|
<input
|
|
id={inputId}
|
|
ref={innerRef}
|
|
type="checkbox"
|
|
aria-describedby={descriptionId}
|
|
className={clsx(
|
|
'col-start-1 row-start-1 appearance-none rounded-sm border border-gray-300 bg-white',
|
|
'checked:border-indigo-600 checked:bg-indigo-600',
|
|
'indeterminate:border-indigo-600 indeterminate:bg-indigo-600',
|
|
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
|
|
'disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100',
|
|
'dark:border-white/10 dark:bg-white/5',
|
|
'dark:checked:border-indigo-500 dark:checked:bg-indigo-500',
|
|
'dark:indeterminate:border-indigo-500 dark:indeterminate:bg-indigo-500',
|
|
'dark:focus-visible:outline-indigo-500',
|
|
'dark:disabled:border-white/5 dark:disabled:bg-white/10 dark:disabled:checked:bg-white/10',
|
|
'forced-colors:appearance-auto',
|
|
className,
|
|
)}
|
|
{...inputProps}
|
|
/>
|
|
<svg
|
|
fill="none"
|
|
viewBox="0 0 14 14"
|
|
aria-hidden="true"
|
|
className="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-disabled:stroke-gray-950/25 dark:group-has-disabled:stroke-white/25"
|
|
>
|
|
{/* Haken */}
|
|
<path
|
|
d="M3 8L6 11L11 3.5"
|
|
strokeWidth={2}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="opacity-0 group-has-checked:opacity-100"
|
|
/>
|
|
{/* Indeterminate-Strich */}
|
|
<path
|
|
d="M3 7H11"
|
|
strokeWidth={2}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="opacity-0 group-has-indeterminate:opacity-100"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Label + Beschreibung (optional) */}
|
|
{(label || description) && (
|
|
<div className="text-sm/6">
|
|
{label && (
|
|
<label
|
|
htmlFor={inputId}
|
|
className={clsx(
|
|
'font-medium text-gray-900 dark:text-white',
|
|
labelClassName,
|
|
)}
|
|
>
|
|
{label}
|
|
</label>
|
|
)}
|
|
{description && (
|
|
<p
|
|
id={descriptionId}
|
|
className={clsx(
|
|
'text-gray-500 dark:text-gray-400',
|
|
descriptionClassName,
|
|
)}
|
|
>
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
);
|