power-focusable 2.0.3 → 2.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 +4 -1
- package/dist/index.cjs +158 -109
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +158 -109
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Used by `getNextFocusable` and `getPreviousFocusable`.
|
|
|
49
49
|
|
|
50
50
|
If `true`, traverses the composed tree (including shadow DOM; slower)
|
|
51
51
|
|
|
52
|
-
Used by `getFocusables`, `getNextFocusable`, and `
|
|
52
|
+
Used by `getFocusables`, `getNextFocusable`, `getPreviousFocusable`, and `hasFocusables`.
|
|
53
53
|
|
|
54
54
|
### `wrap`
|
|
55
55
|
|
|
@@ -123,6 +123,9 @@ hasFocusable(container);
|
|
|
123
123
|
// => boolean
|
|
124
124
|
//
|
|
125
125
|
// container (optional): HTMLElement (default: <body>)
|
|
126
|
+
|
|
127
|
+
// Traverses the composed tree (including shadow DOM; slower)
|
|
128
|
+
hasFocusable(container, { composed: true });
|
|
126
129
|
```
|
|
127
130
|
|
|
128
131
|
### `isFocusable`
|
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
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"])`;
|
|
5
|
-
var tabIndexCache = /* @__PURE__ */ new WeakMap();
|
|
6
5
|
function getFocusables(container = document.body, options = {}) {
|
|
7
6
|
if (!(container instanceof HTMLElement)) {
|
|
8
7
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
@@ -14,7 +13,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
14
13
|
let walk2 = function(node) {
|
|
15
14
|
if (node instanceof HTMLElement) {
|
|
16
15
|
if (isFocusable(node)) {
|
|
17
|
-
elements.
|
|
16
|
+
elements[elements.length] = node;
|
|
18
17
|
}
|
|
19
18
|
const shadow = node.shadowRoot;
|
|
20
19
|
if (shadow && shadow.mode === "open") {
|
|
@@ -23,75 +22,28 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
23
22
|
} else if (node instanceof HTMLSlotElement) {
|
|
24
23
|
const assigned = node.assignedElements({ flatten: true });
|
|
25
24
|
if (assigned.length > 0) {
|
|
26
|
-
assigned.
|
|
27
|
-
walk2(
|
|
28
|
-
}
|
|
25
|
+
for (let i = 0, l = assigned.length; i < l; i++) {
|
|
26
|
+
walk2(assigned[i]);
|
|
27
|
+
}
|
|
29
28
|
return;
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
|
-
node.childNodes
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const children = node.childNodes;
|
|
32
|
+
for (let i = 0, l = children.length; i < l; i++) {
|
|
33
|
+
walk2(children[i]);
|
|
34
|
+
}
|
|
35
35
|
};
|
|
36
36
|
walk2(container);
|
|
37
37
|
} else {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
function sort(elements2) {
|
|
45
|
-
const ordered = [];
|
|
46
|
-
const natural = [];
|
|
47
|
-
function getTabIndex(element) {
|
|
48
|
-
const cached = tabIndexCache.get(element);
|
|
49
|
-
if (cached !== void 0) {
|
|
50
|
-
return cached;
|
|
51
|
-
}
|
|
52
|
-
const number = Number(element.getAttribute("tabindex"));
|
|
53
|
-
tabIndexCache.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 normalize(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
|
-
}
|
|
76
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
77
|
-
for (const group of map.values()) {
|
|
78
|
-
if (group.length > 0) {
|
|
79
|
-
const enabled = group.filter((radio) => isFocusable(radio));
|
|
80
|
-
if (enabled.length > 0) {
|
|
81
|
-
placeholder.add(
|
|
82
|
-
enabled.find((radio) => radio.checked) ?? enabled[0]
|
|
83
|
-
);
|
|
84
|
-
}
|
|
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;
|
|
85
43
|
}
|
|
86
44
|
}
|
|
87
|
-
return elements2.filter((element) => {
|
|
88
|
-
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
89
|
-
return placeholder.has(element);
|
|
90
|
-
}
|
|
91
|
-
return true;
|
|
92
|
-
});
|
|
93
45
|
}
|
|
94
|
-
return
|
|
46
|
+
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
95
47
|
}
|
|
96
48
|
function getNextFocusable(container = document.body, options = {}) {
|
|
97
49
|
if (!(container instanceof HTMLElement)) {
|
|
@@ -107,77 +59,54 @@ function getPreviousFocusable(container = document.body, options = {}) {
|
|
|
107
59
|
}
|
|
108
60
|
return getRelativeFocusable(container, -1, options);
|
|
109
61
|
}
|
|
110
|
-
function hasFocusable(container = document.body) {
|
|
62
|
+
function hasFocusable(container = document.body, options = {}) {
|
|
111
63
|
if (!(container instanceof HTMLElement)) {
|
|
112
64
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
113
65
|
container = document.body;
|
|
114
66
|
}
|
|
115
|
-
return getFocusables(container).length > 0;
|
|
67
|
+
return getFocusables(container, options).length > 0;
|
|
116
68
|
}
|
|
117
69
|
function isFocusable(element) {
|
|
118
70
|
if (!(element instanceof HTMLElement)) {
|
|
119
71
|
console.warn("Invalid element");
|
|
120
72
|
return false;
|
|
121
73
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for (let current = element2; current; current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode) {
|
|
130
|
-
if (!(current instanceof Element)) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (current === element2 && isFormControl(current) && isDisabled(current)) {
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
if (current.matches("[inert]")) {
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
if (isFormControl(element2) && current.tagName === "FIELDSET" && isDisabled(current)) {
|
|
140
|
-
if (current.querySelector(":scope > legend:first-of-type")?.contains(element2)) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
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;
|
|
145
81
|
}
|
|
82
|
+
}
|
|
83
|
+
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (isDisabledDeep(element)) {
|
|
146
87
|
return false;
|
|
147
88
|
}
|
|
148
|
-
|
|
89
|
+
if (!element.checkVisibility({
|
|
149
90
|
contentVisibilityAuto: true,
|
|
150
91
|
opacityProperty: true,
|
|
151
92
|
visibilityProperty: true
|
|
152
|
-
})
|
|
93
|
+
})) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
153
97
|
}
|
|
154
98
|
function getRelativeFocusable(container, offset = 0, options) {
|
|
155
|
-
const { active, composed = false, wrap = false } = options;
|
|
99
|
+
const { active: a = null, composed = false, wrap = false } = options;
|
|
156
100
|
const focusables = getFocusables(container, { composed });
|
|
157
101
|
const { length } = focusables;
|
|
158
102
|
if (length === 0) {
|
|
159
103
|
return null;
|
|
160
104
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
while (active2 instanceof HTMLElement && active2.shadowRoot?.activeElement) {
|
|
164
|
-
active2 = active2.shadowRoot.activeElement;
|
|
165
|
-
}
|
|
166
|
-
return active2 instanceof HTMLElement ? active2 : null;
|
|
167
|
-
}
|
|
168
|
-
const current = active ?? getActiveElement();
|
|
169
|
-
function containsDeep(container2, node) {
|
|
170
|
-
for (let current2 = node; current2; current2 = !(current2 instanceof ShadowRoot) ? current2.parentNode : current2.mode === "open" ? current2.host : null) {
|
|
171
|
-
if (current2 === container2) {
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
if (!current || !containsDeep(container, current)) {
|
|
105
|
+
const active = a ?? getActiveElement();
|
|
106
|
+
if (active === null || !containsDeep(container, active)) {
|
|
178
107
|
return null;
|
|
179
108
|
}
|
|
180
|
-
const currentIndex = focusables.indexOf(
|
|
109
|
+
const currentIndex = focusables.indexOf(active);
|
|
181
110
|
if (currentIndex === -1) {
|
|
182
111
|
return null;
|
|
183
112
|
}
|
|
@@ -187,12 +116,132 @@ function getRelativeFocusable(container, offset = 0, options) {
|
|
|
187
116
|
}
|
|
188
117
|
return focusables[(offsetIndex + length) % length] ?? null;
|
|
189
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
|
+
}
|
|
190
239
|
/**
|
|
191
240
|
* Power Focusable
|
|
192
241
|
* High-precision focus management utility with shadow DOM support.
|
|
193
242
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
194
243
|
*
|
|
195
|
-
* @version 2.
|
|
244
|
+
* @version 2.1.1
|
|
196
245
|
* @author Yusuke Kamiyamane
|
|
197
246
|
* @license MIT
|
|
198
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.
|
|
6
|
+
* @version 2.1.1
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -17,7 +17,7 @@ interface PowerFocusableOptions {
|
|
|
17
17
|
declare function getFocusables(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): HTMLElement[];
|
|
18
18
|
declare function getNextFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
|
|
19
19
|
declare function getPreviousFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
|
|
20
|
-
declare function hasFocusable(container?: HTMLElement): boolean;
|
|
20
|
+
declare function hasFocusable(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
|
|
21
21
|
declare function isFocusable(element: HTMLElement): boolean;
|
|
22
22
|
|
|
23
23
|
export { type PowerFocusableOptions, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
|
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.
|
|
6
|
+
* @version 2.1.1
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -17,7 +17,7 @@ interface PowerFocusableOptions {
|
|
|
17
17
|
declare function getFocusables(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): HTMLElement[];
|
|
18
18
|
declare function getNextFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
|
|
19
19
|
declare function getPreviousFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
|
|
20
|
-
declare function hasFocusable(container?: HTMLElement): boolean;
|
|
20
|
+
declare function hasFocusable(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
|
|
21
21
|
declare function isFocusable(element: HTMLElement): boolean;
|
|
22
22
|
|
|
23
23
|
export { type PowerFocusableOptions, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
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"])`;
|
|
3
|
-
var tabIndexCache = /* @__PURE__ */ new WeakMap();
|
|
4
3
|
function getFocusables(container = document.body, options = {}) {
|
|
5
4
|
if (!(container instanceof HTMLElement)) {
|
|
6
5
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
@@ -12,7 +11,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
12
11
|
let walk2 = function(node) {
|
|
13
12
|
if (node instanceof HTMLElement) {
|
|
14
13
|
if (isFocusable(node)) {
|
|
15
|
-
elements.
|
|
14
|
+
elements[elements.length] = node;
|
|
16
15
|
}
|
|
17
16
|
const shadow = node.shadowRoot;
|
|
18
17
|
if (shadow && shadow.mode === "open") {
|
|
@@ -21,75 +20,28 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
21
20
|
} else if (node instanceof HTMLSlotElement) {
|
|
22
21
|
const assigned = node.assignedElements({ flatten: true });
|
|
23
22
|
if (assigned.length > 0) {
|
|
24
|
-
assigned.
|
|
25
|
-
walk2(
|
|
26
|
-
}
|
|
23
|
+
for (let i = 0, l = assigned.length; i < l; i++) {
|
|
24
|
+
walk2(assigned[i]);
|
|
25
|
+
}
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
|
-
node.childNodes
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const children = node.childNodes;
|
|
30
|
+
for (let i = 0, l = children.length; i < l; i++) {
|
|
31
|
+
walk2(children[i]);
|
|
32
|
+
}
|
|
33
33
|
};
|
|
34
34
|
walk2(container);
|
|
35
35
|
} else {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
function sort(elements2) {
|
|
43
|
-
const ordered = [];
|
|
44
|
-
const natural = [];
|
|
45
|
-
function getTabIndex(element) {
|
|
46
|
-
const cached = tabIndexCache.get(element);
|
|
47
|
-
if (cached !== void 0) {
|
|
48
|
-
return cached;
|
|
49
|
-
}
|
|
50
|
-
const number = Number(element.getAttribute("tabindex"));
|
|
51
|
-
tabIndexCache.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 normalize(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
|
-
}
|
|
74
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
75
|
-
for (const group of map.values()) {
|
|
76
|
-
if (group.length > 0) {
|
|
77
|
-
const enabled = group.filter((radio) => isFocusable(radio));
|
|
78
|
-
if (enabled.length > 0) {
|
|
79
|
-
placeholder.add(
|
|
80
|
-
enabled.find((radio) => radio.checked) ?? enabled[0]
|
|
81
|
-
);
|
|
82
|
-
}
|
|
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;
|
|
83
41
|
}
|
|
84
42
|
}
|
|
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
43
|
}
|
|
92
|
-
return
|
|
44
|
+
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
93
45
|
}
|
|
94
46
|
function getNextFocusable(container = document.body, options = {}) {
|
|
95
47
|
if (!(container instanceof HTMLElement)) {
|
|
@@ -105,77 +57,54 @@ function getPreviousFocusable(container = document.body, options = {}) {
|
|
|
105
57
|
}
|
|
106
58
|
return getRelativeFocusable(container, -1, options);
|
|
107
59
|
}
|
|
108
|
-
function hasFocusable(container = document.body) {
|
|
60
|
+
function hasFocusable(container = document.body, options = {}) {
|
|
109
61
|
if (!(container instanceof HTMLElement)) {
|
|
110
62
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
111
63
|
container = document.body;
|
|
112
64
|
}
|
|
113
|
-
return getFocusables(container).length > 0;
|
|
65
|
+
return getFocusables(container, options).length > 0;
|
|
114
66
|
}
|
|
115
67
|
function isFocusable(element) {
|
|
116
68
|
if (!(element instanceof HTMLElement)) {
|
|
117
69
|
console.warn("Invalid element");
|
|
118
70
|
return false;
|
|
119
71
|
}
|
|
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
|
-
}
|
|
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;
|
|
143
79
|
}
|
|
80
|
+
}
|
|
81
|
+
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (isDisabledDeep(element)) {
|
|
144
85
|
return false;
|
|
145
86
|
}
|
|
146
|
-
|
|
87
|
+
if (!element.checkVisibility({
|
|
147
88
|
contentVisibilityAuto: true,
|
|
148
89
|
opacityProperty: true,
|
|
149
90
|
visibilityProperty: true
|
|
150
|
-
})
|
|
91
|
+
})) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
151
95
|
}
|
|
152
96
|
function getRelativeFocusable(container, offset = 0, options) {
|
|
153
|
-
const { active, composed = false, wrap = false } = options;
|
|
97
|
+
const { active: a = null, composed = false, wrap = false } = options;
|
|
154
98
|
const focusables = getFocusables(container, { composed });
|
|
155
99
|
const { length } = focusables;
|
|
156
100
|
if (length === 0) {
|
|
157
101
|
return null;
|
|
158
102
|
}
|
|
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)) {
|
|
103
|
+
const active = a ?? getActiveElement();
|
|
104
|
+
if (active === null || !containsDeep(container, active)) {
|
|
176
105
|
return null;
|
|
177
106
|
}
|
|
178
|
-
const currentIndex = focusables.indexOf(
|
|
107
|
+
const currentIndex = focusables.indexOf(active);
|
|
179
108
|
if (currentIndex === -1) {
|
|
180
109
|
return null;
|
|
181
110
|
}
|
|
@@ -185,12 +114,132 @@ function getRelativeFocusable(container, offset = 0, options) {
|
|
|
185
114
|
}
|
|
186
115
|
return focusables[(offsetIndex + length) % length] ?? null;
|
|
187
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
|
+
}
|
|
188
237
|
/**
|
|
189
238
|
* Power Focusable
|
|
190
239
|
* High-precision focus management utility with shadow DOM support.
|
|
191
240
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
192
241
|
*
|
|
193
|
-
* @version 2.
|
|
242
|
+
* @version 2.1.1
|
|
194
243
|
* @author Yusuke Kamiyamane
|
|
195
244
|
* @license MIT
|
|
196
245
|
* @copyright Copyright (c) Yusuke Kamiyamane
|