@y14e/roving-tabindex 1.0.2 → 1.1.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 +5 -0
- package/dist/index.cjs +46 -9
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +46 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,10 +43,15 @@ const cleanup = createRovingTabIndex(container, options);
|
|
|
43
43
|
interface RovingTabIndexOptions {
|
|
44
44
|
direction?: 'horizontal' | 'vertical'; // default: both (undefined)
|
|
45
45
|
selector?: string;
|
|
46
|
+
typeahead?: boolean; // default: false
|
|
46
47
|
wrap?: boolean; // default: false
|
|
47
48
|
}
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
### `typeahead`
|
|
52
|
+
|
|
53
|
+
If `true`, enables character-based focus navigation. Typing a character moves focus to the next matching element.
|
|
54
|
+
|
|
50
55
|
### `wrap`
|
|
51
56
|
|
|
52
57
|
If `true`, wraps around to the first or last element when reaching the end.
|
package/dist/index.cjs
CHANGED
|
@@ -238,15 +238,19 @@ function createRovingTabIndex(container, options = {}) {
|
|
|
238
238
|
if (!(container instanceof Element)) {
|
|
239
239
|
throw new Error("Invalid container element");
|
|
240
240
|
}
|
|
241
|
-
const { direction, selector, wrap = false } = options;
|
|
242
|
-
if (direction && !["horizontal", "vertical"].includes(direction)) {
|
|
241
|
+
const { direction, selector, typeahead = false, wrap = false } = options;
|
|
242
|
+
if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
|
|
243
243
|
console.warn("Invalid direction. Fallback: both (undefined).");
|
|
244
244
|
Object.assign(options, { direction: void 0 });
|
|
245
245
|
}
|
|
246
|
-
if (typeof selector !== "string") {
|
|
246
|
+
if (typeof selector !== "undefined" && typeof selector !== "string") {
|
|
247
247
|
console.warn("Invalid selector. Fallback: all focusable elements.");
|
|
248
248
|
Object.assign(options, { selector: void 0 });
|
|
249
249
|
}
|
|
250
|
+
if (typeof typeahead !== "boolean") {
|
|
251
|
+
console.warn("Invalid typeahead. Fallback: false.");
|
|
252
|
+
Object.assign(options, { typeahead: false });
|
|
253
|
+
}
|
|
250
254
|
if (typeof wrap !== "boolean") {
|
|
251
255
|
console.warn("Invalid wrap. Fallback: false.");
|
|
252
256
|
Object.assign(options, { wrap: false });
|
|
@@ -258,6 +262,7 @@ var RovingTabIndex = class {
|
|
|
258
262
|
#container;
|
|
259
263
|
#options;
|
|
260
264
|
#focusables = /* @__PURE__ */ new Set();
|
|
265
|
+
#focusablesByFirstChar = /* @__PURE__ */ new Map();
|
|
261
266
|
#selectorFilter;
|
|
262
267
|
#controller = null;
|
|
263
268
|
#isDestroyed = false;
|
|
@@ -276,6 +281,7 @@ var RovingTabIndex = class {
|
|
|
276
281
|
this.#controller = null;
|
|
277
282
|
restoreAttributes([...this.#focusables]);
|
|
278
283
|
this.#focusables.clear();
|
|
284
|
+
this.#focusablesByFirstChar.clear();
|
|
279
285
|
this.#container.removeAttribute("data-roving-tabindex-initialized");
|
|
280
286
|
}
|
|
281
287
|
#initialize() {
|
|
@@ -295,7 +301,7 @@ var RovingTabIndex = class {
|
|
|
295
301
|
if (altKey || ctrlKey || metaKey) {
|
|
296
302
|
return;
|
|
297
303
|
}
|
|
298
|
-
const { direction } = this.#options;
|
|
304
|
+
const { direction, typeahead, wrap } = this.#options;
|
|
299
305
|
const isBoth = !direction;
|
|
300
306
|
const isHorizontal = direction === "horizontal";
|
|
301
307
|
if (![
|
|
@@ -304,7 +310,9 @@ var RovingTabIndex = class {
|
|
|
304
310
|
...isBoth ? ["ArrowLeft", "ArrowUp"] : [`Arrow${isHorizontal ? "Left" : "Up"}`],
|
|
305
311
|
...isBoth ? ["ArrowRight", "ArrowDown"] : [`Arrow${isHorizontal ? "Right" : "Down"}`]
|
|
306
312
|
].includes(key)) {
|
|
307
|
-
|
|
313
|
+
if (!typeahead || !/^\S$/i.test(key) || !this.#focusablesByFirstChar.has(key.toLowerCase())) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
308
316
|
}
|
|
309
317
|
const active = getActiveElement();
|
|
310
318
|
if (!(active instanceof HTMLElement)) {
|
|
@@ -317,9 +325,9 @@ var RovingTabIndex = class {
|
|
|
317
325
|
event.preventDefault();
|
|
318
326
|
event.stopPropagation();
|
|
319
327
|
const currentIndex = focusables.indexOf(active);
|
|
320
|
-
let rawIndex;
|
|
328
|
+
let rawIndex = currentIndex;
|
|
321
329
|
let newIndex = currentIndex;
|
|
322
|
-
|
|
330
|
+
let target = focusables;
|
|
323
331
|
switch (key) {
|
|
324
332
|
case "End":
|
|
325
333
|
newIndex = -1;
|
|
@@ -337,8 +345,18 @@ var RovingTabIndex = class {
|
|
|
337
345
|
rawIndex = currentIndex + 1;
|
|
338
346
|
newIndex = wrap ? rawIndex % focusables.length : Math.min(rawIndex, focusables.length - 1);
|
|
339
347
|
break;
|
|
348
|
+
default: {
|
|
349
|
+
if (!typeahead) {
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
target = this.#focusablesByFirstChar.get(key.toLowerCase()) ?? [];
|
|
353
|
+
const foundIndex = target.findIndex(
|
|
354
|
+
(focusable2) => focusables.indexOf(focusable2) > currentIndex
|
|
355
|
+
);
|
|
356
|
+
newIndex = foundIndex !== -1 ? foundIndex : 0;
|
|
357
|
+
}
|
|
340
358
|
}
|
|
341
|
-
const focusable =
|
|
359
|
+
const focusable = target.at(newIndex);
|
|
342
360
|
if (!focusable) {
|
|
343
361
|
return;
|
|
344
362
|
}
|
|
@@ -361,6 +379,12 @@ var RovingTabIndex = class {
|
|
|
361
379
|
restoreAttributes([focusable]);
|
|
362
380
|
}
|
|
363
381
|
this.#focusables.delete(focusable);
|
|
382
|
+
this.#focusablesByFirstChar.forEach((focusables) => {
|
|
383
|
+
const index = focusables.indexOf(focusable);
|
|
384
|
+
if (index !== -1) {
|
|
385
|
+
focusables.splice(index, 1);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
364
388
|
});
|
|
365
389
|
current.forEach((c) => {
|
|
366
390
|
if (this.#focusables.has(c)) {
|
|
@@ -369,6 +393,19 @@ var RovingTabIndex = class {
|
|
|
369
393
|
this.#focusables.add(c);
|
|
370
394
|
saveAttributes([c], ["tabindex"]);
|
|
371
395
|
c.setAttribute("tabindex", "-1");
|
|
396
|
+
if (!this.#options.typeahead) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const shortcuts = c.ariaKeyShortcuts;
|
|
400
|
+
const keys = (shortcuts?.split(/\s+/) ?? [c.textContent?.trim()[0] ?? ""]).filter((key) => /^\S$/i.test(key)).map((key) => key.toLowerCase());
|
|
401
|
+
keys.forEach((key) => {
|
|
402
|
+
const focusables = this.#focusablesByFirstChar.get(key) ?? [];
|
|
403
|
+
focusables.push(c);
|
|
404
|
+
this.#focusablesByFirstChar.set(key, focusables);
|
|
405
|
+
});
|
|
406
|
+
const first = keys[0];
|
|
407
|
+
saveAttributes([c], ["aria-keyshortcuts"]);
|
|
408
|
+
!shortcuts && first && c.setAttribute("aria-keyshortcuts", first);
|
|
372
409
|
});
|
|
373
410
|
if (active && this.#focusables.has(active)) {
|
|
374
411
|
this.#focusables.forEach((focusable) => {
|
|
@@ -407,7 +444,7 @@ function getActiveElement() {
|
|
|
407
444
|
* Lightweight roving tabindex utility with fully focus management.
|
|
408
445
|
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
409
446
|
*
|
|
410
|
-
* @version 1.
|
|
447
|
+
* @version 1.1.1
|
|
411
448
|
* @author Yusuke Kamiyamane
|
|
412
449
|
* @license MIT
|
|
413
450
|
* @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.1.1
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
interface RovingTabIndexOptions {
|
|
13
13
|
readonly direction?: 'horizontal' | 'vertical';
|
|
14
14
|
readonly selector?: string;
|
|
15
|
+
readonly typeahead?: boolean;
|
|
15
16
|
readonly wrap?: boolean;
|
|
16
17
|
}
|
|
17
18
|
declare function createRovingTabIndex(container: Element, options?: RovingTabIndexOptions): () => void;
|
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.1.1
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
interface RovingTabIndexOptions {
|
|
13
13
|
readonly direction?: 'horizontal' | 'vertical';
|
|
14
14
|
readonly selector?: string;
|
|
15
|
+
readonly typeahead?: boolean;
|
|
15
16
|
readonly wrap?: boolean;
|
|
16
17
|
}
|
|
17
18
|
declare function createRovingTabIndex(container: Element, options?: RovingTabIndexOptions): () => void;
|
package/dist/index.js
CHANGED
|
@@ -236,15 +236,19 @@ function createRovingTabIndex(container, options = {}) {
|
|
|
236
236
|
if (!(container instanceof Element)) {
|
|
237
237
|
throw new Error("Invalid container element");
|
|
238
238
|
}
|
|
239
|
-
const { direction, selector, wrap = false } = options;
|
|
240
|
-
if (direction && !["horizontal", "vertical"].includes(direction)) {
|
|
239
|
+
const { direction, selector, typeahead = false, wrap = false } = options;
|
|
240
|
+
if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
|
|
241
241
|
console.warn("Invalid direction. Fallback: both (undefined).");
|
|
242
242
|
Object.assign(options, { direction: void 0 });
|
|
243
243
|
}
|
|
244
|
-
if (typeof selector !== "string") {
|
|
244
|
+
if (typeof selector !== "undefined" && typeof selector !== "string") {
|
|
245
245
|
console.warn("Invalid selector. Fallback: all focusable elements.");
|
|
246
246
|
Object.assign(options, { selector: void 0 });
|
|
247
247
|
}
|
|
248
|
+
if (typeof typeahead !== "boolean") {
|
|
249
|
+
console.warn("Invalid typeahead. Fallback: false.");
|
|
250
|
+
Object.assign(options, { typeahead: false });
|
|
251
|
+
}
|
|
248
252
|
if (typeof wrap !== "boolean") {
|
|
249
253
|
console.warn("Invalid wrap. Fallback: false.");
|
|
250
254
|
Object.assign(options, { wrap: false });
|
|
@@ -256,6 +260,7 @@ var RovingTabIndex = class {
|
|
|
256
260
|
#container;
|
|
257
261
|
#options;
|
|
258
262
|
#focusables = /* @__PURE__ */ new Set();
|
|
263
|
+
#focusablesByFirstChar = /* @__PURE__ */ new Map();
|
|
259
264
|
#selectorFilter;
|
|
260
265
|
#controller = null;
|
|
261
266
|
#isDestroyed = false;
|
|
@@ -274,6 +279,7 @@ var RovingTabIndex = class {
|
|
|
274
279
|
this.#controller = null;
|
|
275
280
|
restoreAttributes([...this.#focusables]);
|
|
276
281
|
this.#focusables.clear();
|
|
282
|
+
this.#focusablesByFirstChar.clear();
|
|
277
283
|
this.#container.removeAttribute("data-roving-tabindex-initialized");
|
|
278
284
|
}
|
|
279
285
|
#initialize() {
|
|
@@ -293,7 +299,7 @@ var RovingTabIndex = class {
|
|
|
293
299
|
if (altKey || ctrlKey || metaKey) {
|
|
294
300
|
return;
|
|
295
301
|
}
|
|
296
|
-
const { direction } = this.#options;
|
|
302
|
+
const { direction, typeahead, wrap } = this.#options;
|
|
297
303
|
const isBoth = !direction;
|
|
298
304
|
const isHorizontal = direction === "horizontal";
|
|
299
305
|
if (![
|
|
@@ -302,7 +308,9 @@ var RovingTabIndex = class {
|
|
|
302
308
|
...isBoth ? ["ArrowLeft", "ArrowUp"] : [`Arrow${isHorizontal ? "Left" : "Up"}`],
|
|
303
309
|
...isBoth ? ["ArrowRight", "ArrowDown"] : [`Arrow${isHorizontal ? "Right" : "Down"}`]
|
|
304
310
|
].includes(key)) {
|
|
305
|
-
|
|
311
|
+
if (!typeahead || !/^\S$/i.test(key) || !this.#focusablesByFirstChar.has(key.toLowerCase())) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
306
314
|
}
|
|
307
315
|
const active = getActiveElement();
|
|
308
316
|
if (!(active instanceof HTMLElement)) {
|
|
@@ -315,9 +323,9 @@ var RovingTabIndex = class {
|
|
|
315
323
|
event.preventDefault();
|
|
316
324
|
event.stopPropagation();
|
|
317
325
|
const currentIndex = focusables.indexOf(active);
|
|
318
|
-
let rawIndex;
|
|
326
|
+
let rawIndex = currentIndex;
|
|
319
327
|
let newIndex = currentIndex;
|
|
320
|
-
|
|
328
|
+
let target = focusables;
|
|
321
329
|
switch (key) {
|
|
322
330
|
case "End":
|
|
323
331
|
newIndex = -1;
|
|
@@ -335,8 +343,18 @@ var RovingTabIndex = class {
|
|
|
335
343
|
rawIndex = currentIndex + 1;
|
|
336
344
|
newIndex = wrap ? rawIndex % focusables.length : Math.min(rawIndex, focusables.length - 1);
|
|
337
345
|
break;
|
|
346
|
+
default: {
|
|
347
|
+
if (!typeahead) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
target = this.#focusablesByFirstChar.get(key.toLowerCase()) ?? [];
|
|
351
|
+
const foundIndex = target.findIndex(
|
|
352
|
+
(focusable2) => focusables.indexOf(focusable2) > currentIndex
|
|
353
|
+
);
|
|
354
|
+
newIndex = foundIndex !== -1 ? foundIndex : 0;
|
|
355
|
+
}
|
|
338
356
|
}
|
|
339
|
-
const focusable =
|
|
357
|
+
const focusable = target.at(newIndex);
|
|
340
358
|
if (!focusable) {
|
|
341
359
|
return;
|
|
342
360
|
}
|
|
@@ -359,6 +377,12 @@ var RovingTabIndex = class {
|
|
|
359
377
|
restoreAttributes([focusable]);
|
|
360
378
|
}
|
|
361
379
|
this.#focusables.delete(focusable);
|
|
380
|
+
this.#focusablesByFirstChar.forEach((focusables) => {
|
|
381
|
+
const index = focusables.indexOf(focusable);
|
|
382
|
+
if (index !== -1) {
|
|
383
|
+
focusables.splice(index, 1);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
362
386
|
});
|
|
363
387
|
current.forEach((c) => {
|
|
364
388
|
if (this.#focusables.has(c)) {
|
|
@@ -367,6 +391,19 @@ var RovingTabIndex = class {
|
|
|
367
391
|
this.#focusables.add(c);
|
|
368
392
|
saveAttributes([c], ["tabindex"]);
|
|
369
393
|
c.setAttribute("tabindex", "-1");
|
|
394
|
+
if (!this.#options.typeahead) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const shortcuts = c.ariaKeyShortcuts;
|
|
398
|
+
const keys = (shortcuts?.split(/\s+/) ?? [c.textContent?.trim()[0] ?? ""]).filter((key) => /^\S$/i.test(key)).map((key) => key.toLowerCase());
|
|
399
|
+
keys.forEach((key) => {
|
|
400
|
+
const focusables = this.#focusablesByFirstChar.get(key) ?? [];
|
|
401
|
+
focusables.push(c);
|
|
402
|
+
this.#focusablesByFirstChar.set(key, focusables);
|
|
403
|
+
});
|
|
404
|
+
const first = keys[0];
|
|
405
|
+
saveAttributes([c], ["aria-keyshortcuts"]);
|
|
406
|
+
!shortcuts && first && c.setAttribute("aria-keyshortcuts", first);
|
|
370
407
|
});
|
|
371
408
|
if (active && this.#focusables.has(active)) {
|
|
372
409
|
this.#focusables.forEach((focusable) => {
|
|
@@ -405,7 +442,7 @@ function getActiveElement() {
|
|
|
405
442
|
* Lightweight roving tabindex utility with fully focus management.
|
|
406
443
|
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
407
444
|
*
|
|
408
|
-
* @version 1.
|
|
445
|
+
* @version 1.1.1
|
|
409
446
|
* @author Yusuke Kamiyamane
|
|
410
447
|
* @license MIT
|
|
411
448
|
* @copyright Copyright (c) Yusuke Kamiyamane
|