import { useEffect, useId, useRef } from 'react'; import styles from './Modal.module.css'; /* Sélecteur des éléments qui peuvent recevoir le focus au clavier. */ const ELEMENTS_FOCUSABLES = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; export default function Modal({ isOpen, onClose, title, children }) { const conteneur = useRef(null); /* useId fournit un identifiant stable et unique par instance, sans tirage aléatoire pendant le rendu et sans collision si deux modales coexistent. */ const titreId = useId(); /* Piège le focus dans la modale et gère la touche Échap. Sans cela, la tabulation continue de parcourir la page située derrière, ce qui rend la boîte de dialogue inutilisable au clavier. */ useEffect(() => { if (!isOpen) return undefined; const elementActifAvant = document.activeElement; /* On vise d'abord le contenu : sur une modale de formulaire, ouvrir sur le premier champ évite à l'utilisateur une tabulation inutile. À défaut, on se rabat sur le premier élément focusable, souvent le bouton de fermeture. */ const corps = conteneur.current?.querySelector(`.${styles.modalBody}`); const premier = corps?.querySelector(ELEMENTS_FOCUSABLES) ?? conteneur.current?.querySelector(ELEMENTS_FOCUSABLES); premier?.focus(); function handleKeyDown(evenement) { if (evenement.key === 'Escape') { onClose(); return; } if (evenement.key !== 'Tab') return; const focusables = conteneur.current?.querySelectorAll(ELEMENTS_FOCUSABLES); if (!focusables?.length) return; const premier = focusables[0]; const dernier = focusables[focusables.length - 1]; if (evenement.shiftKey && document.activeElement === premier) { evenement.preventDefault(); dernier.focus(); } else if (!evenement.shiftKey && document.activeElement === dernier) { evenement.preventDefault(); premier.focus(); } } document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); elementActifAvant?.focus?.(); }; }, [isOpen, onClose]); if (!isOpen) return null; return (