@magic-spells/dialog-panel 0.3.0 → 1.0.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 +163 -114
- package/dist/dialog-panel.cjs.js +287 -219
- package/dist/dialog-panel.cjs.js.map +1 -1
- package/dist/dialog-panel.css +105 -59
- package/dist/dialog-panel.esm.js +287 -215
- package/dist/dialog-panel.esm.js.map +1 -1
- package/dist/dialog-panel.js +276 -405
- 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 +8 -17
- package/src/dialog-panel.css +117 -0
- package/src/dialog-panel.js +288 -216
- package/dist/dialog-panel.scss +0 -2
- package/dist/scss/dialog-panel.scss +0 -78
- package/dist/scss/variables.scss +0 -40
- package/src/index.scss +0 -2
- package/src/scss/dialog-panel.scss +0 -78
- package/src/scss/variables.scss +0 -40
package/src/dialog-panel.js
CHANGED
|
@@ -1,291 +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
|
-
|
|
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() {
|
|
15
41
|
const _ = this;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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>'
|
|
20
49
|
);
|
|
50
|
+
return;
|
|
21
51
|
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Initializes the dialog panel, sets up focus trap and overlay
|
|
25
|
-
*/
|
|
26
|
-
constructor() {
|
|
27
|
-
super();
|
|
28
|
-
const _ = this;
|
|
29
|
-
_.id = _.getAttribute('id');
|
|
30
|
-
_.setAttribute('role', 'dialog');
|
|
31
|
-
_.setAttribute('aria-modal', 'true');
|
|
32
|
-
_.setAttribute('aria-hidden', 'true');
|
|
33
|
-
|
|
34
|
-
_.contentPanel = _.querySelector('dialog-content');
|
|
35
|
-
_.triggerEl = null;
|
|
36
|
-
|
|
37
|
-
// Create a handler for transition end events
|
|
38
|
-
_.#handleTransitionEnd = (e) => {
|
|
39
|
-
if (
|
|
40
|
-
e.propertyName === 'opacity' &&
|
|
41
|
-
_.getAttribute('aria-hidden') === 'true'
|
|
42
|
-
) {
|
|
43
|
-
_.contentPanel.classList.add('hidden');
|
|
44
|
-
|
|
45
|
-
// Dispatch afterHide event - dialog has completed its transition
|
|
46
|
-
_.dispatchEvent(
|
|
47
|
-
new CustomEvent('afterHide', {
|
|
48
|
-
bubbles: true,
|
|
49
|
-
detail: { triggerElement: _.triggerEl },
|
|
50
|
-
})
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
52
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
// Set initial state attribute
|
|
60
|
+
_.#setState('hidden');
|
|
61
|
+
|
|
62
|
+
// Bind event handlers
|
|
63
|
+
_.#bindEvents();
|
|
64
|
+
}
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
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;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
//
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
);
|
|
76
89
|
}
|
|
77
|
-
if (
|
|
78
|
-
_.
|
|
90
|
+
if (_.#handlers.cancel) {
|
|
91
|
+
_.#dialog.removeEventListener('cancel', _.#handlers.cancel);
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
|
-
|
|
82
|
-
// Add modal overlay
|
|
83
|
-
_.prepend(document.createElement('dialog-overlay'));
|
|
84
|
-
_.#bindUI();
|
|
85
|
-
_.#bindKeyboard();
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
/**
|
|
89
|
-
*
|
|
97
|
+
* Bind event listeners for close buttons, backdrop, and escape key
|
|
90
98
|
* @private
|
|
91
99
|
*/
|
|
92
|
-
#
|
|
100
|
+
#bindEvents() {
|
|
93
101
|
const _ = this;
|
|
94
102
|
|
|
95
|
-
// Handle
|
|
96
|
-
|
|
97
|
-
const trigger = e.target.closest(
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
e.preventDefault();
|
|
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);
|
|
102
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);
|
|
103
128
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
_.addEventListener('click', (e) => {
|
|
109
|
-
if (!e.target.closest('[data-action-hide-dialog]')) return;
|
|
129
|
+
// Handle escape key - intercept native cancel and animate close
|
|
130
|
+
_.#handlers.cancel = (e) => {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
e.stopPropagation();
|
|
110
133
|
_.hide();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Add transition end listener
|
|
114
|
-
_.contentPanel.addEventListener(
|
|
115
|
-
'transitionend',
|
|
116
|
-
_.#handleTransitionEnd
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Binds keyboard events for accessibility
|
|
122
|
-
* @private
|
|
123
|
-
*/
|
|
124
|
-
#bindKeyboard() {
|
|
125
|
-
this.addEventListener('keydown', (e) => {
|
|
126
|
-
if (e.key === 'Escape') {
|
|
127
|
-
this.hide();
|
|
128
|
-
}
|
|
129
|
-
});
|
|
134
|
+
};
|
|
135
|
+
_.#dialog.addEventListener('cancel', _.#handlers.cancel);
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/**
|
|
133
|
-
*
|
|
134
|
-
* @param {HTMLElement} [triggerEl=null] - The element that triggered the
|
|
135
|
-
* @
|
|
136
|
-
* @fires DialogPanel#show - Fired when the dialog has been shown
|
|
137
|
-
* @returns {boolean} False if the show was prevented by a beforeShow event handler
|
|
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
|
|
138
142
|
*/
|
|
139
143
|
show(triggerEl = null) {
|
|
140
144
|
const _ = this;
|
|
141
|
-
_.triggerEl = triggerEl || false;
|
|
142
145
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
bubbles: true,
|
|
146
|
-
cancelable: true,
|
|
147
|
-
detail: { triggerElement: _.triggerEl },
|
|
148
|
-
});
|
|
146
|
+
// Check if already showing/shown
|
|
147
|
+
if (_.#state === 'showing' || _.#state === 'shown') return true;
|
|
149
148
|
|
|
150
|
-
|
|
149
|
+
// If currently hiding, ignore (let hide complete first)
|
|
150
|
+
if (_.#state === 'hiding') return false;
|
|
151
151
|
|
|
152
|
-
//
|
|
153
|
-
|
|
152
|
+
// Store trigger element for focus return later
|
|
153
|
+
_.#triggerElement = triggerEl || null;
|
|
154
154
|
|
|
155
|
-
//
|
|
156
|
-
_
|
|
155
|
+
// Fire beforeShow (cancelable)
|
|
156
|
+
if (!_.#emit('beforeShow', { cancelable: true })) return false;
|
|
157
157
|
|
|
158
|
-
//
|
|
159
|
-
_
|
|
158
|
+
// Set state to 'showing' and open native dialog
|
|
159
|
+
_.#setState('showing');
|
|
160
|
+
_.#dialog.showModal();
|
|
160
161
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
_
|
|
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');
|
|
165
168
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Focus management
|
|
172
|
-
const firstFocusable = _.querySelector(
|
|
173
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
174
|
-
);
|
|
175
|
-
if (firstFocusable) {
|
|
176
|
-
requestAnimationFrame(() => {
|
|
177
|
-
firstFocusable.focus();
|
|
169
|
+
// Wait for transition to complete before firing 'shown' event
|
|
170
|
+
_.#waitForTransition(() => {
|
|
171
|
+
_.#emit('shown');
|
|
178
172
|
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Dispatch show event - dialog is now visible
|
|
182
|
-
_.dispatchEvent(
|
|
183
|
-
new CustomEvent('show', {
|
|
184
|
-
bubbles: true,
|
|
185
|
-
detail: { triggerElement: _.triggerEl },
|
|
186
|
-
})
|
|
187
|
-
);
|
|
173
|
+
});
|
|
188
174
|
});
|
|
175
|
+
|
|
176
|
+
return true;
|
|
189
177
|
}
|
|
190
178
|
|
|
191
179
|
/**
|
|
192
|
-
*
|
|
193
|
-
* @
|
|
194
|
-
* @
|
|
195
|
-
* @fires DialogPanel#afterHide - Fired when the dialog has completed its hide transition
|
|
196
|
-
* @returns {boolean} False if the hide was prevented by a beforeHide 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
|
|
197
183
|
*/
|
|
198
|
-
hide() {
|
|
184
|
+
hide(triggerEl = null) {
|
|
199
185
|
const _ = this;
|
|
200
186
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
bubbles: true,
|
|
204
|
-
cancelable: true,
|
|
205
|
-
detail: { triggerElement: _.triggerEl },
|
|
206
|
-
});
|
|
187
|
+
// Check if already hiding/hidden
|
|
188
|
+
if (_.#state === 'hiding' || _.#state === 'hidden') return true;
|
|
207
189
|
|
|
208
|
-
|
|
190
|
+
// If currently showing, ignore (let show complete first)
|
|
191
|
+
if (_.#state === 'showing') return false;
|
|
209
192
|
|
|
210
|
-
//
|
|
211
|
-
|
|
193
|
+
// Capture result from trigger element
|
|
194
|
+
_.#result = triggerEl?.dataset?.result ?? null;
|
|
212
195
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
196
|
+
// Fire beforeHide (cancelable)
|
|
197
|
+
if (
|
|
198
|
+
!_.#emit('beforeHide', {
|
|
199
|
+
cancelable: true,
|
|
200
|
+
result: _.#result,
|
|
201
|
+
triggerElement: triggerEl,
|
|
202
|
+
})
|
|
203
|
+
) {
|
|
204
|
+
return false;
|
|
218
205
|
}
|
|
219
206
|
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
// remove focus from modal panel first
|
|
223
|
-
_.triggerEl.focus();
|
|
224
|
-
// mark trigger as no longer expanded
|
|
225
|
-
_.triggerEl.setAttribute('aria-expanded', 'false');
|
|
226
|
-
} else {
|
|
227
|
-
// Move focus to body to ensure it's not trapped
|
|
228
|
-
document.body.focus();
|
|
229
|
-
}
|
|
207
|
+
// Set state to 'hiding' - this triggers CSS exit animation
|
|
208
|
+
_.#setState('hiding');
|
|
230
209
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
210
|
+
// Wait for transition to complete
|
|
211
|
+
_.#waitForTransition(() => {
|
|
212
|
+
_.#dialog.close();
|
|
213
|
+
_.#setState('hidden');
|
|
214
|
+
_.#emit('hidden', {
|
|
215
|
+
result: _.#result,
|
|
216
|
+
triggerElement: triggerEl,
|
|
217
|
+
});
|
|
235
218
|
|
|
236
|
-
|
|
219
|
+
// Return focus to trigger element
|
|
220
|
+
if (_.#triggerElement) _.#triggerElement.focus();
|
|
237
221
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
detail: { triggerElement: _.triggerEl },
|
|
243
|
-
})
|
|
244
|
-
);
|
|
222
|
+
// Clean up
|
|
223
|
+
_.#triggerElement = null;
|
|
224
|
+
_.#result = null;
|
|
225
|
+
});
|
|
245
226
|
|
|
246
227
|
return true;
|
|
247
228
|
}
|
|
248
|
-
}
|
|
249
229
|
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
260
|
}
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
},
|
|
265
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;
|
|
266
310
|
}
|
|
267
311
|
}
|
|
268
312
|
|
|
269
313
|
/**
|
|
270
|
-
*
|
|
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
|
+
*
|
|
271
317
|
* @extends HTMLElement
|
|
272
318
|
*/
|
|
273
|
-
class
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
}
|
|
277
350
|
}
|
|
278
351
|
}
|
|
279
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
|
+
}
|
|
280
359
|
if (!customElements.get('dialog-panel')) {
|
|
281
360
|
customElements.define('dialog-panel', DialogPanel);
|
|
282
361
|
}
|
|
283
|
-
if (!customElements.get('dialog-overlay')) {
|
|
284
|
-
customElements.define('dialog-overlay', DialogOverlay);
|
|
285
|
-
}
|
|
286
|
-
if (!customElements.get('dialog-content')) {
|
|
287
|
-
customElements.define('dialog-content', DialogContent);
|
|
288
|
-
}
|
|
289
362
|
|
|
290
|
-
export { DialogPanel,
|
|
291
|
-
export default DialogPanel;
|
|
363
|
+
export { DialogPanel, DialogBackdrop };
|
package/dist/dialog-panel.scss
DELETED
|
@@ -1,78 +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
|
-
|
|
70
|
-
/* When explicitly hidden, remove from layout */
|
|
71
|
-
dialog-content.hidden {
|
|
72
|
-
display: none;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Body scroll lock when dialog is open using :has() selector
|
|
76
|
-
body:has(dialog-panel[open]) {
|
|
77
|
-
overflow: hidden;
|
|
78
|
-
}
|