veza/apps/web/src/components/ui/checkbox.tsx

114 lines
3.7 KiB
TypeScript

import * as React from 'react';
import { useId } from 'react';
import { Check } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* CheckboxProps - Propriétés du composant Checkbox
*
* @interface CheckboxProps
* @extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>
*/
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
/**
* Label à afficher à côté de la checkbox
*
* @example
* ```tsx
* <Checkbox label="J'accepte les conditions" />
* ```
*/
label?: string;
/**
* Fonction appelée lorsque l'état checked change
* Reçoit la nouvelle valeur booléenne
*
* @param {boolean} checked - Nouvel état checked
*
* @example
* ```tsx
* <Checkbox onCheckedChange={(checked) => console.log(checked)} />
* ```
*/
onCheckedChange?: (checked: boolean) => void;
}
/**
* Checkbox - Composant de case à cocher avec design system Kodo
*
* Composant de checkbox avec support pour les labels et les callbacks.
* Utilise le design system Kodo avec une icône Check animée lors de la sélection.
*
* @example
* ```tsx
* // Checkbox simple
* <Checkbox label="Option 1" />
*
* // Checkbox contrôlée
* <Checkbox
* checked={isChecked}
* onCheckedChange={setIsChecked}
* label="Accepter"
* />
*
* // Checkbox désactivée
* <Checkbox label="Option" disabled />
* ```
*
* @component
* @param {CheckboxProps} props - Propriétés du composant
* @returns {JSX.Element} Élément label contenant une checkbox stylisée
*/
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
({ label, className = '', onCheckedChange, onChange, id, ...props }, ref) => {
// CRITIQUE FIX #37: Utiliser useId() pour générer un ID stable pour l'association label/input
const generatedId = useId();
const checkboxId = id || generatedId;
const labelId = `${checkboxId}-label`;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (onCheckedChange) {
onCheckedChange(e.target.checked);
}
if (onChange) {
onChange(e);
}
};
// CRITIQUE FIX #37: Si aucun label n'est fourni, s'assurer qu'il y a un aria-label
const hasAccessibleLabel = label || props['aria-label'] || props['aria-labelledby'];
return (
<label
htmlFor={checkboxId}
id={labelId}
className={cn(`inline-flex items-center gap-3 cursor-pointer group`, props.disabled ? 'opacity-50 cursor-not-allowed' : '', className)}
>
<div className="relative">
<input
ref={ref}
id={checkboxId}
type="checkbox"
className="peer sr-only"
onChange={handleChange}
// CRITIQUE FIX #37: Ajouter aria-label si aucun label n'est fourni
aria-label={!label && !props['aria-label'] && !props['aria-labelledby'] ? 'Checkbox' : undefined}
aria-labelledby={label ? labelId : undefined}
{...props}
/>
<div className="
w-5 h-5 rounded border border-kodo-steel bg-kodo-graphite
peer-checked:bg-kodo-cyan peer-checked:border-kodo-cyan
peer-focus:ring-2 peer-focus:ring-kodo-cyan/30
transition-all duration-200
"></div>
<Check className="w-3.5 h-3.5 text-black absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 peer-checked:opacity-100 transition-opacity pointer-events-none" strokeWidth={3} />
</div>
{label && <span className="text-sm text-gray-300 group-hover:text-white transition-colors select-none">{label}</span>}
</label>
);
}
);
Checkbox.displayName = 'Checkbox';