@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 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 { composed = false, filter, include } = options;
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 { direction, selector, typeahead = false, wrap = false } = options;
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
- saveAttributes([focusable], ["tabindex"]);
423
- focusable.setAttribute("tabindex", "-1");
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.2.11
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.1.8
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.2.11
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.2.11
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 { composed = false, filter, include } = options;
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 { direction, selector, typeahead = false, wrap = false } = options;
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
- saveAttributes([focusable], ["tabindex"]);
421
- focusable.setAttribute("tabindex", "-1");
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.2.11
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.1.8
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.2.11",
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.1.8",
53
+ "power-focusable": "^4.2.0",
54
54
  "tsup": "^8.0.0",
55
55
  "typescript": "^5.6.0"
56
56
  },