veza/apps/web/src/components/ui/focus-trap.tsx

126 lines
3.1 KiB
TypeScript

import { useEffect, useRef, type ReactNode } from 'react';
/**
* FocusTrapProps - Propriétés du composant FocusTrap
*
* @interface FocusTrapProps
*/
interface FocusTrapProps {
/**
* Contenu à encapsuler dans le piège de focus
*/
children: ReactNode;
/**
* Si `true`, active le piège de focus
*
* @default true
*/
active?: boolean;
/**
* Fonction appelée lorsque la touche Escape est pressée
*
* @example
* ```tsx
* <FocusTrap onEscape={() => setOpen(false)}>
* <Dialog />
* </FocusTrap>
* ```
*/
onEscape?: () => void;
}
/**
* FocusTrap - Composant pour piéger le focus dans une zone
*
* Composant qui piège le focus à l'intérieur d'une zone spécifique,
* utile pour les modales et les dialogues. Le focus reste dans la zone
* et revient au début lorsqu'on atteint la fin avec Tab.
*
* @example
* ```tsx
* // Piège de focus pour une modale
* <FocusTrap active={isOpen} onEscape={() => setIsOpen(false)}>
* <div className="modal">
* <button>Premier bouton</button>
* <button>Deuxième bouton</button>
* <button>Dernier bouton</button>
* </div>
* </FocusTrap>
* ```
*
* @component
* @param {FocusTrapProps} props - Propriétés du composant
* @returns {JSX.Element} Div avec tabIndex={-1} contenant les enfants
*/
export function FocusTrap({
children,
active = true,
onEscape,
}: FocusTrapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<Element | null>(null);
useEffect(() => {
if (!active || !containerRef.current) return;
// Sauvegarder l'élément actuellement actif
previousActiveElement.current = document.activeElement;
// Trouver tous les éléments focusables
const focusableElements = containerRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[
focusableElements.length - 1
] as HTMLElement;
// Focuser le premier élément
if (firstElement) {
firstElement.focus();
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onEscape?.();
return;
}
if (event.key === 'Tab') {
if (event.shiftKey) {
// Shift + Tab
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
}
} else {
// Tab
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Restaurer le focus à l'élément précédent
if (previousActiveElement.current instanceof HTMLElement) {
previousActiveElement.current.focus();
}
};
}, [active, onEscape]);
return (
<div ref={containerRef} tabIndex={-1}>
{children}
</div>
);
}