vanilla-aria-modals 1.0.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/README.md +59 -29
- package/package.json +8 -5
- package/src/ModalHandler.d.ts +7 -2
- package/src/ModalHandler.js +137 -89
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
3
|
+
All notable changes to this project will be documented in this file. Dates use ISO 8601 formatting, and version numbers follow [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
|
+
|
|
7
|
+
## [1.1.1] - 2026-02-15
|
|
8
|
+
### Fixed
|
|
9
|
+
- Fix typos, update changelog, and add an example wrapper usage for the `closeHandler`.
|
|
10
|
+
|
|
11
|
+
## [1.1.0] - 2026-02-15
|
|
12
|
+
### Added
|
|
13
|
+
- Support automatic modal key generation
|
|
14
|
+
- Add a manual method to rebind the trap focus handler
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Improve automation and simplify logic flow
|
|
18
|
+
- Improve event-bubbling guard by binding the overlay method with `capture: true` and removing the dependency on asynchronous behavior and `e.stopPropagation`
|
|
19
|
+
- Extend `closeHandler` method with a new `modalKey` parameter for caller usage
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Prevent overlayless modals from breaking the stacking order
|
|
4
23
|
|
|
5
24
|
## [1.0.3] - 2026-01-23
|
|
6
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# ModalHandler
|
|
2
2
|
|
|
3
|
+
See the full release history and updates in the [Changelog](https://github.com/angelvalentino/vanilla-aria-modals/blob/main/CHANGELOG.md).
|
|
4
|
+
|
|
3
5
|
## Introduction
|
|
4
6
|
|
|
7
|
+
|
|
5
8
|
`ModalHandler` is a framework-agnostic utility for managing accessibility (A11y) events in modals or modal-like UIs within your web application. It supports modal stacking and key accessibility features, including focus trapping, focus management, and closing modals with the Escape key or an outside click.
|
|
6
9
|
|
|
7
10
|
Although designed primarily for modal interactions, it can be used in any UI logic that requires basic ARIA support and focus management.
|
|
@@ -27,9 +30,9 @@ TypeScript types are used in the **docs** and in the **.d.ts** file to indicate
|
|
|
27
30
|
|
|
28
31
|
A fully detailed example including the necessary JavaScript, HTML, and CSS files, can be found [here](https://github.com/angelvalentino/vanilla-aria-modals/tree/main/example).
|
|
29
32
|
|
|
30
|
-
**Note:**
|
|
33
|
+
> **Note:** The **closeHandler** receives both the original **DOM event** and the associated **modalKey** automatically. The caller can use these to inspect the event or determine which modal triggered the close.
|
|
31
34
|
|
|
32
|
-
**Note:** `
|
|
35
|
+
**Note:** `lm` in the code stands for *HTMLElement*.
|
|
33
36
|
|
|
34
37
|
```js
|
|
35
38
|
// Basic example of showing a modal
|
|
@@ -42,40 +45,31 @@ hideModal() {
|
|
|
42
45
|
modalContainerLm.style.display = 'none';
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
openModal() {
|
|
49
|
+
// Generate a key or use your own
|
|
50
|
+
const modalKey = modalHandler.generateKey();
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
52
|
+
const closeModal = () => {
|
|
53
|
+
hideModal();
|
|
54
|
+
// ...Your hide UI logic
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
openModal(e) {
|
|
64
|
-
// Stop event propagation to make sure no events are called on bubbling
|
|
65
|
-
e.stopPropagation();
|
|
56
|
+
// Restore focus
|
|
57
|
+
modalHandler.restoreFocus({ modalKey: modalKey });
|
|
58
|
+
// Remove ARIA events added
|
|
59
|
+
modalHandler.removeA11yEvents({ modalKey: modalKey });
|
|
60
|
+
}
|
|
66
61
|
|
|
67
62
|
showModal();
|
|
68
63
|
// ...Your show UI logic
|
|
69
64
|
|
|
70
65
|
// Add focus
|
|
71
66
|
modalHandler.addFocus({
|
|
72
|
-
modalKey:
|
|
67
|
+
modalKey: modalKey,
|
|
73
68
|
firstFocusableLm: modalFirstFocusableLm
|
|
74
69
|
});
|
|
75
|
-
|
|
76
70
|
// Add ARIA events
|
|
77
71
|
modalHandler.addA11yEvents({
|
|
78
|
-
modalKey:
|
|
72
|
+
modalKey: modalKey,
|
|
79
73
|
modalLm: modalContentLm,
|
|
80
74
|
modalLmOuterLimits: modalContentLm,
|
|
81
75
|
closeLms: [...modalCloseBtns],
|
|
@@ -98,13 +92,14 @@ In Single Page Applications (SPA) or frameworks like React, Vue, or vanilla JS w
|
|
|
98
92
|
```js
|
|
99
93
|
// Suppose your SPA route or component changes
|
|
100
94
|
function onRouteChange() {
|
|
101
|
-
// Clear leftover document events, active modals, and
|
|
95
|
+
// Clear leftover document events, active modals, focus tracking and modal ID key counter
|
|
102
96
|
modalHandler.reset();
|
|
103
97
|
|
|
104
98
|
// Or individually:
|
|
105
99
|
// modalHandler.clearDocumentBodyEvents();
|
|
106
100
|
// modalHandler.clearActiveModals();
|
|
107
101
|
// modalHandler.clearFocusRegistry();
|
|
102
|
+
// modalHandler.resetKeys();
|
|
108
103
|
}
|
|
109
104
|
```
|
|
110
105
|
<br>
|
|
@@ -134,7 +129,22 @@ Registers ARIA events and modal stacking handling:
|
|
|
134
129
|
- **`modalLmOuterLimits?: HTMLElement | null;`** *(optional)* The container that defines the modal boundary. Used to detect clicks outside the modal. **modalLm** is usually used here, but depending on the UI we may not want to trap focus into the same container we want to close, maybe just in a part of it.
|
|
135
130
|
- **`closeLms?: HTMLElement[] | null;`** *(optional)* Array of elements that should trigger closing the modal (e.g., close buttons).
|
|
136
131
|
- **`exemptLms?: HTMLElement[];`** *(optional)* Array of elements that should not trigger closing even if clicked outside.
|
|
137
|
-
- **`closeHandler: () => void;`** Function to call when the modal should close. Usually should call **removeA11yEvents()**.
|
|
132
|
+
- **`closeHandler: (e: Event, modalKey: string) => void;`** Function to call when the modal should close. Usually should call **removeA11yEvents()**. Automatically receives the event **(e)** and **(modalKey)** from the wrapper; no need to pass as arguments. It’s up to the caller whether to use them.
|
|
133
|
+
If you need to pass additional arguments to the close handler, you can wrap it in a function that returns a handler accepting only the two parameters (`e` and `modalKey`):
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const closeModalWrapper = (...args) => {
|
|
137
|
+
// Returns a handler that the utility calls internally with e and modalKey
|
|
138
|
+
return (e, modalKey) => {
|
|
139
|
+
// Your logic for the close handler here
|
|
140
|
+
// Access to parameters thanks to closures
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
modalHandler.addA11yEvents({
|
|
145
|
+
closeHandler: closeModalWrapper(...args)
|
|
146
|
+
});
|
|
147
|
+
```
|
|
138
148
|
|
|
139
149
|
Returns `void`
|
|
140
150
|
|
|
@@ -146,8 +156,7 @@ Removes all accessibility and interaction event listeners for the specific regis
|
|
|
146
156
|
**Takes parameters as a single object, which are destructured inside the method.**
|
|
147
157
|
|
|
148
158
|
- **`modalKey: string;`** Unique modal identifier. Must match the modalKey used in **addA11yEvents()**.
|
|
149
|
-
- **`
|
|
150
|
-
- **`closeLms?: HTMLElement[] | null;`** *(optional)* Array of close elements used in **addA11yEvents()**. Used to be able to properly remove the close event from the given elements.
|
|
159
|
+
- **`isToggle?: boolean;`** Optional flag to indicate that the modal (no overlay, usually popups with toggle logic) is being closed via a toggle action (outside of the usual close handler). Setting this flag to **true** ensures the modal is properly removed from the active stack, even when called outside of any closing handler.
|
|
151
160
|
|
|
152
161
|
Returns `void`
|
|
153
162
|
|
|
@@ -195,4 +204,25 @@ Returns `void`
|
|
|
195
204
|
### reset()
|
|
196
205
|
Combines **clearDocumentBodyEvents()**, **clearActiveModals()**, **clearFocusRegistry()** for a full cleanup.
|
|
197
206
|
|
|
198
|
-
Returns `void`
|
|
207
|
+
Returns `void`
|
|
208
|
+
|
|
209
|
+
### generateKey()
|
|
210
|
+
Generates a unique identifier for the modal.
|
|
211
|
+
|
|
212
|
+
- **`prefix?: string;`** Optional prefix to modify the generated modal key.
|
|
213
|
+
|
|
214
|
+
Returns `string`. The generated modal key to be used later in the code.
|
|
215
|
+
|
|
216
|
+
### resetKeys()
|
|
217
|
+
|
|
218
|
+
Resets the internal modal key counter back to 0.
|
|
219
|
+
|
|
220
|
+
Returns `void`
|
|
221
|
+
|
|
222
|
+
### rebindTrapFocus()
|
|
223
|
+
|
|
224
|
+
Re-attaches the focus-trapping event listener for a specific modal. The internal **trapFocus** method already queries the DOM on each keyboard event, so in most cases, manually rebinding is not necessary.
|
|
225
|
+
|
|
226
|
+
- **`modalKey: string;`** The unique key of the modal whose focus trap should be rebound.
|
|
227
|
+
|
|
228
|
+
returns `void`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-aria-modals",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Framework-agnostic utility for managing accessibility in modals or modal-like UIs, including modal stacking, focus management, and closing via Escape key or outside click.",
|
|
5
5
|
"main": "src/ModalHandler.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
"keywords": [
|
|
14
14
|
"modal",
|
|
15
15
|
"accessibility",
|
|
16
|
-
"
|
|
16
|
+
"ARIA",
|
|
17
|
+
"WAI-ARIA",
|
|
18
|
+
"A11y",
|
|
17
19
|
"trap-focus",
|
|
20
|
+
"focus-trap",
|
|
18
21
|
"keyboard",
|
|
19
22
|
"dialog",
|
|
20
|
-
"a11y",
|
|
21
23
|
"vanilla-js",
|
|
22
|
-
"ui",
|
|
23
|
-
"frontend",
|
|
24
24
|
"javascript",
|
|
25
|
+
"js",
|
|
26
|
+
"UI",
|
|
27
|
+
"frontend",
|
|
25
28
|
"keyboard-navigation",
|
|
26
29
|
"modal-manager",
|
|
27
30
|
"focus-management",
|
package/src/ModalHandler.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export default class ModalHandler {
|
|
|
11
11
|
|
|
12
12
|
reset(): void;
|
|
13
13
|
|
|
14
|
+
generateKey(prefix?: string): string;
|
|
15
|
+
|
|
16
|
+
resetKeys(): void;
|
|
17
|
+
|
|
18
|
+
rebindTrapFocus(modalKey: string): void;
|
|
19
|
+
|
|
14
20
|
addA11yEvents(options: {
|
|
15
21
|
modalKey: string;
|
|
16
22
|
modalLm?: HTMLElement | null;
|
|
@@ -22,8 +28,7 @@ export default class ModalHandler {
|
|
|
22
28
|
|
|
23
29
|
removeA11yEvents(options: {
|
|
24
30
|
modalKey: string;
|
|
25
|
-
|
|
26
|
-
closeLms?: HTMLElement[] | null;
|
|
31
|
+
isToggle?: boolean
|
|
27
32
|
}): void;
|
|
28
33
|
|
|
29
34
|
addFocus(options: {
|
package/src/ModalHandler.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
export default class ModalHandler {
|
|
2
|
+
#debug;
|
|
2
3
|
#eventsHandler;
|
|
3
|
-
#activeModals;
|
|
4
4
|
#focusHandler;
|
|
5
|
-
#
|
|
5
|
+
#activeModals;
|
|
6
|
+
#modalIdCounter
|
|
6
7
|
|
|
7
8
|
constructor() {
|
|
8
9
|
if (ModalHandler.instance) return ModalHandler.instance;
|
|
9
10
|
ModalHandler.instance = this;
|
|
11
|
+
this.#debug = false;
|
|
10
12
|
this.#eventsHandler = {};
|
|
11
|
-
this.#activeModals = [];
|
|
12
13
|
this.#focusHandler = {};
|
|
13
|
-
this.#
|
|
14
|
+
this.#activeModals = [];
|
|
15
|
+
this.#modalIdCounter = 0;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
setDebug(bool) {
|
|
@@ -39,7 +41,7 @@ export default class ModalHandler {
|
|
|
39
41
|
return true;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
#unregisterModal(modalKey) {
|
|
44
|
+
#unregisterModal(modalKey, isOverlayLess) {
|
|
43
45
|
if (!this.#activeModals.includes(modalKey)) {
|
|
44
46
|
console.warn(`[ModalHandler]: Modal with key "${modalKey}" was not registered. Nothing to remove.`);
|
|
45
47
|
return false;
|
|
@@ -49,13 +51,27 @@ export default class ModalHandler {
|
|
|
49
51
|
console.log('[ModalHandler][DEBUG]: Active modal stack before filtering => ', this.#activeModals);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
if (isOverlayLess) {
|
|
55
|
+
if (this.#debug) {
|
|
56
|
+
console.log('[ModalHandler][DEBUG]: Overlayless modal closed ignoring stacking order.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.#activeModals = this.#activeModals.filter(key => key !== modalKey);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.#activeModals.pop();
|
|
63
|
+
}
|
|
53
64
|
|
|
54
65
|
if (this.#debug) {
|
|
55
66
|
console.log(`[ModalHandler][DEBUG]: Unregister modal with key => "${modalKey}"`);
|
|
56
67
|
console.log('[ModalHandler][DEBUG]: Active modal stack after filtering => ', this.#activeModals);
|
|
57
68
|
}
|
|
58
69
|
|
|
70
|
+
if (this.#activeModals.length === 0) {
|
|
71
|
+
if (this.#debug) console.log('[ModalHandler][DEBUG]: Active modal stack is now empty.');
|
|
72
|
+
this.resetKeys();
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
return true;
|
|
60
76
|
}
|
|
61
77
|
|
|
@@ -103,15 +119,15 @@ export default class ModalHandler {
|
|
|
103
119
|
}
|
|
104
120
|
}
|
|
105
121
|
|
|
106
|
-
#handleEscapeKeyClose(closeHandler) {
|
|
122
|
+
#handleEscapeKeyClose(modalKey, closeHandler) {
|
|
107
123
|
return e => {
|
|
108
124
|
if (e.key === 'Escape') {
|
|
109
|
-
closeHandler(e);
|
|
125
|
+
closeHandler(e, modalKey);
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
}
|
|
113
129
|
|
|
114
|
-
#handleOutsideClickClose(closeHandler, modalLmOuterLimits, exemptLms
|
|
130
|
+
#handleOutsideClickClose(modalKey, closeHandler, modalLmOuterLimits, exemptLms) {
|
|
115
131
|
return e => {
|
|
116
132
|
const clickedLm = e.target;
|
|
117
133
|
|
|
@@ -126,20 +142,27 @@ export default class ModalHandler {
|
|
|
126
142
|
return;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
closeHandler(e);
|
|
145
|
+
closeHandler(e, modalKey);
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
#handleActiveModalClose(modalKey, closeHandler) {
|
|
149
|
+
#handleActiveModalClose(modalKey, closeHandler, modalLmOuterLimits) {
|
|
134
150
|
return e => {
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
// Check if modal is overlayless and triggered by click so we can ignore stacking order later
|
|
152
|
+
const isOverlayLess = !modalLmOuterLimits && e?.type === 'click';
|
|
153
|
+
|
|
154
|
+
if (!isOverlayLess && !this.#isActiveModal(modalKey)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const isRegistered = this.#unregisterModal(modalKey, isOverlayLess);
|
|
159
|
+
if (!isRegistered) return;
|
|
137
160
|
|
|
138
161
|
if (this.#debug) {
|
|
139
162
|
console.log(`[ModalHandler][DEBUG]: Close modal with key => "${modalKey}"`);
|
|
140
163
|
}
|
|
141
164
|
|
|
142
|
-
closeHandler(); // Only close if this is the topmost modal
|
|
165
|
+
closeHandler(e, modalKey); // Only close if this is the topmost modal
|
|
143
166
|
}
|
|
144
167
|
}
|
|
145
168
|
|
|
@@ -154,8 +177,13 @@ export default class ModalHandler {
|
|
|
154
177
|
for (const key in documentBodyEvents) {
|
|
155
178
|
const events = documentBodyEvents[key];
|
|
156
179
|
|
|
157
|
-
events.forEach(
|
|
158
|
-
|
|
180
|
+
events.forEach(({ eventName, callback, isOutsideClickHandler }) => {
|
|
181
|
+
if (isOutsideClickHandler) {
|
|
182
|
+
document.body.removeEventListener(eventName, callback, true);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
document.body.removeEventListener(eventName, callback);
|
|
186
|
+
}
|
|
159
187
|
});
|
|
160
188
|
|
|
161
189
|
events.length = 0;
|
|
@@ -202,6 +230,25 @@ export default class ModalHandler {
|
|
|
202
230
|
this.clearDocumentBodyEvents();
|
|
203
231
|
this.clearActiveModals();
|
|
204
232
|
this.clearFocusRegistry();
|
|
233
|
+
this.resetKeys();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
generateKey(prefix = 'modal') {
|
|
237
|
+
this.#modalIdCounter = (this.#modalIdCounter || 0) + 1;
|
|
238
|
+
return `${prefix}-${this.#modalIdCounter}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
resetKeys() {
|
|
242
|
+
this.#modalIdCounter = 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
rebindTrapFocus(modalKey) {
|
|
246
|
+
const eventsHandler = this.#eventsHandler[modalKey];
|
|
247
|
+
const trapFocusHandler = eventsHandler.find(hander => hander.isTrapFocusHandler === true);
|
|
248
|
+
const { lm, eventName, callback } = trapFocusHandler;
|
|
249
|
+
|
|
250
|
+
lm.removeEventListener(eventName, callback);
|
|
251
|
+
lm.addEventListener(eventName, callback);
|
|
205
252
|
}
|
|
206
253
|
|
|
207
254
|
addA11yEvents({
|
|
@@ -212,91 +259,92 @@ export default class ModalHandler {
|
|
|
212
259
|
exemptLms = [],
|
|
213
260
|
closeHandler
|
|
214
261
|
}) {
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
document.body.addEventListener('keydown', escapeKeyHandler);
|
|
229
|
-
|
|
230
|
-
if (modalLmOuterLimits) {
|
|
231
|
-
document.body.addEventListener('click', outsideClickHandler);
|
|
232
|
-
}
|
|
233
|
-
if (modalLm) {
|
|
234
|
-
modalLm.addEventListener('keydown', trapFocusHandler);
|
|
235
|
-
}
|
|
236
|
-
if (closeLms && Array.isArray(closeLms)) {
|
|
237
|
-
closeLms.forEach(closeLm => {
|
|
238
|
-
closeLm.addEventListener('click', handleActiveModalClose);
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Ensure eventsHandler object exists for this modal
|
|
243
|
-
if (!this.#eventsHandler[modalKey]) {
|
|
244
|
-
this.#eventsHandler[modalKey] = {};
|
|
245
|
-
}
|
|
246
|
-
const eventsHandler = this.#eventsHandler[modalKey];
|
|
247
|
-
|
|
248
|
-
// Ensure the documentBody object and array exist for this modal key
|
|
249
|
-
if (!this.#eventsHandler.documentBody) {
|
|
250
|
-
this.#eventsHandler.documentBody = {};
|
|
251
|
-
}
|
|
252
|
-
if (!this.#eventsHandler.documentBody[modalKey]) {
|
|
253
|
-
this.#eventsHandler.documentBody[modalKey] = [];
|
|
254
|
-
}
|
|
255
|
-
const documentEvents = this.#eventsHandler.documentBody[modalKey];
|
|
256
|
-
|
|
257
|
-
// Store event handlers references so they can be removed later
|
|
258
|
-
eventsHandler.escapeKeyHandler = escapeKeyHandler;
|
|
259
|
-
modalLmOuterLimits && (eventsHandler.outsideClickHandler = outsideClickHandler);
|
|
260
|
-
modalLm && (eventsHandler.trapFocusHandler = trapFocusHandler);
|
|
261
|
-
closeLms && (eventsHandler.closeHandler = handleActiveModalClose);
|
|
262
|
-
|
|
263
|
-
// Keep references to body events for SPA view changes,
|
|
264
|
-
// so lingering events can be removed if the modal stays open across re-renders
|
|
265
|
-
documentEvents.push({ eventName: 'keydown', callback: escapeKeyHandler });
|
|
266
|
-
if (modalLmOuterLimits) {
|
|
267
|
-
documentEvents.push({ eventName: 'click', callback: outsideClickHandler });
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
removeA11yEvents({
|
|
273
|
-
modalKey,
|
|
274
|
-
modalLm = null,
|
|
275
|
-
closeLms = null
|
|
276
|
-
}) {
|
|
277
|
-
// Unregister modal key and skip if it wasn't registered
|
|
278
|
-
const isRegistered = this.#unregisterModal(modalKey);
|
|
279
|
-
if (!isRegistered) return;
|
|
280
|
-
|
|
262
|
+
// Register modal key and skip if already registered
|
|
263
|
+
const isNew = this.#registerModal(modalKey);
|
|
264
|
+
if (!isNew) return;
|
|
265
|
+
|
|
266
|
+
// Register event handlers reference
|
|
267
|
+
const handleActiveModalClose = this.#handleActiveModalClose(modalKey, closeHandler, modalLmOuterLimits);
|
|
268
|
+
const escapeKeyHandler = this.#handleEscapeKeyClose(modalKey, handleActiveModalClose);
|
|
269
|
+
|
|
270
|
+
// Ensure storage for this modal exists
|
|
271
|
+
if (!this.#eventsHandler[modalKey]) this.#eventsHandler[modalKey] = [];
|
|
272
|
+
if (!this.#eventsHandler.documentBody) this.#eventsHandler.documentBody = {};
|
|
273
|
+
if (!this.#eventsHandler.documentBody[modalKey]) this.#eventsHandler.documentBody[modalKey] = [];
|
|
274
|
+
|
|
281
275
|
const eventsHandler = this.#eventsHandler[modalKey];
|
|
276
|
+
const documentEvents = this.#eventsHandler.documentBody[modalKey];
|
|
282
277
|
|
|
283
|
-
//
|
|
284
|
-
document.body.
|
|
278
|
+
// Attach event listeners
|
|
279
|
+
document.body.addEventListener('keydown', escapeKeyHandler);
|
|
280
|
+
eventsHandler.push({ eventName: 'keydown', callback: escapeKeyHandler });
|
|
281
|
+
// Keep references to body events for SPA view changes,
|
|
282
|
+
// so lingering events can be removed if the modal stays open across re-renders
|
|
283
|
+
documentEvents.push({ eventName: 'keydown', callback: escapeKeyHandler });
|
|
285
284
|
|
|
286
|
-
if (
|
|
287
|
-
|
|
285
|
+
if (modalLmOuterLimits) {
|
|
286
|
+
const outsideClickHandler = this.#handleOutsideClickClose(modalKey, handleActiveModalClose, modalLmOuterLimits, exemptLms);
|
|
287
|
+
|
|
288
|
+
// Use capture phase to detect outside clicks before event bubbling,
|
|
289
|
+
// preventing auto-close on overlay click after modal opens
|
|
290
|
+
document.body.addEventListener('click', outsideClickHandler, true);
|
|
291
|
+
eventsHandler.push({ eventName: 'click', callback: outsideClickHandler, isOutsideClickHandler: true });
|
|
292
|
+
// Keep references to body events for SPA view changes
|
|
293
|
+
documentEvents.push({ eventName: 'click', callback: outsideClickHandler, isOutsideClickHandler: true });
|
|
288
294
|
}
|
|
289
295
|
if (modalLm) {
|
|
290
|
-
|
|
296
|
+
const trapFocusHandler = this.#handleTrapFocus(modalLm);
|
|
297
|
+
|
|
298
|
+
modalLm.addEventListener('keydown', trapFocusHandler);
|
|
299
|
+
eventsHandler.push({ lm: modalLm, eventName: 'keydown', callback: trapFocusHandler, isTrapFocusHandler: true });
|
|
291
300
|
}
|
|
292
301
|
if (closeLms && Array.isArray(closeLms)) {
|
|
293
302
|
closeLms.forEach(closeLm => {
|
|
294
|
-
closeLm.
|
|
303
|
+
closeLm.addEventListener('click', handleActiveModalClose);
|
|
295
304
|
});
|
|
305
|
+
eventsHandler.push({ lm: closeLms, eventName: 'click', callback: handleActiveModalClose });
|
|
296
306
|
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
removeA11yEvents({
|
|
310
|
+
modalKey,
|
|
311
|
+
isToggle,
|
|
312
|
+
}) {
|
|
313
|
+
if (isToggle) {
|
|
314
|
+
const removed = this.#unregisterModal(modalKey, isToggle);
|
|
315
|
+
if (this.#debug && removed) {
|
|
316
|
+
console.log('[ModalHandler][DEBUG]: Overlayless modal not triggered on close; removed via event cleanup.');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const eventsHandler = this.#eventsHandler[modalKey];
|
|
321
|
+
|
|
322
|
+
// Event cleanup
|
|
323
|
+
eventsHandler.forEach(({ lm, eventName, callback, isOutsideClickHandler }) => {
|
|
324
|
+
// if lm is undefined we just have to clear the body
|
|
325
|
+
if (lm === undefined) lm = document.body;
|
|
326
|
+
|
|
327
|
+
// Check for array of closing lms
|
|
328
|
+
if (Array.isArray(lm)) {
|
|
329
|
+
const lms = lm;
|
|
330
|
+
|
|
331
|
+
lms.forEach(lm => {
|
|
332
|
+
lm.removeEventListener(eventName, callback);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
if (isOutsideClickHandler) {
|
|
337
|
+
lm.removeEventListener(eventName, callback, true);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
lm.removeEventListener(eventName, callback);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
297
344
|
|
|
298
345
|
// Clean up stored handlers
|
|
299
|
-
|
|
346
|
+
this.#eventsHandler[modalKey].length = 0; // empties the array to remove any lingering references
|
|
347
|
+
delete this.#eventsHandler[modalKey]; // removes the property entirely from the eventsHandler object
|
|
300
348
|
const documentEvents = this.#eventsHandler.documentBody[modalKey];
|
|
301
349
|
documentEvents.length = 0;
|
|
302
350
|
}
|