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 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
- - Improved `README.md` format for usage section
41
+ - Improve `README.md` format for usage section
35
42
 
36
43
  ## [1.0.2] - 2026-01-23
37
44
  ### Fixed
38
- - Updated `package.json` keywords
45
+ - Update `package.json` keywords
39
46
 
40
47
  ## [1.0.1] - 2026-01-23
41
48
  ### Fixed
42
- - Updated installation instructions for `README.md`
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
- # ModalHandler
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
- `ModalHandler` is 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).
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 and modal ID key counter
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.clearDocumentBodyEvents();
100
- // modalHandler.clearActiveModals();
101
- // modalHandler.clearFocusRegistry();
102
- // modalHandler.resetKeys();
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 ARIA events and modal stacking handling:
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 accessibility and interaction event listeners for the specific registered modal.
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",
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": {
@@ -3,17 +3,19 @@ export default class ModalHandler {
3
3
 
4
4
  setDebug(bool: boolean): void;
5
5
 
6
- clearDocumentBodyEvents(): void;
6
+ generateKey(prefix?: string): string;
7
7
 
8
- clearActiveModals(): void;
8
+ clearDocumentBodyEvents(): this;
9
9
 
10
- clearFocusRegistry(): void;
10
+ clearActiveModals(): this;
11
11
 
12
- reset(): void;
12
+ clearFocusRegistry(): this;
13
13
 
14
- generateKey(prefix?: string): string;
14
+ resetKeys(): this;
15
15
 
16
- resetKeys(): void;
16
+ clearPopups(): this;
17
+
18
+ reset(): void;
17
19
 
18
20
  rebindTrapFocus(modalKey: string): void;
19
21
 
@@ -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 clearing => ', documentBodyEvents);
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 clearing => ', documentBodyEvents);
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 clearing => ', this.#activeModals);
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 clearing => ', this.#activeModals);
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 clearing => ', this.#focusHandler);
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 clearing => ', this.#focusHandler);
236
+ console.log('[ModalHandler][DEBUG]: Focus registry after clear => ', this.#focusHandler);
226
237
  }
238
+
239
+ return this;
227
240
  }
228
241
 
229
- reset() {
230
- this.clearDocumentBodyEvents();
231
- this.clearActiveModals();
232
- this.clearFocusRegistry();
233
- this.resetKeys();
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
- generateKey(prefix = 'modal') {
237
- this.#modalIdCounter = (this.#modalIdCounter || 0) + 1;
238
- return `${prefix}-${this.#modalIdCounter}`;
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
- resetKeys() {
242
- this.#modalIdCounter = 0;
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
- 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
- }
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