accessible-kit 1.0.3 → 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 CHANGED
@@ -5,6 +5,51 @@ 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
+
32
+ ## [1.0.4] - 2025-12-21
33
+
34
+ ### Fixed
35
+ - **Focus Trap - Initial Implementation**: Improved focus trap in Offcanvas and Modal components
36
+ - Focus trap now correctly excludes elements with `aria-hidden="true"` and their children
37
+ - Fixed timing issue where `updateFocusableElements()` was called before CSS visibility changes applied
38
+ - Focus trap now properly skips hidden elements in collapsed/nested components
39
+ - Added comprehensive filtering for hidden, invisible, and aria-hidden elements
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
42
+
43
+ ### Added
44
+ - Added `:focus-visible` styles to Offcanvas theme for better keyboard navigation visibility
45
+ - Added navigation demo with nested collapse submenus to Offcanvas demo page
46
+
47
+ ### Details
48
+ The focus trap improvements ensure that keyboard navigation works correctly in complex scenarios:
49
+ - When offcanvas/modal contains collapse components, Tab key properly skips hidden submenu items
50
+ - Focus is set after CSS transitions complete, preventing "no focusable elements" issue
51
+ - Users can now navigate nested menus with full accessibility support
52
+
8
53
  ## [1.0.3] - 2025-12-20
9
54
 
10
55
  ### Fixed
@@ -89,6 +134,8 @@ document.addEventListener('DOMContentLoaded', () => {
89
134
  - Zero dependencies
90
135
  - Full TypeScript-ready exports
91
136
 
137
+ [1.0.5]: https://github.com/5ulo/accessible-kit/compare/v1.0.4...v1.0.5
138
+ [1.0.4]: https://github.com/5ulo/accessible-kit/compare/v1.0.3...v1.0.4
92
139
  [1.0.3]: https://github.com/5ulo/accessible-kit/compare/v1.0.2...v1.0.3
93
140
  [1.0.2]: https://github.com/5ulo/accessible-kit/compare/v1.0.1...v1.0.2
94
141
  [1.0.1]: https://github.com/5ulo/accessible-kit/compare/v1.0.0...v1.0.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accessible-kit",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Lightweight, accessible UI component library with full ARIA support. Zero dependencies, vanilla JavaScript.",
5
5
  "main": "src/js/index.js",
6
6
  "type": "module",
@@ -2,6 +2,12 @@
2
2
  /* Obsahuje vizualne nastavenia: farby, velkosti, bordery, radiusy, spacing */
3
3
  /* Pre zmenu vzhladu upravte tento subor */
4
4
 
5
+ /* Focus visible for all focusable elements inside offcanvas */
6
+ [data-offcanvas-panel] :focus-visible {
7
+ outline: 2px solid #3b82f6;
8
+ outline-offset: 2px;
9
+ }
10
+
5
11
  /* Offcanvas panel */
6
12
  [data-offcanvas-panel] {
7
13
  width: 320px;
@@ -70,11 +76,6 @@
70
76
  color: #111827;
71
77
  }
72
78
 
73
- [data-offcanvas-close]:focus {
74
- outline: 2px solid #3b82f6;
75
- outline-offset: 2px;
76
- }
77
-
78
79
  /* Backdrop */
79
80
  [data-offcanvas-backdrop] {
80
81
  background: rgba(0, 0, 0, 0.5);
@@ -151,7 +152,7 @@
151
152
  border: 2px solid currentColor;
152
153
  }
153
154
 
154
- [data-offcanvas-close]:focus {
155
+ [data-offcanvas-panel] :focus-visible {
155
156
  outline-width: 3px;
156
157
  }
157
158
  }
@@ -126,17 +126,16 @@ class AccessibleModal {
126
126
  document.body.classList.add("modal-open");
127
127
  }
128
128
 
129
- // Update focusable elements
130
- this.updateFocusableElements();
131
-
132
- // Focus first element
129
+ // Update focusable elements and focus - needs to wait for CSS to apply
133
130
  setTimeout(() => {
131
+ this.updateFocusableElements();
132
+
134
133
  if (this.firstFocusable) {
135
134
  this.firstFocusable.focus();
136
135
  } else {
137
136
  this.modal.focus();
138
137
  }
139
- }, 100);
138
+ }, 50);
140
139
 
141
140
  // Callback
142
141
  if (this.options.onOpen) {
@@ -188,11 +187,38 @@ class AccessibleModal {
188
187
  this.focusableElements = Array.from(
189
188
  this.dialog.querySelectorAll(focusableSelectors.join(","))
190
189
  ).filter((el) => {
191
- return (
190
+ // Check if element is visible
191
+ const isVisible = (
192
192
  el.offsetWidth > 0 ||
193
193
  el.offsetHeight > 0 ||
194
194
  el.getClientRects().length > 0
195
195
  );
196
+
197
+ if (!isVisible) {
198
+ return false;
199
+ }
200
+
201
+ // Check if element or any parent has aria-hidden="true"
202
+ let currentElement = el;
203
+ while (currentElement && currentElement !== this.dialog) {
204
+ if (currentElement.getAttribute('aria-hidden') === 'true') {
205
+ return false;
206
+ }
207
+ currentElement = currentElement.parentElement;
208
+ }
209
+
210
+ // Check if element has hidden attribute
211
+ if (el.hasAttribute('hidden')) {
212
+ return false;
213
+ }
214
+
215
+ // Check computed style for display (not visibility, as modal is opening)
216
+ const style = window.getComputedStyle(el);
217
+ if (style.display === 'none') {
218
+ return false;
219
+ }
220
+
221
+ return true;
196
222
  });
197
223
 
198
224
  this.firstFocusable = this.focusableElements[0] || null;
@@ -203,24 +229,39 @@ class AccessibleModal {
203
229
  handleFocusTrap(e) {
204
230
  if (!this.isOpen || e.key !== "Tab") return;
205
231
 
232
+ // Update focusable elements before each Tab to catch dynamic changes (e.g., collapse panels)
233
+ this.updateFocusableElements();
234
+
206
235
  // If no focusable elements, prevent default
207
236
  if (this.focusableElements.length === 0) {
208
237
  e.preventDefault();
209
238
  return;
210
239
  }
211
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
+
212
247
  // Shift + Tab (backward)
213
248
  if (e.shiftKey) {
214
- if (document.activeElement === this.firstFocusable) {
215
- e.preventDefault();
249
+ if (currentIndex <= 0) {
250
+ // At first element or not in list - go to last
216
251
  this.lastFocusable.focus();
252
+ } else {
253
+ // Go to previous element
254
+ this.focusableElements[currentIndex - 1].focus();
217
255
  }
218
256
  }
219
257
  // Tab (forward)
220
258
  else {
221
- if (document.activeElement === this.lastFocusable) {
222
- e.preventDefault();
259
+ if (currentIndex === -1 || currentIndex >= this.focusableElements.length - 1) {
260
+ // Not in list or at last element - go to first
223
261
  this.firstFocusable.focus();
262
+ } else {
263
+ // Go to next element
264
+ this.focusableElements[currentIndex + 1].focus();
224
265
  }
225
266
  }
226
267
  }
@@ -131,17 +131,16 @@ class AccessibleOffcanvas {
131
131
  document.body.classList.add("offcanvas-open");
132
132
  }
133
133
 
134
- // Update focusable elements
135
- this.updateFocusableElements();
136
-
137
- // Focus first element
134
+ // Update focusable elements and focus - needs to wait for CSS to apply
138
135
  setTimeout(() => {
136
+ this.updateFocusableElements();
137
+
139
138
  if (this.firstFocusable) {
140
139
  this.firstFocusable.focus();
141
140
  } else {
142
141
  this.panel.focus();
143
142
  }
144
- }, 100);
143
+ }, 50);
145
144
 
146
145
  // Callback
147
146
  if (this.options.onOpen) {
@@ -196,11 +195,38 @@ class AccessibleOffcanvas {
196
195
  this.focusableElements = Array.from(
197
196
  this.panel.querySelectorAll(focusableSelectors.join(","))
198
197
  ).filter((el) => {
199
- return (
198
+ // Check if element is visible
199
+ const isVisible = (
200
200
  el.offsetWidth > 0 ||
201
201
  el.offsetHeight > 0 ||
202
202
  el.getClientRects().length > 0
203
203
  );
204
+
205
+ if (!isVisible) {
206
+ return false;
207
+ }
208
+
209
+ // Check if element or any parent has aria-hidden="true"
210
+ let currentElement = el;
211
+ while (currentElement && currentElement !== this.panel) {
212
+ if (currentElement.getAttribute('aria-hidden') === 'true') {
213
+ return false;
214
+ }
215
+ currentElement = currentElement.parentElement;
216
+ }
217
+
218
+ // Check if element has hidden attribute
219
+ if (el.hasAttribute('hidden')) {
220
+ return false;
221
+ }
222
+
223
+ // Check computed style for display (not visibility, as panel is opening)
224
+ const style = window.getComputedStyle(el);
225
+ if (style.display === 'none') {
226
+ return false;
227
+ }
228
+
229
+ return true;
204
230
  });
205
231
 
206
232
  this.firstFocusable = this.focusableElements[0] || null;
@@ -211,24 +237,39 @@ class AccessibleOffcanvas {
211
237
  handleFocusTrap(e) {
212
238
  if (!this.isOpen || e.key !== "Tab") return;
213
239
 
240
+ // Update focusable elements before each Tab to catch dynamic changes (e.g., collapse panels)
241
+ this.updateFocusableElements();
242
+
214
243
  // If no focusable elements, prevent default
215
244
  if (this.focusableElements.length === 0) {
216
245
  e.preventDefault();
217
246
  return;
218
247
  }
219
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
+
220
255
  // Shift + Tab (backward)
221
256
  if (e.shiftKey) {
222
- if (document.activeElement === this.firstFocusable) {
223
- e.preventDefault();
257
+ if (currentIndex <= 0) {
258
+ // At first element or not in list - go to last
224
259
  this.lastFocusable.focus();
260
+ } else {
261
+ // Go to previous element
262
+ this.focusableElements[currentIndex - 1].focus();
225
263
  }
226
264
  }
227
265
  // Tab (forward)
228
266
  else {
229
- if (document.activeElement === this.lastFocusable) {
230
- e.preventDefault();
267
+ if (currentIndex === -1 || currentIndex >= this.focusableElements.length - 1) {
268
+ // Not in list or at last element - go to first
231
269
  this.firstFocusable.focus();
270
+ } else {
271
+ // Go to next element
272
+ this.focusableElements[currentIndex + 1].focus();
232
273
  }
233
274
  }
234
275
  }
package/src/js/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * a11y-kit
3
3
  * Lightweight, accessible UI component library with full ARIA support
4
4
  *
5
- * @version 1.0.3
5
+ * @version 1.0.5
6
6
  * @license MIT
7
7
  */
8
8