@y14e/roving-tabindex 3.1.11 → 3.1.13

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
@@ -13,11 +13,11 @@ npm i @y14e/roving-tabindex
13
13
  import { createRovingTabIndex } from '@y14e/roving-tabindex';
14
14
 
15
15
  // CDNs
16
- import { createRovingTabIndex } from 'https://esm.sh/@y14e/roving-tabindex@3.1.11';
16
+ import { createRovingTabIndex } from 'https://esm.sh/@y14e/roving-tabindex@3.1.13';
17
17
  // or
18
- import { createRovingTabIndex } from 'https://cdn.jsdelivr.net/npm/@y14e/roving-tabindex@3.1.11/+esm';
18
+ import { createRovingTabIndex } from 'https://cdn.jsdelivr.net/npm/@y14e/roving-tabindex@3.1.13/+esm';
19
19
  // or
20
- import { createRovingTabIndex } from 'https://esm.unpkg.com/@y14e/roving-tabindex@3.1.11';
20
+ import { createRovingTabIndex } from 'https://esm.unpkg.com/@y14e/roving-tabindex@3.1.13';
21
21
  ```
22
22
 
23
23
  ## 📦 APIs
@@ -51,6 +51,10 @@ function saveAttributes(elements, attributes) {
51
51
 
52
52
  // node_modules/power-focusable/dist/index.js
53
53
  var FOCUSABLE_SELECTOR = `:is(a[href], area[href], button, embed, iframe, input:not([type="hidden" i]), object, select, details > summary:first-of-type, textarea, [contenteditable]:not([contenteditable="false" i]), [controls], [tabindex]):not(:disabled, [hidden], [inert], [tabindex="-1"])`;
54
+ var FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX = FOCUSABLE_SELECTOR.replace(
55
+ /(,\s*)?\[tabindex="-1"\]/g,
56
+ ""
57
+ );
54
58
  function getFocusables(container = document.body, options = {}) {
55
59
  if (!(container instanceof Element)) {
56
60
  console.warn("Invalid container element. Fallback: <body> element.");
@@ -90,21 +94,27 @@ function getFocusables(container = document.body, options = {}) {
90
94
  const elements = [];
91
95
  if (composed || include) {
92
96
  let traverse2 = function(node) {
93
- if (!(node instanceof Element)) {
94
- return;
95
- }
96
- if (isFocusable(node, { skipNegativeTabIndexCheck, skipVisibilityCheck }) || include?.(node)) {
97
+ if (isFocusable(node, {
98
+ skipNegativeTabIndexCheck,
99
+ skipVisibilityCheck
100
+ }) || include?.(node)) {
97
101
  elements[elements.length] = node;
98
102
  }
99
- const children = getComposedChildren(node);
100
- for (let i = 0, l = children.length; i < l; i++) {
101
- const child = children[i];
103
+ const children2 = composed ? getComposedChildren(node) : getChildren(node);
104
+ for (let i = 0, l = children2.length; i < l; i++) {
105
+ const child = children2[i];
102
106
  child && traverse2(child);
103
107
  }
104
108
  };
105
- traverse2(container);
109
+ const children = composed ? getComposedChildren(container) : getChildren(container);
110
+ for (let i = 0, l = children.length; i < l; i++) {
111
+ const child = children[i];
112
+ child && traverse2(child);
113
+ }
106
114
  } else {
107
- const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
115
+ const candidates = container.querySelectorAll(
116
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX : FOCUSABLE_SELECTOR
117
+ );
108
118
  for (let i = 0, l = candidates.length; i < l; i++) {
109
119
  const candidate = candidates[i];
110
120
  if (candidate && isFocusable(candidate, {
@@ -115,8 +125,8 @@ function getFocusables(container = document.body, options = {}) {
115
125
  }
116
126
  }
117
127
  }
118
- const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
119
- return filter ? unfiltered.filter(filter) : unfiltered;
128
+ const filtered = filter ? elements.filter(filter) : elements;
129
+ return normalizeRadioGroup(sortByTabIndex(filtered));
120
130
  }
121
131
  function isFocusable(element, options = {}) {
122
132
  if (!(element instanceof Element)) {
@@ -139,7 +149,7 @@ function isFocusable(element, options = {}) {
139
149
  return false;
140
150
  }
141
151
  if (!element.matches(
142
- skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
152
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX : FOCUSABLE_SELECTOR
143
153
  )) {
144
154
  return false;
145
155
  }
@@ -185,7 +195,7 @@ function isDisabledDeep(element) {
185
195
  return false;
186
196
  }
187
197
  function normalizeRadioGroup(elements) {
188
- let map = null;
198
+ let rootMap = null;
189
199
  for (let i = 0, l = elements.length; i < l; i++) {
190
200
  const element = elements[i];
191
201
  if (!(element instanceof HTMLInputElement)) {
@@ -194,25 +204,45 @@ function normalizeRadioGroup(elements) {
194
204
  if (!isUngroupedRadio(element)) {
195
205
  continue;
196
206
  }
197
- if (!map) {
198
- map = /* @__PURE__ */ new Map();
207
+ if (!rootMap) {
208
+ rootMap = /* @__PURE__ */ new Map();
199
209
  }
200
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
201
- const group = map.get(key) ?? map.set(key, []).get(key);
202
- if (group) {
203
- group[group.length] = element;
210
+ const root = element.getRootNode();
211
+ let formMap = rootMap.get(root);
212
+ if (!formMap) {
213
+ formMap = /* @__PURE__ */ new Map();
214
+ rootMap.set(root, formMap);
204
215
  }
216
+ let nameMap = formMap.get(element.form);
217
+ if (!nameMap) {
218
+ nameMap = /* @__PURE__ */ new Map();
219
+ formMap.set(element.form, nameMap);
220
+ }
221
+ let group = nameMap.get(element.name);
222
+ if (!group) {
223
+ group = [];
224
+ nameMap.set(element.name, group);
225
+ }
226
+ group[group.length] = element;
205
227
  }
206
- if (!map) {
228
+ if (!rootMap) {
207
229
  return elements;
208
230
  }
209
231
  const placeholder = /* @__PURE__ */ new Set();
210
- for (const group of map.values()) {
211
- placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
232
+ for (const formMap of rootMap.values()) {
233
+ for (const nameMap of formMap.values()) {
234
+ for (const group of nameMap.values()) {
235
+ const radio = group.find((element) => element.checked) ?? group[0];
236
+ radio && placeholder.add(radio);
237
+ }
238
+ }
212
239
  }
213
- return elements.filter(
214
- (element) => isUngroupedRadio(element) ? placeholder.has(element) : true
215
- );
240
+ return elements.filter((element) => {
241
+ if (!(element instanceof HTMLInputElement)) {
242
+ return true;
243
+ }
244
+ return !isUngroupedRadio(element) || placeholder.has(element);
245
+ });
216
246
  }
217
247
  function sortByTabIndex(elements) {
218
248
  const ordered = [];
@@ -284,7 +314,7 @@ function isInert(element) {
284
314
  return "inert" in element && !!element.inert;
285
315
  }
286
316
  function isUngroupedRadio(element) {
287
- return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
317
+ return element.type === "radio" && !!element.name;
288
318
  }
289
319
 
290
320
  // src/index.ts
@@ -294,14 +324,8 @@ function createRovingTabIndex(container, options = {}) {
294
324
  return () => {
295
325
  };
296
326
  }
297
- try {
298
- const roving = new RovingTabIndex(container, options);
299
- return () => roving.destroy();
300
- } catch (error) {
301
- error instanceof Error && console.warn(error.message || error);
302
- return () => {
303
- };
304
- }
327
+ const roving = new RovingTabIndex(container, options);
328
+ return () => roving.destroy();
305
329
  }
306
330
  var RovingTabIndex = class _RovingTabIndex {
307
331
  static #initialized = /* @__PURE__ */ new Set();
@@ -339,9 +363,18 @@ var RovingTabIndex = class _RovingTabIndex {
339
363
  console.warn("Invalid noStart option. Fallback: false.");
340
364
  noStart = false;
341
365
  }
342
- if (selector !== "" && (typeof selector !== "string" || !selector.trim())) {
343
- console.warn("Invalid selector. Fallback: no selector string.");
344
- selector = "";
366
+ if (selector !== "") {
367
+ if (typeof selector !== "string" || !selector.trim()) {
368
+ console.warn("Invalid selector. Fallback: no selector string.");
369
+ selector = "";
370
+ } else {
371
+ try {
372
+ container.querySelector(selector);
373
+ } catch {
374
+ console.warn("Invalid selector. Fallback: no selector string.");
375
+ selector = "";
376
+ }
377
+ }
345
378
  }
346
379
  if (typeof typeahead !== "boolean") {
347
380
  console.warn("Invalid typeahead option. Fallback: false.");
@@ -370,7 +403,10 @@ var RovingTabIndex = class _RovingTabIndex {
370
403
  this.#isDestroyed = true;
371
404
  this.#controller?.abort();
372
405
  this.#controller = null;
373
- restoreAttributes([...this.#focusables]);
406
+ this.#focusables.forEach((focusable) => {
407
+ _RovingTabIndex.#initialized.delete(focusable);
408
+ restoreAttributes([focusable]);
409
+ });
374
410
  this.#focusables.clear();
375
411
  this.#focusablesByFirstChar.clear();
376
412
  }
@@ -423,7 +459,9 @@ var RovingTabIndex = class _RovingTabIndex {
423
459
  if (!(active instanceof Element)) {
424
460
  return;
425
461
  }
426
- const current = this.#getFocusables();
462
+ const current = this.#getFocusables().filter(
463
+ (focusable2) => this.#focusables.has(focusable2)
464
+ );
427
465
  if (!current.includes(active)) {
428
466
  return;
429
467
  }
@@ -451,7 +489,10 @@ var RovingTabIndex = class _RovingTabIndex {
451
489
  break;
452
490
  }
453
491
  default: {
454
- target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
492
+ const focusables = new Set(
493
+ this.#focusablesByFirstChar.get(key.toUpperCase()) ?? []
494
+ );
495
+ target = current.filter((focusable2) => focusables.has(focusable2));
455
496
  const foundIndex = target.findIndex(
456
497
  (focusable2) => current.indexOf(focusable2) > currentIndex
457
498
  );
@@ -465,12 +506,16 @@ var RovingTabIndex = class _RovingTabIndex {
465
506
  const current = new Set(this.#getFocusables());
466
507
  for (const focusable of this.#focusables) {
467
508
  if (!current.has(focusable)) {
468
- focusable.isConnected && restoreAttributes([focusable]);
509
+ _RovingTabIndex.#initialized.delete(focusable);
510
+ restoreAttributes([focusable]);
469
511
  this.#focusables.delete(focusable);
470
- this.#focusablesByFirstChar.forEach((focusables) => {
512
+ for (const [key, focusables] of this.#focusablesByFirstChar) {
471
513
  const index = focusables.indexOf(focusable);
472
- index >= 0 && focusables.splice(index, 1);
473
- });
514
+ if (index >= 0) {
515
+ focusables.splice(index, 1);
516
+ !focusables.length && this.#focusablesByFirstChar.delete(key);
517
+ }
518
+ }
474
519
  }
475
520
  }
476
521
  const { navigationOnly, noStart, typeahead } = this.#settings;
@@ -479,7 +524,7 @@ var RovingTabIndex = class _RovingTabIndex {
479
524
  continue;
480
525
  }
481
526
  if (_RovingTabIndex.#initialized.has(focusable)) {
482
- throw new TypeError("Already initialized");
527
+ continue;
483
528
  }
484
529
  this.#focusables.add(focusable);
485
530
  _RovingTabIndex.#initialized.add(focusable);
@@ -522,7 +567,7 @@ var RovingTabIndex = class _RovingTabIndex {
522
567
  }
523
568
  #createSelectorFilter() {
524
569
  const { selector } = this.#settings;
525
- return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
570
+ return (element) => !selector || element.matches(selector);
526
571
  }
527
572
  #getFocusables() {
528
573
  return getFocusables(this.#container, {
@@ -538,7 +583,7 @@ var RovingTabIndex = class _RovingTabIndex {
538
583
  * Lightweight roving tabindex utility with fully focus management.
539
584
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
540
585
  *
541
- * @version 3.1.11
586
+ * @version 3.1.13
542
587
  * @author Yusuke Kamiyamane
543
588
  * @license MIT
544
589
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -563,7 +608,7 @@ power-focusable/dist/index.js:
563
608
  * High-precision focus management utility with full composed tree support.
564
609
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
565
610
  *
566
- * @version 4.3.11
611
+ * @version 4.3.12
567
612
  * @author Yusuke Kamiyamane
568
613
  * @license MIT
569
614
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -49,6 +49,10 @@ function saveAttributes(elements, attributes) {
49
49
 
50
50
  // node_modules/power-focusable/dist/index.js
51
51
  var FOCUSABLE_SELECTOR = `:is(a[href], area[href], button, embed, iframe, input:not([type="hidden" i]), object, select, details > summary:first-of-type, textarea, [contenteditable]:not([contenteditable="false" i]), [controls], [tabindex]):not(:disabled, [hidden], [inert], [tabindex="-1"])`;
52
+ var FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX = FOCUSABLE_SELECTOR.replace(
53
+ /(,\s*)?\[tabindex="-1"\]/g,
54
+ ""
55
+ );
52
56
  function getFocusables(container = document.body, options = {}) {
53
57
  if (!(container instanceof Element)) {
54
58
  console.warn("Invalid container element. Fallback: <body> element.");
@@ -88,21 +92,27 @@ function getFocusables(container = document.body, options = {}) {
88
92
  const elements = [];
89
93
  if (composed || include) {
90
94
  let traverse2 = function(node) {
91
- if (!(node instanceof Element)) {
92
- return;
93
- }
94
- if (isFocusable(node, { skipNegativeTabIndexCheck, skipVisibilityCheck }) || include?.(node)) {
95
+ if (isFocusable(node, {
96
+ skipNegativeTabIndexCheck,
97
+ skipVisibilityCheck
98
+ }) || include?.(node)) {
95
99
  elements[elements.length] = node;
96
100
  }
97
- const children = getComposedChildren(node);
98
- for (let i = 0, l = children.length; i < l; i++) {
99
- const child = children[i];
101
+ const children2 = composed ? getComposedChildren(node) : getChildren(node);
102
+ for (let i = 0, l = children2.length; i < l; i++) {
103
+ const child = children2[i];
100
104
  child && traverse2(child);
101
105
  }
102
106
  };
103
- traverse2(container);
107
+ const children = composed ? getComposedChildren(container) : getChildren(container);
108
+ for (let i = 0, l = children.length; i < l; i++) {
109
+ const child = children[i];
110
+ child && traverse2(child);
111
+ }
104
112
  } else {
105
- const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
113
+ const candidates = container.querySelectorAll(
114
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX : FOCUSABLE_SELECTOR
115
+ );
106
116
  for (let i = 0, l = candidates.length; i < l; i++) {
107
117
  const candidate = candidates[i];
108
118
  if (candidate && isFocusable(candidate, {
@@ -113,8 +123,8 @@ function getFocusables(container = document.body, options = {}) {
113
123
  }
114
124
  }
115
125
  }
116
- const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
117
- return filter ? unfiltered.filter(filter) : unfiltered;
126
+ const filtered = filter ? elements.filter(filter) : elements;
127
+ return normalizeRadioGroup(sortByTabIndex(filtered));
118
128
  }
119
129
  function isFocusable(element, options = {}) {
120
130
  if (!(element instanceof Element)) {
@@ -137,7 +147,7 @@ function isFocusable(element, options = {}) {
137
147
  return false;
138
148
  }
139
149
  if (!element.matches(
140
- skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
150
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX : FOCUSABLE_SELECTOR
141
151
  )) {
142
152
  return false;
143
153
  }
@@ -183,7 +193,7 @@ function isDisabledDeep(element) {
183
193
  return false;
184
194
  }
185
195
  function normalizeRadioGroup(elements) {
186
- let map = null;
196
+ let rootMap = null;
187
197
  for (let i = 0, l = elements.length; i < l; i++) {
188
198
  const element = elements[i];
189
199
  if (!(element instanceof HTMLInputElement)) {
@@ -192,25 +202,45 @@ function normalizeRadioGroup(elements) {
192
202
  if (!isUngroupedRadio(element)) {
193
203
  continue;
194
204
  }
195
- if (!map) {
196
- map = /* @__PURE__ */ new Map();
205
+ if (!rootMap) {
206
+ rootMap = /* @__PURE__ */ new Map();
197
207
  }
198
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
199
- const group = map.get(key) ?? map.set(key, []).get(key);
200
- if (group) {
201
- group[group.length] = element;
208
+ const root = element.getRootNode();
209
+ let formMap = rootMap.get(root);
210
+ if (!formMap) {
211
+ formMap = /* @__PURE__ */ new Map();
212
+ rootMap.set(root, formMap);
202
213
  }
214
+ let nameMap = formMap.get(element.form);
215
+ if (!nameMap) {
216
+ nameMap = /* @__PURE__ */ new Map();
217
+ formMap.set(element.form, nameMap);
218
+ }
219
+ let group = nameMap.get(element.name);
220
+ if (!group) {
221
+ group = [];
222
+ nameMap.set(element.name, group);
223
+ }
224
+ group[group.length] = element;
203
225
  }
204
- if (!map) {
226
+ if (!rootMap) {
205
227
  return elements;
206
228
  }
207
229
  const placeholder = /* @__PURE__ */ new Set();
208
- for (const group of map.values()) {
209
- placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
230
+ for (const formMap of rootMap.values()) {
231
+ for (const nameMap of formMap.values()) {
232
+ for (const group of nameMap.values()) {
233
+ const radio = group.find((element) => element.checked) ?? group[0];
234
+ radio && placeholder.add(radio);
235
+ }
236
+ }
210
237
  }
211
- return elements.filter(
212
- (element) => isUngroupedRadio(element) ? placeholder.has(element) : true
213
- );
238
+ return elements.filter((element) => {
239
+ if (!(element instanceof HTMLInputElement)) {
240
+ return true;
241
+ }
242
+ return !isUngroupedRadio(element) || placeholder.has(element);
243
+ });
214
244
  }
215
245
  function sortByTabIndex(elements) {
216
246
  const ordered = [];
@@ -282,7 +312,7 @@ function isInert(element) {
282
312
  return "inert" in element && !!element.inert;
283
313
  }
284
314
  function isUngroupedRadio(element) {
285
- return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
315
+ return element.type === "radio" && !!element.name;
286
316
  }
287
317
 
288
318
  // src/index.ts
@@ -292,14 +322,8 @@ function createRovingTabIndex(container, options = {}) {
292
322
  return () => {
293
323
  };
294
324
  }
295
- try {
296
- const roving = new RovingTabIndex(container, options);
297
- return () => roving.destroy();
298
- } catch (error) {
299
- error instanceof Error && console.warn(error.message || error);
300
- return () => {
301
- };
302
- }
325
+ const roving = new RovingTabIndex(container, options);
326
+ return () => roving.destroy();
303
327
  }
304
328
  var RovingTabIndex = class _RovingTabIndex {
305
329
  static #initialized = /* @__PURE__ */ new Set();
@@ -337,9 +361,18 @@ var RovingTabIndex = class _RovingTabIndex {
337
361
  console.warn("Invalid noStart option. Fallback: false.");
338
362
  noStart = false;
339
363
  }
340
- if (selector !== "" && (typeof selector !== "string" || !selector.trim())) {
341
- console.warn("Invalid selector. Fallback: no selector string.");
342
- selector = "";
364
+ if (selector !== "") {
365
+ if (typeof selector !== "string" || !selector.trim()) {
366
+ console.warn("Invalid selector. Fallback: no selector string.");
367
+ selector = "";
368
+ } else {
369
+ try {
370
+ container.querySelector(selector);
371
+ } catch {
372
+ console.warn("Invalid selector. Fallback: no selector string.");
373
+ selector = "";
374
+ }
375
+ }
343
376
  }
344
377
  if (typeof typeahead !== "boolean") {
345
378
  console.warn("Invalid typeahead option. Fallback: false.");
@@ -368,7 +401,10 @@ var RovingTabIndex = class _RovingTabIndex {
368
401
  this.#isDestroyed = true;
369
402
  this.#controller?.abort();
370
403
  this.#controller = null;
371
- restoreAttributes([...this.#focusables]);
404
+ this.#focusables.forEach((focusable) => {
405
+ _RovingTabIndex.#initialized.delete(focusable);
406
+ restoreAttributes([focusable]);
407
+ });
372
408
  this.#focusables.clear();
373
409
  this.#focusablesByFirstChar.clear();
374
410
  }
@@ -421,7 +457,9 @@ var RovingTabIndex = class _RovingTabIndex {
421
457
  if (!(active instanceof Element)) {
422
458
  return;
423
459
  }
424
- const current = this.#getFocusables();
460
+ const current = this.#getFocusables().filter(
461
+ (focusable2) => this.#focusables.has(focusable2)
462
+ );
425
463
  if (!current.includes(active)) {
426
464
  return;
427
465
  }
@@ -449,7 +487,10 @@ var RovingTabIndex = class _RovingTabIndex {
449
487
  break;
450
488
  }
451
489
  default: {
452
- target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
490
+ const focusables = new Set(
491
+ this.#focusablesByFirstChar.get(key.toUpperCase()) ?? []
492
+ );
493
+ target = current.filter((focusable2) => focusables.has(focusable2));
453
494
  const foundIndex = target.findIndex(
454
495
  (focusable2) => current.indexOf(focusable2) > currentIndex
455
496
  );
@@ -463,12 +504,16 @@ var RovingTabIndex = class _RovingTabIndex {
463
504
  const current = new Set(this.#getFocusables());
464
505
  for (const focusable of this.#focusables) {
465
506
  if (!current.has(focusable)) {
466
- focusable.isConnected && restoreAttributes([focusable]);
507
+ _RovingTabIndex.#initialized.delete(focusable);
508
+ restoreAttributes([focusable]);
467
509
  this.#focusables.delete(focusable);
468
- this.#focusablesByFirstChar.forEach((focusables) => {
510
+ for (const [key, focusables] of this.#focusablesByFirstChar) {
469
511
  const index = focusables.indexOf(focusable);
470
- index >= 0 && focusables.splice(index, 1);
471
- });
512
+ if (index >= 0) {
513
+ focusables.splice(index, 1);
514
+ !focusables.length && this.#focusablesByFirstChar.delete(key);
515
+ }
516
+ }
472
517
  }
473
518
  }
474
519
  const { navigationOnly, noStart, typeahead } = this.#settings;
@@ -477,7 +522,7 @@ var RovingTabIndex = class _RovingTabIndex {
477
522
  continue;
478
523
  }
479
524
  if (_RovingTabIndex.#initialized.has(focusable)) {
480
- throw new TypeError("Already initialized");
525
+ continue;
481
526
  }
482
527
  this.#focusables.add(focusable);
483
528
  _RovingTabIndex.#initialized.add(focusable);
@@ -520,7 +565,7 @@ var RovingTabIndex = class _RovingTabIndex {
520
565
  }
521
566
  #createSelectorFilter() {
522
567
  const { selector } = this.#settings;
523
- return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
568
+ return (element) => !selector || element.matches(selector);
524
569
  }
525
570
  #getFocusables() {
526
571
  return getFocusables(this.#container, {
@@ -536,7 +581,7 @@ var RovingTabIndex = class _RovingTabIndex {
536
581
  * Lightweight roving tabindex utility with fully focus management.
537
582
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
538
583
  *
539
- * @version 3.1.11
584
+ * @version 3.1.13
540
585
  * @author Yusuke Kamiyamane
541
586
  * @license MIT
542
587
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -561,7 +606,7 @@ power-focusable/dist/index.js:
561
606
  * High-precision focus management utility with full composed tree support.
562
607
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
563
608
  *
564
- * @version 4.3.11
609
+ * @version 4.3.12
565
610
  * @author Yusuke Kamiyamane
566
611
  * @license MIT
567
612
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.cjs CHANGED
@@ -10,14 +10,8 @@ function createRovingTabIndex(container, options = {}) {
10
10
  return () => {
11
11
  };
12
12
  }
13
- try {
14
- const roving = new RovingTabIndex(container, options);
15
- return () => roving.destroy();
16
- } catch (error) {
17
- error instanceof Error && console.warn(error.message || error);
18
- return () => {
19
- };
20
- }
13
+ const roving = new RovingTabIndex(container, options);
14
+ return () => roving.destroy();
21
15
  }
22
16
  var RovingTabIndex = class _RovingTabIndex {
23
17
  static #initialized = /* @__PURE__ */ new Set();
@@ -55,9 +49,18 @@ var RovingTabIndex = class _RovingTabIndex {
55
49
  console.warn("Invalid noStart option. Fallback: false.");
56
50
  noStart = false;
57
51
  }
58
- if (selector !== "" && (typeof selector !== "string" || !selector.trim())) {
59
- console.warn("Invalid selector. Fallback: no selector string.");
60
- selector = "";
52
+ if (selector !== "") {
53
+ if (typeof selector !== "string" || !selector.trim()) {
54
+ console.warn("Invalid selector. Fallback: no selector string.");
55
+ selector = "";
56
+ } else {
57
+ try {
58
+ container.querySelector(selector);
59
+ } catch {
60
+ console.warn("Invalid selector. Fallback: no selector string.");
61
+ selector = "";
62
+ }
63
+ }
61
64
  }
62
65
  if (typeof typeahead !== "boolean") {
63
66
  console.warn("Invalid typeahead option. Fallback: false.");
@@ -86,7 +89,10 @@ var RovingTabIndex = class _RovingTabIndex {
86
89
  this.#isDestroyed = true;
87
90
  this.#controller?.abort();
88
91
  this.#controller = null;
89
- attributesUtils.restoreAttributes([...this.#focusables]);
92
+ this.#focusables.forEach((focusable) => {
93
+ _RovingTabIndex.#initialized.delete(focusable);
94
+ attributesUtils.restoreAttributes([focusable]);
95
+ });
90
96
  this.#focusables.clear();
91
97
  this.#focusablesByFirstChar.clear();
92
98
  }
@@ -139,7 +145,9 @@ var RovingTabIndex = class _RovingTabIndex {
139
145
  if (!(active instanceof Element)) {
140
146
  return;
141
147
  }
142
- const current = this.#getFocusables();
148
+ const current = this.#getFocusables().filter(
149
+ (focusable2) => this.#focusables.has(focusable2)
150
+ );
143
151
  if (!current.includes(active)) {
144
152
  return;
145
153
  }
@@ -167,7 +175,10 @@ var RovingTabIndex = class _RovingTabIndex {
167
175
  break;
168
176
  }
169
177
  default: {
170
- target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
178
+ const focusables = new Set(
179
+ this.#focusablesByFirstChar.get(key.toUpperCase()) ?? []
180
+ );
181
+ target = current.filter((focusable2) => focusables.has(focusable2));
171
182
  const foundIndex = target.findIndex(
172
183
  (focusable2) => current.indexOf(focusable2) > currentIndex
173
184
  );
@@ -181,12 +192,16 @@ var RovingTabIndex = class _RovingTabIndex {
181
192
  const current = new Set(this.#getFocusables());
182
193
  for (const focusable of this.#focusables) {
183
194
  if (!current.has(focusable)) {
184
- focusable.isConnected && attributesUtils.restoreAttributes([focusable]);
195
+ _RovingTabIndex.#initialized.delete(focusable);
196
+ attributesUtils.restoreAttributes([focusable]);
185
197
  this.#focusables.delete(focusable);
186
- this.#focusablesByFirstChar.forEach((focusables) => {
198
+ for (const [key, focusables] of this.#focusablesByFirstChar) {
187
199
  const index = focusables.indexOf(focusable);
188
- index >= 0 && focusables.splice(index, 1);
189
- });
200
+ if (index >= 0) {
201
+ focusables.splice(index, 1);
202
+ !focusables.length && this.#focusablesByFirstChar.delete(key);
203
+ }
204
+ }
190
205
  }
191
206
  }
192
207
  const { navigationOnly, noStart, typeahead } = this.#settings;
@@ -195,7 +210,7 @@ var RovingTabIndex = class _RovingTabIndex {
195
210
  continue;
196
211
  }
197
212
  if (_RovingTabIndex.#initialized.has(focusable)) {
198
- throw new TypeError("Already initialized");
213
+ continue;
199
214
  }
200
215
  this.#focusables.add(focusable);
201
216
  _RovingTabIndex.#initialized.add(focusable);
@@ -238,7 +253,7 @@ var RovingTabIndex = class _RovingTabIndex {
238
253
  }
239
254
  #createSelectorFilter() {
240
255
  const { selector } = this.#settings;
241
- return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
256
+ return (element) => !selector || element.matches(selector);
242
257
  }
243
258
  #getFocusables() {
244
259
  return powerFocusable.getFocusables(this.#container, {
@@ -254,7 +269,7 @@ var RovingTabIndex = class _RovingTabIndex {
254
269
  * Lightweight roving tabindex utility with fully focus management.
255
270
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
256
271
  *
257
- * @version 3.1.11
272
+ * @version 3.1.13
258
273
  * @author Yusuke Kamiyamane
259
274
  * @license MIT
260
275
  * @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 3.1.11
6
+ * @version 3.1.13
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
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 3.1.11
6
+ * @version 3.1.13
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.js CHANGED
@@ -8,14 +8,8 @@ function createRovingTabIndex(container, options = {}) {
8
8
  return () => {
9
9
  };
10
10
  }
11
- try {
12
- const roving = new RovingTabIndex(container, options);
13
- return () => roving.destroy();
14
- } catch (error) {
15
- error instanceof Error && console.warn(error.message || error);
16
- return () => {
17
- };
18
- }
11
+ const roving = new RovingTabIndex(container, options);
12
+ return () => roving.destroy();
19
13
  }
20
14
  var RovingTabIndex = class _RovingTabIndex {
21
15
  static #initialized = /* @__PURE__ */ new Set();
@@ -53,9 +47,18 @@ var RovingTabIndex = class _RovingTabIndex {
53
47
  console.warn("Invalid noStart option. Fallback: false.");
54
48
  noStart = false;
55
49
  }
56
- if (selector !== "" && (typeof selector !== "string" || !selector.trim())) {
57
- console.warn("Invalid selector. Fallback: no selector string.");
58
- selector = "";
50
+ if (selector !== "") {
51
+ if (typeof selector !== "string" || !selector.trim()) {
52
+ console.warn("Invalid selector. Fallback: no selector string.");
53
+ selector = "";
54
+ } else {
55
+ try {
56
+ container.querySelector(selector);
57
+ } catch {
58
+ console.warn("Invalid selector. Fallback: no selector string.");
59
+ selector = "";
60
+ }
61
+ }
59
62
  }
60
63
  if (typeof typeahead !== "boolean") {
61
64
  console.warn("Invalid typeahead option. Fallback: false.");
@@ -84,7 +87,10 @@ var RovingTabIndex = class _RovingTabIndex {
84
87
  this.#isDestroyed = true;
85
88
  this.#controller?.abort();
86
89
  this.#controller = null;
87
- restoreAttributes([...this.#focusables]);
90
+ this.#focusables.forEach((focusable) => {
91
+ _RovingTabIndex.#initialized.delete(focusable);
92
+ restoreAttributes([focusable]);
93
+ });
88
94
  this.#focusables.clear();
89
95
  this.#focusablesByFirstChar.clear();
90
96
  }
@@ -137,7 +143,9 @@ var RovingTabIndex = class _RovingTabIndex {
137
143
  if (!(active instanceof Element)) {
138
144
  return;
139
145
  }
140
- const current = this.#getFocusables();
146
+ const current = this.#getFocusables().filter(
147
+ (focusable2) => this.#focusables.has(focusable2)
148
+ );
141
149
  if (!current.includes(active)) {
142
150
  return;
143
151
  }
@@ -165,7 +173,10 @@ var RovingTabIndex = class _RovingTabIndex {
165
173
  break;
166
174
  }
167
175
  default: {
168
- target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
176
+ const focusables = new Set(
177
+ this.#focusablesByFirstChar.get(key.toUpperCase()) ?? []
178
+ );
179
+ target = current.filter((focusable2) => focusables.has(focusable2));
169
180
  const foundIndex = target.findIndex(
170
181
  (focusable2) => current.indexOf(focusable2) > currentIndex
171
182
  );
@@ -179,12 +190,16 @@ var RovingTabIndex = class _RovingTabIndex {
179
190
  const current = new Set(this.#getFocusables());
180
191
  for (const focusable of this.#focusables) {
181
192
  if (!current.has(focusable)) {
182
- focusable.isConnected && restoreAttributes([focusable]);
193
+ _RovingTabIndex.#initialized.delete(focusable);
194
+ restoreAttributes([focusable]);
183
195
  this.#focusables.delete(focusable);
184
- this.#focusablesByFirstChar.forEach((focusables) => {
196
+ for (const [key, focusables] of this.#focusablesByFirstChar) {
185
197
  const index = focusables.indexOf(focusable);
186
- index >= 0 && focusables.splice(index, 1);
187
- });
198
+ if (index >= 0) {
199
+ focusables.splice(index, 1);
200
+ !focusables.length && this.#focusablesByFirstChar.delete(key);
201
+ }
202
+ }
188
203
  }
189
204
  }
190
205
  const { navigationOnly, noStart, typeahead } = this.#settings;
@@ -193,7 +208,7 @@ var RovingTabIndex = class _RovingTabIndex {
193
208
  continue;
194
209
  }
195
210
  if (_RovingTabIndex.#initialized.has(focusable)) {
196
- throw new TypeError("Already initialized");
211
+ continue;
197
212
  }
198
213
  this.#focusables.add(focusable);
199
214
  _RovingTabIndex.#initialized.add(focusable);
@@ -236,7 +251,7 @@ var RovingTabIndex = class _RovingTabIndex {
236
251
  }
237
252
  #createSelectorFilter() {
238
253
  const { selector } = this.#settings;
239
- return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
254
+ return (element) => !selector || element.matches(selector);
240
255
  }
241
256
  #getFocusables() {
242
257
  return getFocusables(this.#container, {
@@ -252,7 +267,7 @@ var RovingTabIndex = class _RovingTabIndex {
252
267
  * Lightweight roving tabindex utility with fully focus management.
253
268
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
254
269
  *
255
- * @version 3.1.11
270
+ * @version 3.1.13
256
271
  * @author Yusuke Kamiyamane
257
272
  * @license MIT
258
273
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/roving-tabindex",
3
- "version": "3.1.11",
3
+ "version": "3.1.13",
4
4
  "description": "Lightweight roving tabindex utility with fully focus management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -57,6 +57,6 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@y14e/attributes-utils": "^1.1.3",
60
- "power-focusable": "^4.3.11"
60
+ "power-focusable": "^4.3.12"
61
61
  }
62
62
  }