Skip to content

Commit 47aa213

Browse files
pavel-fokinPavel Fokin
andauthored
Add Dialog component (#428)
Co-authored-by: Pavel Fokin <pavel.fokin.dev@gmail.com>
1 parent df7ad4f commit 47aa213

2 files changed

Lines changed: 316 additions & 0 deletions

File tree

components/UI/Dialog.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { createPortal } from 'react-dom';
3+
import styles from '../../styles/components/dialog.module.css';
4+
5+
export default function Dialog({
6+
isOpen,
7+
onClose,
8+
title,
9+
children,
10+
size = 'medium',
11+
showCloseButton = true
12+
}) {
13+
const dialogRef = useRef(null);
14+
const [portalTarget, setPortalTarget] = useState(null);
15+
16+
// Set up portal target after component mounts (client-side only)
17+
useEffect(() => {
18+
setPortalTarget(document.body);
19+
}, []);
20+
21+
// Handle escape key to close dialog
22+
useEffect(() => {
23+
const handleEscape = (event) => {
24+
if (event.key === 'Escape' && isOpen) {
25+
onClose();
26+
}
27+
};
28+
29+
if (isOpen) {
30+
document.addEventListener('keydown', handleEscape);
31+
// Prevent body scroll when dialog is open
32+
document.body.style.overflow = 'hidden';
33+
}
34+
35+
return () => {
36+
document.removeEventListener('keydown', handleEscape);
37+
document.body.style.overflow = 'unset';
38+
};
39+
}, [isOpen, onClose]);
40+
41+
// Handle click outside to close dialog
42+
const handleBackdropClick = (event) => {
43+
if (event.target === event.currentTarget) {
44+
onClose();
45+
}
46+
};
47+
48+
if (!isOpen || !portalTarget) return null;
49+
50+
const dialogContent = (
51+
<div className={styles.backdrop} onClick={handleBackdropClick}>
52+
<div
53+
ref={dialogRef}
54+
className={`${styles.dialog} ${styles[size]}`}
55+
role="dialog"
56+
aria-modal="true"
57+
aria-labelledby={title ? "dialog-title" : undefined}
58+
>
59+
<div className={styles.header}>
60+
{title && (
61+
<h2 id="dialog-title" className={styles.title}>
62+
{title}
63+
</h2>
64+
)}
65+
{showCloseButton && (
66+
<button
67+
type="button"
68+
className={styles.closeButton}
69+
onClick={onClose}
70+
aria-label="Close dialog"
71+
>
72+
<svg
73+
width="24"
74+
height="24"
75+
viewBox="0 0 24 24"
76+
fill="none"
77+
stroke="currentColor"
78+
strokeWidth="2"
79+
strokeLinecap="round"
80+
strokeLinejoin="round"
81+
>
82+
<line x1="18" y1="6" x2="6" y2="18"></line>
83+
<line x1="6" y1="6" x2="18" y2="18"></line>
84+
</svg>
85+
</button>
86+
)}
87+
</div>
88+
<div className={styles.content}>
89+
{children}
90+
</div>
91+
</div>
92+
</div>
93+
);
94+
95+
// Use portal to render dialog at document root level
96+
return createPortal(dialogContent, portalTarget);
97+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/* Backdrop */
2+
.backdrop {
3+
position: fixed;
4+
top: 0;
5+
left: 0;
6+
right: 0;
7+
bottom: 0;
8+
background-color: rgba(0, 0, 0, 0.5);
9+
display: flex;
10+
align-items: center;
11+
justify-content: center;
12+
z-index: 9999;
13+
padding: 1rem;
14+
animation: fadeIn 0.2s ease-out;
15+
/* Prevent any pointer events from bubbling through */
16+
pointer-events: auto;
17+
/* Ensure backdrop is above everything */
18+
isolation: isolate;
19+
}
20+
21+
/* Dialog container */
22+
.dialog {
23+
background: var(--card-bg);
24+
/* border-radius: 8px; */
25+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
26+
max-width: 90vw;
27+
max-height: 90vh;
28+
overflow: hidden;
29+
display: flex;
30+
flex-direction: column;
31+
animation: slideIn 0.3s ease-out;
32+
/* Prevent any layout shifts */
33+
transform: translateZ(0);
34+
/* Ensure dialog is above backdrop */
35+
position: relative;
36+
z-index: 1;
37+
}
38+
39+
/* Size variants */
40+
.small {
41+
width: 400px;
42+
}
43+
44+
.medium {
45+
width: 600px;
46+
}
47+
48+
.large {
49+
width: 800px;
50+
}
51+
52+
.xlarge {
53+
width: 1000px;
54+
}
55+
56+
/* Header */
57+
.header {
58+
display: flex;
59+
align-items: center;
60+
justify-content: space-between;
61+
padding: 1.25rem 1.25rem 0 1.25rem;
62+
border-bottom: 1px solid var(--card-border);
63+
/* min-height: 60px; */
64+
/* Prevent layout shifts */
65+
flex-shrink: 0;
66+
}
67+
68+
.title {
69+
margin: 0;
70+
font-size: 1.25rem;
71+
font-weight: 600;
72+
color: var(--card-text);
73+
line-height: 1.5;
74+
}
75+
76+
/* Close button */
77+
.closeButton {
78+
background: none;
79+
border: none;
80+
padding: 0.5rem;
81+
cursor: pointer;
82+
border-radius: 6px;
83+
color: var(--card-text);
84+
transition: all 0.2s ease;
85+
display: flex;
86+
align-items: center;
87+
justify-content: center;
88+
width: 32px;
89+
height: 32px;
90+
/* Prevent button from causing layout shifts */
91+
flex-shrink: 0;
92+
}
93+
94+
.closeButton:hover {
95+
background-color: var(--card-bg);
96+
color: var(--card-text);
97+
}
98+
99+
.closeButton:focus {
100+
outline: 2px solid var(--accent-icon);
101+
outline-offset: 2px;
102+
}
103+
104+
/* Content area */
105+
.content {
106+
padding: 1.5rem;
107+
overflow-y: auto;
108+
flex: 1;
109+
/* Prevent content from causing layout shifts */
110+
min-height: 0;
111+
}
112+
113+
/* Animations - optimized to prevent blinking */
114+
@keyframes fadeIn {
115+
from {
116+
opacity: 0;
117+
}
118+
to {
119+
opacity: 1;
120+
}
121+
}
122+
123+
@keyframes slideIn {
124+
from {
125+
opacity: 0;
126+
transform: translateY(-20px) scale(0.95) translateZ(0);
127+
}
128+
to {
129+
opacity: 1;
130+
transform: translateY(0) scale(1) translateZ(0);
131+
}
132+
}
133+
134+
/* Responsive design */
135+
@media (max-width: 640px) {
136+
.backdrop {
137+
padding: 0.5rem;
138+
}
139+
140+
.dialog {
141+
width: 100%;
142+
max-width: 100%;
143+
max-height: 95vh;
144+
}
145+
146+
.header {
147+
padding: 1rem 1rem 0 1rem;
148+
min-height: 50px;
149+
}
150+
151+
.content {
152+
padding: 1rem;
153+
}
154+
155+
.title {
156+
font-size: 1.125rem;
157+
}
158+
}
159+
160+
/* Dark mode support */
161+
@media (prefers-color-scheme: dark) {
162+
.dialog {
163+
background: var(--card-bg);
164+
border: 1px solid var(--card-border);
165+
}
166+
167+
.title {
168+
color: var(--card-text);
169+
}
170+
171+
.header {
172+
border-bottom-color: var(--card-border);
173+
}
174+
175+
.closeButton {
176+
color: var(--card-text);
177+
}
178+
179+
.closeButton:hover {
180+
background-color: var(--card-bg);
181+
color: var(--card-text);
182+
}
183+
}
184+
185+
/* Focus management */
186+
.dialog:focus {
187+
outline: none;
188+
}
189+
190+
/* Scrollbar styling for content */
191+
.content::-webkit-scrollbar {
192+
width: 6px;
193+
}
194+
195+
.content::-webkit-scrollbar-track {
196+
background: var(--card-bg);
197+
border-radius: 3px;
198+
}
199+
200+
.content::-webkit-scrollbar-thumb {
201+
background: var(--card-text);
202+
border-radius: 3px;
203+
}
204+
205+
.content::-webkit-scrollbar-thumb:hover {
206+
background: var(--card-text);
207+
}
208+
209+
/* Additional fixes to prevent blinking */
210+
.backdrop * {
211+
/* Prevent any child elements from causing reflows */
212+
backface-visibility: hidden;
213+
-webkit-backface-visibility: hidden;
214+
}
215+
216+
/* Ensure dialog stays in place during animations */
217+
.dialog {
218+
will-change: transform, opacity;
219+
}

0 commit comments

Comments
 (0)