vanilla-aria-modals 1.1.3 → 1.1.4
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 +10 -3
- package/README.md +64 -57
- package/package.json +1 -1
- package/src/ModalHandler.d.ts +8 -6
- package/src/ModalHandler.js +66 -30
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. Dates use I
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [1.1.4] - 2026-02-16
|
|
8
|
+
### Fixed
|
|
9
|
+
- Improve overlayless modal close logic. Removed the `isToggle` parameter from `removeA11yEvents` as it is no longer needed. The logic now works more smoothly, only unregisters modals in `removeA11yEvents` instead of the close handler wrapper, and preserves the overlayless modal bypass behavior
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Update documentation, method order and chaining, and debug statements
|
|
13
|
+
|
|
7
14
|
## [1.1.3] - 2026-02-16
|
|
8
15
|
### Fixed
|
|
9
16
|
- Add missing parameter titles to `README.md`.
|
|
@@ -31,15 +38,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
31
38
|
|
|
32
39
|
## [1.0.3] - 2026-01-23
|
|
33
40
|
### Fixed
|
|
34
|
-
-
|
|
41
|
+
- Improve `README.md` format for usage section
|
|
35
42
|
|
|
36
43
|
## [1.0.2] - 2026-01-23
|
|
37
44
|
### Fixed
|
|
38
|
-
-
|
|
45
|
+
- Update `package.json` keywords
|
|
39
46
|
|
|
40
47
|
## [1.0.1] - 2026-01-23
|
|
41
48
|
### Fixed
|
|
42
|
-
-
|
|
49
|
+
- Update installation instructions for `README.md`
|
|
43
50
|
|
|
44
51
|
## [1.0.0] - 2026-01-23
|
|
45
52
|
### Added
|
package/README.md
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# vanilla-aria-modals
|
|
2
2
|
|
|
3
3
|
See the full release history and updates in the [Changelog](https://github.com/angelvalentino/vanilla-aria-modals/blob/main/CHANGELOG.md).
|
|
4
4
|
|
|
5
5
|
## Introduction
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
7
|
+
`vanilla-aria-modals` 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.
|
|
9
8
|
|
|
10
9
|
Although designed primarily for modal interactions, it can be used in any UI logic that requires basic ARIA support and focus management.
|
|
11
10
|
|
|
12
11
|
It supports a dynamic number of modals and events. In SPAs or dynamic interfaces, navigating away or re-rendering a component without closing a modal can leave lingering event listeners on `document.body`, which may interfere with future interactions. A reset method is provided to call on route changes or component unmounts. See more at: [SPA / Advanced Usage](#spa--advanced-usage)
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
Written in vanilla JS for full flexibility. You can modify it directly in `node_modules` if needed. Just update the `.d.ts` file when changing public methods to keep IntelliSense accurate. Internals documentation, such as architecture and logic flow can be found [here](https://github.com/angelvalentino/vanilla-aria-modals/blob/main/docs/architecture.md).
|
|
15
14
|
|
|
16
15
|
## Set up
|
|
17
16
|
|
|
@@ -92,14 +91,17 @@ In Single Page Applications (SPA) or frameworks like React, Vue, or vanilla JS w
|
|
|
92
91
|
```js
|
|
93
92
|
// Suppose your SPA route or component changes
|
|
94
93
|
function onRouteChange() {
|
|
95
|
-
// Clear leftover document events, active modals, focus tracking
|
|
94
|
+
// Clear leftover document events, active modals, focus tracking, modal ID key counter
|
|
95
|
+
// and overlayless modals registry
|
|
96
96
|
modalHandler.reset();
|
|
97
97
|
|
|
98
98
|
// Or individually:
|
|
99
|
-
// modalHandler
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
99
|
+
// modalHandler
|
|
100
|
+
// .clearDocumentBodyEvents()
|
|
101
|
+
// .clearActiveModals()
|
|
102
|
+
// .clearFocusRegistry()
|
|
103
|
+
// .resetKeys()
|
|
104
|
+
// .clearPopups();
|
|
103
105
|
}
|
|
104
106
|
```
|
|
105
107
|
<br>
|
|
@@ -114,8 +116,59 @@ Enables or disables debug logs, aimed for reviewing stacked modals, clear and cl
|
|
|
114
116
|
|
|
115
117
|
Returns `void`
|
|
116
118
|
|
|
119
|
+
### generateKey()
|
|
120
|
+
Generates a unique identifier for the modal.
|
|
121
|
+
|
|
122
|
+
#### Parameters
|
|
123
|
+
|
|
124
|
+
- **`prefix?: string;`** Optional prefix to modify the generated modal key.
|
|
125
|
+
|
|
126
|
+
Returns `string`. The generated modal key to be used later in the code.
|
|
127
|
+
|
|
128
|
+
### clearDocumentBodyEvents()
|
|
129
|
+
Clears any leftover document body event listeners.
|
|
130
|
+
|
|
131
|
+
Returns `this`
|
|
132
|
+
|
|
133
|
+
### clearActiveModals()
|
|
134
|
+
Resets the active modal stack.
|
|
135
|
+
|
|
136
|
+
Returns `this`
|
|
137
|
+
|
|
138
|
+
### clearFocusRegistry()
|
|
139
|
+
Clears stored focus references.
|
|
140
|
+
|
|
141
|
+
Returns `this`
|
|
142
|
+
|
|
143
|
+
### resetKeys()
|
|
144
|
+
|
|
145
|
+
Resets the internal modal key counter back to 0.
|
|
146
|
+
|
|
147
|
+
Returns `this`
|
|
148
|
+
|
|
149
|
+
### clearPopups()
|
|
150
|
+
|
|
151
|
+
Clears the internal overlayless modals (popups) array
|
|
152
|
+
|
|
153
|
+
Returns `this`
|
|
154
|
+
|
|
155
|
+
### reset()
|
|
156
|
+
Combines **clearDocumentBodyEvents()**, **clearActiveModals()**, **clearFocusRegistry()**, **resetKeys**, **clearPopups()** for a full cleanup.
|
|
157
|
+
|
|
158
|
+
Returns `void`
|
|
159
|
+
|
|
160
|
+
### rebindTrapFocus()
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
#### Parameters
|
|
165
|
+
|
|
166
|
+
- **`modalKey: string;`** The unique key of the modal whose focus trap should be rebound.
|
|
167
|
+
|
|
168
|
+
Returns `void`
|
|
169
|
+
|
|
117
170
|
### addA11yEvents()
|
|
118
|
-
Registers
|
|
171
|
+
Registers A11y events and modal stacking handling:
|
|
119
172
|
- Close at overlay click
|
|
120
173
|
- Close at ESC key
|
|
121
174
|
- Trap focus
|
|
@@ -151,13 +204,12 @@ Returns `void`
|
|
|
151
204
|
|
|
152
205
|
### removeA11yEvents()
|
|
153
206
|
|
|
154
|
-
Removes all
|
|
207
|
+
Removes all A11y event listeners for the specific registered modal.
|
|
155
208
|
|
|
156
209
|
#### Parameters
|
|
157
210
|
**Takes parameters as a single object, which are destructured inside the method.**
|
|
158
211
|
|
|
159
212
|
- **`modalKey: string;`** Unique modal identifier. Must match the modalKey used in **addA11yEvents()**.
|
|
160
|
-
- **`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.
|
|
161
213
|
|
|
162
214
|
Returns `void`
|
|
163
215
|
|
|
@@ -185,49 +237,4 @@ Restores focus to the element that was active before the modal opened.
|
|
|
185
237
|
- **`lastFocusedLm?: HTMLElement | null;`** *(optional)* Custom element to restore focus to if auto is **false**.
|
|
186
238
|
- **`auto?: boolean;`** *(optional)* Defaults to **true**. If **false**, uses lastFocusedLm to restore focus instead of the stored one.
|
|
187
239
|
|
|
188
|
-
Returns `void`
|
|
189
|
-
|
|
190
|
-
### clearDocumentBodyEvents()
|
|
191
|
-
Clears any leftover document body event listeners.
|
|
192
|
-
|
|
193
|
-
Returns `void`
|
|
194
|
-
|
|
195
|
-
### clearActiveModals()
|
|
196
|
-
Resets the active modal stack.
|
|
197
|
-
|
|
198
|
-
Returns `void`
|
|
199
|
-
|
|
200
|
-
### clearFocusRegistry()
|
|
201
|
-
Clears stored focus references.
|
|
202
|
-
|
|
203
|
-
Returns `void`
|
|
204
|
-
|
|
205
|
-
### reset()
|
|
206
|
-
Combines **clearDocumentBodyEvents()**, **clearActiveModals()**, **clearFocusRegistry()** for a full cleanup.
|
|
207
|
-
|
|
208
|
-
Returns `void`
|
|
209
|
-
|
|
210
|
-
### generateKey()
|
|
211
|
-
Generates a unique identifier for the modal.
|
|
212
|
-
|
|
213
|
-
#### Parameters
|
|
214
|
-
|
|
215
|
-
- **`prefix?: string;`** Optional prefix to modify the generated modal key.
|
|
216
|
-
|
|
217
|
-
Returns `string`. The generated modal key to be used later in the code.
|
|
218
|
-
|
|
219
|
-
### resetKeys()
|
|
220
|
-
|
|
221
|
-
Resets the internal modal key counter back to 0.
|
|
222
|
-
|
|
223
|
-
Returns `void`
|
|
224
|
-
|
|
225
|
-
### rebindTrapFocus()
|
|
226
|
-
|
|
227
|
-
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.
|
|
228
|
-
|
|
229
|
-
#### Parameters
|
|
230
|
-
|
|
231
|
-
- **`modalKey: string;`** The unique key of the modal whose focus trap should be rebound.
|
|
232
|
-
|
|
233
240
|
Returns `void`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-aria-modals",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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": {
|
package/src/ModalHandler.d.ts
CHANGED
|
@@ -3,17 +3,19 @@ export default class ModalHandler {
|
|
|
3
3
|
|
|
4
4
|
setDebug(bool: boolean): void;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
generateKey(prefix?: string): string;
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
clearDocumentBodyEvents(): this;
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
clearActiveModals(): this;
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
clearFocusRegistry(): this;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
resetKeys(): this;
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
clearPopups(): this;
|
|
17
|
+
|
|
18
|
+
reset(): void;
|
|
17
19
|
|
|
18
20
|
rebindTrapFocus(modalKey: string): void;
|
|
19
21
|
|
package/src/ModalHandler.js
CHANGED
|
@@ -3,7 +3,8 @@ export default class ModalHandler {
|
|
|
3
3
|
#eventsHandler;
|
|
4
4
|
#focusHandler;
|
|
5
5
|
#activeModals;
|
|
6
|
-
#modalIdCounter
|
|
6
|
+
#modalIdCounter;
|
|
7
|
+
#popups;
|
|
7
8
|
|
|
8
9
|
constructor() {
|
|
9
10
|
if (ModalHandler.instance) return ModalHandler.instance;
|
|
@@ -13,6 +14,7 @@ export default class ModalHandler {
|
|
|
13
14
|
this.#focusHandler = {};
|
|
14
15
|
this.#activeModals = [];
|
|
15
16
|
this.#modalIdCounter = 0;
|
|
17
|
+
this.#popups = [];
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
setDebug(bool) {
|
|
@@ -50,10 +52,12 @@ export default class ModalHandler {
|
|
|
50
52
|
if (this.#debug) {
|
|
51
53
|
console.log('[ModalHandler][DEBUG]: Active modal stack before filtering => ', this.#activeModals);
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
|
|
56
|
+
// Check if the modal closed is overlayless (popup),
|
|
57
|
+
// if so clear it from the stack without following normal LIFO order
|
|
54
58
|
if (isOverlayLess) {
|
|
55
59
|
if (this.#debug) {
|
|
56
|
-
console.log('[ModalHandler][DEBUG]: Overlayless modal closed ignoring stacking order.');
|
|
60
|
+
console.log('[ModalHandler][DEBUG]: Overlayless modal closed ignoring stacking order. (only click events)');
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
this.#activeModals = this.#activeModals.filter(key => key !== modalKey);
|
|
@@ -151,13 +155,11 @@ export default class ModalHandler {
|
|
|
151
155
|
// Check if modal is overlayless and triggered by click so we can ignore stacking order later
|
|
152
156
|
const isOverlayLess = !modalLmOuterLimits && e?.type === 'click';
|
|
153
157
|
|
|
158
|
+
// Only check stack for modals with overlay
|
|
154
159
|
if (!isOverlayLess && !this.#isActiveModal(modalKey)) {
|
|
155
160
|
return;
|
|
156
161
|
}
|
|
157
162
|
|
|
158
|
-
const isRegistered = this.#unregisterModal(modalKey, isOverlayLess);
|
|
159
|
-
if (!isRegistered) return;
|
|
160
|
-
|
|
161
163
|
if (this.#debug) {
|
|
162
164
|
console.log(`[ModalHandler][DEBUG]: Close modal with key => "${modalKey}"`);
|
|
163
165
|
}
|
|
@@ -166,12 +168,17 @@ export default class ModalHandler {
|
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
generateKey(prefix = 'modal') {
|
|
172
|
+
this.#modalIdCounter = (this.#modalIdCounter || 0) + 1;
|
|
173
|
+
return `${prefix}-${this.#modalIdCounter}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
169
176
|
clearDocumentBodyEvents() {
|
|
170
177
|
const documentBodyEvents = this.#eventsHandler.documentBody;
|
|
171
178
|
|
|
172
179
|
if (documentBodyEvents) {
|
|
173
180
|
if (this.#debug) {
|
|
174
|
-
console.log('[ModalHandler][DEBUG]: Stored document body events before
|
|
181
|
+
console.log('[ModalHandler][DEBUG]: Stored document body events before clear => ', documentBodyEvents);
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
for (const key in documentBodyEvents) {
|
|
@@ -190,7 +197,7 @@ export default class ModalHandler {
|
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
if (this.#debug) {
|
|
193
|
-
console.log('[ModalHandler][DEBUG]: Stored document body events after
|
|
200
|
+
console.log('[ModalHandler][DEBUG]: Stored document body events after clear => ', documentBodyEvents);
|
|
194
201
|
}
|
|
195
202
|
}
|
|
196
203
|
else {
|
|
@@ -198,23 +205,27 @@ export default class ModalHandler {
|
|
|
198
205
|
console.log('[ModalHandler][DEBUG]: No document body events were found to be cleared.');
|
|
199
206
|
}
|
|
200
207
|
}
|
|
208
|
+
|
|
209
|
+
return this;
|
|
201
210
|
}
|
|
202
211
|
|
|
203
212
|
clearActiveModals() {
|
|
204
213
|
if (this.#debug) {
|
|
205
|
-
console.log('[ModalHandler][DEBUG]: Active modal stack before
|
|
214
|
+
console.log('[ModalHandler][DEBUG]: Active modal stack before clear => ', this.#activeModals);
|
|
206
215
|
}
|
|
207
216
|
|
|
208
217
|
this.#activeModals.length = 0;
|
|
209
218
|
|
|
210
219
|
if (this.#debug) {
|
|
211
|
-
console.log('[ModalHandler][DEBUG]: Active modal stack after
|
|
220
|
+
console.log('[ModalHandler][DEBUG]: Active modal stack after clear => ', this.#activeModals);
|
|
212
221
|
}
|
|
222
|
+
|
|
223
|
+
return this;
|
|
213
224
|
}
|
|
214
225
|
|
|
215
226
|
clearFocusRegistry() {
|
|
216
227
|
if (this.#debug) {
|
|
217
|
-
console.log('[ModalHandler][DEBUG]: Focus registry before
|
|
228
|
+
console.log('[ModalHandler][DEBUG]: Focus registry before clear => ', this.#focusHandler);
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
for (const key in this.#focusHandler) {
|
|
@@ -222,24 +233,47 @@ export default class ModalHandler {
|
|
|
222
233
|
}
|
|
223
234
|
|
|
224
235
|
if (this.#debug) {
|
|
225
|
-
console.log('[ModalHandler][DEBUG]: Focus registry after
|
|
236
|
+
console.log('[ModalHandler][DEBUG]: Focus registry after clear => ', this.#focusHandler);
|
|
226
237
|
}
|
|
238
|
+
|
|
239
|
+
return this;
|
|
227
240
|
}
|
|
228
241
|
|
|
229
|
-
|
|
230
|
-
this
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
242
|
+
resetKeys() {
|
|
243
|
+
if (this.#debug) {
|
|
244
|
+
console.log('[ModalHandler][DEBUG]: Modal ID counter before reset => ', this.#modalIdCounter);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.#modalIdCounter = 0;
|
|
248
|
+
|
|
249
|
+
if (this.#debug) {
|
|
250
|
+
console.log('[ModalHandler][DEBUG]: Modal ID counter after reset => ', this.#modalIdCounter);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return this;
|
|
234
254
|
}
|
|
235
255
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
clearPopups() {
|
|
257
|
+
if (this.#debug) {
|
|
258
|
+
console.log('[ModalHandler][DEBUG]: Overlayless modals registry before clear => ', this.#popups);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.#popups.length = 0;
|
|
262
|
+
|
|
263
|
+
if (this.#debug) {
|
|
264
|
+
console.log('[ModalHandler][DEBUG]: Overlayless modals registry after clear => ', this.#popups);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this;
|
|
239
268
|
}
|
|
240
269
|
|
|
241
|
-
|
|
242
|
-
this
|
|
270
|
+
reset() {
|
|
271
|
+
this
|
|
272
|
+
.clearDocumentBodyEvents()
|
|
273
|
+
.clearActiveModals()
|
|
274
|
+
.clearFocusRegistry()
|
|
275
|
+
.resetKeys()
|
|
276
|
+
.clearPopups();
|
|
243
277
|
}
|
|
244
278
|
|
|
245
279
|
rebindTrapFocus(modalKey) {
|
|
@@ -282,6 +316,10 @@ export default class ModalHandler {
|
|
|
282
316
|
// so lingering events can be removed if the modal stays open across re-renders
|
|
283
317
|
documentEvents.push({ eventName: 'keydown', callback: escapeKeyHandler });
|
|
284
318
|
|
|
319
|
+
if (!modalLmOuterLimits) {
|
|
320
|
+
this.#popups.push(modalKey); // Register overlayless modal
|
|
321
|
+
}
|
|
322
|
+
|
|
285
323
|
if (modalLmOuterLimits) {
|
|
286
324
|
const outsideClickHandler = this.#handleOutsideClickClose(modalKey, handleActiveModalClose, modalLmOuterLimits, exemptLms);
|
|
287
325
|
|
|
@@ -307,15 +345,13 @@ export default class ModalHandler {
|
|
|
307
345
|
}
|
|
308
346
|
|
|
309
347
|
removeA11yEvents({
|
|
310
|
-
modalKey
|
|
311
|
-
isToggle,
|
|
348
|
+
modalKey
|
|
312
349
|
}) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
350
|
+
const index = this.#popups.indexOf(modalKey); // Check if overlayless modal exists
|
|
351
|
+
const isOverlayLess = index !== -1 && this.#popups.splice(index, 1).length // Remove key from popups array
|
|
352
|
+
|
|
353
|
+
const isRegistered = this.#unregisterModal(modalKey, isOverlayLess);
|
|
354
|
+
if (!isRegistered) return;
|
|
319
355
|
|
|
320
356
|
const eventsHandler = this.#eventsHandler[modalKey];
|
|
321
357
|
|