import React, { useState, useEffect, useRef } from 'react'; import { cn } from '@/lib/utils'; import FormLabel from './FormLabel'; import FormError from './FormError'; /** * FormCheckbox Component * Single and group checkboxes with indeterminate state and custom styling */ export interface CheckboxOption { value: string; label: string; disabled?: boolean; } export interface FormCheckboxProps { label?: string; error?: string | string[]; helpText?: string; required?: boolean; checked?: boolean; indeterminate?: boolean; options?: CheckboxOption[]; value?: string[]; onChange?: (value: string[]) => void; containerClassName?: string; checkboxClassName?: string; disabled?: boolean; id?: string; name?: string; } export const FormCheckbox: React.FC = ({ label, error, helpText, required = false, checked = false, indeterminate = false, options, value = [], onChange, containerClassName, checkboxClassName, disabled = false, id, name, }) => { const [internalChecked, setInternalChecked] = useState(checked); const checkboxRef = useRef(null); const hasError = !!error; const showError = hasError; // Handle indeterminate state useEffect(() => { if (checkboxRef.current) { checkboxRef.current.indeterminate = indeterminate; } }, [indeterminate, internalChecked]); // Sync internal state with prop useEffect(() => { setInternalChecked(checked); }, [checked]); const isGroup = Array.isArray(options) && options.length > 0; const handleSingleChange = (e: React.ChangeEvent) => { const newChecked = e.target.checked; setInternalChecked(newChecked); if (onChange && !isGroup) { // For single checkbox, call onChange with boolean // But to maintain consistency, we'll treat it as a group with one option if (newChecked) { onChange([name || 'checkbox']); } else { onChange([]); } } }; const handleGroupChange = (e: React.ChangeEvent) => { const optionValue = e.target.value; const isChecked = e.target.checked; let newValue: string[]; if (isChecked) { newValue = [...value, optionValue]; } else { newValue = value.filter((v) => v !== optionValue); } if (onChange) { onChange(newValue); } }; const inputId = id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined); const baseCheckboxClasses = cn( 'w-4 h-4 rounded border transition-all duration-200', 'focus:outline-none focus:ring-2 focus:ring-primary', { 'border-neutral-dark bg-neutral-light': !internalChecked && !hasError && !indeterminate, 'border-primary bg-primary text-white': internalChecked && !hasError, 'border-danger bg-danger text-white': hasError, 'border-primary bg-primary/50 text-white': indeterminate, 'opacity-60 cursor-not-allowed': disabled, 'cursor-pointer': !disabled, }, checkboxClassName ); const containerClasses = cn( 'flex flex-col gap-2', containerClassName ); const groupContainerClasses = cn( 'flex flex-col gap-2', containerClassName ); const singleWrapperClasses = cn( 'flex items-start gap-2', { 'opacity-60': disabled, } ); const labelClasses = cn( 'text-sm font-medium leading-none', { 'text-text-primary': !hasError, 'text-danger': hasError, } ); // Single checkbox if (!isGroup) { return (
{label && ( )} {helpText && (

{helpText}

)}
{showError && ( )}
); } // Checkbox group const groupLabelId = inputId ? `${inputId}-group-label` : undefined; const allSelected = options.every(opt => value.includes(opt.value)); const someSelected = options.some(opt => value.includes(opt.value)) && !allSelected; // Update indeterminate state for group select all useEffect(() => { if (checkboxRef.current && someSelected) { checkboxRef.current.indeterminate = true; } }, [someSelected, value, options]); return (
{label && (
{label}
)}
{options.map((option) => { const optionId = `${inputId}-${option.value}`; const isChecked = value.includes(option.value); return (
); })}
{helpText && (

{helpText}

)} {showError && ( )}
); }; FormCheckbox.displayName = 'FormCheckbox'; export default FormCheckbox;