accessible-kit 1.0.4 → 1.0.5
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 +28 -2
- package/package.json +1 -1
- package/src/js/a11y-modal.js +24 -5
- package/src/js/a11y-offcanvas.js +19 -4
- package/src/js/index.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,15 +5,40 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.5] - 2025-12-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Focus Trap - Manual Tab Navigation**: Complete rewrite of Tab key handling in Offcanvas and Modal components
|
|
12
|
+
- **BREAKING CHANGE in behavior**: Focus trap now completely overrides native browser Tab behavior
|
|
13
|
+
- Always prevents default Tab action and manually controls focus movement
|
|
14
|
+
- Eliminates `aria-hidden` violations caused by browser trying to focus hidden elements
|
|
15
|
+
- `updateFocusableElements()` is called on every Tab keypress to catch dynamic DOM changes
|
|
16
|
+
- Focus only moves through elements in the filtered `focusableElements` list
|
|
17
|
+
- Properly handles nested collapse components - focus skips closed submenus and includes opened ones
|
|
18
|
+
|
|
19
|
+
### Details
|
|
20
|
+
**Problem solved:**
|
|
21
|
+
When using Tab key in offcanvas/modal with dynamic content (e.g., collapse menus), the browser's native Tab behavior would attempt to focus elements with `aria-hidden="true"`, causing console warnings and accessibility violations.
|
|
22
|
+
|
|
23
|
+
**Solution:**
|
|
24
|
+
The focus trap now takes complete control of Tab navigation:
|
|
25
|
+
1. Every Tab keypress prevents default browser behavior
|
|
26
|
+
2. Updates the list of focusable elements to reflect current DOM state
|
|
27
|
+
3. Manually calculates and focuses the next/previous element from the filtered list
|
|
28
|
+
4. Elements inside `aria-hidden="true"` containers are never focused
|
|
29
|
+
|
|
30
|
+
This ensures perfect compatibility with dynamic components like animated collapse panels in navigation menus.
|
|
31
|
+
|
|
8
32
|
## [1.0.4] - 2025-12-21
|
|
9
33
|
|
|
10
34
|
### Fixed
|
|
11
|
-
- **Focus Trap**:
|
|
35
|
+
- **Focus Trap - Initial Implementation**: Improved focus trap in Offcanvas and Modal components
|
|
12
36
|
- Focus trap now correctly excludes elements with `aria-hidden="true"` and their children
|
|
13
37
|
- Fixed timing issue where `updateFocusableElements()` was called before CSS visibility changes applied
|
|
14
|
-
- Focus trap now properly skips hidden elements in collapsed/nested components
|
|
38
|
+
- Focus trap now properly skips hidden elements in collapsed/nested components
|
|
15
39
|
- Added comprehensive filtering for hidden, invisible, and aria-hidden elements
|
|
16
40
|
- Removed `visibility: hidden` check from filter to prevent false positives during panel opening
|
|
41
|
+
- Works correctly with both animated (CSS Grid) and non-animated collapse panels
|
|
17
42
|
|
|
18
43
|
### Added
|
|
19
44
|
- Added `:focus-visible` styles to Offcanvas theme for better keyboard navigation visibility
|
|
@@ -109,6 +134,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
109
134
|
- Zero dependencies
|
|
110
135
|
- Full TypeScript-ready exports
|
|
111
136
|
|
|
137
|
+
[1.0.5]: https://github.com/5ulo/accessible-kit/compare/v1.0.4...v1.0.5
|
|
112
138
|
[1.0.4]: https://github.com/5ulo/accessible-kit/compare/v1.0.3...v1.0.4
|
|
113
139
|
[1.0.3]: https://github.com/5ulo/accessible-kit/compare/v1.0.2...v1.0.3
|
|
114
140
|
[1.0.2]: https://github.com/5ulo/accessible-kit/compare/v1.0.1...v1.0.2
|
package/package.json
CHANGED
package/src/js/a11y-modal.js
CHANGED
|
@@ -194,6 +194,10 @@ class AccessibleModal {
|
|
|
194
194
|
el.getClientRects().length > 0
|
|
195
195
|
);
|
|
196
196
|
|
|
197
|
+
if (!isVisible) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
197
201
|
// Check if element or any parent has aria-hidden="true"
|
|
198
202
|
let currentElement = el;
|
|
199
203
|
while (currentElement && currentElement !== this.dialog) {
|
|
@@ -214,7 +218,7 @@ class AccessibleModal {
|
|
|
214
218
|
return false;
|
|
215
219
|
}
|
|
216
220
|
|
|
217
|
-
return
|
|
221
|
+
return true;
|
|
218
222
|
});
|
|
219
223
|
|
|
220
224
|
this.firstFocusable = this.focusableElements[0] || null;
|
|
@@ -225,24 +229,39 @@ class AccessibleModal {
|
|
|
225
229
|
handleFocusTrap(e) {
|
|
226
230
|
if (!this.isOpen || e.key !== "Tab") return;
|
|
227
231
|
|
|
232
|
+
// Update focusable elements before each Tab to catch dynamic changes (e.g., collapse panels)
|
|
233
|
+
this.updateFocusableElements();
|
|
234
|
+
|
|
228
235
|
// If no focusable elements, prevent default
|
|
229
236
|
if (this.focusableElements.length === 0) {
|
|
230
237
|
e.preventDefault();
|
|
231
238
|
return;
|
|
232
239
|
}
|
|
233
240
|
|
|
241
|
+
// Always prevent default Tab behavior - we'll handle focus manually
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
|
|
244
|
+
// Find current element index in focusable list
|
|
245
|
+
const currentIndex = this.focusableElements.indexOf(document.activeElement);
|
|
246
|
+
|
|
234
247
|
// Shift + Tab (backward)
|
|
235
248
|
if (e.shiftKey) {
|
|
236
|
-
if (
|
|
237
|
-
|
|
249
|
+
if (currentIndex <= 0) {
|
|
250
|
+
// At first element or not in list - go to last
|
|
238
251
|
this.lastFocusable.focus();
|
|
252
|
+
} else {
|
|
253
|
+
// Go to previous element
|
|
254
|
+
this.focusableElements[currentIndex - 1].focus();
|
|
239
255
|
}
|
|
240
256
|
}
|
|
241
257
|
// Tab (forward)
|
|
242
258
|
else {
|
|
243
|
-
if (
|
|
244
|
-
|
|
259
|
+
if (currentIndex === -1 || currentIndex >= this.focusableElements.length - 1) {
|
|
260
|
+
// Not in list or at last element - go to first
|
|
245
261
|
this.firstFocusable.focus();
|
|
262
|
+
} else {
|
|
263
|
+
// Go to next element
|
|
264
|
+
this.focusableElements[currentIndex + 1].focus();
|
|
246
265
|
}
|
|
247
266
|
}
|
|
248
267
|
}
|
package/src/js/a11y-offcanvas.js
CHANGED
|
@@ -237,24 +237,39 @@ class AccessibleOffcanvas {
|
|
|
237
237
|
handleFocusTrap(e) {
|
|
238
238
|
if (!this.isOpen || e.key !== "Tab") return;
|
|
239
239
|
|
|
240
|
+
// Update focusable elements before each Tab to catch dynamic changes (e.g., collapse panels)
|
|
241
|
+
this.updateFocusableElements();
|
|
242
|
+
|
|
240
243
|
// If no focusable elements, prevent default
|
|
241
244
|
if (this.focusableElements.length === 0) {
|
|
242
245
|
e.preventDefault();
|
|
243
246
|
return;
|
|
244
247
|
}
|
|
245
248
|
|
|
249
|
+
// Always prevent default Tab behavior - we'll handle focus manually
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
|
|
252
|
+
// Find current element index in focusable list
|
|
253
|
+
const currentIndex = this.focusableElements.indexOf(document.activeElement);
|
|
254
|
+
|
|
246
255
|
// Shift + Tab (backward)
|
|
247
256
|
if (e.shiftKey) {
|
|
248
|
-
if (
|
|
249
|
-
|
|
257
|
+
if (currentIndex <= 0) {
|
|
258
|
+
// At first element or not in list - go to last
|
|
250
259
|
this.lastFocusable.focus();
|
|
260
|
+
} else {
|
|
261
|
+
// Go to previous element
|
|
262
|
+
this.focusableElements[currentIndex - 1].focus();
|
|
251
263
|
}
|
|
252
264
|
}
|
|
253
265
|
// Tab (forward)
|
|
254
266
|
else {
|
|
255
|
-
if (
|
|
256
|
-
|
|
267
|
+
if (currentIndex === -1 || currentIndex >= this.focusableElements.length - 1) {
|
|
268
|
+
// Not in list or at last element - go to first
|
|
257
269
|
this.firstFocusable.focus();
|
|
270
|
+
} else {
|
|
271
|
+
// Go to next element
|
|
272
|
+
this.focusableElements[currentIndex + 1].focus();
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
275
|
}
|