67 lines
2.3 KiB
React
67 lines
2.3 KiB
React
import { useEffect, useId, useRef } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
|
|
export default function Modal({ title, children, onClose, maxWidth = 'max-w-lg' }) {
|
|
const { t } = useLanguage();
|
|
const titleId = useId();
|
|
const dialogRef = useRef(null);
|
|
const previouslyFocused = useRef(null);
|
|
|
|
// Keep the latest onClose in a ref so the mount-only effect below can call it
|
|
// without listing onClose as a dependency. Callers pass a fresh inline arrow
|
|
// each render; depending on it would re-run the effect on every keystroke and
|
|
// steal focus back to the dialog (kicking the user out of input fields).
|
|
const onCloseRef = useRef(onClose);
|
|
onCloseRef.current = onClose;
|
|
|
|
useEffect(() => {
|
|
previouslyFocused.current = document.activeElement;
|
|
const handleKey = (e) => {
|
|
if (e.key === 'Escape') onCloseRef.current?.();
|
|
};
|
|
document.addEventListener('keydown', handleKey);
|
|
// Focus the dialog so screen readers announce it and keyboard focus lands inside
|
|
dialogRef.current?.focus();
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKey);
|
|
previouslyFocused.current?.focus?.();
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div
|
|
className="fixed inset-0 bg-black/60 backdrop-blur-xs"
|
|
onClick={onClose}
|
|
aria-hidden="true"
|
|
/>
|
|
<div
|
|
ref={dialogRef}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby={titleId}
|
|
tabIndex={-1}
|
|
className={`relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full ${maxWidth} focus:outline-hidden`}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-th-border rounded-t-2xl">
|
|
<h2 id={titleId} className="text-lg font-semibold text-th-text">{title}</h2>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label={t('common.close')}
|
|
className="p-2 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
{/* Body */}
|
|
<div className="p-6">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|