@magic-spells/dialog-panel 1.0.1 → 1.1.1

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 CHANGED
@@ -54,6 +54,33 @@ Or include directly in HTML:
54
54
  </script>
55
55
  ```
56
56
 
57
+ ## Positioning
58
+
59
+ By default, the panel is centered and uses a fade/scale animation. Set the
60
+ `position` attribute to slide the panel in from an edge instead:
61
+
62
+ ```html
63
+ <dialog-panel position="bottom">...</dialog-panel>
64
+ ```
65
+
66
+ | Value | Animation | Typical use |
67
+ |-------|-----------|-------------|
68
+ | `center` (default) | Fade + scale | Standard modal |
69
+ | `top` | Slide down from top | Predictive search, notifications |
70
+ | `bottom` | Slide up from bottom | iOS-style action sheets |
71
+ | `left` | Slide in from left | Mobile nav menu |
72
+ | `right` | Slide in from right | Cart drawer, side panel |
73
+
74
+ Drawer panels default to full width (top/bottom, max-height `85vh`) or full
75
+ height (left/right, `min(24rem, 90vw)` wide). Override with your own CSS to
76
+ customize:
77
+
78
+ ```css
79
+ dialog-panel[position='right'] > dialog {
80
+ width: 28rem;
81
+ }
82
+ ```
83
+
57
84
  ## How It Works
58
85
 
59
86
  1. Wrap a native `<dialog>` element inside `<dialog-panel>`
@@ -13,13 +13,11 @@ dialog-panel {
13
13
  /* Base dialog styles with transitions */
14
14
  dialog-panel > dialog {
15
15
  border: none;
16
- border-radius: 0.5rem;
17
16
  padding: 0;
18
17
  max-width: 32rem;
19
18
  width: 90vw;
20
19
  max-height: 85vh;
21
20
  overflow: visible;
22
- background: white;
23
21
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
24
22
 
25
23
  /* Start hidden */
@@ -115,3 +113,99 @@ dialog-panel[state='hiding'] > dialog-backdrop {
115
113
  pointer-events: none;
116
114
  transition: opacity 0.2s ease-in;
117
115
  }
116
+
117
+ /* ============================================================
118
+ Position variants — drawer-style slide-in animations.
119
+ Self-contained: declares its own `position: fixed` and
120
+ explicit inset values so it doesn't rely on the UA's
121
+ `dialog:modal` styling or on consumer body overflow rules.
122
+ Default (no position attr) uses the centered fade/scale
123
+ animation defined above.
124
+ ============================================================ */
125
+
126
+ /* Shared base for all four drawer positions */
127
+ dialog-panel[position='top'] > dialog,
128
+ dialog-panel[position='right'] > dialog,
129
+ dialog-panel[position='bottom'] > dialog,
130
+ dialog-panel[position='left'] > dialog {
131
+ position: fixed;
132
+ margin: 0;
133
+ max-width: 100%;
134
+ opacity: 1;
135
+ }
136
+
137
+ /* ---- Top drawer ---- */
138
+ dialog-panel[position='top'] > dialog {
139
+ top: 0;
140
+ right: 0;
141
+ bottom: auto;
142
+ left: 0;
143
+ width: 100%;
144
+ max-height: 85vh;
145
+ transform: translateY(-100%);
146
+ }
147
+
148
+ dialog-panel[position='top'][state='shown'] > dialog {
149
+ transform: translateY(0);
150
+ }
151
+
152
+ /* ---- Bottom drawer ---- */
153
+ dialog-panel[position='bottom'] > dialog {
154
+ top: auto;
155
+ right: 0;
156
+ bottom: 0;
157
+ left: 0;
158
+ width: 100%;
159
+ max-height: 85vh;
160
+ transform: translateY(100%);
161
+ }
162
+
163
+ dialog-panel[position='bottom'][state='shown'] > dialog {
164
+ transform: translateY(0);
165
+ }
166
+
167
+ /* ---- Left drawer ---- */
168
+ dialog-panel[position='left'] > dialog {
169
+ top: 0;
170
+ right: auto;
171
+ bottom: 0;
172
+ left: 0;
173
+ width: min(24rem, 90vw);
174
+ height: 100dvh;
175
+ max-height: 100dvh;
176
+ transform: translateX(-100%);
177
+ }
178
+
179
+ dialog-panel[position='left'][state='shown'] > dialog {
180
+ transform: translateX(0);
181
+ }
182
+
183
+ /* ---- Right drawer ---- */
184
+ dialog-panel[position='right'] > dialog {
185
+ top: 0;
186
+ right: 0;
187
+ bottom: 0;
188
+ left: auto;
189
+ width: min(24rem, 90vw);
190
+ height: 100dvh;
191
+ max-height: 100dvh;
192
+ transform: translateX(100%);
193
+ }
194
+
195
+ dialog-panel[position='right'][state='shown'] > dialog {
196
+ transform: translateX(0);
197
+ }
198
+
199
+ /* ============================================================
200
+ Page scroll lock — prevents the underlying page from
201
+ scrolling while any dialog-panel is open or animating.
202
+ Keyed on the package's own [state='*'] attributes so it
203
+ activates from the first frame of the opening animation
204
+ and stays through the closing animation, regardless of
205
+ when the native <dialog>'s [open] attribute is toggled.
206
+ ============================================================ */
207
+ body:has(dialog-panel[state='showing']),
208
+ body:has(dialog-panel[state='shown']),
209
+ body:has(dialog-panel[state='hiding']) {
210
+ overflow: hidden;
211
+ }
@@ -1 +1 @@
1
- dialog-panel{display:contents}dialog-panel>dialog{background:#fff;border:none;border-radius:.5rem;box-shadow:0 10px 25px rgba(0,0,0,.15);max-height:85vh;max-width:32rem;opacity:0;overflow:visible;padding:0;transform:scale(.95);transition:opacity .3s ease-out,transform .3s ease-out;width:90vw}dialog-panel:has(dialog-backdrop)>dialog::backdrop{background:transparent}dialog-panel:not(:has(dialog-backdrop))>dialog::backdrop{background:rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-out}dialog-backdrop{background:rgba(0,0,0,.3);height:200dvh;left:50%;opacity:0;pointer-events:none;position:fixed;top:50%;transform:translateY(-50%) translateX(-50%);transition:opacity .3s ease-out;width:200vw;z-index:9999999}dialog-panel[state=showing]>dialog{opacity:0;transform:scale(.95)}dialog-panel:not(:has(dialog-backdrop))[state=showing]>dialog::backdrop{opacity:0}dialog-panel[state=shown]>dialog{opacity:1;transform:scale(1)}dialog-panel:not(:has(dialog-backdrop))[state=shown]>dialog::backdrop{opacity:1}dialog-panel[state=hiding]>dialog{opacity:0;transform:scale(.95);transition:opacity .2s ease-in,transform .2s ease-in}dialog-panel:not(:has(dialog-backdrop))[state=hiding]>dialog::backdrop{opacity:0;transition:opacity .2s ease-in}dialog-panel[state=hidden]>dialog-backdrop,dialog-panel[state=showing]>dialog-backdrop{opacity:0;pointer-events:none}dialog-panel[state=shown]>dialog-backdrop{opacity:1;pointer-events:auto}dialog-panel[state=hiding]>dialog-backdrop{opacity:0;pointer-events:none;transition:opacity .2s ease-in}
1
+ dialog-panel{display:contents}dialog-panel>dialog{border:none;box-shadow:0 10px 25px rgba(0,0,0,.15);max-height:85vh;max-width:32rem;opacity:0;overflow:visible;padding:0;transform:scale(.95);transition:opacity .3s ease-out,transform .3s ease-out;width:90vw}dialog-panel:has(dialog-backdrop)>dialog::backdrop{background:transparent}dialog-panel:not(:has(dialog-backdrop))>dialog::backdrop{background:rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-out}dialog-backdrop{background:rgba(0,0,0,.3);height:200dvh;left:50%;opacity:0;pointer-events:none;position:fixed;top:50%;transform:translateY(-50%) translateX(-50%);transition:opacity .3s ease-out;width:200vw;z-index:9999999}dialog-panel[state=showing]>dialog{opacity:0;transform:scale(.95)}dialog-panel:not(:has(dialog-backdrop))[state=showing]>dialog::backdrop{opacity:0}dialog-panel[state=shown]>dialog{opacity:1;transform:scale(1)}dialog-panel:not(:has(dialog-backdrop))[state=shown]>dialog::backdrop{opacity:1}dialog-panel[state=hiding]>dialog{opacity:0;transform:scale(.95);transition:opacity .2s ease-in,transform .2s ease-in}dialog-panel:not(:has(dialog-backdrop))[state=hiding]>dialog::backdrop{opacity:0;transition:opacity .2s ease-in}dialog-panel[state=hidden]>dialog-backdrop,dialog-panel[state=showing]>dialog-backdrop{opacity:0;pointer-events:none}dialog-panel[state=shown]>dialog-backdrop{opacity:1;pointer-events:auto}dialog-panel[state=hiding]>dialog-backdrop{opacity:0;pointer-events:none;transition:opacity .2s ease-in}dialog-panel[position=bottom]>dialog,dialog-panel[position=left]>dialog,dialog-panel[position=right]>dialog,dialog-panel[position=top]>dialog{margin:0;max-width:100%;opacity:1;position:fixed}dialog-panel[position=top]>dialog{bottom:auto;left:0;max-height:85vh;right:0;top:0;transform:translateY(-100%);width:100%}dialog-panel[position=top][state=shown]>dialog{transform:translateY(0)}dialog-panel[position=bottom]>dialog{bottom:0;left:0;max-height:85vh;right:0;top:auto;transform:translateY(100%);width:100%}dialog-panel[position=bottom][state=shown]>dialog{transform:translateY(0)}dialog-panel[position=left]>dialog{bottom:0;height:100dvh;left:0;max-height:100dvh;right:auto;top:0;transform:translateX(-100%);width:min(24rem,90vw)}dialog-panel[position=left][state=shown]>dialog{transform:translateX(0)}dialog-panel[position=right]>dialog{bottom:0;height:100dvh;left:auto;max-height:100dvh;right:0;top:0;transform:translateX(100%);width:min(24rem,90vw)}dialog-panel[position=right][state=shown]>dialog{transform:translateX(0)}body:has(dialog-panel[state=hiding]),body:has(dialog-panel[state=showing]),body:has(dialog-panel[state=shown]){overflow:hidden}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-spells/dialog-panel",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "A lightweight web component wrapper for native <dialog> elements with state-driven animations.",
5
5
  "author": "Cory Schulz",
6
6
  "license": "MIT",
@@ -37,8 +37,7 @@
37
37
  "custom-elements"
38
38
  ],
39
39
  "files": [
40
- "dist/",
41
- "src/"
40
+ "dist/"
42
41
  ],
43
42
  "scripts": {
44
43
  "build": "rollup -c",
@@ -1,117 +0,0 @@
1
- /**
2
- * dialog-panel basic centered modal styles
3
- *
4
- * This provides a simple fade in/out animation for a centered modal dialog.
5
- * Developers can customize or replace these styles to match their design.
6
- */
7
-
8
- /* Wrapper doesn't affect layout */
9
- dialog-panel {
10
- display: contents;
11
- }
12
-
13
- /* Base dialog styles with transitions */
14
- dialog-panel > dialog {
15
- border: none;
16
- border-radius: 0.5rem;
17
- padding: 0;
18
- max-width: 32rem;
19
- width: 90vw;
20
- max-height: 85vh;
21
- overflow: visible;
22
- background: white;
23
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
24
-
25
- /* Start hidden */
26
- opacity: 0;
27
- transform: scale(0.95);
28
-
29
- /* Transition always defined on base */
30
- transition:
31
- opacity 0.3s ease-out,
32
- transform 0.3s ease-out;
33
- }
34
-
35
- /* When using dialog-backdrop element, make native ::backdrop transparent
36
- but keep it clickable (clicks detected via bounding rect check) */
37
- dialog-panel:has(dialog-backdrop) > dialog::backdrop {
38
- background: transparent;
39
- }
40
-
41
- /* Native backdrop (when not using dialog-backdrop) */
42
- dialog-panel:not(:has(dialog-backdrop)) > dialog::backdrop {
43
- background: rgba(0, 0, 0, 0.3);
44
- opacity: 0;
45
- transition: opacity 0.3s ease-out;
46
- }
47
-
48
- /* Custom dialog-backdrop element */
49
- dialog-backdrop {
50
- position: fixed;
51
- background: rgba(0, 0, 0, 0.3);
52
- opacity: 0;
53
- transition: opacity 0.3s ease-out;
54
- pointer-events: none;
55
- z-index: 9999999;
56
- top: 50%;
57
- left: 50%;
58
- width: 200vw;
59
- height: 200dvh;
60
- transform: translateY(-50%) translateX(-50%);
61
- }
62
-
63
- /* Showing state - dialog is open but still at initial values
64
- (this is the starting point for the fade-in animation) */
65
- dialog-panel[state='showing'] > dialog {
66
- opacity: 0;
67
- transform: scale(0.95);
68
- }
69
-
70
- dialog-panel:not(:has(dialog-backdrop))[state='showing']
71
- > dialog::backdrop {
72
- opacity: 0;
73
- }
74
-
75
- /* Shown state - fully visible (animates from showing → shown) */
76
- dialog-panel[state='shown'] > dialog {
77
- opacity: 1;
78
- transform: scale(1);
79
- }
80
-
81
- dialog-panel:not(:has(dialog-backdrop))[state='shown']
82
- > dialog::backdrop {
83
- opacity: 1;
84
- }
85
-
86
- /* Hiding state - faster exit animation */
87
- dialog-panel[state='hiding'] > dialog {
88
- opacity: 0;
89
- transform: scale(0.95);
90
- transition:
91
- opacity 0.2s ease-in,
92
- transform 0.2s ease-in;
93
- }
94
-
95
- dialog-panel:not(:has(dialog-backdrop))[state='hiding']
96
- > dialog::backdrop {
97
- opacity: 0;
98
- transition: opacity 0.2s ease-in;
99
- }
100
-
101
- /* dialog-backdrop state animations */
102
- dialog-panel[state='showing'] > dialog-backdrop,
103
- dialog-panel[state='hidden'] > dialog-backdrop {
104
- opacity: 0;
105
- pointer-events: none;
106
- }
107
-
108
- dialog-panel[state='shown'] > dialog-backdrop {
109
- opacity: 1;
110
- pointer-events: auto;
111
- }
112
-
113
- dialog-panel[state='hiding'] > dialog-backdrop {
114
- opacity: 0;
115
- pointer-events: none;
116
- transition: opacity 0.2s ease-in;
117
- }
@@ -1,363 +0,0 @@
1
- import './dialog-panel.css';
2
-
3
- /**
4
- * DialogPanel - A lightweight web component wrapper for native <dialog> elements
5
- * with state-driven animations.
6
- *
7
- * @extends HTMLElement
8
- *
9
- * @property {string} state - Current state: 'hidden' | 'showing' | 'shown' | 'hiding'
10
- * @property {HTMLDialogElement} dialog - Reference to inner <dialog> element
11
- * @property {boolean} isOpen - True if state is 'showing' or 'shown'
12
- * @property {HTMLElement|null} triggerElement - Element that triggered current action
13
- *
14
- * @fires beforeShow - Fired before showing starts (cancelable)
15
- * @fires shown - Fired after show animation completes
16
- * @fires beforeHide - Fired before hiding starts (cancelable)
17
- * @fires hidden - Fired after hide animation completes
18
- */
19
- class DialogPanel extends HTMLElement {
20
- // Private fields
21
- #state = 'hidden';
22
- #triggerElement = null;
23
- #dialog = null;
24
- #result = null;
25
-
26
- // Event handler references for cleanup
27
- #handlers = {
28
- click: null,
29
- dialogClick: null,
30
- cancel: null,
31
- };
32
-
33
- // Animation cleanup references
34
- #pendingRAF = null;
35
- #pendingTimeout = null;
36
-
37
- // Fallback timeout for transitionend (in ms)
38
- static TRANSITION_FALLBACK_TIMEOUT = 700;
39
-
40
- connectedCallback() {
41
- const _ = this;
42
-
43
- // Find inner dialog element
44
- _.#dialog = _.querySelector('dialog');
45
-
46
- if (!_.#dialog) {
47
- console.warn(
48
- 'DialogPanel: No <dialog> element found inside <dialog-panel>'
49
- );
50
- return;
51
- }
52
-
53
- // Auto-create dialog-backdrop if not present
54
- if (!_.querySelector('dialog-backdrop')) {
55
- const backdrop = document.createElement('dialog-backdrop');
56
- _.insertBefore(backdrop, _.firstChild);
57
- }
58
-
59
- // Set initial state attribute
60
- _.#setState('hidden');
61
-
62
- // Bind event handlers
63
- _.#bindEvents();
64
- }
65
-
66
- disconnectedCallback() {
67
- const _ = this;
68
-
69
- // Cancel pending animations
70
- if (_.#pendingRAF) {
71
- cancelAnimationFrame(_.#pendingRAF);
72
- _.#pendingRAF = null;
73
- }
74
- if (_.#pendingTimeout) {
75
- clearTimeout(_.#pendingTimeout);
76
- _.#pendingTimeout = null;
77
- }
78
-
79
- // Clean up event listeners
80
- if (_.#handlers.click) {
81
- _.removeEventListener('click', _.#handlers.click);
82
- }
83
- if (_.#dialog) {
84
- if (_.#handlers.dialogClick) {
85
- _.#dialog.removeEventListener(
86
- 'click',
87
- _.#handlers.dialogClick
88
- );
89
- }
90
- if (_.#handlers.cancel) {
91
- _.#dialog.removeEventListener('cancel', _.#handlers.cancel);
92
- }
93
- }
94
- }
95
-
96
- /**
97
- * Bind event listeners for close buttons, backdrop, and escape key
98
- * @private
99
- */
100
- #bindEvents() {
101
- const _ = this;
102
-
103
- // Handle close buttons with data-action-hide-dialog
104
- _.#handlers.click = (e) => {
105
- const trigger = e.target.closest('[data-action-hide-dialog]');
106
- if (trigger) {
107
- e.stopPropagation();
108
- _.hide(trigger);
109
- }
110
- };
111
- _.addEventListener('click', _.#handlers.click);
112
-
113
- // Handle backdrop click - detect clicks outside dialog bounds
114
- // This works because clicks on ::backdrop still fire on the dialog element
115
- _.#handlers.dialogClick = (e) => {
116
- const rect = _.#dialog.getBoundingClientRect();
117
- const clickedOutside =
118
- e.clientX < rect.left ||
119
- e.clientX > rect.right ||
120
- e.clientY < rect.top ||
121
- e.clientY > rect.bottom;
122
- if (clickedOutside) {
123
- e.stopPropagation();
124
- _.hide();
125
- }
126
- };
127
- _.#dialog.addEventListener('click', _.#handlers.dialogClick);
128
-
129
- // Handle escape key - intercept native cancel and animate close
130
- _.#handlers.cancel = (e) => {
131
- e.preventDefault();
132
- e.stopPropagation();
133
- _.hide();
134
- };
135
- _.#dialog.addEventListener('cancel', _.#handlers.cancel);
136
- }
137
-
138
- /**
139
- * Show the dialog with animation
140
- * @param {HTMLElement} [triggerEl=null] - The element that triggered the show
141
- * @returns {boolean} False if show was prevented via beforeShow event
142
- */
143
- show(triggerEl = null) {
144
- const _ = this;
145
-
146
- // Check if already showing/shown
147
- if (_.#state === 'showing' || _.#state === 'shown') return true;
148
-
149
- // If currently hiding, ignore (let hide complete first)
150
- if (_.#state === 'hiding') return false;
151
-
152
- // Store trigger element for focus return later
153
- _.#triggerElement = triggerEl || null;
154
-
155
- // Fire beforeShow (cancelable)
156
- if (!_.#emit('beforeShow', { cancelable: true })) return false;
157
-
158
- // Set state to 'showing' and open native dialog
159
- _.#setState('showing');
160
- _.#dialog.showModal();
161
-
162
- // Double RAF ensures browser has painted the 'showing' state
163
- // before we transition to 'shown'
164
- _.#pendingRAF = requestAnimationFrame(() => {
165
- _.#pendingRAF = requestAnimationFrame(() => {
166
- _.#pendingRAF = null;
167
- _.#setState('shown');
168
-
169
- // Wait for transition to complete before firing 'shown' event
170
- _.#waitForTransition(() => {
171
- _.#emit('shown');
172
- });
173
- });
174
- });
175
-
176
- return true;
177
- }
178
-
179
- /**
180
- * Hide the dialog with animation
181
- * @param {HTMLElement} [triggerEl=null] - The element that triggered the hide
182
- * @returns {boolean} False if hide was prevented via beforeHide event
183
- */
184
- hide(triggerEl = null) {
185
- const _ = this;
186
-
187
- // Check if already hiding/hidden
188
- if (_.#state === 'hiding' || _.#state === 'hidden') return true;
189
-
190
- // If currently showing, ignore (let show complete first)
191
- if (_.#state === 'showing') return false;
192
-
193
- // Capture result from trigger element
194
- _.#result = triggerEl?.dataset?.result ?? null;
195
-
196
- // Fire beforeHide (cancelable)
197
- if (
198
- !_.#emit('beforeHide', {
199
- cancelable: true,
200
- result: _.#result,
201
- triggerElement: triggerEl,
202
- })
203
- ) {
204
- return false;
205
- }
206
-
207
- // Set state to 'hiding' - this triggers CSS exit animation
208
- _.#setState('hiding');
209
-
210
- // Wait for transition to complete
211
- _.#waitForTransition(() => {
212
- _.#dialog.close();
213
- _.#setState('hidden');
214
- _.#emit('hidden', {
215
- result: _.#result,
216
- triggerElement: triggerEl,
217
- });
218
-
219
- // Return focus to trigger element
220
- if (_.#triggerElement) _.#triggerElement.focus();
221
-
222
- // Clean up
223
- _.#triggerElement = null;
224
- _.#result = null;
225
- });
226
-
227
- return true;
228
- }
229
-
230
- /**
231
- * Wait for CSS transition to complete with fallback timeout
232
- * @param {Function} callback - Called when transition completes
233
- * @private
234
- */
235
- #waitForTransition(callback) {
236
- const _ = this;
237
- let called = false;
238
-
239
- const done = () => {
240
- if (called) return;
241
- called = true;
242
- _.#dialog.removeEventListener('transitionend', onTransitionEnd);
243
- if (_.#pendingTimeout) {
244
- clearTimeout(_.#pendingTimeout);
245
- _.#pendingTimeout = null;
246
- }
247
- callback();
248
- };
249
-
250
- const onTransitionEnd = (e) => {
251
- if (e.target === _.#dialog) done();
252
- };
253
-
254
- _.#dialog.addEventListener('transitionend', onTransitionEnd);
255
-
256
- _.#pendingTimeout = setTimeout(
257
- done,
258
- DialogPanel.TRANSITION_FALLBACK_TIMEOUT
259
- );
260
- }
261
-
262
- /**
263
- * Set the component state
264
- * @param {string} newState - The new state
265
- * @private
266
- */
267
- #setState(newState) {
268
- this.#state = newState;
269
- this.setAttribute('state', newState);
270
- }
271
-
272
- /**
273
- * Emit a custom event
274
- * @param {string} name - Event name
275
- * @param {Object} options - Event options
276
- * @returns {boolean} False if event was cancelled
277
- * @private
278
- */
279
- #emit(name, options = {}) {
280
- const _ = this;
281
- const { cancelable = false, ...detail } = options;
282
-
283
- const event = new CustomEvent(name, {
284
- bubbles: true,
285
- composed: true,
286
- cancelable,
287
- detail: {
288
- triggerElement: _.#triggerElement,
289
- result: _.#result,
290
- state: _.#state,
291
- ...detail,
292
- },
293
- });
294
-
295
- return _.dispatchEvent(event);
296
- }
297
-
298
- // Read-only properties
299
- get state() {
300
- return this.#state;
301
- }
302
- get dialog() {
303
- return this.#dialog;
304
- }
305
- get isOpen() {
306
- return this.#state === 'showing' || this.#state === 'shown';
307
- }
308
- get triggerElement() {
309
- return this.#triggerElement;
310
- }
311
- }
312
-
313
- /**
314
- * DialogBackdrop - A custom backdrop element that animates with the dialog-panel state.
315
- * Use this instead of native ::backdrop for consistent cross-browser animations.
316
- *
317
- * @extends HTMLElement
318
- */
319
- class DialogBackdrop extends HTMLElement {
320
- #panel = null;
321
- #handlers = {
322
- click: null,
323
- };
324
-
325
- connectedCallback() {
326
- const _ = this;
327
-
328
- // Find parent dialog-panel
329
- _.#panel = _.closest('dialog-panel');
330
-
331
- if (!_.#panel) {
332
- console.warn(
333
- 'DialogBackdrop: Must be inside a <dialog-panel> element'
334
- );
335
- return;
336
- }
337
-
338
- // Handle click to close
339
- _.#handlers.click = () => {
340
- _.#panel.hide();
341
- };
342
- _.addEventListener('click', _.#handlers.click);
343
- }
344
-
345
- disconnectedCallback() {
346
- const _ = this;
347
- if (_.#handlers.click) {
348
- _.removeEventListener('click', _.#handlers.click);
349
- }
350
- }
351
- }
352
-
353
- // Register the custom elements
354
- // Note: dialog-backdrop must be defined BEFORE dialog-panel,
355
- // because dialog-panel's connectedCallback may create dialog-backdrop elements
356
- if (!customElements.get('dialog-backdrop')) {
357
- customElements.define('dialog-backdrop', DialogBackdrop);
358
- }
359
- if (!customElements.get('dialog-panel')) {
360
- customElements.define('dialog-panel', DialogPanel);
361
- }
362
-
363
- export { DialogPanel, DialogBackdrop };