power-focusable 2.0.2 → 2.1.0
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 -6
- package/dist/index.cjs +156 -105
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +156 -105
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Returns all focusable elements within the container.
|
|
|
67
67
|
getFocusables(container);
|
|
68
68
|
// => HTMLElement[]
|
|
69
69
|
//
|
|
70
|
-
// container (optional): HTMLElement (default:
|
|
70
|
+
// container (optional): HTMLElement (default: <body>)
|
|
71
71
|
|
|
72
72
|
// Traverses the composed tree (including shadow DOM; slower)
|
|
73
73
|
getFocusables(container, { composed: true });
|
|
@@ -81,7 +81,7 @@ Returns the next focusable element within the container, starting from `document
|
|
|
81
81
|
getNextFocusable(container);
|
|
82
82
|
// => HTMLElement | null
|
|
83
83
|
//
|
|
84
|
-
// container (optional): HTMLElement (default:
|
|
84
|
+
// container (optional): HTMLElement (default: <body>)
|
|
85
85
|
|
|
86
86
|
// Specifies the starting element
|
|
87
87
|
getNextFocusable(container, { active: document.querySelector('.button') });
|
|
@@ -89,7 +89,7 @@ getNextFocusable(container, { active: document.querySelector('.button') });
|
|
|
89
89
|
// Traverses the composed tree (including shadow DOM; slower)
|
|
90
90
|
getNextFocusable(container, { composed: true });
|
|
91
91
|
|
|
92
|
-
// Wraps around to the first element when reaching the end
|
|
92
|
+
// Wraps around to the first element when reaching the end
|
|
93
93
|
getNextFocusable(container, { wrap: true });
|
|
94
94
|
```
|
|
95
95
|
|
|
@@ -101,7 +101,7 @@ Returns the previous focusable element within the container, starting from `docu
|
|
|
101
101
|
getPreviousFocusable(container);
|
|
102
102
|
// => HTMLElement | null
|
|
103
103
|
//
|
|
104
|
-
// container (optional): HTMLElement (default:
|
|
104
|
+
// container (optional): HTMLElement (default: <body>)
|
|
105
105
|
|
|
106
106
|
// Specifies the starting element
|
|
107
107
|
getPreviousFocusable(container, { active: document.querySelector('.button') });
|
|
@@ -109,7 +109,7 @@ getPreviousFocusable(container, { active: document.querySelector('.button') });
|
|
|
109
109
|
// Traverses the composed tree (including shadow DOM; slower)
|
|
110
110
|
getPreviousFocusable(container, { composed: true });
|
|
111
111
|
|
|
112
|
-
//Wraps around to the last element when reaching the end
|
|
112
|
+
//Wraps around to the last element when reaching the end
|
|
113
113
|
getPreviousFocusable(container, { wrap: true });
|
|
114
114
|
|
|
115
115
|
```
|
|
@@ -122,7 +122,7 @@ Returns whether the container contains at least one focusable element.
|
|
|
122
122
|
hasFocusable(container);
|
|
123
123
|
// => boolean
|
|
124
124
|
//
|
|
125
|
-
// container (optional): HTMLElement (default:
|
|
125
|
+
// container (optional): HTMLElement (default: <body>)
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
### `isFocusable`
|
package/dist/index.cjs
CHANGED
|
@@ -13,7 +13,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
13
13
|
let walk2 = function(node) {
|
|
14
14
|
if (node instanceof HTMLElement) {
|
|
15
15
|
if (isFocusable(node)) {
|
|
16
|
-
elements.
|
|
16
|
+
elements[elements.length] = node;
|
|
17
17
|
}
|
|
18
18
|
const shadow = node.shadowRoot;
|
|
19
19
|
if (shadow && shadow.mode === "open") {
|
|
@@ -22,74 +22,28 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
22
22
|
} else if (node instanceof HTMLSlotElement) {
|
|
23
23
|
const assigned = node.assignedElements({ flatten: true });
|
|
24
24
|
if (assigned.length > 0) {
|
|
25
|
-
assigned.
|
|
26
|
-
walk2(
|
|
27
|
-
}
|
|
25
|
+
for (let i = 0, l = assigned.length; i < l; i++) {
|
|
26
|
+
walk2(assigned[i]);
|
|
27
|
+
}
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
node.childNodes
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const children = node.childNodes;
|
|
32
|
+
for (let i = 0, l = children.length; i < l; i++) {
|
|
33
|
+
walk2(children[i]);
|
|
34
|
+
}
|
|
34
35
|
};
|
|
35
36
|
walk2(container);
|
|
36
37
|
} else {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
const cache = /* @__PURE__ */ new WeakMap();
|
|
44
|
-
function sort(elements2) {
|
|
45
|
-
const ordered = [];
|
|
46
|
-
const natural = [];
|
|
47
|
-
function getTabIndex(element) {
|
|
48
|
-
const cached = cache.get(element);
|
|
49
|
-
if (cached !== void 0) {
|
|
50
|
-
return cached;
|
|
38
|
+
const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
39
|
+
for (let i = 0, l = candidates.length; i < l; i++) {
|
|
40
|
+
const candidate = candidates[i];
|
|
41
|
+
if (isFocusable(candidate)) {
|
|
42
|
+
elements[elements.length] = candidate;
|
|
51
43
|
}
|
|
52
|
-
const number = Number(element.getAttribute("tabindex"));
|
|
53
|
-
cache.set(element, number);
|
|
54
|
-
return number;
|
|
55
|
-
}
|
|
56
|
-
elements2.forEach((element) => {
|
|
57
|
-
(getTabIndex(element) > 0 ? ordered : natural).push(element);
|
|
58
|
-
});
|
|
59
|
-
ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
|
|
60
|
-
return [...ordered, ...natural];
|
|
61
|
-
}
|
|
62
|
-
function normalizeRadioGroup(elements2) {
|
|
63
|
-
let map = null;
|
|
64
|
-
for (const element of elements2) {
|
|
65
|
-
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
66
|
-
if (!map) {
|
|
67
|
-
map = /* @__PURE__ */ new Map();
|
|
68
|
-
}
|
|
69
|
-
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
70
|
-
(map.get(key) ?? map.set(key, []).get(key)).push(element);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (!map) {
|
|
74
|
-
return elements2;
|
|
75
44
|
}
|
|
76
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
77
|
-
for (const radios of map.values()) {
|
|
78
|
-
if (radios.length > 0) {
|
|
79
|
-
const enabled = radios.filter((radio) => isFocusable(radio));
|
|
80
|
-
if (enabled.length > 0) {
|
|
81
|
-
placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return elements2.filter((element) => {
|
|
86
|
-
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
87
|
-
return placeholder.has(element);
|
|
88
|
-
}
|
|
89
|
-
return true;
|
|
90
|
-
});
|
|
91
45
|
}
|
|
92
|
-
return normalizeRadioGroup(
|
|
46
|
+
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
93
47
|
}
|
|
94
48
|
function getNextFocusable(container = document.body, options = {}) {
|
|
95
49
|
if (!(container instanceof HTMLElement)) {
|
|
@@ -117,65 +71,42 @@ function isFocusable(element) {
|
|
|
117
71
|
console.warn("Invalid element");
|
|
118
72
|
return false;
|
|
119
73
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for (let current = element2; current; current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode) {
|
|
128
|
-
if (!(current instanceof Element)) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (current === element2 && isFormControl(current) && isDisabled(current)) {
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
if (current.matches("[inert]")) {
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
if (isFormControl(element2) && current.tagName === "FIELDSET" && isDisabled(current)) {
|
|
138
|
-
if (current.querySelector(":scope > legend:first-of-type")?.contains(element2)) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
74
|
+
if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const tabIndex = element.getAttribute("tabindex");
|
|
78
|
+
if (tabIndex !== null) {
|
|
79
|
+
if (Number(tabIndex) < 0) {
|
|
80
|
+
return false;
|
|
143
81
|
}
|
|
82
|
+
}
|
|
83
|
+
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
144
84
|
return false;
|
|
145
85
|
}
|
|
146
|
-
|
|
86
|
+
if (isDisabledDeep(element)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (!element.checkVisibility({
|
|
147
90
|
contentVisibilityAuto: true,
|
|
148
91
|
opacityProperty: true,
|
|
149
92
|
visibilityProperty: true
|
|
150
|
-
})
|
|
93
|
+
})) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
151
97
|
}
|
|
152
98
|
function getRelativeFocusable(container, offset = 0, options) {
|
|
153
|
-
const { active, composed = false, wrap = false } = options;
|
|
99
|
+
const { active: a = null, composed = false, wrap = false } = options;
|
|
154
100
|
const focusables = getFocusables(container, { composed });
|
|
155
101
|
const { length } = focusables;
|
|
156
102
|
if (length === 0) {
|
|
157
103
|
return null;
|
|
158
104
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
while (active2 instanceof HTMLElement && active2.shadowRoot?.activeElement) {
|
|
162
|
-
active2 = active2.shadowRoot.activeElement;
|
|
163
|
-
}
|
|
164
|
-
return active2 instanceof HTMLElement ? active2 : null;
|
|
165
|
-
}
|
|
166
|
-
const current = active ?? getActiveElement();
|
|
167
|
-
function containsDeep(container2, node) {
|
|
168
|
-
for (let current2 = node; current2; current2 = !(current2 instanceof ShadowRoot) ? current2.parentNode : current2.mode === "open" ? current2.host : null) {
|
|
169
|
-
if (current2 === container2) {
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
if (!current || !containsDeep(container, current)) {
|
|
105
|
+
const active = a ?? getActiveElement();
|
|
106
|
+
if (active === null || !containsDeep(container, active)) {
|
|
176
107
|
return null;
|
|
177
108
|
}
|
|
178
|
-
const currentIndex = focusables.indexOf(
|
|
109
|
+
const currentIndex = focusables.indexOf(active);
|
|
179
110
|
if (currentIndex === -1) {
|
|
180
111
|
return null;
|
|
181
112
|
}
|
|
@@ -185,12 +116,132 @@ function getRelativeFocusable(container, offset = 0, options) {
|
|
|
185
116
|
}
|
|
186
117
|
return focusables[(offsetIndex + length) % length] ?? null;
|
|
187
118
|
}
|
|
119
|
+
function containsDeep(container, element) {
|
|
120
|
+
function walk(node) {
|
|
121
|
+
if (node === null) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (node === container) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (node instanceof ShadowRoot) {
|
|
128
|
+
return node.mode === "open" ? walk(node.host) : false;
|
|
129
|
+
}
|
|
130
|
+
return walk(node.parentNode);
|
|
131
|
+
}
|
|
132
|
+
return walk(element);
|
|
133
|
+
}
|
|
134
|
+
function getActiveElement() {
|
|
135
|
+
function walk(node) {
|
|
136
|
+
if (node === null) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const active = node.shadowRoot?.activeElement;
|
|
140
|
+
return active ? walk(active) : node;
|
|
141
|
+
}
|
|
142
|
+
return walk(document.activeElement);
|
|
143
|
+
}
|
|
144
|
+
var tabIndexCache = /* @__PURE__ */ new WeakMap();
|
|
145
|
+
function getTabIndex(element) {
|
|
146
|
+
const cached = tabIndexCache.get(element);
|
|
147
|
+
if (cached !== void 0) {
|
|
148
|
+
return cached;
|
|
149
|
+
}
|
|
150
|
+
const number = Number(element.getAttribute("tabindex"));
|
|
151
|
+
tabIndexCache.set(element, number);
|
|
152
|
+
return number;
|
|
153
|
+
}
|
|
154
|
+
function isDisabled(element) {
|
|
155
|
+
return "disabled" in element && element.disabled;
|
|
156
|
+
}
|
|
157
|
+
function isDisabledDeep(element) {
|
|
158
|
+
function walk(node) {
|
|
159
|
+
if (node === null) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (node instanceof ShadowRoot) {
|
|
163
|
+
return node.mode === "open" ? walk(node.host) : false;
|
|
164
|
+
}
|
|
165
|
+
if (!(node instanceof Element)) {
|
|
166
|
+
return walk(node.parentNode);
|
|
167
|
+
}
|
|
168
|
+
if (node === element && isFormControl(node) && isDisabled(node)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (node.hasAttribute("inert")) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (isFormControl(element) && node.tagName === "FIELDSET" && isDisabled(node)) {
|
|
175
|
+
if (node.querySelector(":scope > legend:first-of-type")?.contains(element)) {
|
|
176
|
+
return walk(node.parentNode);
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return walk(node.parentNode);
|
|
181
|
+
}
|
|
182
|
+
return walk(element);
|
|
183
|
+
}
|
|
184
|
+
function isFormControl(element) {
|
|
185
|
+
const tagName = element.tagName;
|
|
186
|
+
return tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
|
|
187
|
+
}
|
|
188
|
+
function normalizeRadioGroup(elements) {
|
|
189
|
+
let map = null;
|
|
190
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
191
|
+
const element = elements[i];
|
|
192
|
+
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
193
|
+
if (!map) {
|
|
194
|
+
map = /* @__PURE__ */ new Map();
|
|
195
|
+
}
|
|
196
|
+
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
197
|
+
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
198
|
+
group[group.length] = element;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!map) {
|
|
202
|
+
return elements;
|
|
203
|
+
}
|
|
204
|
+
const placeholder = /* @__PURE__ */ new Set();
|
|
205
|
+
for (const group of map.values()) {
|
|
206
|
+
if (group.length > 0) {
|
|
207
|
+
const enabled = group.filter(isFocusable);
|
|
208
|
+
if (enabled.length > 0) {
|
|
209
|
+
placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return elements.filter((element) => {
|
|
214
|
+
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
215
|
+
return placeholder.has(element);
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function sortByTabIndex(elements) {
|
|
221
|
+
const ordered = [];
|
|
222
|
+
const natural = [];
|
|
223
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
224
|
+
const element = elements[i];
|
|
225
|
+
const target = getTabIndex(element) > 0 ? ordered : natural;
|
|
226
|
+
target[target.length] = element;
|
|
227
|
+
}
|
|
228
|
+
ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
|
|
229
|
+
let count = 0;
|
|
230
|
+
const sorted = new Array(ordered.length + natural.length);
|
|
231
|
+
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
232
|
+
sorted[count++] = ordered[i];
|
|
233
|
+
}
|
|
234
|
+
for (let i = 0, l = natural.length; i < l; i++) {
|
|
235
|
+
sorted[count++] = natural[i];
|
|
236
|
+
}
|
|
237
|
+
return sorted;
|
|
238
|
+
}
|
|
188
239
|
/**
|
|
189
240
|
* Power Focusable
|
|
190
241
|
* High-precision focus management utility with shadow DOM support.
|
|
191
242
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
192
243
|
*
|
|
193
|
-
* @version 2.0
|
|
244
|
+
* @version 2.1.0
|
|
194
245
|
* @author Yusuke Kamiyamane
|
|
195
246
|
* @license MIT
|
|
196
247
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.d.cts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* High-precision focus management utility with shadow DOM support.
|
|
4
4
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
5
5
|
*
|
|
6
|
-
* @version 2.0
|
|
6
|
+
* @version 2.1.0
|
|
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
|
* High-precision focus management utility with shadow DOM support.
|
|
4
4
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
5
5
|
*
|
|
6
|
-
* @version 2.0
|
|
6
|
+
* @version 2.1.0
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
11
11
|
let walk2 = function(node) {
|
|
12
12
|
if (node instanceof HTMLElement) {
|
|
13
13
|
if (isFocusable(node)) {
|
|
14
|
-
elements.
|
|
14
|
+
elements[elements.length] = node;
|
|
15
15
|
}
|
|
16
16
|
const shadow = node.shadowRoot;
|
|
17
17
|
if (shadow && shadow.mode === "open") {
|
|
@@ -20,74 +20,28 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
20
20
|
} else if (node instanceof HTMLSlotElement) {
|
|
21
21
|
const assigned = node.assignedElements({ flatten: true });
|
|
22
22
|
if (assigned.length > 0) {
|
|
23
|
-
assigned.
|
|
24
|
-
walk2(
|
|
25
|
-
}
|
|
23
|
+
for (let i = 0, l = assigned.length; i < l; i++) {
|
|
24
|
+
walk2(assigned[i]);
|
|
25
|
+
}
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
node.childNodes
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const children = node.childNodes;
|
|
30
|
+
for (let i = 0, l = children.length; i < l; i++) {
|
|
31
|
+
walk2(children[i]);
|
|
32
|
+
}
|
|
32
33
|
};
|
|
33
34
|
walk2(container);
|
|
34
35
|
} else {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
const cache = /* @__PURE__ */ new WeakMap();
|
|
42
|
-
function sort(elements2) {
|
|
43
|
-
const ordered = [];
|
|
44
|
-
const natural = [];
|
|
45
|
-
function getTabIndex(element) {
|
|
46
|
-
const cached = cache.get(element);
|
|
47
|
-
if (cached !== void 0) {
|
|
48
|
-
return cached;
|
|
36
|
+
const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
37
|
+
for (let i = 0, l = candidates.length; i < l; i++) {
|
|
38
|
+
const candidate = candidates[i];
|
|
39
|
+
if (isFocusable(candidate)) {
|
|
40
|
+
elements[elements.length] = candidate;
|
|
49
41
|
}
|
|
50
|
-
const number = Number(element.getAttribute("tabindex"));
|
|
51
|
-
cache.set(element, number);
|
|
52
|
-
return number;
|
|
53
|
-
}
|
|
54
|
-
elements2.forEach((element) => {
|
|
55
|
-
(getTabIndex(element) > 0 ? ordered : natural).push(element);
|
|
56
|
-
});
|
|
57
|
-
ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
|
|
58
|
-
return [...ordered, ...natural];
|
|
59
|
-
}
|
|
60
|
-
function normalizeRadioGroup(elements2) {
|
|
61
|
-
let map = null;
|
|
62
|
-
for (const element of elements2) {
|
|
63
|
-
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
64
|
-
if (!map) {
|
|
65
|
-
map = /* @__PURE__ */ new Map();
|
|
66
|
-
}
|
|
67
|
-
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
68
|
-
(map.get(key) ?? map.set(key, []).get(key)).push(element);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (!map) {
|
|
72
|
-
return elements2;
|
|
73
42
|
}
|
|
74
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
75
|
-
for (const radios of map.values()) {
|
|
76
|
-
if (radios.length > 0) {
|
|
77
|
-
const enabled = radios.filter((radio) => isFocusable(radio));
|
|
78
|
-
if (enabled.length > 0) {
|
|
79
|
-
placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return elements2.filter((element) => {
|
|
84
|
-
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
85
|
-
return placeholder.has(element);
|
|
86
|
-
}
|
|
87
|
-
return true;
|
|
88
|
-
});
|
|
89
43
|
}
|
|
90
|
-
return normalizeRadioGroup(
|
|
44
|
+
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
91
45
|
}
|
|
92
46
|
function getNextFocusable(container = document.body, options = {}) {
|
|
93
47
|
if (!(container instanceof HTMLElement)) {
|
|
@@ -115,65 +69,42 @@ function isFocusable(element) {
|
|
|
115
69
|
console.warn("Invalid element");
|
|
116
70
|
return false;
|
|
117
71
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
for (let current = element2; current; current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode) {
|
|
126
|
-
if (!(current instanceof Element)) {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (current === element2 && isFormControl(current) && isDisabled(current)) {
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
if (current.matches("[inert]")) {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
if (isFormControl(element2) && current.tagName === "FIELDSET" && isDisabled(current)) {
|
|
136
|
-
if (current.querySelector(":scope > legend:first-of-type")?.contains(element2)) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
72
|
+
if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const tabIndex = element.getAttribute("tabindex");
|
|
76
|
+
if (tabIndex !== null) {
|
|
77
|
+
if (Number(tabIndex) < 0) {
|
|
78
|
+
return false;
|
|
141
79
|
}
|
|
80
|
+
}
|
|
81
|
+
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
142
82
|
return false;
|
|
143
83
|
}
|
|
144
|
-
|
|
84
|
+
if (isDisabledDeep(element)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (!element.checkVisibility({
|
|
145
88
|
contentVisibilityAuto: true,
|
|
146
89
|
opacityProperty: true,
|
|
147
90
|
visibilityProperty: true
|
|
148
|
-
})
|
|
91
|
+
})) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
149
95
|
}
|
|
150
96
|
function getRelativeFocusable(container, offset = 0, options) {
|
|
151
|
-
const { active, composed = false, wrap = false } = options;
|
|
97
|
+
const { active: a = null, composed = false, wrap = false } = options;
|
|
152
98
|
const focusables = getFocusables(container, { composed });
|
|
153
99
|
const { length } = focusables;
|
|
154
100
|
if (length === 0) {
|
|
155
101
|
return null;
|
|
156
102
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
while (active2 instanceof HTMLElement && active2.shadowRoot?.activeElement) {
|
|
160
|
-
active2 = active2.shadowRoot.activeElement;
|
|
161
|
-
}
|
|
162
|
-
return active2 instanceof HTMLElement ? active2 : null;
|
|
163
|
-
}
|
|
164
|
-
const current = active ?? getActiveElement();
|
|
165
|
-
function containsDeep(container2, node) {
|
|
166
|
-
for (let current2 = node; current2; current2 = !(current2 instanceof ShadowRoot) ? current2.parentNode : current2.mode === "open" ? current2.host : null) {
|
|
167
|
-
if (current2 === container2) {
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
if (!current || !containsDeep(container, current)) {
|
|
103
|
+
const active = a ?? getActiveElement();
|
|
104
|
+
if (active === null || !containsDeep(container, active)) {
|
|
174
105
|
return null;
|
|
175
106
|
}
|
|
176
|
-
const currentIndex = focusables.indexOf(
|
|
107
|
+
const currentIndex = focusables.indexOf(active);
|
|
177
108
|
if (currentIndex === -1) {
|
|
178
109
|
return null;
|
|
179
110
|
}
|
|
@@ -183,12 +114,132 @@ function getRelativeFocusable(container, offset = 0, options) {
|
|
|
183
114
|
}
|
|
184
115
|
return focusables[(offsetIndex + length) % length] ?? null;
|
|
185
116
|
}
|
|
117
|
+
function containsDeep(container, element) {
|
|
118
|
+
function walk(node) {
|
|
119
|
+
if (node === null) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (node === container) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (node instanceof ShadowRoot) {
|
|
126
|
+
return node.mode === "open" ? walk(node.host) : false;
|
|
127
|
+
}
|
|
128
|
+
return walk(node.parentNode);
|
|
129
|
+
}
|
|
130
|
+
return walk(element);
|
|
131
|
+
}
|
|
132
|
+
function getActiveElement() {
|
|
133
|
+
function walk(node) {
|
|
134
|
+
if (node === null) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const active = node.shadowRoot?.activeElement;
|
|
138
|
+
return active ? walk(active) : node;
|
|
139
|
+
}
|
|
140
|
+
return walk(document.activeElement);
|
|
141
|
+
}
|
|
142
|
+
var tabIndexCache = /* @__PURE__ */ new WeakMap();
|
|
143
|
+
function getTabIndex(element) {
|
|
144
|
+
const cached = tabIndexCache.get(element);
|
|
145
|
+
if (cached !== void 0) {
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
const number = Number(element.getAttribute("tabindex"));
|
|
149
|
+
tabIndexCache.set(element, number);
|
|
150
|
+
return number;
|
|
151
|
+
}
|
|
152
|
+
function isDisabled(element) {
|
|
153
|
+
return "disabled" in element && element.disabled;
|
|
154
|
+
}
|
|
155
|
+
function isDisabledDeep(element) {
|
|
156
|
+
function walk(node) {
|
|
157
|
+
if (node === null) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if (node instanceof ShadowRoot) {
|
|
161
|
+
return node.mode === "open" ? walk(node.host) : false;
|
|
162
|
+
}
|
|
163
|
+
if (!(node instanceof Element)) {
|
|
164
|
+
return walk(node.parentNode);
|
|
165
|
+
}
|
|
166
|
+
if (node === element && isFormControl(node) && isDisabled(node)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (node.hasAttribute("inert")) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if (isFormControl(element) && node.tagName === "FIELDSET" && isDisabled(node)) {
|
|
173
|
+
if (node.querySelector(":scope > legend:first-of-type")?.contains(element)) {
|
|
174
|
+
return walk(node.parentNode);
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return walk(node.parentNode);
|
|
179
|
+
}
|
|
180
|
+
return walk(element);
|
|
181
|
+
}
|
|
182
|
+
function isFormControl(element) {
|
|
183
|
+
const tagName = element.tagName;
|
|
184
|
+
return tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
|
|
185
|
+
}
|
|
186
|
+
function normalizeRadioGroup(elements) {
|
|
187
|
+
let map = null;
|
|
188
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
189
|
+
const element = elements[i];
|
|
190
|
+
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
191
|
+
if (!map) {
|
|
192
|
+
map = /* @__PURE__ */ new Map();
|
|
193
|
+
}
|
|
194
|
+
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
195
|
+
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
196
|
+
group[group.length] = element;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!map) {
|
|
200
|
+
return elements;
|
|
201
|
+
}
|
|
202
|
+
const placeholder = /* @__PURE__ */ new Set();
|
|
203
|
+
for (const group of map.values()) {
|
|
204
|
+
if (group.length > 0) {
|
|
205
|
+
const enabled = group.filter(isFocusable);
|
|
206
|
+
if (enabled.length > 0) {
|
|
207
|
+
placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return elements.filter((element) => {
|
|
212
|
+
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
213
|
+
return placeholder.has(element);
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function sortByTabIndex(elements) {
|
|
219
|
+
const ordered = [];
|
|
220
|
+
const natural = [];
|
|
221
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
222
|
+
const element = elements[i];
|
|
223
|
+
const target = getTabIndex(element) > 0 ? ordered : natural;
|
|
224
|
+
target[target.length] = element;
|
|
225
|
+
}
|
|
226
|
+
ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
|
|
227
|
+
let count = 0;
|
|
228
|
+
const sorted = new Array(ordered.length + natural.length);
|
|
229
|
+
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
230
|
+
sorted[count++] = ordered[i];
|
|
231
|
+
}
|
|
232
|
+
for (let i = 0, l = natural.length; i < l; i++) {
|
|
233
|
+
sorted[count++] = natural[i];
|
|
234
|
+
}
|
|
235
|
+
return sorted;
|
|
236
|
+
}
|
|
186
237
|
/**
|
|
187
238
|
* Power Focusable
|
|
188
239
|
* High-precision focus management utility with shadow DOM support.
|
|
189
240
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
190
241
|
*
|
|
191
|
-
* @version 2.0
|
|
242
|
+
* @version 2.1.0
|
|
192
243
|
* @author Yusuke Kamiyamane
|
|
193
244
|
* @license MIT
|
|
194
245
|
* @copyright Copyright (c) Yusuke Kamiyamane
|