@magic-spells/dialog-panel 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Magic Spells (Cory Schulz)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # Dialog Panel Web Component
2
+
3
+ A lightweight, customizable Web Component for creating accessible modal dialogs. Ideal for dialogs, alerts, cart panels, or content panels with smooth animations and accessibility features.
4
+
5
+ ## Features
6
+
7
+ - No dependencies
8
+ - Lightweight
9
+ - Follows accessibility best practices with proper ARIA attributes
10
+ - Smooth animations
11
+ - Focus management and keyboard navigation
12
+ - WAI-ARIA compliant modal dialog behavior
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @magic-spells/dialog-panel
18
+ ```
19
+
20
+ ```javascript
21
+ // Add to your JavaScript file
22
+ import '@magic-spells/dialog-panel';
23
+ ```
24
+
25
+ Or include directly in your HTML:
26
+
27
+ ```html
28
+ <script src="https://unpkg.com/@magic-spells/dialog-panel"></script>
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```html
34
+ <button
35
+ id="show-dialog-button"
36
+ aria-haspopup="dialog"
37
+ aria-controls="demo-dialog"
38
+ aria-expanded="false">
39
+ Open Dialog
40
+ </button>
41
+
42
+ <dialog-panel
43
+ id="demo-dialog"
44
+ role="dialog"
45
+ aria-modal="true"
46
+ aria-labelledby="dialog-title"
47
+ aria-describedby="dialog-description"
48
+ aria-hidden="true">
49
+ <dialog-overlay></dialog-overlay>
50
+ <dialog-content>
51
+ <button aria-label="Close dialog" data-action="hide-dialog">
52
+ &times;
53
+ </button>
54
+ <div>
55
+ <h2 id="dialog-title">Dialog Title</h2>
56
+ <p id="dialog-description">
57
+ This is a demonstration of the dialog panel component. Add
58
+ your content here.
59
+ </p>
60
+ </div>
61
+ </dialog-content>
62
+ </dialog-panel>
63
+
64
+ <script>
65
+ const button = document.getElementById('show-dialog-button');
66
+ const dialog = document.getElementById('demo-dialog');
67
+ button.addEventListener('click', () => dialog.show());
68
+ </script>
69
+ ```
70
+
71
+ ## How It Works
72
+
73
+ - The dialog is initially hidden (`aria-hidden="true"`)
74
+ - Clicking the button triggers the `show()` method, making the dialog visible
75
+ - When opened, sets `aria-modal="true"` to indicate modal behavior
76
+ - Clicking the overlay or a close button (`data-action="hide-dialog"`) triggers the `hide()` method
77
+ - Keyboard focus is automatically trapped within the dialog when open
78
+ - Pressing ESC closes the dialog
79
+ - Focus returns to the trigger button when closed
80
+
81
+ ## Customization
82
+
83
+ ### Styling
84
+
85
+ You can style the Dialog Panel by overriding or extending the provided CSS:
86
+
87
+ ```css
88
+ dialog-panel {
89
+ /* Customize your dialog panel */
90
+ }
91
+
92
+ dialog-overlay {
93
+ background-color: rgba(0, 0, 0, 0.5);
94
+ }
95
+
96
+ [data-action='hide-dialog'] {
97
+ font-size: 24px;
98
+ color: #333;
99
+ }
100
+ ```
101
+
102
+ ### JavaScript API
103
+
104
+ #### Methods
105
+
106
+ - `show()`: Opens the dialog panel
107
+ - `hide()`: Closes the dialog panel
108
+
109
+ ## Browser Support
110
+
111
+ This component works in all modern browsers that support Web Components.
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,371 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * Retrieves all focusable elements within a given container.
7
+ *
8
+ * @param {HTMLElement} container - The container element to search for focusable elements.
9
+ * @returns {HTMLElement[]} An array of focusable elements found within the container.
10
+ */
11
+ const getFocusableElements = (container) => {
12
+ const focusableSelectors =
13
+ 'summary, a[href], button:not(:disabled), [tabindex]:not([tabindex^="-"]):not(focus-trap-start):not(focus-trap-end), [draggable], area, input:not([type=hidden]):not(:disabled), select:not(:disabled), textarea:not(:disabled), object, iframe';
14
+ return Array.from(container.querySelectorAll(focusableSelectors));
15
+ };
16
+
17
+ class FocusTrap extends HTMLElement {
18
+ /** @type {boolean} Indicates whether the styles have been injected into the DOM. */
19
+ static styleInjected = false;
20
+
21
+ constructor() {
22
+ super();
23
+ this.trapStart = null;
24
+ this.trapEnd = null;
25
+
26
+ // Inject styles only once, when the first FocusTrap instance is created.
27
+ if (!FocusTrap.styleInjected) {
28
+ this.injectStyles();
29
+ FocusTrap.styleInjected = true;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Injects necessary styles for the focus trap into the document's head.
35
+ * This ensures that focus-trap-start and focus-trap-end elements are hidden.
36
+ */
37
+ injectStyles() {
38
+ const style = document.createElement('style');
39
+ style.textContent = `
40
+ focus-trap-start,
41
+ focus-trap-end {
42
+ position: absolute;
43
+ width: 1px;
44
+ height: 1px;
45
+ margin: -1px;
46
+ padding: 0;
47
+ border: 0;
48
+ clip: rect(0, 0, 0, 0);
49
+ overflow: hidden;
50
+ white-space: nowrap;
51
+ }
52
+ `;
53
+ document.head.appendChild(style);
54
+ }
55
+
56
+ /**
57
+ * Called when the element is connected to the DOM.
58
+ * Sets up the focus trap and adds the keydown event listener.
59
+ */
60
+ connectedCallback() {
61
+ this.setupTrap();
62
+ this.addEventListener('keydown', this.handleKeyDown);
63
+ }
64
+
65
+ /**
66
+ * Called when the element is disconnected from the DOM.
67
+ * Removes the keydown event listener.
68
+ */
69
+ disconnectedCallback() {
70
+ this.removeEventListener('keydown', this.handleKeyDown);
71
+ }
72
+
73
+ /**
74
+ * Sets up the focus trap by adding trap start and trap end elements.
75
+ * Focuses the trap start element to initiate the focus trap.
76
+ */
77
+ setupTrap() {
78
+ // check to see it there are any focusable children
79
+ const focusableElements = getFocusableElements(this);
80
+ // exit if there aren't any
81
+ if (focusableElements.length === 0) return;
82
+
83
+ // create trap start and end elements
84
+ this.trapStart = document.createElement('focus-trap-start');
85
+ this.trapEnd = document.createElement('focus-trap-end');
86
+
87
+ // add to DOM
88
+ this.prepend(this.trapStart);
89
+ this.append(this.trapEnd);
90
+ }
91
+
92
+ /**
93
+ * Handles the keydown event. If the Escape key is pressed, the focus trap is exited.
94
+ *
95
+ * @param {KeyboardEvent} e - The keyboard event object.
96
+ */
97
+ handleKeyDown = (e) => {
98
+ if (e.key === 'Escape') {
99
+ e.preventDefault();
100
+ this.exitTrap();
101
+ }
102
+ };
103
+
104
+ /**
105
+ * Exits the focus trap by hiding the current container and shifting focus
106
+ * back to the trigger element that opened the trap.
107
+ */
108
+ exitTrap() {
109
+ const container = this.closest('[aria-hidden="false"]');
110
+ if (!container) return;
111
+
112
+ container.setAttribute('aria-hidden', 'true');
113
+
114
+ const trigger = document.querySelector(
115
+ `[aria-expanded="true"][aria-controls="${container.id}"]`
116
+ );
117
+ if (trigger) {
118
+ trigger.setAttribute('aria-expanded', 'false');
119
+ trigger.focus();
120
+ }
121
+ }
122
+ }
123
+
124
+ class FocusTrapStart extends HTMLElement {
125
+ /**
126
+ * Called when the element is connected to the DOM.
127
+ * Sets the tabindex and adds the focus event listener.
128
+ */
129
+ connectedCallback() {
130
+ this.setAttribute('tabindex', '0');
131
+ this.addEventListener('focus', this.handleFocus);
132
+ }
133
+
134
+ /**
135
+ * Called when the element is disconnected from the DOM.
136
+ * Removes the focus event listener.
137
+ */
138
+ disconnectedCallback() {
139
+ this.removeEventListener('focus', this.handleFocus);
140
+ }
141
+
142
+ /**
143
+ * Handles the focus event. If focus moves backwards from the first focusable element,
144
+ * it is cycled to the last focusable element, and vice versa.
145
+ *
146
+ * @param {FocusEvent} e - The focus event object.
147
+ */
148
+ handleFocus = (e) => {
149
+ const trap = this.closest('focus-trap');
150
+ const focusableElements = getFocusableElements(trap);
151
+
152
+ if (focusableElements.length === 0) return;
153
+
154
+ const firstElement = focusableElements[0];
155
+ const lastElement =
156
+ focusableElements[focusableElements.length - 1];
157
+
158
+ if (e.relatedTarget === firstElement) {
159
+ lastElement.focus();
160
+ } else {
161
+ firstElement.focus();
162
+ }
163
+ };
164
+ }
165
+
166
+ class FocusTrapEnd extends HTMLElement {
167
+ /**
168
+ * Called when the element is connected to the DOM.
169
+ * Sets the tabindex and adds the focus event listener.
170
+ */
171
+ connectedCallback() {
172
+ this.setAttribute('tabindex', '0');
173
+ this.addEventListener('focus', this.handleFocus);
174
+ }
175
+
176
+ /**
177
+ * Called when the element is disconnected from the DOM.
178
+ * Removes the focus event listener.
179
+ */
180
+ disconnectedCallback() {
181
+ this.removeEventListener('focus', this.handleFocus);
182
+ }
183
+
184
+ /**
185
+ * Handles the focus event. When the trap end is focused, focus is shifted back to the trap start.
186
+ */
187
+ handleFocus = () => {
188
+ const trap = this.closest('focus-trap');
189
+ const trapStart = trap.querySelector('focus-trap-start');
190
+ trapStart.focus();
191
+ };
192
+ }
193
+
194
+ customElements.define('focus-trap', FocusTrap);
195
+ customElements.define('focus-trap-start', FocusTrapStart);
196
+ customElements.define('focus-trap-end', FocusTrapEnd);
197
+
198
+ /**
199
+ * Custom element that creates an accessible modal dialog panel with focus management
200
+ * @extends HTMLElement
201
+ */
202
+ class DialogPanel extends HTMLElement {
203
+ /**
204
+ * Initializes the dialog panel, sets up focus trap and overlay
205
+ */
206
+ constructor() {
207
+ super();
208
+ const _ = this;
209
+ _.id = _.getAttribute('id');
210
+ _.setAttribute('role', 'dialog');
211
+ _.setAttribute('aria-modal', 'true');
212
+ _.setAttribute('aria-hidden', 'true');
213
+
214
+ _.contentPanel = _.querySelector('dialog-content');
215
+ _.focusTrap = document.createElement('focus-trap');
216
+ _.triggerEl = null;
217
+
218
+ // Ensure we have labelledby and describedby references
219
+ if (!_.getAttribute('aria-labelledby')) {
220
+ const heading = _.querySelector('h1, h2, h3');
221
+ if (heading && !heading.id) {
222
+ heading.id = `${_.id}-title`;
223
+ }
224
+ if (heading?.id) {
225
+ _.setAttribute('aria-labelledby', heading.id);
226
+ }
227
+ }
228
+
229
+ _.contentPanel.parentNode.insertBefore(
230
+ _.focusTrap,
231
+ _.contentPanel
232
+ );
233
+ _.focusTrap.appendChild(_.contentPanel);
234
+
235
+ _.focusTrap.setupTrap();
236
+
237
+ // Add modal overlay
238
+ _.prepend(document.createElement('dialog-overlay'));
239
+ _.#bindUI();
240
+ _.#bindKeyboard();
241
+ }
242
+
243
+ /**
244
+ * Binds click events for showing and hiding the dialog
245
+ * @private
246
+ */
247
+ #bindUI() {
248
+ // Handle trigger buttons
249
+ document.addEventListener('click', (e) => {
250
+ const trigger = e.target.closest(
251
+ `[aria-controls="${this.id}"]`
252
+ );
253
+ if (!trigger) return;
254
+
255
+ if (trigger.getAttribute('data-prevent-default') === 'true') {
256
+ e.preventDefault();
257
+ }
258
+
259
+ // this.triggerEl = trigger;
260
+ this.show(trigger);
261
+ });
262
+
263
+ // Handle close buttons
264
+ this.addEventListener('click', (e) => {
265
+ if (!e.target.closest('[data-action="hide-dialog"]')) return;
266
+ this.hide();
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Binds keyboard events for accessibility
272
+ * @private
273
+ */
274
+ #bindKeyboard() {
275
+ this.addEventListener('keydown', (e) => {
276
+ if (e.key === 'Escape') {
277
+ this.hide();
278
+ }
279
+ });
280
+ }
281
+
282
+ /**
283
+ * Shows the dialog and traps focus within it
284
+ * @param {HTMLElement} [triggerEl=null] - The element that triggered the dialog
285
+ */
286
+ show(triggerEl = null) {
287
+ this.triggerEl = triggerEl || false;
288
+
289
+ // Update ARIA states
290
+ this.setAttribute('aria-hidden', 'false');
291
+ if (this.triggerEl) {
292
+ this.triggerEl.setAttribute('aria-expanded', 'true');
293
+ }
294
+
295
+ // prevent body from scrolling
296
+ document.body.classList.add('overflow-hidden');
297
+
298
+ // Focus management
299
+ const firstFocusable = this.querySelector(
300
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
301
+ );
302
+ if (firstFocusable) {
303
+ requestAnimationFrame(() => {
304
+ firstFocusable.focus();
305
+ });
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Hides the dialog and restores focus
311
+ */
312
+ hide() {
313
+ // allow body to scroll
314
+ document.body.classList.remove('overflow-hidden');
315
+
316
+ // Update ARIA states
317
+ if (this.triggerEl) {
318
+ this.triggerEl.setAttribute('aria-expanded', 'false');
319
+ // Restore focus to trigger element
320
+ this.triggerEl.focus();
321
+ } else {
322
+ console.log('we need to blur focus');
323
+ }
324
+
325
+ // hide dialog panel
326
+ setTimeout(() => {
327
+ this.setAttribute('aria-hidden', 'true');
328
+ }, 1);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Custom element that creates a clickable overlay for the dialog
334
+ * @extends HTMLElement
335
+ */
336
+ class DialogOverlay extends HTMLElement {
337
+ constructor() {
338
+ super();
339
+ this.setAttribute('tabindex', '-1'); // Changed to -1 as it shouldn't be focusable
340
+ this.setAttribute('aria-hidden', 'true');
341
+ this.dialogPanel = this.closest('dialog-panel');
342
+ this.#bindUI();
343
+ }
344
+
345
+ #bindUI() {
346
+ this.addEventListener('click', () => {
347
+ this.dialogPanel.hide();
348
+ });
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Custom element that wraps the content of the dialog
354
+ * @extends HTMLElement
355
+ */
356
+ class DialogContent extends HTMLElement {
357
+ constructor() {
358
+ super();
359
+ this.setAttribute('role', 'document'); // Optional: helps with document structure
360
+ }
361
+ }
362
+
363
+ customElements.define('dialog-panel', DialogPanel);
364
+ customElements.define('dialog-overlay', DialogOverlay);
365
+ customElements.define('dialog-content', DialogContent);
366
+
367
+ exports.DialogContent = DialogContent;
368
+ exports.DialogOverlay = DialogOverlay;
369
+ exports.DialogPanel = DialogPanel;
370
+ exports.default = DialogPanel;
371
+ //# sourceMappingURL=dialog-panel.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog-panel.cjs.js","sources":["../node_modules/@magic-spells/focus-trap/dist/focus-trap.esm.js","../src/dialog-panel.js"],"sourcesContent":["/**\n * Retrieves all focusable elements within a given container.\n *\n * @param {HTMLElement} container - The container element to search for focusable elements.\n * @returns {HTMLElement[]} An array of focusable elements found within the container.\n */\nconst getFocusableElements = (container) => {\n\tconst focusableSelectors =\n\t\t'summary, a[href], button:not(:disabled), [tabindex]:not([tabindex^=\"-\"]):not(focus-trap-start):not(focus-trap-end), [draggable], area, input:not([type=hidden]):not(:disabled), select:not(:disabled), textarea:not(:disabled), object, iframe';\n\treturn Array.from(container.querySelectorAll(focusableSelectors));\n};\n\nclass FocusTrap extends HTMLElement {\n\t/** @type {boolean} Indicates whether the styles have been injected into the DOM. */\n\tstatic styleInjected = false;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.trapStart = null;\n\t\tthis.trapEnd = null;\n\n\t\t// Inject styles only once, when the first FocusTrap instance is created.\n\t\tif (!FocusTrap.styleInjected) {\n\t\t\tthis.injectStyles();\n\t\t\tFocusTrap.styleInjected = true;\n\t\t}\n\t}\n\n\t/**\n\t * Injects necessary styles for the focus trap into the document's head.\n\t * This ensures that focus-trap-start and focus-trap-end elements are hidden.\n\t */\n\tinjectStyles() {\n\t\tconst style = document.createElement('style');\n\t\tstyle.textContent = `\n focus-trap-start,\n focus-trap-end {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0, 0, 0, 0);\n overflow: hidden;\n white-space: nowrap;\n }\n `;\n\t\tdocument.head.appendChild(style);\n\t}\n\n\t/**\n\t * Called when the element is connected to the DOM.\n\t * Sets up the focus trap and adds the keydown event listener.\n\t */\n\tconnectedCallback() {\n\t\tthis.setupTrap();\n\t\tthis.addEventListener('keydown', this.handleKeyDown);\n\t}\n\n\t/**\n\t * Called when the element is disconnected from the DOM.\n\t * Removes the keydown event listener.\n\t */\n\tdisconnectedCallback() {\n\t\tthis.removeEventListener('keydown', this.handleKeyDown);\n\t}\n\n\t/**\n\t * Sets up the focus trap by adding trap start and trap end elements.\n\t * Focuses the trap start element to initiate the focus trap.\n\t */\n\tsetupTrap() {\n\t\t// check to see it there are any focusable children\n\t\tconst focusableElements = getFocusableElements(this);\n\t\t// exit if there aren't any\n\t\tif (focusableElements.length === 0) return;\n\n\t\t// create trap start and end elements\n\t\tthis.trapStart = document.createElement('focus-trap-start');\n\t\tthis.trapEnd = document.createElement('focus-trap-end');\n\n\t\t// add to DOM\n\t\tthis.prepend(this.trapStart);\n\t\tthis.append(this.trapEnd);\n\t}\n\n\t/**\n\t * Handles the keydown event. If the Escape key is pressed, the focus trap is exited.\n\t *\n\t * @param {KeyboardEvent} e - The keyboard event object.\n\t */\n\thandleKeyDown = (e) => {\n\t\tif (e.key === 'Escape') {\n\t\t\te.preventDefault();\n\t\t\tthis.exitTrap();\n\t\t}\n\t};\n\n\t/**\n\t * Exits the focus trap by hiding the current container and shifting focus\n\t * back to the trigger element that opened the trap.\n\t */\n\texitTrap() {\n\t\tconst container = this.closest('[aria-hidden=\"false\"]');\n\t\tif (!container) return;\n\n\t\tcontainer.setAttribute('aria-hidden', 'true');\n\n\t\tconst trigger = document.querySelector(\n\t\t\t`[aria-expanded=\"true\"][aria-controls=\"${container.id}\"]`\n\t\t);\n\t\tif (trigger) {\n\t\t\ttrigger.setAttribute('aria-expanded', 'false');\n\t\t\ttrigger.focus();\n\t\t}\n\t}\n}\n\nclass FocusTrapStart extends HTMLElement {\n\t/**\n\t * Called when the element is connected to the DOM.\n\t * Sets the tabindex and adds the focus event listener.\n\t */\n\tconnectedCallback() {\n\t\tthis.setAttribute('tabindex', '0');\n\t\tthis.addEventListener('focus', this.handleFocus);\n\t}\n\n\t/**\n\t * Called when the element is disconnected from the DOM.\n\t * Removes the focus event listener.\n\t */\n\tdisconnectedCallback() {\n\t\tthis.removeEventListener('focus', this.handleFocus);\n\t}\n\n\t/**\n\t * Handles the focus event. If focus moves backwards from the first focusable element,\n\t * it is cycled to the last focusable element, and vice versa.\n\t *\n\t * @param {FocusEvent} e - The focus event object.\n\t */\n\thandleFocus = (e) => {\n\t\tconst trap = this.closest('focus-trap');\n\t\tconst focusableElements = getFocusableElements(trap);\n\n\t\tif (focusableElements.length === 0) return;\n\n\t\tconst firstElement = focusableElements[0];\n\t\tconst lastElement =\n\t\t\tfocusableElements[focusableElements.length - 1];\n\n\t\tif (e.relatedTarget === firstElement) {\n\t\t\tlastElement.focus();\n\t\t} else {\n\t\t\tfirstElement.focus();\n\t\t}\n\t};\n}\n\nclass FocusTrapEnd extends HTMLElement {\n\t/**\n\t * Called when the element is connected to the DOM.\n\t * Sets the tabindex and adds the focus event listener.\n\t */\n\tconnectedCallback() {\n\t\tthis.setAttribute('tabindex', '0');\n\t\tthis.addEventListener('focus', this.handleFocus);\n\t}\n\n\t/**\n\t * Called when the element is disconnected from the DOM.\n\t * Removes the focus event listener.\n\t */\n\tdisconnectedCallback() {\n\t\tthis.removeEventListener('focus', this.handleFocus);\n\t}\n\n\t/**\n\t * Handles the focus event. When the trap end is focused, focus is shifted back to the trap start.\n\t */\n\thandleFocus = () => {\n\t\tconst trap = this.closest('focus-trap');\n\t\tconst trapStart = trap.querySelector('focus-trap-start');\n\t\ttrapStart.focus();\n\t};\n}\n\ncustomElements.define('focus-trap', FocusTrap);\ncustomElements.define('focus-trap-start', FocusTrapStart);\ncustomElements.define('focus-trap-end', FocusTrapEnd);\n//# sourceMappingURL=focus-trap.esm.js.map\n","import './dialog-panel.css';\nimport '@magic-spells/focus-trap';\n\n/**\n * Custom element that creates an accessible modal dialog panel with focus management\n * @extends HTMLElement\n */\nclass DialogPanel extends HTMLElement {\n\t/**\n\t * Initializes the dialog panel, sets up focus trap and overlay\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\t\t_.id = _.getAttribute('id');\n\t\t_.setAttribute('role', 'dialog');\n\t\t_.setAttribute('aria-modal', 'true');\n\t\t_.setAttribute('aria-hidden', 'true');\n\n\t\t_.contentPanel = _.querySelector('dialog-content');\n\t\t_.focusTrap = document.createElement('focus-trap');\n\t\t_.triggerEl = null;\n\n\t\t// Ensure we have labelledby and describedby references\n\t\tif (!_.getAttribute('aria-labelledby')) {\n\t\t\tconst heading = _.querySelector('h1, h2, h3');\n\t\t\tif (heading && !heading.id) {\n\t\t\t\theading.id = `${_.id}-title`;\n\t\t\t}\n\t\t\tif (heading?.id) {\n\t\t\t\t_.setAttribute('aria-labelledby', heading.id);\n\t\t\t}\n\t\t}\n\n\t\t_.contentPanel.parentNode.insertBefore(\n\t\t\t_.focusTrap,\n\t\t\t_.contentPanel\n\t\t);\n\t\t_.focusTrap.appendChild(_.contentPanel);\n\n\t\t_.focusTrap.setupTrap();\n\n\t\t// Add modal overlay\n\t\t_.prepend(document.createElement('dialog-overlay'));\n\t\t_.#bindUI();\n\t\t_.#bindKeyboard();\n\t}\n\n\t/**\n\t * Binds click events for showing and hiding the dialog\n\t * @private\n\t */\n\t#bindUI() {\n\t\t// Handle trigger buttons\n\t\tdocument.addEventListener('click', (e) => {\n\t\t\tconst trigger = e.target.closest(\n\t\t\t\t`[aria-controls=\"${this.id}\"]`\n\t\t\t);\n\t\t\tif (!trigger) return;\n\n\t\t\tif (trigger.getAttribute('data-prevent-default') === 'true') {\n\t\t\t\te.preventDefault();\n\t\t\t}\n\n\t\t\t// this.triggerEl = trigger;\n\t\t\tthis.show(trigger);\n\t\t});\n\n\t\t// Handle close buttons\n\t\tthis.addEventListener('click', (e) => {\n\t\t\tif (!e.target.closest('[data-action=\"hide-dialog\"]')) return;\n\t\t\tthis.hide();\n\t\t});\n\t}\n\n\t/**\n\t * Binds keyboard events for accessibility\n\t * @private\n\t */\n\t#bindKeyboard() {\n\t\tthis.addEventListener('keydown', (e) => {\n\t\t\tif (e.key === 'Escape') {\n\t\t\t\tthis.hide();\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Shows the dialog and traps focus within it\n\t * @param {HTMLElement} [triggerEl=null] - The element that triggered the dialog\n\t */\n\tshow(triggerEl = null) {\n\t\tthis.triggerEl = triggerEl || false;\n\n\t\t// Update ARIA states\n\t\tthis.setAttribute('aria-hidden', 'false');\n\t\tif (this.triggerEl) {\n\t\t\tthis.triggerEl.setAttribute('aria-expanded', 'true');\n\t\t}\n\n\t\t// prevent body from scrolling\n\t\tdocument.body.classList.add('overflow-hidden');\n\n\t\t// Focus management\n\t\tconst firstFocusable = this.querySelector(\n\t\t\t'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n\t\t);\n\t\tif (firstFocusable) {\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tfirstFocusable.focus();\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Hides the dialog and restores focus\n\t */\n\thide() {\n\t\t// allow body to scroll\n\t\tdocument.body.classList.remove('overflow-hidden');\n\n\t\t// Update ARIA states\n\t\tif (this.triggerEl) {\n\t\t\tthis.triggerEl.setAttribute('aria-expanded', 'false');\n\t\t\t// Restore focus to trigger element\n\t\t\tthis.triggerEl.focus();\n\t\t} else {\n\t\t\tconsole.log('we need to blur focus');\n\t\t}\n\n\t\t// hide dialog panel\n\t\tsetTimeout(() => {\n\t\t\tthis.setAttribute('aria-hidden', 'true');\n\t\t}, 1);\n\t}\n}\n\n/**\n * Custom element that creates a clickable overlay for the dialog\n * @extends HTMLElement\n */\nclass DialogOverlay extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.setAttribute('tabindex', '-1'); // Changed to -1 as it shouldn't be focusable\n\t\tthis.setAttribute('aria-hidden', 'true');\n\t\tthis.dialogPanel = this.closest('dialog-panel');\n\t\tthis.#bindUI();\n\t}\n\n\t#bindUI() {\n\t\tthis.addEventListener('click', () => {\n\t\t\tthis.dialogPanel.hide();\n\t\t});\n\t}\n}\n\n/**\n * Custom element that wraps the content of the dialog\n * @extends HTMLElement\n */\nclass DialogContent extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.setAttribute('role', 'document'); // Optional: helps with document structure\n\t}\n}\n\ncustomElements.define('dialog-panel', DialogPanel);\ncustomElements.define('dialog-overlay', DialogOverlay);\ncustomElements.define('dialog-content', DialogContent);\n\nexport { DialogPanel, DialogOverlay, DialogContent };\nexport default DialogPanel;\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Retrieves all focusable elements within a given container.
3
+ *
4
+ * @param {HTMLElement} container - The container element to search for focusable elements.
5
+ * @returns {HTMLElement[]} An array of focusable elements found within the container.
6
+ */
7
+ const getFocusableElements = (container) => {
8
+ const focusableSelectors =
9
+ 'summary, a[href], button:not(:disabled), [tabindex]:not([tabindex^="-"]):not(focus-trap-start):not(focus-trap-end), [draggable], area, input:not([type=hidden]):not(:disabled), select:not(:disabled), textarea:not(:disabled), object, iframe';
10
+ return Array.from(container.querySelectorAll(focusableSelectors));
11
+ };
12
+
13
+ class FocusTrap extends HTMLElement {
14
+ /** @type {boolean} Indicates whether the styles have been injected into the DOM. */
15
+ static styleInjected = false;
16
+
17
+ constructor() {
18
+ super();
19
+ this.trapStart = null;
20
+ this.trapEnd = null;
21
+
22
+ // Inject styles only once, when the first FocusTrap instance is created.
23
+ if (!FocusTrap.styleInjected) {
24
+ this.injectStyles();
25
+ FocusTrap.styleInjected = true;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Injects necessary styles for the focus trap into the document's head.
31
+ * This ensures that focus-trap-start and focus-trap-end elements are hidden.
32
+ */
33
+ injectStyles() {
34
+ const style = document.createElement('style');
35
+ style.textContent = `
36
+ focus-trap-start,
37
+ focus-trap-end {
38
+ position: absolute;
39
+ width: 1px;
40
+ height: 1px;
41
+ margin: -1px;
42
+ padding: 0;
43
+ border: 0;
44
+ clip: rect(0, 0, 0, 0);
45
+ overflow: hidden;
46
+ white-space: nowrap;
47
+ }
48
+ `;
49
+ document.head.appendChild(style);
50
+ }
51
+
52
+ /**
53
+ * Called when the element is connected to the DOM.
54
+ * Sets up the focus trap and adds the keydown event listener.
55
+ */
56
+ connectedCallback() {
57
+ this.setupTrap();
58
+ this.addEventListener('keydown', this.handleKeyDown);
59
+ }
60
+
61
+ /**
62
+ * Called when the element is disconnected from the DOM.
63
+ * Removes the keydown event listener.
64
+ */
65
+ disconnectedCallback() {
66
+ this.removeEventListener('keydown', this.handleKeyDown);
67
+ }
68
+
69
+ /**
70
+ * Sets up the focus trap by adding trap start and trap end elements.
71
+ * Focuses the trap start element to initiate the focus trap.
72
+ */
73
+ setupTrap() {
74
+ // check to see it there are any focusable children
75
+ const focusableElements = getFocusableElements(this);
76
+ // exit if there aren't any
77
+ if (focusableElements.length === 0) return;
78
+
79
+ // create trap start and end elements
80
+ this.trapStart = document.createElement('focus-trap-start');
81
+ this.trapEnd = document.createElement('focus-trap-end');
82
+
83
+ // add to DOM
84
+ this.prepend(this.trapStart);
85
+ this.append(this.trapEnd);
86
+ }
87
+
88
+ /**
89
+ * Handles the keydown event. If the Escape key is pressed, the focus trap is exited.
90
+ *
91
+ * @param {KeyboardEvent} e - The keyboard event object.
92
+ */
93
+ handleKeyDown = (e) => {
94
+ if (e.key === 'Escape') {
95
+ e.preventDefault();
96
+ this.exitTrap();
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Exits the focus trap by hiding the current container and shifting focus
102
+ * back to the trigger element that opened the trap.
103
+ */
104
+ exitTrap() {
105
+ const container = this.closest('[aria-hidden="false"]');
106
+ if (!container) return;
107
+
108
+ container.setAttribute('aria-hidden', 'true');
109
+
110
+ const trigger = document.querySelector(
111
+ `[aria-expanded="true"][aria-controls="${container.id}"]`
112
+ );
113
+ if (trigger) {
114
+ trigger.setAttribute('aria-expanded', 'false');
115
+ trigger.focus();
116
+ }
117
+ }
118
+ }
119
+
120
+ class FocusTrapStart extends HTMLElement {
121
+ /**
122
+ * Called when the element is connected to the DOM.
123
+ * Sets the tabindex and adds the focus event listener.
124
+ */
125
+ connectedCallback() {
126
+ this.setAttribute('tabindex', '0');
127
+ this.addEventListener('focus', this.handleFocus);
128
+ }
129
+
130
+ /**
131
+ * Called when the element is disconnected from the DOM.
132
+ * Removes the focus event listener.
133
+ */
134
+ disconnectedCallback() {
135
+ this.removeEventListener('focus', this.handleFocus);
136
+ }
137
+
138
+ /**
139
+ * Handles the focus event. If focus moves backwards from the first focusable element,
140
+ * it is cycled to the last focusable element, and vice versa.
141
+ *
142
+ * @param {FocusEvent} e - The focus event object.
143
+ */
144
+ handleFocus = (e) => {
145
+ const trap = this.closest('focus-trap');
146
+ const focusableElements = getFocusableElements(trap);
147
+
148
+ if (focusableElements.length === 0) return;
149
+
150
+ const firstElement = focusableElements[0];
151
+ const lastElement =
152
+ focusableElements[focusableElements.length - 1];
153
+
154
+ if (e.relatedTarget === firstElement) {
155
+ lastElement.focus();
156
+ } else {
157
+ firstElement.focus();
158
+ }
159
+ };
160
+ }
161
+
162
+ class FocusTrapEnd extends HTMLElement {
163
+ /**
164
+ * Called when the element is connected to the DOM.
165
+ * Sets the tabindex and adds the focus event listener.
166
+ */
167
+ connectedCallback() {
168
+ this.setAttribute('tabindex', '0');
169
+ this.addEventListener('focus', this.handleFocus);
170
+ }
171
+
172
+ /**
173
+ * Called when the element is disconnected from the DOM.
174
+ * Removes the focus event listener.
175
+ */
176
+ disconnectedCallback() {
177
+ this.removeEventListener('focus', this.handleFocus);
178
+ }
179
+
180
+ /**
181
+ * Handles the focus event. When the trap end is focused, focus is shifted back to the trap start.
182
+ */
183
+ handleFocus = () => {
184
+ const trap = this.closest('focus-trap');
185
+ const trapStart = trap.querySelector('focus-trap-start');
186
+ trapStart.focus();
187
+ };
188
+ }
189
+
190
+ customElements.define('focus-trap', FocusTrap);
191
+ customElements.define('focus-trap-start', FocusTrapStart);
192
+ customElements.define('focus-trap-end', FocusTrapEnd);
193
+
194
+ /**
195
+ * Custom element that creates an accessible modal dialog panel with focus management
196
+ * @extends HTMLElement
197
+ */
198
+ class DialogPanel extends HTMLElement {
199
+ /**
200
+ * Initializes the dialog panel, sets up focus trap and overlay
201
+ */
202
+ constructor() {
203
+ super();
204
+ const _ = this;
205
+ _.id = _.getAttribute('id');
206
+ _.setAttribute('role', 'dialog');
207
+ _.setAttribute('aria-modal', 'true');
208
+ _.setAttribute('aria-hidden', 'true');
209
+
210
+ _.contentPanel = _.querySelector('dialog-content');
211
+ _.focusTrap = document.createElement('focus-trap');
212
+ _.triggerEl = null;
213
+
214
+ // Ensure we have labelledby and describedby references
215
+ if (!_.getAttribute('aria-labelledby')) {
216
+ const heading = _.querySelector('h1, h2, h3');
217
+ if (heading && !heading.id) {
218
+ heading.id = `${_.id}-title`;
219
+ }
220
+ if (heading?.id) {
221
+ _.setAttribute('aria-labelledby', heading.id);
222
+ }
223
+ }
224
+
225
+ _.contentPanel.parentNode.insertBefore(
226
+ _.focusTrap,
227
+ _.contentPanel
228
+ );
229
+ _.focusTrap.appendChild(_.contentPanel);
230
+
231
+ _.focusTrap.setupTrap();
232
+
233
+ // Add modal overlay
234
+ _.prepend(document.createElement('dialog-overlay'));
235
+ _.#bindUI();
236
+ _.#bindKeyboard();
237
+ }
238
+
239
+ /**
240
+ * Binds click events for showing and hiding the dialog
241
+ * @private
242
+ */
243
+ #bindUI() {
244
+ // Handle trigger buttons
245
+ document.addEventListener('click', (e) => {
246
+ const trigger = e.target.closest(
247
+ `[aria-controls="${this.id}"]`
248
+ );
249
+ if (!trigger) return;
250
+
251
+ if (trigger.getAttribute('data-prevent-default') === 'true') {
252
+ e.preventDefault();
253
+ }
254
+
255
+ // this.triggerEl = trigger;
256
+ this.show(trigger);
257
+ });
258
+
259
+ // Handle close buttons
260
+ this.addEventListener('click', (e) => {
261
+ if (!e.target.closest('[data-action="hide-dialog"]')) return;
262
+ this.hide();
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Binds keyboard events for accessibility
268
+ * @private
269
+ */
270
+ #bindKeyboard() {
271
+ this.addEventListener('keydown', (e) => {
272
+ if (e.key === 'Escape') {
273
+ this.hide();
274
+ }
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Shows the dialog and traps focus within it
280
+ * @param {HTMLElement} [triggerEl=null] - The element that triggered the dialog
281
+ */
282
+ show(triggerEl = null) {
283
+ this.triggerEl = triggerEl || false;
284
+
285
+ // Update ARIA states
286
+ this.setAttribute('aria-hidden', 'false');
287
+ if (this.triggerEl) {
288
+ this.triggerEl.setAttribute('aria-expanded', 'true');
289
+ }
290
+
291
+ // prevent body from scrolling
292
+ document.body.classList.add('overflow-hidden');
293
+
294
+ // Focus management
295
+ const firstFocusable = this.querySelector(
296
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
297
+ );
298
+ if (firstFocusable) {
299
+ requestAnimationFrame(() => {
300
+ firstFocusable.focus();
301
+ });
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Hides the dialog and restores focus
307
+ */
308
+ hide() {
309
+ // allow body to scroll
310
+ document.body.classList.remove('overflow-hidden');
311
+
312
+ // Update ARIA states
313
+ if (this.triggerEl) {
314
+ this.triggerEl.setAttribute('aria-expanded', 'false');
315
+ // Restore focus to trigger element
316
+ this.triggerEl.focus();
317
+ } else {
318
+ console.log('we need to blur focus');
319
+ }
320
+
321
+ // hide dialog panel
322
+ setTimeout(() => {
323
+ this.setAttribute('aria-hidden', 'true');
324
+ }, 1);
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Custom element that creates a clickable overlay for the dialog
330
+ * @extends HTMLElement
331
+ */
332
+ class DialogOverlay extends HTMLElement {
333
+ constructor() {
334
+ super();
335
+ this.setAttribute('tabindex', '-1'); // Changed to -1 as it shouldn't be focusable
336
+ this.setAttribute('aria-hidden', 'true');
337
+ this.dialogPanel = this.closest('dialog-panel');
338
+ this.#bindUI();
339
+ }
340
+
341
+ #bindUI() {
342
+ this.addEventListener('click', () => {
343
+ this.dialogPanel.hide();
344
+ });
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Custom element that wraps the content of the dialog
350
+ * @extends HTMLElement
351
+ */
352
+ class DialogContent extends HTMLElement {
353
+ constructor() {
354
+ super();
355
+ this.setAttribute('role', 'document'); // Optional: helps with document structure
356
+ }
357
+ }
358
+
359
+ customElements.define('dialog-panel', DialogPanel);
360
+ customElements.define('dialog-overlay', DialogOverlay);
361
+ customElements.define('dialog-content', DialogContent);
362
+
363
+ export { DialogContent, DialogOverlay, DialogPanel, DialogPanel as default };
364
+ //# sourceMappingURL=dialog-panel.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog-panel.esm.js","sources":["../node_modules/@magic-spells/focus-trap/dist/focus-trap.esm.js","../src/dialog-panel.js"],"sourcesContent":["/**\n * Retrieves all focusable elements within a given container.\n *\n * @param {HTMLElement} container - The container element to search for focusable elements.\n * @returns {HTMLElement[]} An array of focusable elements found within the container.\n */\nconst getFocusableElements = (container) => {\n\tconst focusableSelectors =\n\t\t'summary, a[href], button:not(:disabled), [tabindex]:not([tabindex^=\"-\"]):not(focus-trap-start):not(focus-trap-end), [draggable], area, input:not([type=hidden]):not(:disabled), select:not(:disabled), textarea:not(:disabled), object, iframe';\n\treturn Array.from(container.querySelectorAll(focusableSelectors));\n};\n\nclass FocusTrap extends HTMLElement {\n\t/** @type {boolean} Indicates whether the styles have been injected into the DOM. */\n\tstatic styleInjected = false;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.trapStart = null;\n\t\tthis.trapEnd = null;\n\n\t\t// Inject styles only once, when the first FocusTrap instance is created.\n\t\tif (!FocusTrap.styleInjected) {\n\t\t\tthis.injectStyles();\n\t\t\tFocusTrap.styleInjected = true;\n\t\t}\n\t}\n\n\t/**\n\t * Injects necessary styles for the focus trap into the document's head.\n\t * This ensures that focus-trap-start and focus-trap-end elements are hidden.\n\t */\n\tinjectStyles() {\n\t\tconst style = document.createElement('style');\n\t\tstyle.textContent = `\n focus-trap-start,\n focus-trap-end {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0, 0, 0, 0);\n overflow: hidden;\n white-space: nowrap;\n }\n `;\n\t\tdocument.head.appendChild(style);\n\t}\n\n\t/**\n\t * Called when the element is connected to the DOM.\n\t * Sets up the focus trap and adds the keydown event listener.\n\t */\n\tconnectedCallback() {\n\t\tthis.setupTrap();\n\t\tthis.addEventListener('keydown', this.handleKeyDown);\n\t}\n\n\t/**\n\t * Called when the element is disconnected from the DOM.\n\t * Removes the keydown event listener.\n\t */\n\tdisconnectedCallback() {\n\t\tthis.removeEventListener('keydown', this.handleKeyDown);\n\t}\n\n\t/**\n\t * Sets up the focus trap by adding trap start and trap end elements.\n\t * Focuses the trap start element to initiate the focus trap.\n\t */\n\tsetupTrap() {\n\t\t// check to see it there are any focusable children\n\t\tconst focusableElements = getFocusableElements(this);\n\t\t// exit if there aren't any\n\t\tif (focusableElements.length === 0) return;\n\n\t\t// create trap start and end elements\n\t\tthis.trapStart = document.createElement('focus-trap-start');\n\t\tthis.trapEnd = document.createElement('focus-trap-end');\n\n\t\t// add to DOM\n\t\tthis.prepend(this.trapStart);\n\t\tthis.append(this.trapEnd);\n\t}\n\n\t/**\n\t * Handles the keydown event. If the Escape key is pressed, the focus trap is exited.\n\t *\n\t * @param {KeyboardEvent} e - The keyboard event object.\n\t */\n\thandleKeyDown = (e) => {\n\t\tif (e.key === 'Escape') {\n\t\t\te.preventDefault();\n\t\t\tthis.exitTrap();\n\t\t}\n\t};\n\n\t/**\n\t * Exits the focus trap by hiding the current container and shifting focus\n\t * back to the trigger element that opened the trap.\n\t */\n\texitTrap() {\n\t\tconst container = this.closest('[aria-hidden=\"false\"]');\n\t\tif (!container) return;\n\n\t\tcontainer.setAttribute('aria-hidden', 'true');\n\n\t\tconst trigger = document.querySelector(\n\t\t\t`[aria-expanded=\"true\"][aria-controls=\"${container.id}\"]`\n\t\t);\n\t\tif (trigger) {\n\t\t\ttrigger.setAttribute('aria-expanded', 'false');\n\t\t\ttrigger.focus();\n\t\t}\n\t}\n}\n\nclass FocusTrapStart extends HTMLElement {\n\t/**\n\t * Called when the element is connected to the DOM.\n\t * Sets the tabindex and adds the focus event listener.\n\t */\n\tconnectedCallback() {\n\t\tthis.setAttribute('tabindex', '0');\n\t\tthis.addEventListener('focus', this.handleFocus);\n\t}\n\n\t/**\n\t * Called when the element is disconnected from the DOM.\n\t * Removes the focus event listener.\n\t */\n\tdisconnectedCallback() {\n\t\tthis.removeEventListener('focus', this.handleFocus);\n\t}\n\n\t/**\n\t * Handles the focus event. If focus moves backwards from the first focusable element,\n\t * it is cycled to the last focusable element, and vice versa.\n\t *\n\t * @param {FocusEvent} e - The focus event object.\n\t */\n\thandleFocus = (e) => {\n\t\tconst trap = this.closest('focus-trap');\n\t\tconst focusableElements = getFocusableElements(trap);\n\n\t\tif (focusableElements.length === 0) return;\n\n\t\tconst firstElement = focusableElements[0];\n\t\tconst lastElement =\n\t\t\tfocusableElements[focusableElements.length - 1];\n\n\t\tif (e.relatedTarget === firstElement) {\n\t\t\tlastElement.focus();\n\t\t} else {\n\t\t\tfirstElement.focus();\n\t\t}\n\t};\n}\n\nclass FocusTrapEnd extends HTMLElement {\n\t/**\n\t * Called when the element is connected to the DOM.\n\t * Sets the tabindex and adds the focus event listener.\n\t */\n\tconnectedCallback() {\n\t\tthis.setAttribute('tabindex', '0');\n\t\tthis.addEventListener('focus', this.handleFocus);\n\t}\n\n\t/**\n\t * Called when the element is disconnected from the DOM.\n\t * Removes the focus event listener.\n\t */\n\tdisconnectedCallback() {\n\t\tthis.removeEventListener('focus', this.handleFocus);\n\t}\n\n\t/**\n\t * Handles the focus event. When the trap end is focused, focus is shifted back to the trap start.\n\t */\n\thandleFocus = () => {\n\t\tconst trap = this.closest('focus-trap');\n\t\tconst trapStart = trap.querySelector('focus-trap-start');\n\t\ttrapStart.focus();\n\t};\n}\n\ncustomElements.define('focus-trap', FocusTrap);\ncustomElements.define('focus-trap-start', FocusTrapStart);\ncustomElements.define('focus-trap-end', FocusTrapEnd);\n//# sourceMappingURL=focus-trap.esm.js.map\n","import './dialog-panel.css';\nimport '@magic-spells/focus-trap';\n\n/**\n * Custom element that creates an accessible modal dialog panel with focus management\n * @extends HTMLElement\n */\nclass DialogPanel extends HTMLElement {\n\t/**\n\t * Initializes the dialog panel, sets up focus trap and overlay\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\t\t_.id = _.getAttribute('id');\n\t\t_.setAttribute('role', 'dialog');\n\t\t_.setAttribute('aria-modal', 'true');\n\t\t_.setAttribute('aria-hidden', 'true');\n\n\t\t_.contentPanel = _.querySelector('dialog-content');\n\t\t_.focusTrap = document.createElement('focus-trap');\n\t\t_.triggerEl = null;\n\n\t\t// Ensure we have labelledby and describedby references\n\t\tif (!_.getAttribute('aria-labelledby')) {\n\t\t\tconst heading = _.querySelector('h1, h2, h3');\n\t\t\tif (heading && !heading.id) {\n\t\t\t\theading.id = `${_.id}-title`;\n\t\t\t}\n\t\t\tif (heading?.id) {\n\t\t\t\t_.setAttribute('aria-labelledby', heading.id);\n\t\t\t}\n\t\t}\n\n\t\t_.contentPanel.parentNode.insertBefore(\n\t\t\t_.focusTrap,\n\t\t\t_.contentPanel\n\t\t);\n\t\t_.focusTrap.appendChild(_.contentPanel);\n\n\t\t_.focusTrap.setupTrap();\n\n\t\t// Add modal overlay\n\t\t_.prepend(document.createElement('dialog-overlay'));\n\t\t_.#bindUI();\n\t\t_.#bindKeyboard();\n\t}\n\n\t/**\n\t * Binds click events for showing and hiding the dialog\n\t * @private\n\t */\n\t#bindUI() {\n\t\t// Handle trigger buttons\n\t\tdocument.addEventListener('click', (e) => {\n\t\t\tconst trigger = e.target.closest(\n\t\t\t\t`[aria-controls=\"${this.id}\"]`\n\t\t\t);\n\t\t\tif (!trigger) return;\n\n\t\t\tif (trigger.getAttribute('data-prevent-default') === 'true') {\n\t\t\t\te.preventDefault();\n\t\t\t}\n\n\t\t\t// this.triggerEl = trigger;\n\t\t\tthis.show(trigger);\n\t\t});\n\n\t\t// Handle close buttons\n\t\tthis.addEventListener('click', (e) => {\n\t\t\tif (!e.target.closest('[data-action=\"hide-dialog\"]')) return;\n\t\t\tthis.hide();\n\t\t});\n\t}\n\n\t/**\n\t * Binds keyboard events for accessibility\n\t * @private\n\t */\n\t#bindKeyboard() {\n\t\tthis.addEventListener('keydown', (e) => {\n\t\t\tif (e.key === 'Escape') {\n\t\t\t\tthis.hide();\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Shows the dialog and traps focus within it\n\t * @param {HTMLElement} [triggerEl=null] - The element that triggered the dialog\n\t */\n\tshow(triggerEl = null) {\n\t\tthis.triggerEl = triggerEl || false;\n\n\t\t// Update ARIA states\n\t\tthis.setAttribute('aria-hidden', 'false');\n\t\tif (this.triggerEl) {\n\t\t\tthis.triggerEl.setAttribute('aria-expanded', 'true');\n\t\t}\n\n\t\t// prevent body from scrolling\n\t\tdocument.body.classList.add('overflow-hidden');\n\n\t\t// Focus management\n\t\tconst firstFocusable = this.querySelector(\n\t\t\t'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n\t\t);\n\t\tif (firstFocusable) {\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tfirstFocusable.focus();\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Hides the dialog and restores focus\n\t */\n\thide() {\n\t\t// allow body to scroll\n\t\tdocument.body.classList.remove('overflow-hidden');\n\n\t\t// Update ARIA states\n\t\tif (this.triggerEl) {\n\t\t\tthis.triggerEl.setAttribute('aria-expanded', 'false');\n\t\t\t// Restore focus to trigger element\n\t\t\tthis.triggerEl.focus();\n\t\t} else {\n\t\t\tconsole.log('we need to blur focus');\n\t\t}\n\n\t\t// hide dialog panel\n\t\tsetTimeout(() => {\n\t\t\tthis.setAttribute('aria-hidden', 'true');\n\t\t}, 1);\n\t}\n}\n\n/**\n * Custom element that creates a clickable overlay for the dialog\n * @extends HTMLElement\n */\nclass DialogOverlay extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.setAttribute('tabindex', '-1'); // Changed to -1 as it shouldn't be focusable\n\t\tthis.setAttribute('aria-hidden', 'true');\n\t\tthis.dialogPanel = this.closest('dialog-panel');\n\t\tthis.#bindUI();\n\t}\n\n\t#bindUI() {\n\t\tthis.addEventListener('click', () => {\n\t\t\tthis.dialogPanel.hide();\n\t\t});\n\t}\n}\n\n/**\n * Custom element that wraps the content of the dialog\n * @extends HTMLElement\n */\nclass DialogContent extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.setAttribute('role', 'document'); // Optional: helps with document structure\n\t}\n}\n\ncustomElements.define('dialog-panel', DialogPanel);\ncustomElements.define('dialog-overlay', DialogOverlay);\ncustomElements.define('dialog-content', DialogContent);\n\nexport { DialogPanel, DialogOverlay, DialogContent };\nexport default DialogPanel;\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
@@ -0,0 +1 @@
1
+ dialog-panel{position:fixed;z-index:10}dialog-overlay,dialog-panel{height:100vh;left:0;pointer-events:none;top:0;width:100vw}dialog-overlay{backdrop-filter:blur(2px) saturate(120%);background-color:rgba(20,23,26,.4);opacity:0;position:absolute;transition:all .3s ease-out}dialog-content{background:#fff;display:block;opacity:0}dialog-panel[aria-hidden=false]{pointer-events:all}dialog-panel[aria-hidden=false] dialog-overlay{filter:blur(0);opacity:1;pointer-events:all;transform:scale(1)}dialog-panel[aria-hidden=false] dialog-content{filter:blur(0);opacity:1;transform:scale(1);z-index:10}
@@ -0,0 +1 @@
1
+ var DialogPanel=function(e){"use strict";const t=e=>Array.from(e.querySelectorAll('summary, a[href], button:not(:disabled), [tabindex]:not([tabindex^="-"]):not(focus-trap-start):not(focus-trap-end), [draggable], area, input:not([type=hidden]):not(:disabled), select:not(:disabled), textarea:not(:disabled), object, iframe'));class FocusTrap extends HTMLElement{static styleInjected=!1;constructor(){super(),this.trapStart=null,this.trapEnd=null,FocusTrap.styleInjected||(this.injectStyles(),FocusTrap.styleInjected=!0)}injectStyles(){const e=document.createElement("style");e.textContent="\n focus-trap-start,\n focus-trap-end {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0, 0, 0, 0);\n overflow: hidden;\n white-space: nowrap;\n }\n ",document.head.appendChild(e)}connectedCallback(){this.setupTrap(),this.addEventListener("keydown",this.handleKeyDown)}disconnectedCallback(){this.removeEventListener("keydown",this.handleKeyDown)}setupTrap(){0!==t(this).length&&(this.trapStart=document.createElement("focus-trap-start"),this.trapEnd=document.createElement("focus-trap-end"),this.prepend(this.trapStart),this.append(this.trapEnd))}handleKeyDown=e=>{"Escape"===e.key&&(e.preventDefault(),this.exitTrap())};exitTrap(){const e=this.closest('[aria-hidden="false"]');if(!e)return;e.setAttribute("aria-hidden","true");const t=document.querySelector(`[aria-expanded="true"][aria-controls="${e.id}"]`);t&&(t.setAttribute("aria-expanded","false"),t.focus())}}class FocusTrapStart extends HTMLElement{connectedCallback(){this.setAttribute("tabindex","0"),this.addEventListener("focus",this.handleFocus)}disconnectedCallback(){this.removeEventListener("focus",this.handleFocus)}handleFocus=e=>{const n=this.closest("focus-trap"),s=t(n);if(0===s.length)return;const a=s[0],i=s[s.length-1];e.relatedTarget===a?i.focus():a.focus()}}class FocusTrapEnd extends HTMLElement{connectedCallback(){this.setAttribute("tabindex","0"),this.addEventListener("focus",this.handleFocus)}disconnectedCallback(){this.removeEventListener("focus",this.handleFocus)}handleFocus=()=>{this.closest("focus-trap").querySelector("focus-trap-start").focus()}}customElements.define("focus-trap",FocusTrap),customElements.define("focus-trap-start",FocusTrapStart),customElements.define("focus-trap-end",FocusTrapEnd);class DialogPanel extends HTMLElement{constructor(){super();const e=this;if(e.id=e.getAttribute("id"),e.setAttribute("role","dialog"),e.setAttribute("aria-modal","true"),e.setAttribute("aria-hidden","true"),e.contentPanel=e.querySelector("dialog-content"),e.focusTrap=document.createElement("focus-trap"),e.triggerEl=null,!e.getAttribute("aria-labelledby")){const t=e.querySelector("h1, h2, h3");t&&!t.id&&(t.id=`${e.id}-title`),t?.id&&e.setAttribute("aria-labelledby",t.id)}e.contentPanel.parentNode.insertBefore(e.focusTrap,e.contentPanel),e.focusTrap.appendChild(e.contentPanel),e.focusTrap.setupTrap(),e.prepend(document.createElement("dialog-overlay")),e.#e(),e.#t()}#e(){document.addEventListener("click",(e=>{const t=e.target.closest(`[aria-controls="${this.id}"]`);t&&("true"===t.getAttribute("data-prevent-default")&&e.preventDefault(),this.show(t))})),this.addEventListener("click",(e=>{e.target.closest('[data-action="hide-dialog"]')&&this.hide()}))}#t(){this.addEventListener("keydown",(e=>{"Escape"===e.key&&this.hide()}))}show(e=null){this.triggerEl=e||!1,this.setAttribute("aria-hidden","false"),this.triggerEl&&this.triggerEl.setAttribute("aria-expanded","true"),document.body.classList.add("overflow-hidden");const t=this.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');t&&requestAnimationFrame((()=>{t.focus()}))}hide(){document.body.classList.remove("overflow-hidden"),this.triggerEl?(this.triggerEl.setAttribute("aria-expanded","false"),this.triggerEl.focus()):console.log("we need to blur focus"),setTimeout((()=>{this.setAttribute("aria-hidden","true")}),1)}}class DialogOverlay extends HTMLElement{constructor(){super(),this.setAttribute("tabindex","-1"),this.setAttribute("aria-hidden","true"),this.dialogPanel=this.closest("dialog-panel"),this.#e()}#e(){this.addEventListener("click",(()=>{this.dialogPanel.hide()}))}}class DialogContent extends HTMLElement{constructor(){super(),this.setAttribute("role","document")}}return customElements.define("dialog-panel",DialogPanel),customElements.define("dialog-overlay",DialogOverlay),customElements.define("dialog-content",DialogContent),e.DialogContent=DialogContent,e.DialogOverlay=DialogOverlay,e.DialogPanel=DialogPanel,e.default=DialogPanel,Object.defineProperty(e,"__esModule",{value:!0}),e}({});
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@magic-spells/dialog-panel",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight, customizable Dialog Panel web component for creating accessible and responsive modal dialogs.",
5
+ "author": "Cory Schulz",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/dialog-panel.cjs.js",
9
+ "module": "dist/dialog-panel.esm.js",
10
+ "unpkg": "dist/dialog-panel.min.js",
11
+ "style": "dist/dialog-panel.min.css",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/dialog-panel.esm.js",
15
+ "require": "./dist/dialog-panel.cjs.js",
16
+ "default": "./dist/dialog-panel.esm.js"
17
+ },
18
+ "./style.css": "./dist/dialog-panel.min.css"
19
+ },
20
+ "sideEffects": true,
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/magic-spells/dialog-panel.git"
24
+ },
25
+ "homepage": "https://github.com/magic-spells/dialog-panel#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/magic-spells/dialog-panel/issues"
28
+ },
29
+ "keywords": [
30
+ "dialog-panel",
31
+ "modal-dialog",
32
+ "web-components",
33
+ "accessibility",
34
+ "a11y",
35
+ "keyboard-navigation",
36
+ "custom-elements",
37
+ "modal",
38
+ "dialog"
39
+ ],
40
+ "files": [
41
+ "dist/",
42
+ "src/"
43
+ ],
44
+ "scripts": {
45
+ "build": "rollup -c",
46
+ "lint": "eslint src/ rollup.config.mjs",
47
+ "format": "prettier --write .",
48
+ "prepublishOnly": "npm run build",
49
+ "serve": "rollup -c --watch"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
55
+ "browserslist": [
56
+ "last 2 versions",
57
+ "not dead",
58
+ "not ie <= 11"
59
+ ],
60
+ "devDependencies": {
61
+ "@eslint/js": "^8.57.0",
62
+ "@rollup/plugin-node-resolve": "^15.2.3",
63
+ "@rollup/plugin-terser": "^0.4.4",
64
+ "eslint": "^8.0.0",
65
+ "globals": "^13.24.0",
66
+ "prettier": "^3.3.3",
67
+ "rollup": "^3.0.0",
68
+ "rollup-plugin-copy": "^3.5.0",
69
+ "rollup-plugin-postcss": "^4.0.2",
70
+ "rollup-plugin-serve": "^1.1.1"
71
+ },
72
+ "dependencies": {
73
+ "@magic-spells/focus-trap": "^1.0.6"
74
+ }
75
+ }
@@ -0,0 +1,47 @@
1
+ dialog-panel {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100vw;
6
+ height: 100vh;
7
+ z-index: 10;
8
+ pointer-events: none;
9
+ }
10
+
11
+ /* transparent background */
12
+ dialog-overlay {
13
+ position: absolute;
14
+ top: 0;
15
+ left: 0;
16
+ width: 100vw;
17
+ height: 100vh;
18
+ opacity: 0;
19
+ pointer-events: none;
20
+ transition: all 300ms ease-out;
21
+ background-color: rgba(20, 23, 26, 0.4);
22
+ backdrop-filter: blur(2px) saturate(120%);
23
+ }
24
+
25
+ dialog-content {
26
+ display: block;
27
+ opacity: 0;
28
+ background: white;
29
+ }
30
+
31
+ dialog-panel[aria-hidden='false'] {
32
+ pointer-events: all;
33
+ }
34
+
35
+ dialog-panel[aria-hidden='false'] dialog-overlay {
36
+ pointer-events: all;
37
+ opacity: 1;
38
+ transform: scale(1);
39
+ filter: blur(0px);
40
+ }
41
+
42
+ dialog-panel[aria-hidden='false'] dialog-content {
43
+ opacity: 1;
44
+ z-index: 10;
45
+ transform: scale(1);
46
+ filter: blur(0px);
47
+ }
@@ -0,0 +1,174 @@
1
+ import './dialog-panel.css';
2
+ import '@magic-spells/focus-trap';
3
+
4
+ /**
5
+ * Custom element that creates an accessible modal dialog panel with focus management
6
+ * @extends HTMLElement
7
+ */
8
+ class DialogPanel extends HTMLElement {
9
+ /**
10
+ * Initializes the dialog panel, sets up focus trap and overlay
11
+ */
12
+ constructor() {
13
+ super();
14
+ const _ = this;
15
+ _.id = _.getAttribute('id');
16
+ _.setAttribute('role', 'dialog');
17
+ _.setAttribute('aria-modal', 'true');
18
+ _.setAttribute('aria-hidden', 'true');
19
+
20
+ _.contentPanel = _.querySelector('dialog-content');
21
+ _.focusTrap = document.createElement('focus-trap');
22
+ _.triggerEl = null;
23
+
24
+ // Ensure we have labelledby and describedby references
25
+ if (!_.getAttribute('aria-labelledby')) {
26
+ const heading = _.querySelector('h1, h2, h3');
27
+ if (heading && !heading.id) {
28
+ heading.id = `${_.id}-title`;
29
+ }
30
+ if (heading?.id) {
31
+ _.setAttribute('aria-labelledby', heading.id);
32
+ }
33
+ }
34
+
35
+ _.contentPanel.parentNode.insertBefore(
36
+ _.focusTrap,
37
+ _.contentPanel
38
+ );
39
+ _.focusTrap.appendChild(_.contentPanel);
40
+
41
+ _.focusTrap.setupTrap();
42
+
43
+ // Add modal overlay
44
+ _.prepend(document.createElement('dialog-overlay'));
45
+ _.#bindUI();
46
+ _.#bindKeyboard();
47
+ }
48
+
49
+ /**
50
+ * Binds click events for showing and hiding the dialog
51
+ * @private
52
+ */
53
+ #bindUI() {
54
+ // Handle trigger buttons
55
+ document.addEventListener('click', (e) => {
56
+ const trigger = e.target.closest(
57
+ `[aria-controls="${this.id}"]`
58
+ );
59
+ if (!trigger) return;
60
+
61
+ if (trigger.getAttribute('data-prevent-default') === 'true') {
62
+ e.preventDefault();
63
+ }
64
+
65
+ // this.triggerEl = trigger;
66
+ this.show(trigger);
67
+ });
68
+
69
+ // Handle close buttons
70
+ this.addEventListener('click', (e) => {
71
+ if (!e.target.closest('[data-action="hide-dialog"]')) return;
72
+ this.hide();
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Binds keyboard events for accessibility
78
+ * @private
79
+ */
80
+ #bindKeyboard() {
81
+ this.addEventListener('keydown', (e) => {
82
+ if (e.key === 'Escape') {
83
+ this.hide();
84
+ }
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Shows the dialog and traps focus within it
90
+ * @param {HTMLElement} [triggerEl=null] - The element that triggered the dialog
91
+ */
92
+ show(triggerEl = null) {
93
+ this.triggerEl = triggerEl || false;
94
+
95
+ // Update ARIA states
96
+ this.setAttribute('aria-hidden', 'false');
97
+ if (this.triggerEl) {
98
+ this.triggerEl.setAttribute('aria-expanded', 'true');
99
+ }
100
+
101
+ // prevent body from scrolling
102
+ document.body.classList.add('overflow-hidden');
103
+
104
+ // Focus management
105
+ const firstFocusable = this.querySelector(
106
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
107
+ );
108
+ if (firstFocusable) {
109
+ requestAnimationFrame(() => {
110
+ firstFocusable.focus();
111
+ });
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Hides the dialog and restores focus
117
+ */
118
+ hide() {
119
+ // allow body to scroll
120
+ document.body.classList.remove('overflow-hidden');
121
+
122
+ // Update ARIA states
123
+ if (this.triggerEl) {
124
+ this.triggerEl.setAttribute('aria-expanded', 'false');
125
+ // Restore focus to trigger element
126
+ this.triggerEl.focus();
127
+ } else {
128
+ console.log('we need to blur focus');
129
+ }
130
+
131
+ // hide dialog panel
132
+ setTimeout(() => {
133
+ this.setAttribute('aria-hidden', 'true');
134
+ }, 1);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Custom element that creates a clickable overlay for the dialog
140
+ * @extends HTMLElement
141
+ */
142
+ class DialogOverlay extends HTMLElement {
143
+ constructor() {
144
+ super();
145
+ this.setAttribute('tabindex', '-1'); // Changed to -1 as it shouldn't be focusable
146
+ this.setAttribute('aria-hidden', 'true');
147
+ this.dialogPanel = this.closest('dialog-panel');
148
+ this.#bindUI();
149
+ }
150
+
151
+ #bindUI() {
152
+ this.addEventListener('click', () => {
153
+ this.dialogPanel.hide();
154
+ });
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Custom element that wraps the content of the dialog
160
+ * @extends HTMLElement
161
+ */
162
+ class DialogContent extends HTMLElement {
163
+ constructor() {
164
+ super();
165
+ this.setAttribute('role', 'document'); // Optional: helps with document structure
166
+ }
167
+ }
168
+
169
+ customElements.define('dialog-panel', DialogPanel);
170
+ customElements.define('dialog-overlay', DialogOverlay);
171
+ customElements.define('dialog-content', DialogContent);
172
+
173
+ export { DialogPanel, DialogOverlay, DialogContent };
174
+ export default DialogPanel;