import { useEffect, useId, useRef } from 'react' import { X } from 'lucide-react' const SIZES = { sm: 'max-w-md', md: 'max-w-lg', lg: 'max-w-3xl', xl: 'max-w-4xl', } const FOCUSABLE = [ 'a[href]', 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex="-1"])', ].join(', ') /** * Modale de base : fermeture par Échap et clic sur le fond, piège à focus, * restitution du focus à la fermeture, verrouillage du défilement, sémantique * ARIA. Tous les modals de l'application passent par ici. */ export default function Modal({ title, subtitle, icon, size = 'md', onClose, headerRight, footer, children, bodyClassName = 'overflow-y-auto p-4', panelClassName = '', }) { const panelRef = useRef(null) const onCloseRef = useRef(onClose) const titleId = useId() // `onClose` est souvent une lambda recréée à chaque rendu du parent : on la // lit via une ref pour que l'effet ne se rejoue pas (sinon le focus sauterait // au premier champ à chaque rafraîchissement automatique). useEffect(() => { onCloseRef.current = onClose }) useEffect(() => { const previouslyFocused = document.activeElement const panel = panelRef.current const firstFocusable = panel?.querySelector(FOCUSABLE) ;(firstFocusable ?? panel)?.focus({ preventScroll: true }) const onKeyDown = (e) => { if (e.key === 'Escape') { e.stopPropagation() onCloseRef.current?.() return } if (e.key !== 'Tab' || !panel) return const items = [...panel.querySelectorAll(FOCUSABLE)].filter(el => el.offsetParent !== null) if (items.length === 0) return const first = items[0] const last = items[items.length - 1] if (e.shiftKey && document.activeElement === first) { e.preventDefault() last.focus() } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault() first.focus() } } document.addEventListener('keydown', onKeyDown) const previousOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' return () => { document.removeEventListener('keydown', onKeyDown) document.body.style.overflow = previousOverflow previouslyFocused?.focus?.({ preventScroll: true }) } }, []) return (
{ if (e.target === e.currentTarget) onClose() }} >
{icon}

{title}

{subtitle &&

{subtitle}

}
{headerRight}
{/* `min-h-0` autorise l'enfant à défiler dans un conteneur flex */}
{children}
{footer && (
{footer}
)}
) }