@magic-spells/dialog-panel 0.3.0 → 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 +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/dist/dialog-panel.js
CHANGED
|
@@ -5,496 +5,367 @@
|
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* DialogPanel - A lightweight web component wrapper for native <dialog> elements
|
|
9
|
+
* with state-driven animations.
|
|
9
10
|
*
|
|
10
|
-
* @
|
|
11
|
-
*
|
|
11
|
+
* @extends HTMLElement
|
|
12
|
+
*
|
|
13
|
+
* @property {string} state - Current state: 'hidden' | 'showing' | 'shown' | 'hiding'
|
|
14
|
+
* @property {HTMLDialogElement} dialog - Reference to inner <dialog> element
|
|
15
|
+
* @property {boolean} isOpen - True if state is 'showing' or 'shown'
|
|
16
|
+
* @property {HTMLElement|null} triggerElement - Element that triggered current action
|
|
17
|
+
*
|
|
18
|
+
* @fires beforeShow - Fired before showing starts (cancelable)
|
|
19
|
+
* @fires shown - Fired after show animation completes
|
|
20
|
+
* @fires beforeHide - Fired before hiding starts (cancelable)
|
|
21
|
+
* @fires hidden - Fired after hide animation completes
|
|
12
22
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this.trapEnd = null;
|
|
27
|
-
|
|
28
|
-
// Inject styles only once, when the first FocusTrap instance is created.
|
|
29
|
-
if (!FocusTrap.styleInjected) {
|
|
30
|
-
this.injectStyles();
|
|
31
|
-
FocusTrap.styleInjected = true;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
23
|
+
class DialogPanel extends HTMLElement {
|
|
24
|
+
// Private fields
|
|
25
|
+
#state = 'hidden';
|
|
26
|
+
#triggerElement = null;
|
|
27
|
+
#dialog = null;
|
|
28
|
+
#result = null;
|
|
29
|
+
|
|
30
|
+
// Event handler references for cleanup
|
|
31
|
+
#handlers = {
|
|
32
|
+
click: null,
|
|
33
|
+
dialogClick: null,
|
|
34
|
+
cancel: null,
|
|
35
|
+
};
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
style.textContent = `
|
|
42
|
-
focus-trap-start,
|
|
43
|
-
focus-trap-end {
|
|
44
|
-
position: absolute;
|
|
45
|
-
width: 1px;
|
|
46
|
-
height: 1px;
|
|
47
|
-
margin: -1px;
|
|
48
|
-
padding: 0;
|
|
49
|
-
border: 0;
|
|
50
|
-
clip: rect(0, 0, 0, 0);
|
|
51
|
-
overflow: hidden;
|
|
52
|
-
white-space: nowrap;
|
|
53
|
-
}
|
|
54
|
-
`;
|
|
55
|
-
document.head.appendChild(style);
|
|
56
|
-
}
|
|
37
|
+
// Animation cleanup references
|
|
38
|
+
#pendingRAF = null;
|
|
39
|
+
#pendingTimeout = null;
|
|
40
|
+
|
|
41
|
+
// Fallback timeout for transitionend (in ms)
|
|
42
|
+
static TRANSITION_FALLBACK_TIMEOUT = 500;
|
|
57
43
|
|
|
58
|
-
/**
|
|
59
|
-
* Called when the element is connected to the DOM.
|
|
60
|
-
* Sets up the focus trap and adds the keydown event listener.
|
|
61
|
-
*/
|
|
62
44
|
connectedCallback() {
|
|
63
|
-
this
|
|
64
|
-
this.addEventListener('keydown', this.handleKeyDown);
|
|
65
|
-
}
|
|
45
|
+
const _ = this;
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* Removes the keydown event listener.
|
|
70
|
-
*/
|
|
71
|
-
disconnectedCallback() {
|
|
72
|
-
this.removeEventListener('keydown', this.handleKeyDown);
|
|
73
|
-
}
|
|
47
|
+
// Find inner dialog element
|
|
48
|
+
_.#dialog = _.querySelector('dialog');
|
|
74
49
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const focusableElements = getFocusableElements(this);
|
|
82
|
-
// exit if there aren't any
|
|
83
|
-
if (focusableElements.length === 0) return;
|
|
84
|
-
|
|
85
|
-
// create trap start and end elements
|
|
86
|
-
this.trapStart = document.createElement('focus-trap-start');
|
|
87
|
-
this.trapEnd = document.createElement('focus-trap-end');
|
|
88
|
-
|
|
89
|
-
// add to DOM
|
|
90
|
-
this.prepend(this.trapStart);
|
|
91
|
-
this.append(this.trapEnd);
|
|
92
|
-
}
|
|
50
|
+
if (!_.#dialog) {
|
|
51
|
+
console.warn(
|
|
52
|
+
'DialogPanel: No <dialog> element found inside <dialog-panel>'
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
93
56
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
*/
|
|
99
|
-
handleKeyDown = (e) => {
|
|
100
|
-
if (e.key === 'Escape') {
|
|
101
|
-
e.preventDefault();
|
|
102
|
-
this.exitTrap();
|
|
57
|
+
// Auto-create dialog-backdrop if not present
|
|
58
|
+
if (!_.querySelector('dialog-backdrop')) {
|
|
59
|
+
const backdrop = document.createElement('dialog-backdrop');
|
|
60
|
+
_.insertBefore(backdrop, _.firstChild);
|
|
103
61
|
}
|
|
104
|
-
};
|
|
105
62
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
* back to the trigger element that opened the trap.
|
|
109
|
-
*/
|
|
110
|
-
exitTrap() {
|
|
111
|
-
const container = this.closest('[aria-hidden="false"]');
|
|
112
|
-
if (!container) return;
|
|
63
|
+
// Set initial state attribute
|
|
64
|
+
_.#setState('hidden');
|
|
113
65
|
|
|
114
|
-
|
|
66
|
+
// Bind event handlers
|
|
67
|
+
_.#bindEvents();
|
|
68
|
+
}
|
|
115
69
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
70
|
+
disconnectedCallback() {
|
|
71
|
+
const _ = this;
|
|
72
|
+
|
|
73
|
+
// Cancel pending animations
|
|
74
|
+
if (_.#pendingRAF) {
|
|
75
|
+
cancelAnimationFrame(_.#pendingRAF);
|
|
76
|
+
_.#pendingRAF = null;
|
|
77
|
+
}
|
|
78
|
+
if (_.#pendingTimeout) {
|
|
79
|
+
clearTimeout(_.#pendingTimeout);
|
|
80
|
+
_.#pendingTimeout = null;
|
|
122
81
|
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
82
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
83
|
+
// Clean up event listeners
|
|
84
|
+
if (_.#handlers.click) {
|
|
85
|
+
_.removeEventListener('click', _.#handlers.click);
|
|
86
|
+
}
|
|
87
|
+
if (_.#dialog) {
|
|
88
|
+
if (_.#handlers.dialogClick) {
|
|
89
|
+
_.#dialog.removeEventListener(
|
|
90
|
+
'click',
|
|
91
|
+
_.#handlers.dialogClick
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (_.#handlers.cancel) {
|
|
95
|
+
_.#dialog.removeEventListener('cancel', _.#handlers.cancel);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
134
98
|
}
|
|
135
99
|
|
|
136
100
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
101
|
+
* Bind event listeners for close buttons, backdrop, and escape key
|
|
102
|
+
* @private
|
|
139
103
|
*/
|
|
140
|
-
|
|
141
|
-
|
|
104
|
+
#bindEvents() {
|
|
105
|
+
const _ = this;
|
|
106
|
+
|
|
107
|
+
// Handle close buttons with data-action-hide-dialog
|
|
108
|
+
_.#handlers.click = (e) => {
|
|
109
|
+
const trigger = e.target.closest('[data-action-hide-dialog]');
|
|
110
|
+
if (trigger) {
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
_.hide(trigger);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
_.addEventListener('click', _.#handlers.click);
|
|
116
|
+
|
|
117
|
+
// Handle backdrop click - detect clicks outside dialog bounds
|
|
118
|
+
// This works because clicks on ::backdrop still fire on the dialog element
|
|
119
|
+
_.#handlers.dialogClick = (e) => {
|
|
120
|
+
const rect = _.#dialog.getBoundingClientRect();
|
|
121
|
+
const clickedOutside =
|
|
122
|
+
e.clientX < rect.left ||
|
|
123
|
+
e.clientX > rect.right ||
|
|
124
|
+
e.clientY < rect.top ||
|
|
125
|
+
e.clientY > rect.bottom;
|
|
126
|
+
if (clickedOutside) {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
_.hide();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
_.#dialog.addEventListener('click', _.#handlers.dialogClick);
|
|
132
|
+
|
|
133
|
+
// Handle escape key - intercept native cancel and animate close
|
|
134
|
+
_.#handlers.cancel = (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
_.hide();
|
|
138
|
+
};
|
|
139
|
+
_.#dialog.addEventListener('cancel', _.#handlers.cancel);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* @param {FocusEvent} e - The focus event object.
|
|
143
|
+
* Show the dialog with animation
|
|
144
|
+
* @param {HTMLElement} [triggerEl=null] - The element that triggered the show
|
|
145
|
+
* @returns {boolean} False if show was prevented via beforeShow event
|
|
149
146
|
*/
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const focusableElements = getFocusableElements(trap);
|
|
147
|
+
show(triggerEl = null) {
|
|
148
|
+
const _ = this;
|
|
153
149
|
|
|
154
|
-
|
|
150
|
+
// Check if already showing/shown
|
|
151
|
+
if (_.#state === 'showing' || _.#state === 'shown') return true;
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
focusableElements[focusableElements.length - 1];
|
|
153
|
+
// If currently hiding, ignore (let hide complete first)
|
|
154
|
+
if (_.#state === 'hiding') return false;
|
|
159
155
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} else {
|
|
163
|
-
firstElement.focus();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
}
|
|
156
|
+
// Store trigger element for focus return later
|
|
157
|
+
_.#triggerElement = triggerEl || null;
|
|
167
158
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* Called when the element is connected to the DOM.
|
|
171
|
-
* Sets the tabindex and adds the focus event listener.
|
|
172
|
-
*/
|
|
173
|
-
connectedCallback() {
|
|
174
|
-
this.setAttribute('tabindex', '0');
|
|
175
|
-
this.addEventListener('focus', this.handleFocus);
|
|
176
|
-
}
|
|
159
|
+
// Fire beforeShow (cancelable)
|
|
160
|
+
if (!_.#emit('beforeShow', { cancelable: true })) return false;
|
|
177
161
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
*/
|
|
182
|
-
disconnectedCallback() {
|
|
183
|
-
this.removeEventListener('focus', this.handleFocus);
|
|
184
|
-
}
|
|
162
|
+
// Set state to 'showing' and open native dialog
|
|
163
|
+
_.#setState('showing');
|
|
164
|
+
_.#dialog.showModal();
|
|
185
165
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
trapStart.focus();
|
|
193
|
-
};
|
|
194
|
-
}
|
|
166
|
+
// Double RAF ensures browser has painted the 'showing' state
|
|
167
|
+
// before we transition to 'shown'
|
|
168
|
+
_.#pendingRAF = requestAnimationFrame(() => {
|
|
169
|
+
_.#pendingRAF = requestAnimationFrame(() => {
|
|
170
|
+
_.#pendingRAF = null;
|
|
171
|
+
_.#setState('shown');
|
|
195
172
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (!customElements.get('focus-trap-end')) {
|
|
203
|
-
customElements.define('focus-trap-end', FocusTrapEnd);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Custom element that creates an accessible modal dialog panel with focus management
|
|
208
|
-
* @extends HTMLElement
|
|
209
|
-
*/
|
|
210
|
-
class DialogPanel extends HTMLElement {
|
|
211
|
-
#handleTransitionEnd;
|
|
173
|
+
// Wait for transition to complete before firing 'shown' event
|
|
174
|
+
_.#waitForTransition(() => {
|
|
175
|
+
_.#emit('shown');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
212
179
|
|
|
213
|
-
|
|
214
|
-
* Clean up event listeners when component is removed from DOM
|
|
215
|
-
*/
|
|
216
|
-
disconnectedCallback() {
|
|
217
|
-
const _ = this;
|
|
218
|
-
if (_.contentPanel) {
|
|
219
|
-
_.contentPanel.removeEventListener(
|
|
220
|
-
'transitionend',
|
|
221
|
-
_.#handleTransitionEnd
|
|
222
|
-
);
|
|
223
|
-
}
|
|
180
|
+
return true;
|
|
224
181
|
}
|
|
182
|
+
|
|
225
183
|
/**
|
|
226
|
-
*
|
|
184
|
+
* Hide the dialog with animation
|
|
185
|
+
* @param {HTMLElement} [triggerEl=null] - The element that triggered the hide
|
|
186
|
+
* @returns {boolean} False if hide was prevented via beforeHide event
|
|
227
187
|
*/
|
|
228
|
-
|
|
229
|
-
super();
|
|
188
|
+
hide(triggerEl = null) {
|
|
230
189
|
const _ = this;
|
|
231
|
-
_.id = _.getAttribute('id');
|
|
232
|
-
_.setAttribute('role', 'dialog');
|
|
233
|
-
_.setAttribute('aria-modal', 'true');
|
|
234
|
-
_.setAttribute('aria-hidden', 'true');
|
|
235
|
-
|
|
236
|
-
_.contentPanel = _.querySelector('dialog-content');
|
|
237
|
-
_.triggerEl = null;
|
|
238
|
-
|
|
239
|
-
// Create a handler for transition end events
|
|
240
|
-
_.#handleTransitionEnd = (e) => {
|
|
241
|
-
if (
|
|
242
|
-
e.propertyName === 'opacity' &&
|
|
243
|
-
_.getAttribute('aria-hidden') === 'true'
|
|
244
|
-
) {
|
|
245
|
-
_.contentPanel.classList.add('hidden');
|
|
246
|
-
|
|
247
|
-
// Dispatch afterHide event - dialog has completed its transition
|
|
248
|
-
_.dispatchEvent(
|
|
249
|
-
new CustomEvent('afterHide', {
|
|
250
|
-
bubbles: true,
|
|
251
|
-
detail: { triggerElement: _.triggerEl },
|
|
252
|
-
})
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
190
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
_.focusTrap = _.contentPanel.querySelector('focus-trap');
|
|
260
|
-
if (!_.focusTrap) {
|
|
261
|
-
_.focusTrap = document.createElement('focus-trap');
|
|
191
|
+
// Check if already hiding/hidden
|
|
192
|
+
if (_.#state === 'hiding' || _.#state === 'hidden') return true;
|
|
262
193
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
existingContent.forEach((child) =>
|
|
266
|
-
_.focusTrap.appendChild(child)
|
|
267
|
-
);
|
|
194
|
+
// If currently showing, ignore (let show complete first)
|
|
195
|
+
if (_.#state === 'showing') return false;
|
|
268
196
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
197
|
+
// Capture result from trigger element
|
|
198
|
+
_.#result = triggerEl?.dataset?.result ?? null;
|
|
272
199
|
|
|
273
|
-
//
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
200
|
+
// Fire beforeHide (cancelable)
|
|
201
|
+
if (
|
|
202
|
+
!_.#emit('beforeHide', {
|
|
203
|
+
cancelable: true,
|
|
204
|
+
result: _.#result,
|
|
205
|
+
triggerElement: triggerEl,
|
|
206
|
+
})
|
|
207
|
+
) {
|
|
208
|
+
return false;
|
|
282
209
|
}
|
|
283
210
|
|
|
284
|
-
//
|
|
285
|
-
_
|
|
286
|
-
|
|
287
|
-
|
|
211
|
+
// Set state to 'hiding' - this triggers CSS exit animation
|
|
212
|
+
_.#setState('hiding');
|
|
213
|
+
|
|
214
|
+
// Wait for transition to complete
|
|
215
|
+
_.#waitForTransition(() => {
|
|
216
|
+
_.#dialog.close();
|
|
217
|
+
_.#setState('hidden');
|
|
218
|
+
_.#emit('hidden', {
|
|
219
|
+
result: _.#result,
|
|
220
|
+
triggerElement: triggerEl,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Return focus to trigger element
|
|
224
|
+
if (_.#triggerElement) _.#triggerElement.focus();
|
|
225
|
+
|
|
226
|
+
// Clean up
|
|
227
|
+
_.#triggerElement = null;
|
|
228
|
+
_.#result = null;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return true;
|
|
288
232
|
}
|
|
289
233
|
|
|
290
234
|
/**
|
|
291
|
-
*
|
|
235
|
+
* Wait for CSS transition to complete with fallback timeout
|
|
236
|
+
* @param {Function} callback - Called when transition completes
|
|
292
237
|
* @private
|
|
293
238
|
*/
|
|
294
|
-
#
|
|
239
|
+
#waitForTransition(callback) {
|
|
295
240
|
const _ = this;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
|
|
241
|
+
let called = false;
|
|
242
|
+
|
|
243
|
+
const done = () => {
|
|
244
|
+
if (called) return;
|
|
245
|
+
called = true;
|
|
246
|
+
_.#dialog.removeEventListener('transitionend', onTransitionEnd);
|
|
247
|
+
if (_.#pendingTimeout) {
|
|
248
|
+
clearTimeout(_.#pendingTimeout);
|
|
249
|
+
_.#pendingTimeout = null;
|
|
304
250
|
}
|
|
251
|
+
callback();
|
|
252
|
+
};
|
|
305
253
|
|
|
306
|
-
|
|
307
|
-
|
|
254
|
+
const onTransitionEnd = (e) => {
|
|
255
|
+
if (e.target === _.#dialog) done();
|
|
256
|
+
};
|
|
308
257
|
|
|
309
|
-
|
|
310
|
-
_.addEventListener('click', (e) => {
|
|
311
|
-
if (!e.target.closest('[data-action-hide-dialog]')) return;
|
|
312
|
-
_.hide();
|
|
313
|
-
});
|
|
258
|
+
_.#dialog.addEventListener('transitionend', onTransitionEnd);
|
|
314
259
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
_.#handleTransitionEnd
|
|
260
|
+
_.#pendingTimeout = setTimeout(
|
|
261
|
+
done,
|
|
262
|
+
DialogPanel.TRANSITION_FALLBACK_TIMEOUT
|
|
319
263
|
);
|
|
320
264
|
}
|
|
321
265
|
|
|
322
266
|
/**
|
|
323
|
-
*
|
|
267
|
+
* Set the component state
|
|
268
|
+
* @param {string} newState - The new state
|
|
324
269
|
* @private
|
|
325
270
|
*/
|
|
326
|
-
#
|
|
327
|
-
this
|
|
328
|
-
|
|
329
|
-
this.hide();
|
|
330
|
-
}
|
|
331
|
-
});
|
|
271
|
+
#setState(newState) {
|
|
272
|
+
this.#state = newState;
|
|
273
|
+
this.setAttribute('state', newState);
|
|
332
274
|
}
|
|
333
275
|
|
|
334
276
|
/**
|
|
335
|
-
*
|
|
336
|
-
* @param {
|
|
337
|
-
* @
|
|
338
|
-
* @
|
|
339
|
-
* @
|
|
277
|
+
* Emit a custom event
|
|
278
|
+
* @param {string} name - Event name
|
|
279
|
+
* @param {Object} options - Event options
|
|
280
|
+
* @returns {boolean} False if event was cancelled
|
|
281
|
+
* @private
|
|
340
282
|
*/
|
|
341
|
-
|
|
283
|
+
#emit(name, options = {}) {
|
|
342
284
|
const _ = this;
|
|
343
|
-
|
|
285
|
+
const { cancelable = false, ...detail } = options;
|
|
344
286
|
|
|
345
|
-
|
|
346
|
-
const beforeShowEvent = new CustomEvent('beforeShow', {
|
|
287
|
+
const event = new CustomEvent(name, {
|
|
347
288
|
bubbles: true,
|
|
348
|
-
|
|
349
|
-
|
|
289
|
+
composed: true,
|
|
290
|
+
cancelable,
|
|
291
|
+
detail: {
|
|
292
|
+
triggerElement: _.#triggerElement,
|
|
293
|
+
result: _.#result,
|
|
294
|
+
state: _.#state,
|
|
295
|
+
...detail,
|
|
296
|
+
},
|
|
350
297
|
});
|
|
351
298
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// If event was canceled (preventDefault was called), don't show the dialog
|
|
355
|
-
if (!showAllowed) return false;
|
|
299
|
+
return _.dispatchEvent(event);
|
|
300
|
+
}
|
|
356
301
|
|
|
357
|
-
|
|
358
|
-
|
|
302
|
+
// Read-only properties
|
|
303
|
+
get state() {
|
|
304
|
+
return this.#state;
|
|
305
|
+
}
|
|
306
|
+
get dialog() {
|
|
307
|
+
return this.#dialog;
|
|
308
|
+
}
|
|
309
|
+
get isOpen() {
|
|
310
|
+
return this.#state === 'showing' || this.#state === 'shown';
|
|
311
|
+
}
|
|
312
|
+
get triggerElement() {
|
|
313
|
+
return this.#triggerElement;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
359
316
|
|
|
360
|
-
|
|
361
|
-
|
|
317
|
+
/**
|
|
318
|
+
* DialogBackdrop - A custom backdrop element that animates with the dialog-panel state.
|
|
319
|
+
* Use this instead of native ::backdrop for consistent cross-browser animations.
|
|
320
|
+
*
|
|
321
|
+
* @extends HTMLElement
|
|
322
|
+
*/
|
|
323
|
+
class DialogBackdrop extends HTMLElement {
|
|
324
|
+
#panel = null;
|
|
325
|
+
#handlers = {
|
|
326
|
+
click: null,
|
|
327
|
+
};
|
|
362
328
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// Update ARIA states
|
|
366
|
-
_.setAttribute('aria-hidden', 'false');
|
|
329
|
+
connectedCallback() {
|
|
330
|
+
const _ = this;
|
|
367
331
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
_.triggerEl.setAttribute('aria-expanded', 'true');
|
|
371
|
-
}
|
|
332
|
+
// Find parent dialog-panel
|
|
333
|
+
_.#panel = _.closest('dialog-panel');
|
|
372
334
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
'
|
|
335
|
+
if (!_.#panel) {
|
|
336
|
+
console.warn(
|
|
337
|
+
'DialogBackdrop: Must be inside a <dialog-panel> element'
|
|
376
338
|
);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
firstFocusable.focus();
|
|
380
|
-
});
|
|
381
|
-
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
382
341
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
})
|
|
389
|
-
);
|
|
390
|
-
});
|
|
342
|
+
// Handle click to close
|
|
343
|
+
_.#handlers.click = () => {
|
|
344
|
+
_.#panel.hide();
|
|
345
|
+
};
|
|
346
|
+
_.addEventListener('click', _.#handlers.click);
|
|
391
347
|
}
|
|
392
348
|
|
|
393
|
-
|
|
394
|
-
* Hides the dialog and restores focus
|
|
395
|
-
* @fires DialogPanel#beforeHide - Fired before the dialog starts to hide
|
|
396
|
-
* @fires DialogPanel#hide - Fired when the dialog has started hiding (transition begins)
|
|
397
|
-
* @fires DialogPanel#afterHide - Fired when the dialog has completed its hide transition
|
|
398
|
-
* @returns {boolean} False if the hide was prevented by a beforeHide event handler
|
|
399
|
-
*/
|
|
400
|
-
hide() {
|
|
349
|
+
disconnectedCallback() {
|
|
401
350
|
const _ = this;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const beforeHideEvent = new CustomEvent('beforeHide', {
|
|
405
|
-
bubbles: true,
|
|
406
|
-
cancelable: true,
|
|
407
|
-
detail: { triggerElement: _.triggerEl },
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
const hideAllowed = _.dispatchEvent(beforeHideEvent);
|
|
411
|
-
|
|
412
|
-
// If event was canceled (preventDefault was called), don't hide the dialog
|
|
413
|
-
if (!hideAllowed) return false;
|
|
414
|
-
|
|
415
|
-
// ensure focus is moved out of the dialog
|
|
416
|
-
const activeElement = document.activeElement;
|
|
417
|
-
if (activeElement && _.contains(activeElement)) {
|
|
418
|
-
// Blur the currently focused element if it's inside the dialog
|
|
419
|
-
activeElement.blur();
|
|
351
|
+
if (_.#handlers.click) {
|
|
352
|
+
_.removeEventListener('click', _.#handlers.click);
|
|
420
353
|
}
|
|
421
|
-
|
|
422
|
-
// Update ARIA states and restore focus
|
|
423
|
-
if (_.triggerEl) {
|
|
424
|
-
// remove focus from modal panel first
|
|
425
|
-
_.triggerEl.focus();
|
|
426
|
-
// mark trigger as no longer expanded
|
|
427
|
-
_.triggerEl.setAttribute('aria-expanded', 'false');
|
|
428
|
-
} else {
|
|
429
|
-
// Move focus to body to ensure it's not trapped
|
|
430
|
-
document.body.focus();
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Set aria-hidden to start transition
|
|
434
|
-
// The transitionend event handler will add display:none when complete
|
|
435
|
-
_.setAttribute('aria-hidden', 'true');
|
|
436
|
-
// Remove open attribute for CSS :has() selector
|
|
437
|
-
|
|
438
|
-
_.removeAttribute('open');
|
|
439
|
-
|
|
440
|
-
// Dispatch hide event - dialog is now starting to hide
|
|
441
|
-
_.dispatchEvent(
|
|
442
|
-
new CustomEvent('hide', {
|
|
443
|
-
bubbles: true,
|
|
444
|
-
detail: { triggerElement: _.triggerEl },
|
|
445
|
-
})
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
return true;
|
|
449
354
|
}
|
|
450
355
|
}
|
|
451
356
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
constructor() {
|
|
458
|
-
super();
|
|
459
|
-
this.setAttribute('tabindex', '-1');
|
|
460
|
-
this.dialogPanel = this.closest('dialog-panel');
|
|
461
|
-
this.#bindUI();
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
#bindUI() {
|
|
465
|
-
this.addEventListener('click', () => {
|
|
466
|
-
this.dialogPanel.hide();
|
|
467
|
-
});
|
|
468
|
-
}
|
|
357
|
+
// Register the custom elements
|
|
358
|
+
// Note: dialog-backdrop must be defined BEFORE dialog-panel,
|
|
359
|
+
// because dialog-panel's connectedCallback may create dialog-backdrop elements
|
|
360
|
+
if (!customElements.get('dialog-backdrop')) {
|
|
361
|
+
customElements.define('dialog-backdrop', DialogBackdrop);
|
|
469
362
|
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Custom element that wraps the content of the dialog
|
|
473
|
-
* @extends HTMLElement
|
|
474
|
-
*/
|
|
475
|
-
class DialogContent extends HTMLElement {
|
|
476
|
-
constructor() {
|
|
477
|
-
super();
|
|
478
|
-
this.setAttribute('role', 'document');
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
363
|
if (!customElements.get('dialog-panel')) {
|
|
483
364
|
customElements.define('dialog-panel', DialogPanel);
|
|
484
365
|
}
|
|
485
|
-
if (!customElements.get('dialog-overlay')) {
|
|
486
|
-
customElements.define('dialog-overlay', DialogOverlay);
|
|
487
|
-
}
|
|
488
|
-
if (!customElements.get('dialog-content')) {
|
|
489
|
-
customElements.define('dialog-content', DialogContent);
|
|
490
|
-
}
|
|
491
366
|
|
|
492
|
-
exports.
|
|
493
|
-
exports.DialogOverlay = DialogOverlay;
|
|
367
|
+
exports.DialogBackdrop = DialogBackdrop;
|
|
494
368
|
exports.DialogPanel = DialogPanel;
|
|
495
|
-
exports.default = DialogPanel;
|
|
496
|
-
|
|
497
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
498
369
|
|
|
499
370
|
}));
|
|
500
371
|
//# sourceMappingURL=dialog-panel.js.map
|