Skip to content

Commit 2ef8791

Browse files
committed
Fix: Use React Portal for Modal to resolve focus trap and ARIA hidden issues
1 parent 887d062 commit 2ef8791

1 file changed

Lines changed: 35 additions & 7 deletions

File tree

web/src/components/common/Modal.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

33
import { X } from 'lucide-react';
4-
import { ReactNode } from 'react';
4+
import { ReactNode, useEffect, useState } from 'react';
5+
import { createPortal } from 'react-dom';
56

67
interface ModalProps {
78
isOpen: boolean;
@@ -13,19 +14,45 @@ interface ModalProps {
1314
}
1415

1516
export function Modal({ isOpen, onClose, title, children, actions, flush = false }: ModalProps) {
16-
if (!isOpen) return null;
17+
const [mounted, setMounted] = useState(false);
1718

18-
return (
19+
useEffect(() => {
20+
setMounted(true);
21+
if (isOpen) {
22+
document.body.style.overflow = 'hidden';
23+
}
24+
return () => {
25+
document.body.style.overflow = 'unset';
26+
// Clean up potentially left-over strict overflow if unmounted while open
27+
if (isOpen) {
28+
document.body.style.overflow = 'unset';
29+
}
30+
};
31+
}, [isOpen]);
32+
33+
if (!mounted || !isOpen) return null;
34+
35+
return createPortal(
1936
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
2037
<div
2138
className="absolute inset-0 bg-black/60 backdrop-blur-[2px] transition-opacity"
2239
onClick={onClose}
40+
aria-hidden="true"
2341
/>
24-
<div className={`bg-white w-full max-w-md rounded-2xl shadow-2xl relative z-10 overflow-hidden transform transition-all animate-in zoom-in duration-300 ${flush ? 'mb-0' : 'mb-0'}`}>
42+
<div
43+
className={`bg-white w-full max-w-md rounded-2xl shadow-2xl relative z-10 overflow-hidden transform transition-all animate-in zoom-in duration-300 ${flush ? 'mb-0' : 'mb-0'}`}
44+
role="dialog"
45+
aria-modal="true"
46+
aria-labelledby="modal-title"
47+
>
2548
<div className={flush ? '' : 'p-6'}>
2649
<div className={`flex justify-between items-center ${flush ? 'p-6 pb-2' : 'mb-4'}`}>
27-
<h3 className="text-xl font-black text-gray-900 tracking-tight">{title}</h3>
28-
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
50+
<h3 id="modal-title" className="text-xl font-black text-gray-900 tracking-tight">{title}</h3>
51+
<button
52+
onClick={onClose}
53+
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
54+
aria-label="Close modal"
55+
>
2956
<X className="w-5 h-5 text-gray-500" />
3057
</button>
3158
</div>
@@ -39,6 +66,7 @@ export function Modal({ isOpen, onClose, title, children, actions, flush = false
3966
)}
4067
</div>
4168
</div>
42-
</div>
69+
</div>,
70+
document.body
4371
);
4472
}

0 commit comments

Comments
 (0)