2025-12-03 21:56:50 +00:00
|
|
|
import { useEffect, useRef, type ReactNode } from 'react';
|
|
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* FocusTrapProps - Propriétés du composant FocusTrap
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @interface FocusTrapProps
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
interface FocusTrapProps {
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Contenu à encapsuler dans le piège de focus
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
children: ReactNode;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Si `true`, active le piège de focus
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @default true
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
active?: boolean;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Fonction appelée lorsque la touche Escape est pressée
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* <FocusTrap onEscape={() => setOpen(false)}>
|
|
|
|
|
* <Dialog />
|
|
|
|
|
* </FocusTrap>
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
2025-12-03 21:56:50 +00:00
|
|
|
onEscape?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* FocusTrap - Composant pour piéger le focus dans une zone
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* 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.
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @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>
|
|
|
|
|
* ```
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @component
|
|
|
|
|
* @param {FocusTrapProps} props - Propriétés du composant
|
|
|
|
|
* @returns {JSX.Element} Div avec tabIndex={-1} contenant les enfants
|
|
|
|
|
*/
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
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(
|
2025-12-13 02:34:34 +00:00
|
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|