vanilla-aria-modals 1.0.2 → 1.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/CHANGELOG.md +25 -4
- package/README.md +44 -28
- package/package.json +8 -5
- package/src/ModalHandler.d.ts +7 -2
- package/src/ModalHandler.js +137 -89
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
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.0] - 2026-02-15
|
|
4
8
|
|
|
5
|
-
## [1.0.2] - 2026-01-23
|
|
6
9
|
### Added
|
|
7
|
-
-
|
|
10
|
+
- Support automatic modal key generation
|
|
11
|
+
- Add a manual method to rebind the trap focus handler
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Improve automation and simplify logic flow
|
|
15
|
+
- Improve event-bubbling guard by binding the overlay method with `capture: true` and removing the dependency on asynchronous behavior and `e.stopPropagation`
|
|
16
|
+
- Extend `closeHandler` method with a new `modalKey` parameter for caller usage
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Prevent overlayless modals from breaking the stacking order
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## [1.0.3] - 2026-01-23
|
|
23
|
+
### Fixed
|
|
24
|
+
- Improved `README.md` format for usage section
|
|
25
|
+
|
|
26
|
+
## [1.0.2] - 2026-01-23
|
|
27
|
+
### Fixed
|
|
28
|
+
- Updated `package.json` keywords
|
|
8
29
|
|
|
9
30
|
## [1.0.1] - 2026-01-23
|
|
10
31
|
### Fixed
|
|
11
|
-
- Updated installation instructions for README
|
|
32
|
+
- Updated installation instructions for `README.md`
|
|
12
33
|
|
|
13
34
|
## [1.0.0] - 2026-01-23
|
|
14
35
|
### Added
|
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,8 +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
|
|
|
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.
|
|
34
|
+
|
|
30
35
|
**Note:** `lm` in the code stands for *HTMLElement*.
|
|
31
|
-
**Note:** `e.stopPropagation()` prevents other click events (like overlay clicks) from triggering while opening a modal. This can happen because when adding the open modal event, the ARIA events are also added during propagation and can trigger the overlay click event. It is already managed via the class with a timeout, but it is better for robustness to stop propagation here as well if bubbling is not needed in that instance.
|
|
32
36
|
|
|
33
37
|
```js
|
|
34
38
|
// Basic example of showing a modal
|
|
@@ -41,40 +45,31 @@ hideModal() {
|
|
|
41
45
|
modalContainerLm.style.display = 'none';
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
openModal() {
|
|
49
|
+
// Generate a key or use your own
|
|
50
|
+
const modalKey = modalHandler.generateKey();
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
+
const closeModal = () => {
|
|
53
|
+
hideModal();
|
|
54
|
+
// ...Your hide UI logic
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
openModal(e) {
|
|
63
|
-
// Stop event propagation to make sure no events are called on bubbling
|
|
64
|
-
e.stopPropagation();
|
|
56
|
+
// Restore focus
|
|
57
|
+
modalHandler.restoreFocus({ modalKey: modalKey });
|
|
58
|
+
// Remove ARIA events added
|
|
59
|
+
modalHandler.removeA11yEvents({ modalKey: modalKey });
|
|
60
|
+
}
|
|
65
61
|
|
|
66
62
|
showModal();
|
|
67
63
|
// ...Your show UI logic
|
|
68
64
|
|
|
69
65
|
// Add focus
|
|
70
66
|
modalHandler.addFocus({
|
|
71
|
-
modalKey:
|
|
67
|
+
modalKey: modalKey,
|
|
72
68
|
firstFocusableLm: modalFirstFocusableLm
|
|
73
69
|
});
|
|
74
|
-
|
|
75
70
|
// Add ARIA events
|
|
76
71
|
modalHandler.addA11yEvents({
|
|
77
|
-
modalKey:
|
|
72
|
+
modalKey: modalKey,
|
|
78
73
|
modalLm: modalContentLm,
|
|
79
74
|
modalLmOuterLimits: modalContentLm,
|
|
80
75
|
closeLms: [...modalCloseBtns],
|
|
@@ -97,13 +92,14 @@ In Single Page Applications (SPA) or frameworks like React, Vue, or vanilla JS w
|
|
|
97
92
|
```js
|
|
98
93
|
// Suppose your SPA route or component changes
|
|
99
94
|
function onRouteChange() {
|
|
100
|
-
// Clear leftover document events, active modals, and
|
|
95
|
+
// Clear leftover document events, active modals, focus tracking and modal ID key counter
|
|
101
96
|
modalHandler.reset();
|
|
102
97
|
|
|
103
98
|
// Or individually:
|
|
104
99
|
// modalHandler.clearDocumentBodyEvents();
|
|
105
100
|
// modalHandler.clearActiveModals();
|
|
106
101
|
// modalHandler.clearFocusRegistry();
|
|
102
|
+
// modalHandler.resetKeys();
|
|
107
103
|
}
|
|
108
104
|
```
|
|
109
105
|
<br>
|
|
@@ -133,7 +129,7 @@ Registers ARIA events and modal stacking handling:
|
|
|
133
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.
|
|
134
130
|
- **`closeLms?: HTMLElement[] | null;`** *(optional)* Array of elements that should trigger closing the modal (e.g., close buttons).
|
|
135
131
|
- **`exemptLms?: HTMLElement[];`** *(optional)* Array of elements that should not trigger closing even if clicked outside.
|
|
136
|
-
- **`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.
|
|
137
133
|
|
|
138
134
|
Returns `void`
|
|
139
135
|
|
|
@@ -145,8 +141,7 @@ Removes all accessibility and interaction event listeners for the specific regis
|
|
|
145
141
|
**Takes parameters as a single object, which are destructured inside the method.**
|
|
146
142
|
|
|
147
143
|
- **`modalKey: string;`** Unique modal identifier. Must match the modalKey used in **addA11yEvents()**.
|
|
148
|
-
- **`
|
|
149
|
-
- **`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.
|
|
144
|
+
- **`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.
|
|
150
145
|
|
|
151
146
|
Returns `void`
|
|
152
147
|
|
|
@@ -194,4 +189,25 @@ Returns `void`
|
|
|
194
189
|
### reset()
|
|
195
190
|
Combines **clearDocumentBodyEvents()**, **clearActiveModals()**, **clearFocusRegistry()** for a full cleanup.
|
|
196
191
|
|
|
197
|
-
Returns `void`
|
|
192
|
+
Returns `void`
|
|
193
|
+
|
|
194
|
+
### generateKey()
|
|
195
|
+
Generates a unique identifier for the modal.
|
|
196
|
+
|
|
197
|
+
- **`prefix?: string;`** Optional prefix to modify the generated modal key.
|
|
198
|
+
|
|
199
|
+
Returns `string`. The generated modal key to be used later in the code.
|
|
200
|
+
|
|
201
|
+
### resetKeys()
|
|
202
|
+
|
|
203
|
+
Resets the internal modal key counter back to 0.
|
|
204
|
+
|
|
205
|
+
Returns `void`
|
|
206
|
+
|
|
207
|
+
### rebindTrapFocus()
|
|
208
|
+
|
|
209
|
+
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.
|
|
210
|
+
|
|
211
|
+
- **`modalKey: string`** The unique key of the modal whose focus trap should be rebound.
|
|
212
|
+
|
|
213
|
+
returns `void`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-aria-modals",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
}
|