@y14e/roving-tabindex 1.2.11 → 1.3.1
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/README.md +6 -1
- package/dist/index.cjs +41 -12
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +41 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Roving Tabindex
|
|
2
2
|
|
|
3
|
-
Lightweight roving tabindex utility with fully focus management. Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
3
|
+
Lightweight roving `tabindex` utility with fully focus management. Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
4
4
|
|
|
5
5
|
> [!NOTE]
|
|
6
6
|
> Focus traversal works across shadow DOM boundaries using composed-tree-aware focus detection powered by [Power Focusable](https://github.com/y14e/power-focusable).
|
|
@@ -42,12 +42,17 @@ const cleanup = createRovingTabIndex(container, options);
|
|
|
42
42
|
```ts
|
|
43
43
|
interface RovingTabIndexOptions {
|
|
44
44
|
direction?: 'horizontal' | 'vertical' | undefined; // default: both (undefined)
|
|
45
|
+
navigationOnly?: boolean; // default: false
|
|
45
46
|
selector?: string | undefined;
|
|
46
47
|
typeahead?: boolean; // default: false
|
|
47
48
|
wrap?: boolean; // default: false
|
|
48
49
|
}
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
### `navigationOnly`
|
|
53
|
+
|
|
54
|
+
If `true`, enables keyboard navigation without modifying `tabindex`. Useful for widgets like accordions where all items should remain tabbable while still supporting arrow key navigation.
|
|
55
|
+
|
|
51
56
|
### `typeahead`
|
|
52
57
|
|
|
53
58
|
If `true`, enables character-based focus navigation. Typing a character moves focus to the next matching element.
|
package/dist/index.cjs
CHANGED
|
@@ -57,7 +57,12 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
57
57
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
58
58
|
container = document.body;
|
|
59
59
|
}
|
|
60
|
-
let {
|
|
60
|
+
let {
|
|
61
|
+
composed = false,
|
|
62
|
+
filter,
|
|
63
|
+
include,
|
|
64
|
+
skipVisibilityCheck = false
|
|
65
|
+
} = options;
|
|
61
66
|
if (typeof composed !== "boolean") {
|
|
62
67
|
console.warn("Invalid composed option. Fallback: false.");
|
|
63
68
|
composed = false;
|
|
@@ -78,7 +83,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
78
83
|
if (composed || include) {
|
|
79
84
|
let traverse2 = function(node) {
|
|
80
85
|
if (node instanceof Element) {
|
|
81
|
-
if (isFocusable(node) || include?.(node)) {
|
|
86
|
+
if (isFocusable(node, { skipVisibilityCheck }) || include?.(node)) {
|
|
82
87
|
elements[elements.length] = node;
|
|
83
88
|
}
|
|
84
89
|
}
|
|
@@ -99,7 +104,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
99
104
|
if (!(candidate instanceof Element)) {
|
|
100
105
|
continue;
|
|
101
106
|
}
|
|
102
|
-
if (isFocusable(candidate)) {
|
|
107
|
+
if (isFocusable(candidate, { skipVisibilityCheck })) {
|
|
103
108
|
elements[elements.length] = candidate;
|
|
104
109
|
}
|
|
105
110
|
}
|
|
@@ -107,11 +112,16 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
107
112
|
const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
|
|
108
113
|
return filter ? unfiltered.filter(filter) : unfiltered;
|
|
109
114
|
}
|
|
110
|
-
function isFocusable(element) {
|
|
115
|
+
function isFocusable(element, options = {}) {
|
|
111
116
|
if (!(element instanceof Element)) {
|
|
112
117
|
console.warn("Invalid element");
|
|
113
118
|
return false;
|
|
114
119
|
}
|
|
120
|
+
let { skipVisibilityCheck = false } = options;
|
|
121
|
+
if (typeof skipVisibilityCheck !== "boolean") {
|
|
122
|
+
console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
|
|
123
|
+
skipVisibilityCheck = false;
|
|
124
|
+
}
|
|
115
125
|
if (element.hasAttribute("hidden") || isInert(element)) {
|
|
116
126
|
return false;
|
|
117
127
|
}
|
|
@@ -124,7 +134,7 @@ function isFocusable(element) {
|
|
|
124
134
|
if (isDisabledDeep(element)) {
|
|
125
135
|
return false;
|
|
126
136
|
}
|
|
127
|
-
if (!element.checkVisibility({
|
|
137
|
+
if (!skipVisibilityCheck && !element.checkVisibility({
|
|
128
138
|
contentVisibilityAuto: true,
|
|
129
139
|
opacityProperty: true,
|
|
130
140
|
visibilityProperty: true
|
|
@@ -266,11 +276,21 @@ function createRovingTabIndex(container, options = {}) {
|
|
|
266
276
|
return () => {
|
|
267
277
|
};
|
|
268
278
|
}
|
|
269
|
-
let {
|
|
279
|
+
let {
|
|
280
|
+
direction,
|
|
281
|
+
navigationOnly = false,
|
|
282
|
+
selector,
|
|
283
|
+
typeahead = false,
|
|
284
|
+
wrap = false
|
|
285
|
+
} = options;
|
|
270
286
|
if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
|
|
271
287
|
console.warn("Invalid direction option. Fallback: both (undefined).");
|
|
272
288
|
direction = void 0;
|
|
273
289
|
}
|
|
290
|
+
if (typeof navigationOnly !== "boolean") {
|
|
291
|
+
console.warn("Invalid navigationOnly option. Fallback: false.");
|
|
292
|
+
navigationOnly = false;
|
|
293
|
+
}
|
|
274
294
|
if (typeof selector !== "undefined" && typeof selector !== "string") {
|
|
275
295
|
console.warn(
|
|
276
296
|
"Invalid selector. Fallback: all focusable elements (undefined)."
|
|
@@ -287,6 +307,7 @@ function createRovingTabIndex(container, options = {}) {
|
|
|
287
307
|
}
|
|
288
308
|
const roving = new RovingTabIndex(container, {
|
|
289
309
|
direction,
|
|
310
|
+
navigationOnly,
|
|
290
311
|
selector,
|
|
291
312
|
typeahead,
|
|
292
313
|
wrap
|
|
@@ -400,7 +421,8 @@ var RovingTabIndex = class {
|
|
|
400
421
|
...this.#getFocusables(),
|
|
401
422
|
...getFocusables(this.#container, {
|
|
402
423
|
composed: true,
|
|
403
|
-
filter: this.#selectorFilter
|
|
424
|
+
filter: this.#selectorFilter,
|
|
425
|
+
skipVisibilityCheck: true
|
|
404
426
|
})
|
|
405
427
|
]);
|
|
406
428
|
for (const focusable of this.#focusables) {
|
|
@@ -414,13 +436,16 @@ var RovingTabIndex = class {
|
|
|
414
436
|
index >= 0 && focusables.splice(index, 1);
|
|
415
437
|
});
|
|
416
438
|
}
|
|
439
|
+
const { navigationOnly } = this.#options;
|
|
417
440
|
for (const focusable of current) {
|
|
418
441
|
if (this.#focusables.has(focusable)) {
|
|
419
442
|
continue;
|
|
420
443
|
}
|
|
421
444
|
this.#focusables.add(focusable);
|
|
422
|
-
|
|
423
|
-
|
|
445
|
+
if (!navigationOnly) {
|
|
446
|
+
saveAttributes([focusable], ["tabindex"]);
|
|
447
|
+
focusable.setAttribute("tabindex", "-1");
|
|
448
|
+
}
|
|
424
449
|
if (!this.#options.typeahead) {
|
|
425
450
|
continue;
|
|
426
451
|
}
|
|
@@ -442,6 +467,9 @@ var RovingTabIndex = class {
|
|
|
442
467
|
this.#focusablesByFirstChar.set(key, focusables);
|
|
443
468
|
});
|
|
444
469
|
}
|
|
470
|
+
if (navigationOnly) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
445
473
|
if (active && this.#focusables.has(active)) {
|
|
446
474
|
this.#focusables.forEach((focusable) => {
|
|
447
475
|
focusable.setAttribute("tabindex", focusable === active ? "0" : "-1");
|
|
@@ -460,7 +488,8 @@ var RovingTabIndex = class {
|
|
|
460
488
|
return getFocusables(this.#container, {
|
|
461
489
|
composed: true,
|
|
462
490
|
filter: this.#selectorFilter,
|
|
463
|
-
include: (element) => this.#focusables.has(element)
|
|
491
|
+
include: (element) => this.#focusables.has(element),
|
|
492
|
+
skipVisibilityCheck: true
|
|
464
493
|
});
|
|
465
494
|
}
|
|
466
495
|
};
|
|
@@ -479,7 +508,7 @@ function getActiveElement() {
|
|
|
479
508
|
* Lightweight roving tabindex utility with fully focus management.
|
|
480
509
|
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
481
510
|
*
|
|
482
|
-
* @version 1.
|
|
511
|
+
* @version 1.3.1
|
|
483
512
|
* @author Yusuke Kamiyamane
|
|
484
513
|
* @license MIT
|
|
485
514
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -504,7 +533,7 @@ power-focusable/dist/index.js:
|
|
|
504
533
|
* High-precision focus management utility with full composed tree support.
|
|
505
534
|
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
506
535
|
*
|
|
507
|
-
* @version 4.
|
|
536
|
+
* @version 4.2.0
|
|
508
537
|
* @author Yusuke Kamiyamane
|
|
509
538
|
* @license MIT
|
|
510
539
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.d.cts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Lightweight roving tabindex utility with fully focus management.
|
|
4
4
|
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
5
5
|
*
|
|
6
|
-
* @version 1.
|
|
6
|
+
* @version 1.3.1
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
interface RovingTabIndexOptions {
|
|
13
13
|
readonly direction?: 'horizontal' | 'vertical' | undefined;
|
|
14
|
+
readonly navigationOnly?: boolean;
|
|
14
15
|
readonly selector?: string | undefined;
|
|
15
16
|
readonly typeahead?: boolean;
|
|
16
17
|
readonly wrap?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Lightweight roving tabindex utility with fully focus management.
|
|
4
4
|
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
5
5
|
*
|
|
6
|
-
* @version 1.
|
|
6
|
+
* @version 1.3.1
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
interface RovingTabIndexOptions {
|
|
13
13
|
readonly direction?: 'horizontal' | 'vertical' | undefined;
|
|
14
|
+
readonly navigationOnly?: boolean;
|
|
14
15
|
readonly selector?: string | undefined;
|
|
15
16
|
readonly typeahead?: boolean;
|
|
16
17
|
readonly wrap?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,12 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
55
55
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
56
56
|
container = document.body;
|
|
57
57
|
}
|
|
58
|
-
let {
|
|
58
|
+
let {
|
|
59
|
+
composed = false,
|
|
60
|
+
filter,
|
|
61
|
+
include,
|
|
62
|
+
skipVisibilityCheck = false
|
|
63
|
+
} = options;
|
|
59
64
|
if (typeof composed !== "boolean") {
|
|
60
65
|
console.warn("Invalid composed option. Fallback: false.");
|
|
61
66
|
composed = false;
|
|
@@ -76,7 +81,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
76
81
|
if (composed || include) {
|
|
77
82
|
let traverse2 = function(node) {
|
|
78
83
|
if (node instanceof Element) {
|
|
79
|
-
if (isFocusable(node) || include?.(node)) {
|
|
84
|
+
if (isFocusable(node, { skipVisibilityCheck }) || include?.(node)) {
|
|
80
85
|
elements[elements.length] = node;
|
|
81
86
|
}
|
|
82
87
|
}
|
|
@@ -97,7 +102,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
97
102
|
if (!(candidate instanceof Element)) {
|
|
98
103
|
continue;
|
|
99
104
|
}
|
|
100
|
-
if (isFocusable(candidate)) {
|
|
105
|
+
if (isFocusable(candidate, { skipVisibilityCheck })) {
|
|
101
106
|
elements[elements.length] = candidate;
|
|
102
107
|
}
|
|
103
108
|
}
|
|
@@ -105,11 +110,16 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
105
110
|
const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
|
|
106
111
|
return filter ? unfiltered.filter(filter) : unfiltered;
|
|
107
112
|
}
|
|
108
|
-
function isFocusable(element) {
|
|
113
|
+
function isFocusable(element, options = {}) {
|
|
109
114
|
if (!(element instanceof Element)) {
|
|
110
115
|
console.warn("Invalid element");
|
|
111
116
|
return false;
|
|
112
117
|
}
|
|
118
|
+
let { skipVisibilityCheck = false } = options;
|
|
119
|
+
if (typeof skipVisibilityCheck !== "boolean") {
|
|
120
|
+
console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
|
|
121
|
+
skipVisibilityCheck = false;
|
|
122
|
+
}
|
|
113
123
|
if (element.hasAttribute("hidden") || isInert(element)) {
|
|
114
124
|
return false;
|
|
115
125
|
}
|
|
@@ -122,7 +132,7 @@ function isFocusable(element) {
|
|
|
122
132
|
if (isDisabledDeep(element)) {
|
|
123
133
|
return false;
|
|
124
134
|
}
|
|
125
|
-
if (!element.checkVisibility({
|
|
135
|
+
if (!skipVisibilityCheck && !element.checkVisibility({
|
|
126
136
|
contentVisibilityAuto: true,
|
|
127
137
|
opacityProperty: true,
|
|
128
138
|
visibilityProperty: true
|
|
@@ -264,11 +274,21 @@ function createRovingTabIndex(container, options = {}) {
|
|
|
264
274
|
return () => {
|
|
265
275
|
};
|
|
266
276
|
}
|
|
267
|
-
let {
|
|
277
|
+
let {
|
|
278
|
+
direction,
|
|
279
|
+
navigationOnly = false,
|
|
280
|
+
selector,
|
|
281
|
+
typeahead = false,
|
|
282
|
+
wrap = false
|
|
283
|
+
} = options;
|
|
268
284
|
if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
|
|
269
285
|
console.warn("Invalid direction option. Fallback: both (undefined).");
|
|
270
286
|
direction = void 0;
|
|
271
287
|
}
|
|
288
|
+
if (typeof navigationOnly !== "boolean") {
|
|
289
|
+
console.warn("Invalid navigationOnly option. Fallback: false.");
|
|
290
|
+
navigationOnly = false;
|
|
291
|
+
}
|
|
272
292
|
if (typeof selector !== "undefined" && typeof selector !== "string") {
|
|
273
293
|
console.warn(
|
|
274
294
|
"Invalid selector. Fallback: all focusable elements (undefined)."
|
|
@@ -285,6 +305,7 @@ function createRovingTabIndex(container, options = {}) {
|
|
|
285
305
|
}
|
|
286
306
|
const roving = new RovingTabIndex(container, {
|
|
287
307
|
direction,
|
|
308
|
+
navigationOnly,
|
|
288
309
|
selector,
|
|
289
310
|
typeahead,
|
|
290
311
|
wrap
|
|
@@ -398,7 +419,8 @@ var RovingTabIndex = class {
|
|
|
398
419
|
...this.#getFocusables(),
|
|
399
420
|
...getFocusables(this.#container, {
|
|
400
421
|
composed: true,
|
|
401
|
-
filter: this.#selectorFilter
|
|
422
|
+
filter: this.#selectorFilter,
|
|
423
|
+
skipVisibilityCheck: true
|
|
402
424
|
})
|
|
403
425
|
]);
|
|
404
426
|
for (const focusable of this.#focusables) {
|
|
@@ -412,13 +434,16 @@ var RovingTabIndex = class {
|
|
|
412
434
|
index >= 0 && focusables.splice(index, 1);
|
|
413
435
|
});
|
|
414
436
|
}
|
|
437
|
+
const { navigationOnly } = this.#options;
|
|
415
438
|
for (const focusable of current) {
|
|
416
439
|
if (this.#focusables.has(focusable)) {
|
|
417
440
|
continue;
|
|
418
441
|
}
|
|
419
442
|
this.#focusables.add(focusable);
|
|
420
|
-
|
|
421
|
-
|
|
443
|
+
if (!navigationOnly) {
|
|
444
|
+
saveAttributes([focusable], ["tabindex"]);
|
|
445
|
+
focusable.setAttribute("tabindex", "-1");
|
|
446
|
+
}
|
|
422
447
|
if (!this.#options.typeahead) {
|
|
423
448
|
continue;
|
|
424
449
|
}
|
|
@@ -440,6 +465,9 @@ var RovingTabIndex = class {
|
|
|
440
465
|
this.#focusablesByFirstChar.set(key, focusables);
|
|
441
466
|
});
|
|
442
467
|
}
|
|
468
|
+
if (navigationOnly) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
443
471
|
if (active && this.#focusables.has(active)) {
|
|
444
472
|
this.#focusables.forEach((focusable) => {
|
|
445
473
|
focusable.setAttribute("tabindex", focusable === active ? "0" : "-1");
|
|
@@ -458,7 +486,8 @@ var RovingTabIndex = class {
|
|
|
458
486
|
return getFocusables(this.#container, {
|
|
459
487
|
composed: true,
|
|
460
488
|
filter: this.#selectorFilter,
|
|
461
|
-
include: (element) => this.#focusables.has(element)
|
|
489
|
+
include: (element) => this.#focusables.has(element),
|
|
490
|
+
skipVisibilityCheck: true
|
|
462
491
|
});
|
|
463
492
|
}
|
|
464
493
|
};
|
|
@@ -477,7 +506,7 @@ function getActiveElement() {
|
|
|
477
506
|
* Lightweight roving tabindex utility with fully focus management.
|
|
478
507
|
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
479
508
|
*
|
|
480
|
-
* @version 1.
|
|
509
|
+
* @version 1.3.1
|
|
481
510
|
* @author Yusuke Kamiyamane
|
|
482
511
|
* @license MIT
|
|
483
512
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -502,7 +531,7 @@ power-focusable/dist/index.js:
|
|
|
502
531
|
* High-precision focus management utility with full composed tree support.
|
|
503
532
|
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
504
533
|
*
|
|
505
|
-
* @version 4.
|
|
534
|
+
* @version 4.2.0
|
|
506
535
|
* @author Yusuke Kamiyamane
|
|
507
536
|
* @license MIT
|
|
508
537
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@y14e/roving-tabindex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Lightweight roving tabindex utility with fully focus management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@y14e/attributes-utils": "^1.0.5",
|
|
52
52
|
"bun-types": "latest",
|
|
53
|
-
"power-focusable": "^4.
|
|
53
|
+
"power-focusable": "^4.2.0",
|
|
54
54
|
"tsup": "^8.0.0",
|
|
55
55
|
"typescript": "^5.6.0"
|
|
56
56
|
},
|