@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.
@@ -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="hide-dialog"]')) return;
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
- // Restore body scroll and scroll position
240
- _.#restoreScroll();
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'); // Changed to -1 as it shouldn't be focusable
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'); // Optional: helps with document structure
276
+ this.setAttribute('role', 'document');
294
277
  }
295
278
  }
296
279
 
package/src/index.scss CHANGED
@@ -1,2 +1,2 @@
1
- @forward "scss/variables";
2
- @forward "scss/dialog-panel";
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
- /* Make it take no space and be invisible in the document flow */
6
- display: contents;
5
+ /* Make it take no space and be invisible in the document flow */
6
+ display: contents;
7
7
 
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
- }
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
- 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
- );
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
- 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);
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
- /* When shown, reset transform to center */
65
- dialog-panel[aria-hidden='false'] & {
66
- transform: translate(-50%, -50%) scale(1);
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
- /* When explicitly hidden, remove from layout */
70
- &.hidden {
71
- display: none;
72
- }
75
+ // Body scroll lock when dialog is open using :has() selector
76
+ body:has(dialog-panel[open]) {
77
+ overflow: hidden;
73
78
  }
@@ -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
- // Layout
32
- --dp-panel-top: #{$panel-top};
33
- --dp-panel-left: #{$panel-left};
34
- --dp-panel-width: #{$panel-width};
35
- --dp-panel-height: #{$panel-height};
36
- --dp-panel-z-index: #{$panel-z-index};
37
-
38
- // Overlay
39
- --dp-overlay-z-index: #{$overlay-z-index};
40
- --dp-overlay-background: #{$overlay-background};
41
- --dp-overlay-backdrop-filter: #{$overlay-backdrop-filter};
42
- --dp-overlay-transition: #{$overlay-transition};
43
-
44
- // Content
45
- --dp-content-display: #{$content-display};
46
- --dp-content-background: #{$content-background};
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
  }