@magic-spells/dialog-panel 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/dialog-panel.cjs.js +37 -54
- package/dist/dialog-panel.cjs.js.map +1 -1
- package/dist/dialog-panel.css +6 -6
- package/dist/dialog-panel.esm.js +37 -54
- package/dist/dialog-panel.esm.js.map +1 -1
- package/dist/dialog-panel.js +46 -57
- package/dist/dialog-panel.js.map +1 -1
- package/dist/dialog-panel.min.css +1 -1
- package/dist/dialog-panel.min.js +1 -1
- package/dist/dialog-panel.scss +2 -2
- package/dist/scss/dialog-panel.scss +63 -58
- package/dist/scss/variables.scss +16 -30
- package/package.json +5 -5
- package/src/dialog-panel.js +37 -54
- package/src/index.scss +2 -2
- package/src/scss/dialog-panel.scss +63 -58
- package/src/scss/variables.scss +16 -30
package/src/dialog-panel.js
CHANGED
|
@@ -7,7 +7,6 @@ import '@magic-spells/focus-trap';
|
|
|
7
7
|
*/
|
|
8
8
|
class DialogPanel extends HTMLElement {
|
|
9
9
|
#handleTransitionEnd;
|
|
10
|
-
#scrollPosition = 0;
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Clean up event listeners when component is removed from DOM
|
|
@@ -20,38 +19,6 @@ class DialogPanel extends HTMLElement {
|
|
|
20
19
|
_.#handleTransitionEnd
|
|
21
20
|
);
|
|
22
21
|
}
|
|
23
|
-
|
|
24
|
-
// Ensure body scroll is restored if component is removed while open
|
|
25
|
-
document.body.classList.remove('overflow-hidden');
|
|
26
|
-
this.#restoreScroll();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Saves current scroll position and locks body scrolling
|
|
31
|
-
* @private
|
|
32
|
-
*/
|
|
33
|
-
#lockScroll() {
|
|
34
|
-
const _ = this;
|
|
35
|
-
// Save current scroll position
|
|
36
|
-
_.#scrollPosition = window.pageYOffset;
|
|
37
|
-
|
|
38
|
-
// Apply fixed position to body
|
|
39
|
-
document.body.classList.add('overflow-hidden');
|
|
40
|
-
document.body.style.top = `-${_.#scrollPosition}px`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Restores scroll position when dialog is closed
|
|
45
|
-
* @private
|
|
46
|
-
*/
|
|
47
|
-
#restoreScroll() {
|
|
48
|
-
const _ = this;
|
|
49
|
-
// Remove fixed positioning
|
|
50
|
-
document.body.classList.remove('overflow-hidden');
|
|
51
|
-
document.body.style.removeProperty('top');
|
|
52
|
-
|
|
53
|
-
// Restore scroll position
|
|
54
|
-
window.scrollTo(0, _.#scrollPosition);
|
|
55
22
|
}
|
|
56
23
|
/**
|
|
57
24
|
* Initializes the dialog panel, sets up focus trap and overlay
|
|
@@ -65,7 +32,6 @@ class DialogPanel extends HTMLElement {
|
|
|
65
32
|
_.setAttribute('aria-hidden', 'true');
|
|
66
33
|
|
|
67
34
|
_.contentPanel = _.querySelector('dialog-content');
|
|
68
|
-
_.focusTrap = document.createElement('focus-trap');
|
|
69
35
|
_.triggerEl = null;
|
|
70
36
|
|
|
71
37
|
// Create a handler for transition end events
|
|
@@ -86,6 +52,22 @@ class DialogPanel extends HTMLElement {
|
|
|
86
52
|
}
|
|
87
53
|
};
|
|
88
54
|
|
|
55
|
+
// Set up focus-trap inside dialog-content
|
|
56
|
+
// Check if focus-trap already exists
|
|
57
|
+
_.focusTrap = _.contentPanel.querySelector('focus-trap');
|
|
58
|
+
if (!_.focusTrap) {
|
|
59
|
+
_.focusTrap = document.createElement('focus-trap');
|
|
60
|
+
|
|
61
|
+
// Move all existing dialog-content children into focus-trap
|
|
62
|
+
const existingContent = Array.from(_.contentPanel.childNodes);
|
|
63
|
+
existingContent.forEach((child) =>
|
|
64
|
+
_.focusTrap.appendChild(child)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Insert focus-trap inside dialog-content
|
|
68
|
+
_.contentPanel.appendChild(_.focusTrap);
|
|
69
|
+
}
|
|
70
|
+
|
|
89
71
|
// Ensure we have labelledby and describedby references
|
|
90
72
|
if (!_.getAttribute('aria-labelledby')) {
|
|
91
73
|
const heading = _.querySelector('h1, h2, h3');
|
|
@@ -97,14 +79,6 @@ class DialogPanel extends HTMLElement {
|
|
|
97
79
|
}
|
|
98
80
|
}
|
|
99
81
|
|
|
100
|
-
_.contentPanel.parentNode.insertBefore(
|
|
101
|
-
_.focusTrap,
|
|
102
|
-
_.contentPanel
|
|
103
|
-
);
|
|
104
|
-
_.focusTrap.appendChild(_.contentPanel);
|
|
105
|
-
|
|
106
|
-
_.focusTrap.setupTrap();
|
|
107
|
-
|
|
108
82
|
// Add modal overlay
|
|
109
83
|
_.prepend(document.createElement('dialog-overlay'));
|
|
110
84
|
_.#bindUI();
|
|
@@ -132,7 +106,7 @@ class DialogPanel extends HTMLElement {
|
|
|
132
106
|
|
|
133
107
|
// Handle close buttons
|
|
134
108
|
_.addEventListener('click', (e) => {
|
|
135
|
-
if (!e.target.closest('[data-action
|
|
109
|
+
if (!e.target.closest('[data-action-hide-dialog]')) return;
|
|
136
110
|
_.hide();
|
|
137
111
|
});
|
|
138
112
|
|
|
@@ -178,6 +152,9 @@ class DialogPanel extends HTMLElement {
|
|
|
178
152
|
// If event was canceled (preventDefault was called), don't show the dialog
|
|
179
153
|
if (!showAllowed) return false;
|
|
180
154
|
|
|
155
|
+
// Add open attribute for CSS :has() selector
|
|
156
|
+
_.setAttribute('open', '');
|
|
157
|
+
|
|
181
158
|
// Remove the hidden class first to ensure content is rendered
|
|
182
159
|
_.contentPanel.classList.remove('hidden');
|
|
183
160
|
|
|
@@ -185,13 +162,12 @@ class DialogPanel extends HTMLElement {
|
|
|
185
162
|
requestAnimationFrame(() => {
|
|
186
163
|
// Update ARIA states
|
|
187
164
|
_.setAttribute('aria-hidden', 'false');
|
|
165
|
+
|
|
166
|
+
// set trigger element to expanded: true
|
|
188
167
|
if (_.triggerEl) {
|
|
189
168
|
_.triggerEl.setAttribute('aria-expanded', 'true');
|
|
190
169
|
}
|
|
191
170
|
|
|
192
|
-
// Lock body scrolling and save scroll position
|
|
193
|
-
_.#lockScroll();
|
|
194
|
-
|
|
195
171
|
// Focus management
|
|
196
172
|
const firstFocusable = _.querySelector(
|
|
197
173
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
@@ -210,8 +186,6 @@ class DialogPanel extends HTMLElement {
|
|
|
210
186
|
})
|
|
211
187
|
);
|
|
212
188
|
});
|
|
213
|
-
|
|
214
|
-
return true;
|
|
215
189
|
}
|
|
216
190
|
|
|
217
191
|
/**
|
|
@@ -236,20 +210,30 @@ class DialogPanel extends HTMLElement {
|
|
|
236
210
|
// If event was canceled (preventDefault was called), don't hide the dialog
|
|
237
211
|
if (!hideAllowed) return false;
|
|
238
212
|
|
|
239
|
-
//
|
|
240
|
-
|
|
213
|
+
// ensure focus is moved out of the dialog
|
|
214
|
+
const activeElement = document.activeElement;
|
|
215
|
+
if (activeElement && _.contains(activeElement)) {
|
|
216
|
+
// Blur the currently focused element if it's inside the dialog
|
|
217
|
+
activeElement.blur();
|
|
218
|
+
}
|
|
241
219
|
|
|
242
|
-
// Update ARIA states
|
|
220
|
+
// Update ARIA states and restore focus
|
|
243
221
|
if (_.triggerEl) {
|
|
244
222
|
// remove focus from modal panel first
|
|
245
223
|
_.triggerEl.focus();
|
|
246
224
|
// mark trigger as no longer expanded
|
|
247
225
|
_.triggerEl.setAttribute('aria-expanded', 'false');
|
|
226
|
+
} else {
|
|
227
|
+
// Move focus to body to ensure it's not trapped
|
|
228
|
+
document.body.focus();
|
|
248
229
|
}
|
|
249
230
|
|
|
250
231
|
// Set aria-hidden to start transition
|
|
251
232
|
// The transitionend event handler will add display:none when complete
|
|
252
233
|
_.setAttribute('aria-hidden', 'true');
|
|
234
|
+
// Remove open attribute for CSS :has() selector
|
|
235
|
+
|
|
236
|
+
_.removeAttribute('open');
|
|
253
237
|
|
|
254
238
|
// Dispatch hide event - dialog is now starting to hide
|
|
255
239
|
_.dispatchEvent(
|
|
@@ -270,8 +254,7 @@ class DialogPanel extends HTMLElement {
|
|
|
270
254
|
class DialogOverlay extends HTMLElement {
|
|
271
255
|
constructor() {
|
|
272
256
|
super();
|
|
273
|
-
this.setAttribute('tabindex', '-1');
|
|
274
|
-
this.setAttribute('aria-hidden', 'true');
|
|
257
|
+
this.setAttribute('tabindex', '-1');
|
|
275
258
|
this.dialogPanel = this.closest('dialog-panel');
|
|
276
259
|
this.#bindUI();
|
|
277
260
|
}
|
|
@@ -290,7 +273,7 @@ class DialogOverlay extends HTMLElement {
|
|
|
290
273
|
class DialogContent extends HTMLElement {
|
|
291
274
|
constructor() {
|
|
292
275
|
super();
|
|
293
|
-
this.setAttribute('role', 'document');
|
|
276
|
+
this.setAttribute('role', 'document');
|
|
294
277
|
}
|
|
295
278
|
}
|
|
296
279
|
|
package/src/index.scss
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
@forward
|
|
2
|
-
@forward
|
|
1
|
+
@forward 'scss/variables';
|
|
2
|
+
@forward 'scss/dialog-panel';
|
|
@@ -2,72 +2,77 @@
|
|
|
2
2
|
@use 'variables' as vars;
|
|
3
3
|
|
|
4
4
|
dialog-panel {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/* Make it take no space and be invisible in the document flow */
|
|
6
|
+
display: contents;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
&[aria-hidden='false'] {
|
|
9
|
+
dialog-overlay,
|
|
10
|
+
dialog-content {
|
|
11
|
+
pointer-events: auto;
|
|
12
|
+
opacity: 1;
|
|
13
|
+
transform: scale(1);
|
|
14
|
+
filter: blur(0px);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/* Overlay background */
|
|
20
20
|
dialog-overlay {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
position: fixed;
|
|
22
|
+
top: 0;
|
|
23
|
+
left: 0;
|
|
24
|
+
width: 100vw;
|
|
25
|
+
height: 100vh;
|
|
26
|
+
opacity: 0;
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
z-index: var(--dp-overlay-z-index, 1000);
|
|
29
|
+
transition: var(--dp-overlay-transition, all 300ms ease-out);
|
|
30
|
+
background-color: var(
|
|
31
|
+
--dp-overlay-background,
|
|
32
|
+
rgba(20, 23, 26, 0.4)
|
|
33
|
+
);
|
|
34
|
+
backdrop-filter: var(
|
|
35
|
+
--dp-overlay-backdrop-filter,
|
|
36
|
+
blur(2px) saturate(120%)
|
|
37
|
+
);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
dialog-content {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
position: fixed;
|
|
42
|
+
top: 50%;
|
|
43
|
+
left: 50%;
|
|
44
|
+
transform: translate(-50%, -50%) scale(0.95);
|
|
45
|
+
max-width: 90vw;
|
|
46
|
+
max-height: 85vh;
|
|
47
|
+
display: var(--dp-content-display, block);
|
|
48
|
+
opacity: 0;
|
|
49
|
+
background: var(--dp-content-background, white);
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
z-index: var(--dp-content-z-index, 1001);
|
|
52
|
+
box-shadow: var(
|
|
53
|
+
--dp-content-shadow,
|
|
54
|
+
0 10px 25px rgba(0, 0, 0, 0.15)
|
|
55
|
+
);
|
|
56
|
+
border-radius: var(--dp-content-border-radius, 8px);
|
|
57
|
+
overflow: auto;
|
|
58
|
+
transition:
|
|
59
|
+
opacity var(--dp-transition-duration, 300ms)
|
|
60
|
+
var(--dp-transition-timing, ease-out),
|
|
61
|
+
transform var(--dp-transition-duration, 300ms)
|
|
62
|
+
var(--dp-transition-timing, ease-out);
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
/* When shown, reset transform to center */
|
|
65
|
+
dialog-panel[aria-hidden='false'] & {
|
|
66
|
+
transform: translate(-50%, -50%) scale(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* When explicitly hidden, remove from layout */
|
|
71
|
+
dialog-content.hidden {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
75
|
+
// Body scroll lock when dialog is open using :has() selector
|
|
76
|
+
body:has(dialog-panel[open]) {
|
|
77
|
+
overflow: hidden;
|
|
73
78
|
}
|
package/src/scss/variables.scss
CHANGED
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
// SCSS variables for internal component use and customization
|
|
3
3
|
// These variables can be customized by the user
|
|
4
4
|
|
|
5
|
-
// Layout
|
|
6
|
-
$panel-top: 0 !default;
|
|
7
|
-
$panel-left: 0 !default;
|
|
8
|
-
$panel-width: 100vw !default;
|
|
9
|
-
$panel-height: 100vh !default;
|
|
10
|
-
$panel-z-index: 10 !default;
|
|
11
|
-
|
|
12
5
|
// Overlay
|
|
13
6
|
$overlay-z-index: 1000 !default;
|
|
14
7
|
$overlay-background: rgba(20, 23, 26, 0.4) !default;
|
|
@@ -28,27 +21,20 @@ $transition-timing: ease-out !default;
|
|
|
28
21
|
|
|
29
22
|
// Define CSS Custom Properties using SCSS values
|
|
30
23
|
:root {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
--dp-content-z-index: #{$content-z-index};
|
|
48
|
-
--dp-content-shadow: #{$content-shadow};
|
|
49
|
-
--dp-content-border-radius: #{$content-border-radius};
|
|
50
|
-
|
|
51
|
-
// Animation
|
|
52
|
-
--dp-transition-duration: #{$transition-duration};
|
|
53
|
-
--dp-transition-timing: #{$transition-timing};
|
|
24
|
+
// Overlay
|
|
25
|
+
--dp-overlay-z-index: #{$overlay-z-index};
|
|
26
|
+
--dp-overlay-background: #{$overlay-background};
|
|
27
|
+
--dp-overlay-backdrop-filter: #{$overlay-backdrop-filter};
|
|
28
|
+
--dp-overlay-transition: #{$overlay-transition};
|
|
29
|
+
|
|
30
|
+
// Content
|
|
31
|
+
--dp-content-display: #{$content-display};
|
|
32
|
+
--dp-content-background: #{$content-background};
|
|
33
|
+
--dp-content-z-index: #{$content-z-index};
|
|
34
|
+
--dp-content-shadow: #{$content-shadow};
|
|
35
|
+
--dp-content-border-radius: #{$content-border-radius};
|
|
36
|
+
|
|
37
|
+
// Animation
|
|
38
|
+
--dp-transition-duration: #{$transition-duration};
|
|
39
|
+
--dp-transition-timing: #{$transition-timing};
|
|
54
40
|
}
|