accessible-kit 1.0.4 → 1.0.6
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 +47 -2
- package/README.md +68 -2
- package/package.json +1 -1
- package/src/js/a11y-dropdown.js +63 -23
- 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,58 @@ 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.6] - 2026-01-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Dropdown - Multiple ARIA Pattern Support**: Added `data-dropdown-role` attribute to support dialog and listbox patterns in addition to the default menu pattern
|
|
12
|
+
- **New attribute**: `data-dropdown-role` accepts `"menu"` (default), `"dialog"`, or `"listbox"`
|
|
13
|
+
- Automatically sets appropriate `aria-haspopup` value based on role type:
|
|
14
|
+
- `role="menu"` → `aria-haspopup="true"`
|
|
15
|
+
- `role="dialog"` → `aria-haspopup="dialog"`
|
|
16
|
+
- `role="listbox"` → `aria-haspopup="listbox"`
|
|
17
|
+
- Conditionally applies correct item roles (`menuitem`, none for dialog, `option` for listbox)
|
|
18
|
+
- Pattern-specific keyboard navigation:
|
|
19
|
+
- Menu/listbox: Arrow keys, Home/End, Tab closes dropdown
|
|
20
|
+
- Dialog: Arrow keys, Tab navigates within (doesn't close)
|
|
21
|
+
- **Use case example**: Radio playlists, media controls, or complex interactive widgets that aren't semantic "menus"
|
|
22
|
+
- Added comprehensive documentation and live example (radio playlist) to demo page
|
|
23
|
+
- Updated README with ARIA pattern guidance and when to use each pattern
|
|
24
|
+
- **Backwards compatible**: Defaults to `"menu"` pattern, existing implementations unchanged
|
|
25
|
+
|
|
26
|
+
## [1.0.5] - 2025-12-21
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Focus Trap - Manual Tab Navigation**: Complete rewrite of Tab key handling in Offcanvas and Modal components
|
|
30
|
+
- **BREAKING CHANGE in behavior**: Focus trap now completely overrides native browser Tab behavior
|
|
31
|
+
- Always prevents default Tab action and manually controls focus movement
|
|
32
|
+
- Eliminates `aria-hidden` violations caused by browser trying to focus hidden elements
|
|
33
|
+
- `updateFocusableElements()` is called on every Tab keypress to catch dynamic DOM changes
|
|
34
|
+
- Focus only moves through elements in the filtered `focusableElements` list
|
|
35
|
+
- Properly handles nested collapse components - focus skips closed submenus and includes opened ones
|
|
36
|
+
|
|
37
|
+
### Details
|
|
38
|
+
**Problem solved:**
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
**Solution:**
|
|
42
|
+
The focus trap now takes complete control of Tab navigation:
|
|
43
|
+
1. Every Tab keypress prevents default browser behavior
|
|
44
|
+
2. Updates the list of focusable elements to reflect current DOM state
|
|
45
|
+
3. Manually calculates and focuses the next/previous element from the filtered list
|
|
46
|
+
4. Elements inside `aria-hidden="true"` containers are never focused
|
|
47
|
+
|
|
48
|
+
This ensures perfect compatibility with dynamic components like animated collapse panels in navigation menus.
|
|
49
|
+
|
|
8
50
|
## [1.0.4] - 2025-12-21
|
|
9
51
|
|
|
10
52
|
### Fixed
|
|
11
|
-
- **Focus Trap**:
|
|
53
|
+
- **Focus Trap - Initial Implementation**: Improved focus trap in Offcanvas and Modal components
|
|
12
54
|
- Focus trap now correctly excludes elements with `aria-hidden="true"` and their children
|
|
13
55
|
- Fixed timing issue where `updateFocusableElements()` was called before CSS visibility changes applied
|
|
14
|
-
- Focus trap now properly skips hidden elements in collapsed/nested components
|
|
56
|
+
- Focus trap now properly skips hidden elements in collapsed/nested components
|
|
15
57
|
- Added comprehensive filtering for hidden, invisible, and aria-hidden elements
|
|
16
58
|
- Removed `visibility: hidden` check from filter to prevent false positives during panel opening
|
|
59
|
+
- Works correctly with both animated (CSS Grid) and non-animated collapse panels
|
|
17
60
|
|
|
18
61
|
### Added
|
|
19
62
|
- Added `:focus-visible` styles to Offcanvas theme for better keyboard navigation visibility
|
|
@@ -109,6 +152,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
109
152
|
- Zero dependencies
|
|
110
153
|
- Full TypeScript-ready exports
|
|
111
154
|
|
|
155
|
+
[1.0.6]: https://github.com/5ulo/accessible-kit/compare/v1.0.5...v1.0.6
|
|
156
|
+
[1.0.5]: https://github.com/5ulo/accessible-kit/compare/v1.0.4...v1.0.5
|
|
112
157
|
[1.0.4]: https://github.com/5ulo/accessible-kit/compare/v1.0.3...v1.0.4
|
|
113
158
|
[1.0.3]: https://github.com/5ulo/accessible-kit/compare/v1.0.2...v1.0.3
|
|
114
159
|
[1.0.2]: https://github.com/5ulo/accessible-kit/compare/v1.0.1...v1.0.2
|
package/README.md
CHANGED
|
@@ -168,11 +168,12 @@ const dropdown = new AccessibleDropdown(element, {
|
|
|
168
168
|
|
|
169
169
|
## Dropdown
|
|
170
170
|
|
|
171
|
-
Fully accessible dropdown component with keyboard navigation and ARIA support.
|
|
171
|
+
Fully accessible dropdown component with keyboard navigation and ARIA support. Supports multiple ARIA patterns (menu, dialog, listbox) for different use cases.
|
|
172
172
|
|
|
173
173
|
### Features
|
|
174
174
|
|
|
175
|
-
- ✅ Full ARIA attributes support
|
|
175
|
+
- ✅ Full ARIA attributes support (menu, dialog, and listbox patterns)
|
|
176
|
+
- ✅ Flexible role patterns via `data-dropdown-role` attribute
|
|
176
177
|
- ✅ Keyboard navigation (arrows, Enter, Space, Esc, Home, End)
|
|
177
178
|
- ✅ Focus management
|
|
178
179
|
- ✅ Auto-close on outside click
|
|
@@ -213,6 +214,57 @@ Fully accessible dropdown component with keyboard navigation and ARIA support.
|
|
|
213
214
|
</div>
|
|
214
215
|
```
|
|
215
216
|
|
|
217
|
+
### ARIA Patterns
|
|
218
|
+
|
|
219
|
+
The dropdown component supports multiple ARIA patterns to match different use cases. By default, it uses the **menu pattern**, but you can change this using the `data-dropdown-role` attribute.
|
|
220
|
+
|
|
221
|
+
#### When to Use Each Pattern
|
|
222
|
+
|
|
223
|
+
**Menu Pattern (default)** - Use `role="menu"` for:
|
|
224
|
+
- Navigation menus
|
|
225
|
+
- Action menus (Edit, Delete, etc.)
|
|
226
|
+
- Language/region selectors
|
|
227
|
+
- User profile menus
|
|
228
|
+
- Context menus
|
|
229
|
+
|
|
230
|
+
**Dialog Pattern** - Use `data-dropdown-role="dialog"` for:
|
|
231
|
+
- Media player playlists/controls
|
|
232
|
+
- Complex interactive widgets
|
|
233
|
+
- Custom form controls
|
|
234
|
+
- Rich content with multiple focusable elements
|
|
235
|
+
- Non-menu disclosure patterns
|
|
236
|
+
|
|
237
|
+
**Example: Dialog Pattern (Radio Playlist)**
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
<!-- Use data-dropdown-role="dialog" for playlists, controls, or interactive widgets -->
|
|
241
|
+
<div data-dropdown data-dropdown-role="dialog">
|
|
242
|
+
<button data-dropdown-button>
|
|
243
|
+
🎵 Playlist
|
|
244
|
+
<span data-dropdown-arrow></span>
|
|
245
|
+
</button>
|
|
246
|
+
<div data-dropdown-menu>
|
|
247
|
+
<div>
|
|
248
|
+
<div>
|
|
249
|
+
<button data-dropdown-item>Station 1</button>
|
|
250
|
+
<button data-dropdown-item>Station 2</button>
|
|
251
|
+
<button data-dropdown-item>Station 3</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!-- This automatically sets:
|
|
258
|
+
- Button: aria-haspopup="dialog" (instead of "true")
|
|
259
|
+
- Menu: role="dialog" (instead of "menu")
|
|
260
|
+
- Items: No role attribute (instead of "menuitem")
|
|
261
|
+
- Keyboard: Tab allowed within dropdown (menu pattern closes on Tab)
|
|
262
|
+
-->
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Why use dialog pattern?**
|
|
266
|
+
When your dropdown contains complex interactive content (like a media player playlist), it's not semantically a "menu" of commands. The dialog pattern better represents this content structure and provides more appropriate keyboard behavior. Tab/Shift+Tab cycles through items within the dropdown (instead of closing it like menu pattern does).
|
|
267
|
+
|
|
216
268
|
### JavaScript Initialization
|
|
217
269
|
|
|
218
270
|
```javascript
|
|
@@ -225,6 +277,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
225
277
|
|
|
226
278
|
// Or manual initialization with options
|
|
227
279
|
const dropdown = new Dropdown(element, {
|
|
280
|
+
dropdownRole: 'menu', // 'menu' (default), 'dialog', or 'listbox'
|
|
228
281
|
closeOnSelect: true,
|
|
229
282
|
closeOnOutsideClick: true,
|
|
230
283
|
closeOnEscape: true,
|
|
@@ -269,6 +322,9 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
269
322
|
**Data attributes:**
|
|
270
323
|
|
|
271
324
|
```html
|
|
325
|
+
<!-- Use dialog pattern for non-menu content -->
|
|
326
|
+
<div data-dropdown data-dropdown-role="dialog">
|
|
327
|
+
|
|
272
328
|
<!-- Keep dropdown open after selection -->
|
|
273
329
|
<div data-dropdown data-close-on-select="false">
|
|
274
330
|
|
|
@@ -283,6 +339,7 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
283
339
|
|
|
284
340
|
```javascript
|
|
285
341
|
{
|
|
342
|
+
dropdownRole: 'menu', // ARIA pattern: 'menu', 'dialog', or 'listbox'
|
|
286
343
|
closeOnSelect: true, // Close after selecting item
|
|
287
344
|
closeOnOutsideClick: true, // Close on outside click
|
|
288
345
|
closeOnEscape: true, // Close on Escape key
|
|
@@ -295,12 +352,21 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
295
352
|
|
|
296
353
|
### Keyboard Navigation
|
|
297
354
|
|
|
355
|
+
**Menu Pattern (default):**
|
|
356
|
+
|
|
298
357
|
- **Enter** / **Space** - Open/close menu or select item
|
|
299
358
|
- **↓** / **↑** - Navigate between items
|
|
300
359
|
- **Home** / **End** - Jump to first/last item
|
|
301
360
|
- **Esc** - Close menu and return focus to button
|
|
302
361
|
- **Tab** - Close menu and move focus
|
|
303
362
|
|
|
363
|
+
**Dialog Pattern:**
|
|
364
|
+
|
|
365
|
+
- **Enter** / **Space** - Open/close or select item
|
|
366
|
+
- **↓** / **↑** - Navigate between items
|
|
367
|
+
- **Esc** - Close and return focus to button
|
|
368
|
+
- **Tab** / **Shift+Tab** - Cycle through items within dialog (does not close)
|
|
369
|
+
|
|
304
370
|
### Variants
|
|
305
371
|
|
|
306
372
|
**Navigation menu:**
|
package/package.json
CHANGED
package/src/js/a11y-dropdown.js
CHANGED
|
@@ -18,6 +18,7 @@ class AccessibleDropdown {
|
|
|
18
18
|
|
|
19
19
|
// Options
|
|
20
20
|
this.options = {
|
|
21
|
+
dropdownRole: options.dropdownRole || "menu",
|
|
21
22
|
closeOnSelect: options.closeOnSelect !== false,
|
|
22
23
|
closeOnOutsideClick: options.closeOnOutsideClick !== false,
|
|
23
24
|
closeOnEscape: options.closeOnEscape !== false,
|
|
@@ -91,18 +92,24 @@ class AccessibleDropdown {
|
|
|
91
92
|
.substr(2, 9)}`;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
// Button ARIA attributes
|
|
95
|
-
this.
|
|
95
|
+
// Button ARIA attributes - set aria-haspopup based on dropdown role
|
|
96
|
+
const ariaHaspopupValue = this.options.dropdownRole === "menu" ? "true" : this.options.dropdownRole;
|
|
97
|
+
this.button.setAttribute("aria-haspopup", ariaHaspopupValue);
|
|
96
98
|
this.button.setAttribute("aria-expanded", "false");
|
|
97
99
|
this.button.setAttribute("aria-controls", this.menu.id);
|
|
98
100
|
|
|
99
|
-
// Menu ARIA attributes
|
|
100
|
-
this.menu.setAttribute("role",
|
|
101
|
+
// Menu ARIA attributes - set role based on dropdown role
|
|
102
|
+
this.menu.setAttribute("role", this.options.dropdownRole);
|
|
101
103
|
this.menu.setAttribute("aria-labelledby", this.button.id);
|
|
102
104
|
|
|
103
|
-
// Menu items ARIA attributes
|
|
105
|
+
// Menu items ARIA attributes - set item role based on dropdown role
|
|
106
|
+
const itemRole = this.options.dropdownRole === "menu" ? "menuitem" :
|
|
107
|
+
this.options.dropdownRole === "listbox" ? "option" : null;
|
|
108
|
+
|
|
104
109
|
this.items.forEach((item) => {
|
|
105
|
-
|
|
110
|
+
if (itemRole) {
|
|
111
|
+
item.setAttribute("role", itemRole);
|
|
112
|
+
}
|
|
106
113
|
if (!item.hasAttribute("tabindex")) {
|
|
107
114
|
item.setAttribute("tabindex", "-1");
|
|
108
115
|
}
|
|
@@ -184,32 +191,64 @@ class AccessibleDropdown {
|
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
handleItemKeydown(e, index) {
|
|
194
|
+
// Common navigation for all patterns
|
|
187
195
|
switch (e.key) {
|
|
188
196
|
case "Enter":
|
|
189
197
|
case " ":
|
|
190
198
|
e.preventDefault();
|
|
191
199
|
this.selectItem(index);
|
|
192
200
|
break;
|
|
193
|
-
case "ArrowDown":
|
|
194
|
-
e.preventDefault();
|
|
195
|
-
this.focusNextItem();
|
|
196
|
-
break;
|
|
197
|
-
case "ArrowUp":
|
|
198
|
-
e.preventDefault();
|
|
199
|
-
this.focusPreviousItem();
|
|
200
|
-
break;
|
|
201
|
-
case "Home":
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
this.setFocusedItem(0);
|
|
204
|
-
break;
|
|
205
|
-
case "End":
|
|
206
|
-
e.preventDefault();
|
|
207
|
-
this.setFocusedItem(this.items.length - 1);
|
|
208
|
-
break;
|
|
209
201
|
case "Tab":
|
|
210
|
-
|
|
202
|
+
// For dialog pattern, Tab should cycle within items
|
|
203
|
+
// For menu and listbox, close on Tab
|
|
204
|
+
if (this.options.dropdownRole === "dialog") {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
// Tab forward or backward through items
|
|
207
|
+
if (e.shiftKey) {
|
|
208
|
+
this.focusPreviousItem();
|
|
209
|
+
} else {
|
|
210
|
+
this.focusNextItem();
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
// Menu and listbox patterns close on Tab
|
|
214
|
+
this.close();
|
|
215
|
+
}
|
|
211
216
|
break;
|
|
212
217
|
}
|
|
218
|
+
|
|
219
|
+
// Pattern-specific navigation
|
|
220
|
+
if (this.options.dropdownRole === "menu" || this.options.dropdownRole === "listbox") {
|
|
221
|
+
switch (e.key) {
|
|
222
|
+
case "ArrowDown":
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
this.focusNextItem();
|
|
225
|
+
break;
|
|
226
|
+
case "ArrowUp":
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
this.focusPreviousItem();
|
|
229
|
+
break;
|
|
230
|
+
case "Home":
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
this.setFocusedItem(0);
|
|
233
|
+
break;
|
|
234
|
+
case "End":
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
this.setFocusedItem(this.items.length - 1);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
} else if (this.options.dropdownRole === "dialog") {
|
|
240
|
+
// Dialog pattern: Arrow keys for navigation (less strict)
|
|
241
|
+
switch (e.key) {
|
|
242
|
+
case "ArrowDown":
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
this.focusNextItem();
|
|
245
|
+
break;
|
|
246
|
+
case "ArrowUp":
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
this.focusPreviousItem();
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
213
252
|
}
|
|
214
253
|
|
|
215
254
|
handleItemClick(e, index) {
|
|
@@ -320,6 +359,7 @@ function initDropdowns() {
|
|
|
320
359
|
|
|
321
360
|
dropdowns.forEach((dropdown) => {
|
|
322
361
|
const options = {
|
|
362
|
+
dropdownRole: dropdown.dataset.dropdownRole || "menu",
|
|
323
363
|
closeOnSelect: dropdown.dataset.closeOnSelect !== "false",
|
|
324
364
|
closeOnOutsideClick:
|
|
325
365
|
dropdown.dataset.closeOnOutsideClick !== "false",
|
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
|
}
|