import { useEffect, useId, useRef, type ReactNode } from 'react'; import { useI18n } from '../i18n'; interface ModalProps { title: string; onClose(): void; children: ReactNode; footer?: ReactNode; wide?: boolean; } /** * A dialog that behaves like one: Escape closes it, focus starts inside and * cannot Tab out, and it returns focus to whatever opened it. */ export function Modal({ title, onClose, children, footer, wide }: ModalProps): ReactNode { const ref = useRef(null); const titleId = useId(); const returnFocusTo = useRef(null); useEffect(() => { returnFocusTo.current = document.activeElement; const first = ref.current?.querySelector( 'input:not([type=hidden]), select, textarea, button', ); first?.focus(); return () => { if (returnFocusTo.current instanceof HTMLElement) returnFocusTo.current.focus(); }; }, []); useEffect(() => { function onKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') { event.stopPropagation(); onClose(); return; } if (event.key !== 'Tab') return; const focusable = ref.current?.querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', ); if (!focusable?.length) return; const first = focusable[0]!; const last = focusable[focusable.length - 1]!; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } document.addEventListener('keydown', onKeyDown, true); return () => document.removeEventListener('keydown', onKeyDown, true); }, [onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }} >

{title}

{children} {footer &&
{footer}
}
); } interface ConfirmProps { title: string; message: ReactNode; confirmLabel?: string; destructive?: boolean; busy?: boolean; onConfirm(): void; onCancel(): void; } export function ConfirmDialog({ title, message, confirmLabel, destructive, busy, onConfirm, onCancel, }: ConfirmProps): ReactNode { const { t } = useI18n(); return ( } >
{message}
); }