Files
redlight/src/components/Modal.jsx
T
Michelle fbbcd79719
Build & Push Docker Image / build (push) Successful in 4m6s
Build & Push Docker Image / build (release) Successful in 4m12s
fix: prevent focus loss by using ref for onClose in Modal component
2026-06-03 10:40:38 +02:00

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>
);
}