Major categories fixed: - TS6133 (188): Remove unused imports (React, icons, types) and variables - TS2322 (222): Fix type mismatches in stories (satisfies Meta -> const meta: Meta), add nullish coalescing for optional values, fix component prop types - TS2345 (43): Fix argument type mismatches with proper null checks and type narrowing - TS2741 (21): Add missing required properties to mock/story data - TS2339 (19): Fix property access on incorrect types, add type guards - TS2353 (13): Remove extra properties from object literals or extend interfaces - TS2352 (11): Fix type conversion chains - TS2307 (9): Fix import paths and module references - Other (42): Fix implicit any, possibly undefined, export declarations Vite build and tsc --noEmit both pass cleanly. Co-authored-by: Cursor <cursoragent@cursor.com>
229 lines
6.6 KiB
TypeScript
229 lines
6.6 KiB
TypeScript
import * as React from 'react';
|
|
import { Circle } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* RadioGroupProps - Propriétés du composant RadioGroup
|
|
*
|
|
* @interface RadioGroupProps
|
|
* @extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>
|
|
*/
|
|
export interface RadioGroupProps
|
|
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
/**
|
|
* Valeur sélectionnée du groupe de boutons radio
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <RadioGroup value={selected} onValueChange={setSelected}>
|
|
* <RadioGroupItem value="option1" />
|
|
* <RadioGroupItem value="option2" />
|
|
* </RadioGroup>
|
|
* ```
|
|
*/
|
|
value?: string;
|
|
|
|
/**
|
|
* Fonction appelée lorsque la valeur sélectionnée change
|
|
*
|
|
* @param {string} value - Nouvelle valeur sélectionnée
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <RadioGroup onValueChange={(value) => console.log('Selected:', value)}>
|
|
* ...
|
|
* </RadioGroup>
|
|
* ```
|
|
*/
|
|
onValueChange?: (value: string) => void;
|
|
|
|
/**
|
|
* Si `true`, désactive tous les boutons radio du groupe
|
|
*
|
|
* @default false
|
|
*/
|
|
disabled?: boolean;
|
|
}
|
|
|
|
/**
|
|
* RadioGroup - Composant de groupe de boutons radio avec design system Kodo
|
|
*
|
|
* Composant pour gérer un groupe de boutons radio mutuellement exclusifs.
|
|
* Utilise le design system Kodo avec des styles cohérents et support pour l'accessibilité.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* // Groupe de boutons radio simple
|
|
* <RadioGroup value={selected} onValueChange={setSelected}>
|
|
* <RadioGroupItem value="option1" />
|
|
* <RadioGroupItem value="option2" />
|
|
* <RadioGroupItem value="option3" />
|
|
* </RadioGroup>
|
|
* ```
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* // Avec labels
|
|
* <RadioGroup value={selected} onValueChange={setSelected}>
|
|
* <label>
|
|
* <RadioGroupItem value="option1" />
|
|
* Option 1
|
|
* </label>
|
|
* <label>
|
|
* <RadioGroupItem value="option2" />
|
|
* Option 2
|
|
* </label>
|
|
* </RadioGroup>
|
|
* ```
|
|
*
|
|
* @component
|
|
* @param {RadioGroupProps} props - Propriétés du composant
|
|
* @returns {JSX.Element} Élément div avec role="radiogroup" contenant les boutons radio
|
|
*/
|
|
|
|
const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
({ className, value, onValueChange, disabled, children, ...props }, ref) => {
|
|
// Collect RadioGroupItem values for arrow key navigation
|
|
const itemValues: string[] = [];
|
|
React.Children.forEach(children, (child) => {
|
|
if (React.isValidElement(child) && child.type === RadioGroupItem && !child.props.disabled) {
|
|
itemValues.push(child.props.value);
|
|
}
|
|
});
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
if (itemValues.length === 0) return;
|
|
const currentIndex = value ? itemValues.indexOf(value) : -1;
|
|
let nextIndex: number | undefined;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
case 'ArrowRight':
|
|
nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % itemValues.length;
|
|
break;
|
|
case 'ArrowUp':
|
|
case 'ArrowLeft':
|
|
nextIndex = currentIndex === -1 ? itemValues.length - 1 : (currentIndex - 1 + itemValues.length) % itemValues.length;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
const nextValue = itemValues[nextIndex];
|
|
if (nextValue !== undefined) onValueChange?.(nextValue);
|
|
// Focus the newly selected radio
|
|
const radios = (e.currentTarget as HTMLElement).querySelectorAll<HTMLInputElement>('input[type="radio"]');
|
|
radios[nextIndex]?.focus();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn('grid gap-2', className)}
|
|
role="radiogroup"
|
|
onKeyDown={handleKeyDown}
|
|
{...props}
|
|
>
|
|
{React.Children.map(children, (child) => {
|
|
if (React.isValidElement(child) && child.type === RadioGroupItem) {
|
|
const isChecked = child.props.value === value;
|
|
const isFirstItem = child.props.value === itemValues[0];
|
|
return React.cloneElement(child, {
|
|
checked: isChecked,
|
|
onCheckedChange: () => onValueChange?.(child.props.value),
|
|
disabled: disabled || child.props.disabled,
|
|
// Only the checked item (or first if none checked) gets tabIndex 0
|
|
tabIndex: isChecked ? 0 : (value === undefined && isFirstItem ? 0 : -1),
|
|
} as any);
|
|
}
|
|
return child;
|
|
})}
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
RadioGroup.displayName = 'RadioGroup';
|
|
|
|
/**
|
|
* RadioGroupItemProps - Propriétés du composant RadioGroupItem
|
|
*
|
|
* @interface RadioGroupItemProps
|
|
* @extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'>
|
|
*/
|
|
export interface RadioGroupItemProps
|
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
/**
|
|
* Valeur unique du bouton radio (doit être unique dans le groupe)
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <RadioGroupItem value="option1" />
|
|
* ```
|
|
*/
|
|
value: string;
|
|
|
|
/**
|
|
* État checked du bouton radio (géré automatiquement par RadioGroup)
|
|
* @internal
|
|
*/
|
|
checked?: boolean;
|
|
|
|
/**
|
|
* Fonction appelée lors du clic (gérée automatiquement par RadioGroup)
|
|
* @internal
|
|
*/
|
|
onCheckedChange?: () => void;
|
|
|
|
/**
|
|
* Tab index for keyboard navigation (managed by RadioGroup)
|
|
* @internal
|
|
*/
|
|
tabIndex?: number;
|
|
}
|
|
|
|
/**
|
|
* RadioGroupItem - Bouton radio individuel
|
|
*
|
|
* Bouton radio à utiliser à l'intérieur d'un RadioGroup.
|
|
* L'état checked est géré automatiquement par le RadioGroup parent.
|
|
*
|
|
* @component
|
|
*/
|
|
|
|
const RadioGroupItem = React.forwardRef<HTMLInputElement, RadioGroupItemProps>(
|
|
({ className, value, checked, onCheckedChange, disabled, tabIndex, ...props }, ref) => {
|
|
return (
|
|
<label
|
|
role="radio"
|
|
aria-checked={!!checked}
|
|
className={cn(
|
|
'aspect-square h-4 w-4 rounded-full border border-border text-muted-foreground',
|
|
'ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
|
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
'cursor-pointer relative inline-flex items-center justify-center',
|
|
checked && 'border-primary text-primary',
|
|
className,
|
|
)}
|
|
>
|
|
<input
|
|
ref={ref}
|
|
type="radio"
|
|
value={value}
|
|
checked={checked}
|
|
onChange={onCheckedChange}
|
|
disabled={disabled}
|
|
tabIndex={tabIndex}
|
|
className="sr-only"
|
|
{...props}
|
|
/>
|
|
{checked && (
|
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
|
)}
|
|
</label>
|
|
);
|
|
},
|
|
);
|
|
RadioGroupItem.displayName = 'RadioGroupItem';
|
|
|
|
export { RadioGroup, RadioGroupItem };
|