@magic-spells/dialog-panel 0.2.3 → 1.0.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 +163 -114
- package/dist/dialog-panel.cjs.js +281 -230
- package/dist/dialog-panel.cjs.js.map +1 -1
- package/dist/dialog-panel.css +108 -62
- package/dist/dialog-panel.esm.js +281 -226
- package/dist/dialog-panel.esm.js.map +1 -1
- package/dist/dialog-panel.js +273 -413
- 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/package.json +11 -20
- package/src/dialog-panel.css +117 -0
- package/src/dialog-panel.js +282 -227
- package/dist/dialog-panel.scss +0 -2
- package/dist/scss/dialog-panel.scss +0 -73
- package/dist/scss/variables.scss +0 -54
- package/src/index.scss +0 -2
- package/src/scss/dialog-panel.scss +0 -73
- package/src/scss/variables.scss +0 -54
package/src/dialog-panel.js
CHANGED
|
@@ -1,308 +1,363 @@
|
|
|
1
|
-
import './
|
|
2
|
-
import '@magic-spells/focus-trap';
|
|
1
|
+
import './dialog-panel.css';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
4
|
+
* DialogPanel - A lightweight web component wrapper for native <dialog> elements
|
|
5
|
+
* with state-driven animations.
|
|
6
|
+
*
|
|
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
|
|
7
18
|
*/
|
|
8
19
|
class DialogPanel extends HTMLElement {
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 = 500;
|
|
39
|
+
|
|
40
|
+
connectedCallback() {
|
|
16
41
|
const _ = this;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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>'
|
|
21
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);
|
|
22
57
|
}
|
|
23
58
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
59
|
+
// Set initial state attribute
|
|
60
|
+
_.#setState('hidden');
|
|
61
|
+
|
|
62
|
+
// Bind event handlers
|
|
63
|
+
_.#bindEvents();
|
|
27
64
|
}
|
|
28
65
|
|
|
29
|
-
|
|
30
|
-
* Saves current scroll position and locks body scrolling
|
|
31
|
-
* @private
|
|
32
|
-
*/
|
|
33
|
-
#lockScroll() {
|
|
66
|
+
disconnectedCallback() {
|
|
34
67
|
const _ = this;
|
|
35
|
-
// Save current scroll position
|
|
36
|
-
_.#scrollPosition = window.pageYOffset;
|
|
37
68
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
}
|
|
41
94
|
}
|
|
42
95
|
|
|
43
96
|
/**
|
|
44
|
-
*
|
|
97
|
+
* Bind event listeners for close buttons, backdrop, and escape key
|
|
45
98
|
* @private
|
|
46
99
|
*/
|
|
47
|
-
#
|
|
100
|
+
#bindEvents() {
|
|
48
101
|
const _ = this;
|
|
49
|
-
// Remove fixed positioning
|
|
50
|
-
document.body.classList.remove('overflow-hidden');
|
|
51
|
-
document.body.style.removeProperty('top');
|
|
52
102
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
constructor() {
|
|
60
|
-
super();
|
|
61
|
-
const _ = this;
|
|
62
|
-
_.id = _.getAttribute('id');
|
|
63
|
-
_.setAttribute('role', 'dialog');
|
|
64
|
-
_.setAttribute('aria-modal', 'true');
|
|
65
|
-
_.setAttribute('aria-hidden', 'true');
|
|
66
|
-
|
|
67
|
-
_.contentPanel = _.querySelector('dialog-content');
|
|
68
|
-
_.focusTrap = document.createElement('focus-trap');
|
|
69
|
-
_.triggerEl = null;
|
|
70
|
-
|
|
71
|
-
// Create a handler for transition end events
|
|
72
|
-
_.#handleTransitionEnd = (e) => {
|
|
73
|
-
if (
|
|
74
|
-
e.propertyName === 'opacity' &&
|
|
75
|
-
_.getAttribute('aria-hidden') === 'true'
|
|
76
|
-
) {
|
|
77
|
-
_.contentPanel.classList.add('hidden');
|
|
78
|
-
|
|
79
|
-
// Dispatch afterHide event - dialog has completed its transition
|
|
80
|
-
_.dispatchEvent(
|
|
81
|
-
new CustomEvent('afterHide', {
|
|
82
|
-
bubbles: true,
|
|
83
|
-
detail: { triggerElement: _.triggerEl },
|
|
84
|
-
})
|
|
85
|
-
);
|
|
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);
|
|
86
109
|
}
|
|
87
110
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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();
|
|
94
125
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
_.contentPanel.parentNode.insertBefore(
|
|
101
|
-
_.focusTrap,
|
|
102
|
-
_.contentPanel
|
|
103
|
-
);
|
|
104
|
-
_.focusTrap.appendChild(_.contentPanel);
|
|
105
|
-
|
|
106
|
-
_.focusTrap.setupTrap();
|
|
126
|
+
};
|
|
127
|
+
_.#dialog.addEventListener('click', _.#handlers.dialogClick);
|
|
107
128
|
|
|
108
|
-
//
|
|
109
|
-
_.
|
|
110
|
-
|
|
111
|
-
|
|
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);
|
|
112
136
|
}
|
|
113
137
|
|
|
114
138
|
/**
|
|
115
|
-
*
|
|
116
|
-
* @
|
|
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
|
|
117
142
|
*/
|
|
118
|
-
|
|
143
|
+
show(triggerEl = null) {
|
|
119
144
|
const _ = this;
|
|
120
145
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
const trigger = e.target.closest(`[aria-controls="${_.id}"]`);
|
|
124
|
-
if (!trigger) return;
|
|
146
|
+
// Check if already showing/shown
|
|
147
|
+
if (_.#state === 'showing' || _.#state === 'shown') return true;
|
|
125
148
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
149
|
+
// If currently hiding, ignore (let hide complete first)
|
|
150
|
+
if (_.#state === 'hiding') return false;
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|
|
152
|
+
// Store trigger element for focus return later
|
|
153
|
+
_.#triggerElement = triggerEl || null;
|
|
132
154
|
|
|
133
|
-
//
|
|
134
|
-
_
|
|
135
|
-
if (!e.target.closest('[data-action="hide-dialog"]')) return;
|
|
136
|
-
_.hide();
|
|
137
|
-
});
|
|
155
|
+
// Fire beforeShow (cancelable)
|
|
156
|
+
if (!_.#emit('beforeShow', { cancelable: true })) return false;
|
|
138
157
|
|
|
139
|
-
//
|
|
140
|
-
_
|
|
141
|
-
|
|
142
|
-
_.#handleTransitionEnd
|
|
143
|
-
);
|
|
144
|
-
}
|
|
158
|
+
// Set state to 'showing' and open native dialog
|
|
159
|
+
_.#setState('showing');
|
|
160
|
+
_.#dialog.showModal();
|
|
145
161
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
});
|
|
155
174
|
});
|
|
175
|
+
|
|
176
|
+
return true;
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
/**
|
|
159
|
-
*
|
|
160
|
-
* @param {HTMLElement} [triggerEl=null] - The element that triggered the
|
|
161
|
-
* @
|
|
162
|
-
* @fires DialogPanel#show - Fired when the dialog has been shown
|
|
163
|
-
* @returns {boolean} False if the show was prevented by a beforeShow event handler
|
|
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
|
|
164
183
|
*/
|
|
165
|
-
|
|
184
|
+
hide(triggerEl = null) {
|
|
166
185
|
const _ = this;
|
|
167
|
-
_.triggerEl = triggerEl || false;
|
|
168
186
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
bubbles: true,
|
|
172
|
-
cancelable: true,
|
|
173
|
-
detail: { triggerElement: _.triggerEl },
|
|
174
|
-
});
|
|
187
|
+
// Check if already hiding/hidden
|
|
188
|
+
if (_.#state === 'hiding' || _.#state === 'hidden') return true;
|
|
175
189
|
|
|
176
|
-
|
|
190
|
+
// If currently showing, ignore (let show complete first)
|
|
191
|
+
if (_.#state === 'showing') return false;
|
|
177
192
|
|
|
178
|
-
//
|
|
179
|
-
|
|
193
|
+
// Capture result from trigger element
|
|
194
|
+
_.#result = triggerEl?.dataset?.result ?? null;
|
|
180
195
|
|
|
181
|
-
//
|
|
182
|
-
|
|
196
|
+
// Fire beforeHide (cancelable)
|
|
197
|
+
if (
|
|
198
|
+
!_.#emit('beforeHide', {
|
|
199
|
+
cancelable: true,
|
|
200
|
+
result: _.#result,
|
|
201
|
+
triggerElement: triggerEl,
|
|
202
|
+
})
|
|
203
|
+
) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
183
206
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
// Update ARIA states
|
|
187
|
-
_.setAttribute('aria-hidden', 'false');
|
|
188
|
-
if (_.triggerEl) {
|
|
189
|
-
_.triggerEl.setAttribute('aria-expanded', 'true');
|
|
190
|
-
}
|
|
207
|
+
// Set state to 'hiding' - this triggers CSS exit animation
|
|
208
|
+
_.#setState('hiding');
|
|
191
209
|
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
// Wait for transition to complete
|
|
211
|
+
_.#waitForTransition(() => {
|
|
212
|
+
_.#dialog.close();
|
|
213
|
+
_.#setState('hidden');
|
|
214
|
+
_.#emit('hidden', {
|
|
215
|
+
result: _.#result,
|
|
216
|
+
triggerElement: triggerEl,
|
|
217
|
+
});
|
|
194
218
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
198
|
-
);
|
|
199
|
-
if (firstFocusable) {
|
|
200
|
-
requestAnimationFrame(() => {
|
|
201
|
-
firstFocusable.focus();
|
|
202
|
-
});
|
|
203
|
-
}
|
|
219
|
+
// Return focus to trigger element
|
|
220
|
+
if (_.#triggerElement) _.#triggerElement.focus();
|
|
204
221
|
|
|
205
|
-
//
|
|
206
|
-
_
|
|
207
|
-
|
|
208
|
-
bubbles: true,
|
|
209
|
-
detail: { triggerElement: _.triggerEl },
|
|
210
|
-
})
|
|
211
|
-
);
|
|
222
|
+
// Clean up
|
|
223
|
+
_.#triggerElement = null;
|
|
224
|
+
_.#result = null;
|
|
212
225
|
});
|
|
213
226
|
|
|
214
227
|
return true;
|
|
215
228
|
}
|
|
216
229
|
|
|
217
230
|
/**
|
|
218
|
-
*
|
|
219
|
-
* @
|
|
220
|
-
* @
|
|
221
|
-
* @fires DialogPanel#afterHide - Fired when the dialog has completed its hide transition
|
|
222
|
-
* @returns {boolean} False if the hide was prevented by a beforeHide event handler
|
|
231
|
+
* Wait for CSS transition to complete with fallback timeout
|
|
232
|
+
* @param {Function} callback - Called when transition completes
|
|
233
|
+
* @private
|
|
223
234
|
*/
|
|
224
|
-
|
|
235
|
+
#waitForTransition(callback) {
|
|
225
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
|
+
};
|
|
226
249
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
cancelable: true,
|
|
231
|
-
detail: { triggerElement: _.triggerEl },
|
|
232
|
-
});
|
|
250
|
+
const onTransitionEnd = (e) => {
|
|
251
|
+
if (e.target === _.#dialog) done();
|
|
252
|
+
};
|
|
233
253
|
|
|
234
|
-
|
|
254
|
+
_.#dialog.addEventListener('transitionend', onTransitionEnd);
|
|
235
255
|
|
|
236
|
-
|
|
237
|
-
|
|
256
|
+
_.#pendingTimeout = setTimeout(
|
|
257
|
+
done,
|
|
258
|
+
DialogPanel.TRANSITION_FALLBACK_TIMEOUT
|
|
259
|
+
);
|
|
260
|
+
}
|
|
238
261
|
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
271
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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;
|
|
249
282
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
});
|
|
253
294
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
new CustomEvent('hide', {
|
|
257
|
-
bubbles: true,
|
|
258
|
-
detail: { triggerElement: _.triggerEl },
|
|
259
|
-
})
|
|
260
|
-
);
|
|
295
|
+
return _.dispatchEvent(event);
|
|
296
|
+
}
|
|
261
297
|
|
|
262
|
-
|
|
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;
|
|
263
310
|
}
|
|
264
311
|
}
|
|
265
312
|
|
|
266
313
|
/**
|
|
267
|
-
*
|
|
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
|
+
*
|
|
268
317
|
* @extends HTMLElement
|
|
269
318
|
*/
|
|
270
|
-
class
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
this.dialogPanel = this.closest('dialog-panel');
|
|
276
|
-
this.#bindUI();
|
|
277
|
-
}
|
|
319
|
+
class DialogBackdrop extends HTMLElement {
|
|
320
|
+
#panel = null;
|
|
321
|
+
#handlers = {
|
|
322
|
+
click: null,
|
|
323
|
+
};
|
|
278
324
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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);
|
|
283
343
|
}
|
|
284
|
-
}
|
|
285
344
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
constructor() {
|
|
292
|
-
super();
|
|
293
|
-
this.setAttribute('role', 'document'); // Optional: helps with document structure
|
|
345
|
+
disconnectedCallback() {
|
|
346
|
+
const _ = this;
|
|
347
|
+
if (_.#handlers.click) {
|
|
348
|
+
_.removeEventListener('click', _.#handlers.click);
|
|
349
|
+
}
|
|
294
350
|
}
|
|
295
351
|
}
|
|
296
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
|
+
}
|
|
297
359
|
if (!customElements.get('dialog-panel')) {
|
|
298
360
|
customElements.define('dialog-panel', DialogPanel);
|
|
299
361
|
}
|
|
300
|
-
if (!customElements.get('dialog-overlay')) {
|
|
301
|
-
customElements.define('dialog-overlay', DialogOverlay);
|
|
302
|
-
}
|
|
303
|
-
if (!customElements.get('dialog-content')) {
|
|
304
|
-
customElements.define('dialog-content', DialogContent);
|
|
305
|
-
}
|
|
306
362
|
|
|
307
|
-
export { DialogPanel,
|
|
308
|
-
export default DialogPanel;
|
|
363
|
+
export { DialogPanel, DialogBackdrop };
|
package/dist/dialog-panel.scss
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// Import variables using the modern @use rule
|
|
2
|
-
@use 'variables' as vars;
|
|
3
|
-
|
|
4
|
-
dialog-panel {
|
|
5
|
-
/* Make it take no space and be invisible in the document flow */
|
|
6
|
-
display: contents;
|
|
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
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/* Overlay background */
|
|
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
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
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);
|
|
63
|
-
|
|
64
|
-
/* When shown, reset transform to center */
|
|
65
|
-
dialog-panel[aria-hidden='false'] & {
|
|
66
|
-
transform: translate(-50%, -50%) scale(1);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/* When explicitly hidden, remove from layout */
|
|
70
|
-
&.hidden {
|
|
71
|
-
display: none;
|
|
72
|
-
}
|
|
73
|
-
}
|