@y14e/roving-tabindex 3.0.9 → 3.0.11
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 +4 -4
- package/dist/index.bundle.cjs +257 -0
- package/dist/index.bundle.js +255 -0
- package/dist/index.cjs +19 -337
- package/dist/index.d.cts +1 -5
- package/dist/index.d.ts +1 -5
- package/dist/index.js +12 -324
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -10,14 +10,14 @@ npm i @y14e/roving-tabindex
|
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
12
|
// npm
|
|
13
|
-
import { createRovingTabIndex } from '@y14e/roving-tabindex@3.0.
|
|
13
|
+
import { createRovingTabIndex } from '@y14e/roving-tabindex@3.0.10';
|
|
14
14
|
|
|
15
15
|
// CDNs
|
|
16
|
-
import { createRovingTabIndex } from 'https://esm.sh/@y14e/roving-tabindex@3.0.
|
|
16
|
+
import { createRovingTabIndex } from 'https://esm.sh/@y14e/roving-tabindex@3.0.10';
|
|
17
17
|
// or
|
|
18
|
-
import { createRovingTabIndex } from 'https://cdn.jsdelivr.net/npm/@y14e/roving-tabindex@3.0.
|
|
18
|
+
import { createRovingTabIndex } from 'https://cdn.jsdelivr.net/npm/@y14e/roving-tabindex@3.0.10/+esm';
|
|
19
19
|
// or
|
|
20
|
-
import { createRovingTabIndex } from 'https://esm.unpkg.com/@y14e/roving-tabindex@3.0.
|
|
20
|
+
import { createRovingTabIndex } from 'https://esm.unpkg.com/@y14e/roving-tabindex@3.0.10';
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## 📦 APIs
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var attributesUtils = require('@y14e/attributes-utils');
|
|
4
|
+
var powerFocusable = require('power-focusable');
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
function createRovingTabIndex(container, options = {}) {
|
|
8
|
+
if (!(container instanceof Element)) {
|
|
9
|
+
console.warn("Invalid container element");
|
|
10
|
+
return () => {
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const roving = new RovingTabIndex(container, options);
|
|
14
|
+
return () => roving.destroy();
|
|
15
|
+
}
|
|
16
|
+
var RovingTabIndex = class _RovingTabIndex {
|
|
17
|
+
static #initialized = /* @__PURE__ */ new Set();
|
|
18
|
+
#container;
|
|
19
|
+
#settings;
|
|
20
|
+
#focusables = /* @__PURE__ */ new Set();
|
|
21
|
+
#focusablesByFirstChar = /* @__PURE__ */ new Map();
|
|
22
|
+
#selectorFilter;
|
|
23
|
+
#controller = null;
|
|
24
|
+
#isDestroyed = false;
|
|
25
|
+
constructor(container, options = {}) {
|
|
26
|
+
this.#container = container;
|
|
27
|
+
let {
|
|
28
|
+
direction,
|
|
29
|
+
navigationOnly = false,
|
|
30
|
+
noMemory = false,
|
|
31
|
+
noStart = false,
|
|
32
|
+
selector,
|
|
33
|
+
typeahead = false,
|
|
34
|
+
wrap = false
|
|
35
|
+
} = options;
|
|
36
|
+
if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
|
|
37
|
+
console.warn("Invalid direction option. Fallback: both (undefined).");
|
|
38
|
+
direction = void 0;
|
|
39
|
+
}
|
|
40
|
+
if (typeof navigationOnly !== "boolean") {
|
|
41
|
+
console.warn("Invalid navigationOnly option. Fallback: false.");
|
|
42
|
+
navigationOnly = false;
|
|
43
|
+
}
|
|
44
|
+
if (typeof noMemory !== "boolean") {
|
|
45
|
+
console.warn("Invalid noMemory option. Fallback: false.");
|
|
46
|
+
noMemory = false;
|
|
47
|
+
}
|
|
48
|
+
if (typeof noStart !== "boolean") {
|
|
49
|
+
console.warn("Invalid noStart option. Fallback: false.");
|
|
50
|
+
noStart = false;
|
|
51
|
+
}
|
|
52
|
+
if (typeof selector !== "undefined" && (typeof selector !== "string" || !selector.trim())) {
|
|
53
|
+
console.warn(
|
|
54
|
+
"Invalid selector. Fallback: all focusable elements (undefined)."
|
|
55
|
+
);
|
|
56
|
+
selector = void 0;
|
|
57
|
+
}
|
|
58
|
+
if (typeof typeahead !== "boolean") {
|
|
59
|
+
console.warn("Invalid typeahead option. Fallback: false.");
|
|
60
|
+
typeahead = false;
|
|
61
|
+
}
|
|
62
|
+
if (typeof wrap !== "boolean") {
|
|
63
|
+
console.warn("Invalid wrap option. Fallback: false.");
|
|
64
|
+
wrap = false;
|
|
65
|
+
}
|
|
66
|
+
this.#settings = {
|
|
67
|
+
navigationOnly,
|
|
68
|
+
noMemory,
|
|
69
|
+
noStart,
|
|
70
|
+
typeahead,
|
|
71
|
+
wrap
|
|
72
|
+
};
|
|
73
|
+
direction && Object.assign(this.#settings, { direction });
|
|
74
|
+
selector && Object.assign(this.#settings, { selector });
|
|
75
|
+
this.#selectorFilter = this.#createSelectorFilter();
|
|
76
|
+
this.#initialize();
|
|
77
|
+
}
|
|
78
|
+
destroy() {
|
|
79
|
+
if (this.#isDestroyed) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.#isDestroyed = true;
|
|
83
|
+
this.#controller?.abort();
|
|
84
|
+
this.#controller = null;
|
|
85
|
+
attributesUtils.restoreAttributes([...this.#focusables]);
|
|
86
|
+
this.#focusables.clear();
|
|
87
|
+
this.#focusablesByFirstChar.clear();
|
|
88
|
+
}
|
|
89
|
+
#initialize() {
|
|
90
|
+
this.#update(document.activeElement);
|
|
91
|
+
if (!(this.#container instanceof HTMLElement)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.#controller = new AbortController();
|
|
95
|
+
const { signal } = this.#controller;
|
|
96
|
+
this.#container.addEventListener("focusin", this.#onFocusIn, {
|
|
97
|
+
capture: true,
|
|
98
|
+
signal
|
|
99
|
+
});
|
|
100
|
+
this.#container.addEventListener("keydown", this.#onKeyDown, {
|
|
101
|
+
capture: true,
|
|
102
|
+
signal
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
#onFocusIn = (event) => {
|
|
106
|
+
const { target } = event;
|
|
107
|
+
if (!(target instanceof Element)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const isFocusable = this.#focusables.has(target);
|
|
111
|
+
this.#settings.noMemory && !isFocusable ? this.#update(null) : isFocusable && this.#update(target);
|
|
112
|
+
};
|
|
113
|
+
#onKeyDown = (event) => {
|
|
114
|
+
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
115
|
+
if (altKey || ctrlKey || metaKey || shiftKey) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { direction, typeahead, wrap } = this.#settings;
|
|
119
|
+
const isBoth = !direction;
|
|
120
|
+
const isHorizontal = direction === "horizontal";
|
|
121
|
+
if (![
|
|
122
|
+
"End",
|
|
123
|
+
"Home",
|
|
124
|
+
...isBoth ? ["ArrowLeft", "ArrowUp"] : [`Arrow${isHorizontal ? "Left" : "Up"}`],
|
|
125
|
+
...isBoth ? ["ArrowRight", "ArrowDown"] : [`Arrow${isHorizontal ? "Right" : "Down"}`]
|
|
126
|
+
].includes(key)) {
|
|
127
|
+
if (!typeahead || !/^\S$/i.test(key) || !this.#focusablesByFirstChar.has(key.toUpperCase())) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const active = powerFocusable.getActiveElement();
|
|
132
|
+
if (!(active instanceof HTMLElement)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const current = this.#getFocusables();
|
|
136
|
+
if (!current.includes(active)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
const currentIndex = current.indexOf(active);
|
|
141
|
+
let newIndex;
|
|
142
|
+
let target = current;
|
|
143
|
+
switch (key) {
|
|
144
|
+
case "End":
|
|
145
|
+
newIndex = -1;
|
|
146
|
+
break;
|
|
147
|
+
case "Home":
|
|
148
|
+
newIndex = 0;
|
|
149
|
+
break;
|
|
150
|
+
case "ArrowLeft":
|
|
151
|
+
case "ArrowUp": {
|
|
152
|
+
const rawIndex = currentIndex - 1;
|
|
153
|
+
newIndex = wrap ? rawIndex : Math.max(rawIndex, 0);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "ArrowRight":
|
|
157
|
+
case "ArrowDown": {
|
|
158
|
+
const rawIndex = currentIndex + 1;
|
|
159
|
+
newIndex = wrap ? rawIndex % current.length : Math.min(rawIndex, current.length - 1);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
default: {
|
|
163
|
+
target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
|
|
164
|
+
const foundIndex = target.findIndex(
|
|
165
|
+
(focusable2) => current.indexOf(focusable2) > currentIndex
|
|
166
|
+
);
|
|
167
|
+
newIndex = foundIndex >= 0 ? foundIndex : 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const focusable = target.at(newIndex);
|
|
171
|
+
focusable && powerFocusable.focusElement(focusable);
|
|
172
|
+
};
|
|
173
|
+
#update(active) {
|
|
174
|
+
const current = new Set(this.#getFocusables());
|
|
175
|
+
for (const focusable of this.#focusables) {
|
|
176
|
+
if (!current.has(focusable)) {
|
|
177
|
+
focusable.isConnected && attributesUtils.restoreAttributes([focusable]);
|
|
178
|
+
this.#focusables.delete(focusable);
|
|
179
|
+
this.#focusablesByFirstChar.forEach((focusables) => {
|
|
180
|
+
const index = focusables.indexOf(focusable);
|
|
181
|
+
index >= 0 && focusables.splice(index, 1);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const { navigationOnly, noStart, typeahead } = this.#settings;
|
|
186
|
+
for (const focusable of current) {
|
|
187
|
+
if (this.#focusables.has(focusable)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (_RovingTabIndex.#initialized.has(focusable)) {
|
|
191
|
+
throw new TypeError("Already initialized");
|
|
192
|
+
}
|
|
193
|
+
this.#focusables.add(focusable);
|
|
194
|
+
_RovingTabIndex.#initialized.add(focusable);
|
|
195
|
+
if (!navigationOnly) {
|
|
196
|
+
attributesUtils.saveAttributes([focusable], ["tabindex"]);
|
|
197
|
+
focusable.setAttribute("tabindex", "-1");
|
|
198
|
+
}
|
|
199
|
+
if (!typeahead) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const char = focusable.textContent?.trim()?.at(0)?.toUpperCase();
|
|
203
|
+
const value = focusable.ariaKeyShortcuts?.trim();
|
|
204
|
+
const keys = new Set(
|
|
205
|
+
value ? value.split(/\s+/).filter((key) => /^\S$/i.test(key)).map((key) => key.toUpperCase()) : []
|
|
206
|
+
);
|
|
207
|
+
if (char) {
|
|
208
|
+
keys.add(char);
|
|
209
|
+
attributesUtils.saveAttributes([focusable], ["aria-keyshortcuts"]);
|
|
210
|
+
attributesUtils.addTokenToAttribute(focusable, "aria-keyshortcuts", char, {
|
|
211
|
+
caseInsensitive: true
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
keys.forEach((key) => {
|
|
215
|
+
const focusables = this.#focusablesByFirstChar.get(key) ?? [];
|
|
216
|
+
focusables.push(focusable);
|
|
217
|
+
this.#focusablesByFirstChar.set(key, focusables);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (!navigationOnly) {
|
|
221
|
+
if (active && this.#focusables.has(active)) {
|
|
222
|
+
this.#focusables.forEach((focusable) => {
|
|
223
|
+
focusable.setAttribute("tabindex", focusable === active ? "0" : "-1");
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
[...this.#focusables].forEach((focusable, i) => {
|
|
227
|
+
focusable.setAttribute("tabindex", i || noStart ? "-1" : "0");
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
#createSelectorFilter() {
|
|
233
|
+
const { selector } = this.#settings;
|
|
234
|
+
return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
|
|
235
|
+
}
|
|
236
|
+
#getFocusables() {
|
|
237
|
+
return powerFocusable.getFocusables(this.#container, {
|
|
238
|
+
composed: true,
|
|
239
|
+
filter: this.#selectorFilter,
|
|
240
|
+
skipNegativeTabIndexCheck: !this.#settings.navigationOnly,
|
|
241
|
+
skipVisibilityCheck: true
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Roving Tabindex
|
|
247
|
+
* Lightweight roving tabindex utility with fully focus management.
|
|
248
|
+
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
249
|
+
*
|
|
250
|
+
* @version 3.0.11
|
|
251
|
+
* @author Yusuke Kamiyamane
|
|
252
|
+
* @license MIT
|
|
253
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
254
|
+
* @see {@link https://github.com/y14e/roving-tabindex}
|
|
255
|
+
*/
|
|
256
|
+
|
|
257
|
+
exports.createRovingTabIndex = createRovingTabIndex;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { restoreAttributes, saveAttributes, addTokenToAttribute } from '@y14e/attributes-utils';
|
|
2
|
+
import { getActiveElement, focusElement, getFocusables } from 'power-focusable';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
function createRovingTabIndex(container, options = {}) {
|
|
6
|
+
if (!(container instanceof Element)) {
|
|
7
|
+
console.warn("Invalid container element");
|
|
8
|
+
return () => {
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const roving = new RovingTabIndex(container, options);
|
|
12
|
+
return () => roving.destroy();
|
|
13
|
+
}
|
|
14
|
+
var RovingTabIndex = class _RovingTabIndex {
|
|
15
|
+
static #initialized = /* @__PURE__ */ new Set();
|
|
16
|
+
#container;
|
|
17
|
+
#settings;
|
|
18
|
+
#focusables = /* @__PURE__ */ new Set();
|
|
19
|
+
#focusablesByFirstChar = /* @__PURE__ */ new Map();
|
|
20
|
+
#selectorFilter;
|
|
21
|
+
#controller = null;
|
|
22
|
+
#isDestroyed = false;
|
|
23
|
+
constructor(container, options = {}) {
|
|
24
|
+
this.#container = container;
|
|
25
|
+
let {
|
|
26
|
+
direction,
|
|
27
|
+
navigationOnly = false,
|
|
28
|
+
noMemory = false,
|
|
29
|
+
noStart = false,
|
|
30
|
+
selector,
|
|
31
|
+
typeahead = false,
|
|
32
|
+
wrap = false
|
|
33
|
+
} = options;
|
|
34
|
+
if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
|
|
35
|
+
console.warn("Invalid direction option. Fallback: both (undefined).");
|
|
36
|
+
direction = void 0;
|
|
37
|
+
}
|
|
38
|
+
if (typeof navigationOnly !== "boolean") {
|
|
39
|
+
console.warn("Invalid navigationOnly option. Fallback: false.");
|
|
40
|
+
navigationOnly = false;
|
|
41
|
+
}
|
|
42
|
+
if (typeof noMemory !== "boolean") {
|
|
43
|
+
console.warn("Invalid noMemory option. Fallback: false.");
|
|
44
|
+
noMemory = false;
|
|
45
|
+
}
|
|
46
|
+
if (typeof noStart !== "boolean") {
|
|
47
|
+
console.warn("Invalid noStart option. Fallback: false.");
|
|
48
|
+
noStart = false;
|
|
49
|
+
}
|
|
50
|
+
if (typeof selector !== "undefined" && (typeof selector !== "string" || !selector.trim())) {
|
|
51
|
+
console.warn(
|
|
52
|
+
"Invalid selector. Fallback: all focusable elements (undefined)."
|
|
53
|
+
);
|
|
54
|
+
selector = void 0;
|
|
55
|
+
}
|
|
56
|
+
if (typeof typeahead !== "boolean") {
|
|
57
|
+
console.warn("Invalid typeahead option. Fallback: false.");
|
|
58
|
+
typeahead = false;
|
|
59
|
+
}
|
|
60
|
+
if (typeof wrap !== "boolean") {
|
|
61
|
+
console.warn("Invalid wrap option. Fallback: false.");
|
|
62
|
+
wrap = false;
|
|
63
|
+
}
|
|
64
|
+
this.#settings = {
|
|
65
|
+
navigationOnly,
|
|
66
|
+
noMemory,
|
|
67
|
+
noStart,
|
|
68
|
+
typeahead,
|
|
69
|
+
wrap
|
|
70
|
+
};
|
|
71
|
+
direction && Object.assign(this.#settings, { direction });
|
|
72
|
+
selector && Object.assign(this.#settings, { selector });
|
|
73
|
+
this.#selectorFilter = this.#createSelectorFilter();
|
|
74
|
+
this.#initialize();
|
|
75
|
+
}
|
|
76
|
+
destroy() {
|
|
77
|
+
if (this.#isDestroyed) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.#isDestroyed = true;
|
|
81
|
+
this.#controller?.abort();
|
|
82
|
+
this.#controller = null;
|
|
83
|
+
restoreAttributes([...this.#focusables]);
|
|
84
|
+
this.#focusables.clear();
|
|
85
|
+
this.#focusablesByFirstChar.clear();
|
|
86
|
+
}
|
|
87
|
+
#initialize() {
|
|
88
|
+
this.#update(document.activeElement);
|
|
89
|
+
if (!(this.#container instanceof HTMLElement)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.#controller = new AbortController();
|
|
93
|
+
const { signal } = this.#controller;
|
|
94
|
+
this.#container.addEventListener("focusin", this.#onFocusIn, {
|
|
95
|
+
capture: true,
|
|
96
|
+
signal
|
|
97
|
+
});
|
|
98
|
+
this.#container.addEventListener("keydown", this.#onKeyDown, {
|
|
99
|
+
capture: true,
|
|
100
|
+
signal
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
#onFocusIn = (event) => {
|
|
104
|
+
const { target } = event;
|
|
105
|
+
if (!(target instanceof Element)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const isFocusable = this.#focusables.has(target);
|
|
109
|
+
this.#settings.noMemory && !isFocusable ? this.#update(null) : isFocusable && this.#update(target);
|
|
110
|
+
};
|
|
111
|
+
#onKeyDown = (event) => {
|
|
112
|
+
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
113
|
+
if (altKey || ctrlKey || metaKey || shiftKey) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const { direction, typeahead, wrap } = this.#settings;
|
|
117
|
+
const isBoth = !direction;
|
|
118
|
+
const isHorizontal = direction === "horizontal";
|
|
119
|
+
if (![
|
|
120
|
+
"End",
|
|
121
|
+
"Home",
|
|
122
|
+
...isBoth ? ["ArrowLeft", "ArrowUp"] : [`Arrow${isHorizontal ? "Left" : "Up"}`],
|
|
123
|
+
...isBoth ? ["ArrowRight", "ArrowDown"] : [`Arrow${isHorizontal ? "Right" : "Down"}`]
|
|
124
|
+
].includes(key)) {
|
|
125
|
+
if (!typeahead || !/^\S$/i.test(key) || !this.#focusablesByFirstChar.has(key.toUpperCase())) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const active = getActiveElement();
|
|
130
|
+
if (!(active instanceof HTMLElement)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const current = this.#getFocusables();
|
|
134
|
+
if (!current.includes(active)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
const currentIndex = current.indexOf(active);
|
|
139
|
+
let newIndex;
|
|
140
|
+
let target = current;
|
|
141
|
+
switch (key) {
|
|
142
|
+
case "End":
|
|
143
|
+
newIndex = -1;
|
|
144
|
+
break;
|
|
145
|
+
case "Home":
|
|
146
|
+
newIndex = 0;
|
|
147
|
+
break;
|
|
148
|
+
case "ArrowLeft":
|
|
149
|
+
case "ArrowUp": {
|
|
150
|
+
const rawIndex = currentIndex - 1;
|
|
151
|
+
newIndex = wrap ? rawIndex : Math.max(rawIndex, 0);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "ArrowRight":
|
|
155
|
+
case "ArrowDown": {
|
|
156
|
+
const rawIndex = currentIndex + 1;
|
|
157
|
+
newIndex = wrap ? rawIndex % current.length : Math.min(rawIndex, current.length - 1);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
default: {
|
|
161
|
+
target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
|
|
162
|
+
const foundIndex = target.findIndex(
|
|
163
|
+
(focusable2) => current.indexOf(focusable2) > currentIndex
|
|
164
|
+
);
|
|
165
|
+
newIndex = foundIndex >= 0 ? foundIndex : 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const focusable = target.at(newIndex);
|
|
169
|
+
focusable && focusElement(focusable);
|
|
170
|
+
};
|
|
171
|
+
#update(active) {
|
|
172
|
+
const current = new Set(this.#getFocusables());
|
|
173
|
+
for (const focusable of this.#focusables) {
|
|
174
|
+
if (!current.has(focusable)) {
|
|
175
|
+
focusable.isConnected && restoreAttributes([focusable]);
|
|
176
|
+
this.#focusables.delete(focusable);
|
|
177
|
+
this.#focusablesByFirstChar.forEach((focusables) => {
|
|
178
|
+
const index = focusables.indexOf(focusable);
|
|
179
|
+
index >= 0 && focusables.splice(index, 1);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const { navigationOnly, noStart, typeahead } = this.#settings;
|
|
184
|
+
for (const focusable of current) {
|
|
185
|
+
if (this.#focusables.has(focusable)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (_RovingTabIndex.#initialized.has(focusable)) {
|
|
189
|
+
throw new TypeError("Already initialized");
|
|
190
|
+
}
|
|
191
|
+
this.#focusables.add(focusable);
|
|
192
|
+
_RovingTabIndex.#initialized.add(focusable);
|
|
193
|
+
if (!navigationOnly) {
|
|
194
|
+
saveAttributes([focusable], ["tabindex"]);
|
|
195
|
+
focusable.setAttribute("tabindex", "-1");
|
|
196
|
+
}
|
|
197
|
+
if (!typeahead) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const char = focusable.textContent?.trim()?.at(0)?.toUpperCase();
|
|
201
|
+
const value = focusable.ariaKeyShortcuts?.trim();
|
|
202
|
+
const keys = new Set(
|
|
203
|
+
value ? value.split(/\s+/).filter((key) => /^\S$/i.test(key)).map((key) => key.toUpperCase()) : []
|
|
204
|
+
);
|
|
205
|
+
if (char) {
|
|
206
|
+
keys.add(char);
|
|
207
|
+
saveAttributes([focusable], ["aria-keyshortcuts"]);
|
|
208
|
+
addTokenToAttribute(focusable, "aria-keyshortcuts", char, {
|
|
209
|
+
caseInsensitive: true
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
keys.forEach((key) => {
|
|
213
|
+
const focusables = this.#focusablesByFirstChar.get(key) ?? [];
|
|
214
|
+
focusables.push(focusable);
|
|
215
|
+
this.#focusablesByFirstChar.set(key, focusables);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (!navigationOnly) {
|
|
219
|
+
if (active && this.#focusables.has(active)) {
|
|
220
|
+
this.#focusables.forEach((focusable) => {
|
|
221
|
+
focusable.setAttribute("tabindex", focusable === active ? "0" : "-1");
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
[...this.#focusables].forEach((focusable, i) => {
|
|
225
|
+
focusable.setAttribute("tabindex", i || noStart ? "-1" : "0");
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
#createSelectorFilter() {
|
|
231
|
+
const { selector } = this.#settings;
|
|
232
|
+
return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
|
|
233
|
+
}
|
|
234
|
+
#getFocusables() {
|
|
235
|
+
return getFocusables(this.#container, {
|
|
236
|
+
composed: true,
|
|
237
|
+
filter: this.#selectorFilter,
|
|
238
|
+
skipNegativeTabIndexCheck: !this.#settings.navigationOnly,
|
|
239
|
+
skipVisibilityCheck: true
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Roving Tabindex
|
|
245
|
+
* Lightweight roving tabindex utility with fully focus management.
|
|
246
|
+
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
247
|
+
*
|
|
248
|
+
* @version 3.0.11
|
|
249
|
+
* @author Yusuke Kamiyamane
|
|
250
|
+
* @license MIT
|
|
251
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
252
|
+
* @see {@link https://github.com/y14e/roving-tabindex}
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
export { createRovingTabIndex };
|