power-focusable 2.1.13 → 2.2.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/dist/index.cjs +95 -87
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +95 -87
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
5
|
function getFocusables(container = document.body, options = {}) {
|
|
6
|
-
if (!(container instanceof
|
|
6
|
+
if (!(container instanceof Element)) {
|
|
7
7
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
8
8
|
container = document.body;
|
|
9
9
|
}
|
|
@@ -11,32 +11,12 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
11
11
|
const elements = [];
|
|
12
12
|
if (composed) {
|
|
13
13
|
let traverse2 = function(node) {
|
|
14
|
-
if (node instanceof
|
|
14
|
+
if (node instanceof Element) {
|
|
15
15
|
if (isFocusable(node)) {
|
|
16
16
|
elements[elements.length] = node;
|
|
17
17
|
}
|
|
18
|
-
const shadow = node.shadowRoot;
|
|
19
|
-
if (shadow && shadow.mode === "open") {
|
|
20
|
-
traverse2(shadow);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (node instanceof HTMLSlotElement) {
|
|
24
|
-
const assigneds = node.assignedElements({ flatten: true });
|
|
25
|
-
if (assigneds.length) {
|
|
26
|
-
for (let i = 0, l = assigneds.length; i < l; i++) {
|
|
27
|
-
const assigned = assigneds[i];
|
|
28
|
-
if (!assigned) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
traverse2(assigned);
|
|
32
|
-
}
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (!(node instanceof Element || node instanceof ShadowRoot)) {
|
|
37
|
-
return;
|
|
38
18
|
}
|
|
39
|
-
const children = node
|
|
19
|
+
const children = getComposedChildren(node);
|
|
40
20
|
for (let i = 0, l = children.length; i < l; i++) {
|
|
41
21
|
const child = children[i];
|
|
42
22
|
if (!child) {
|
|
@@ -50,7 +30,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
50
30
|
const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
51
31
|
for (let i = 0, l = candidates.length; i < l; i++) {
|
|
52
32
|
const candidate = candidates[i];
|
|
53
|
-
if (!(candidate instanceof
|
|
33
|
+
if (!(candidate instanceof Element)) {
|
|
54
34
|
continue;
|
|
55
35
|
}
|
|
56
36
|
if (isFocusable(candidate)) {
|
|
@@ -61,35 +41,35 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
61
41
|
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
62
42
|
}
|
|
63
43
|
function getNextFocusable(container = document.body, options = {}) {
|
|
64
|
-
if (!(container instanceof
|
|
44
|
+
if (!(container instanceof Element)) {
|
|
65
45
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
66
46
|
container = document.body;
|
|
67
47
|
}
|
|
68
48
|
return getRelativeFocusable(container, 1, options);
|
|
69
49
|
}
|
|
70
50
|
function getPreviousFocusable(container = document.body, options = {}) {
|
|
71
|
-
if (!(container instanceof
|
|
51
|
+
if (!(container instanceof Element)) {
|
|
72
52
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
73
53
|
container = document.body;
|
|
74
54
|
}
|
|
75
55
|
return getRelativeFocusable(container, -1, options);
|
|
76
56
|
}
|
|
77
57
|
function hasFocusable(container = document.body, options = {}) {
|
|
78
|
-
if (!(container instanceof
|
|
58
|
+
if (!(container instanceof Element)) {
|
|
79
59
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
80
60
|
container = document.body;
|
|
81
61
|
}
|
|
82
62
|
return !!getFocusables(container, options).length;
|
|
83
63
|
}
|
|
84
64
|
function isFocusable(element) {
|
|
85
|
-
if (!(element instanceof
|
|
65
|
+
if (!(element instanceof Element)) {
|
|
86
66
|
console.warn("Invalid element");
|
|
87
67
|
return false;
|
|
88
68
|
}
|
|
89
|
-
if (element.hidden || element.inert) {
|
|
69
|
+
if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
|
|
90
70
|
return false;
|
|
91
71
|
}
|
|
92
|
-
if (element
|
|
72
|
+
if (getTabIndex(element) < 0) {
|
|
93
73
|
return false;
|
|
94
74
|
}
|
|
95
75
|
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
@@ -107,6 +87,24 @@ function isFocusable(element) {
|
|
|
107
87
|
}
|
|
108
88
|
return true;
|
|
109
89
|
}
|
|
90
|
+
function getComposedChildren(node) {
|
|
91
|
+
if (node instanceof ShadowRoot) {
|
|
92
|
+
return getChildren(node);
|
|
93
|
+
}
|
|
94
|
+
if (!(node instanceof Element)) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
if (node instanceof HTMLSlotElement) {
|
|
98
|
+
const assigned = node.assignedElements({ flatten: true });
|
|
99
|
+
if (assigned.length) {
|
|
100
|
+
return assigned;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
|
|
104
|
+
return getChildren(node.shadowRoot);
|
|
105
|
+
}
|
|
106
|
+
return getChildren(node);
|
|
107
|
+
}
|
|
110
108
|
function getRelativeFocusable(container, offset, options) {
|
|
111
109
|
const {
|
|
112
110
|
active = getActiveElement(),
|
|
@@ -121,7 +119,7 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
121
119
|
if (!active || !containsDeep(container, active)) {
|
|
122
120
|
return null;
|
|
123
121
|
}
|
|
124
|
-
if (!(active instanceof
|
|
122
|
+
if (!(active instanceof Element)) {
|
|
125
123
|
return null;
|
|
126
124
|
}
|
|
127
125
|
const currentIndex = focusables.indexOf(active);
|
|
@@ -134,6 +132,61 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
134
132
|
}
|
|
135
133
|
return focusables[(offsetIndex + length) % length] ?? null;
|
|
136
134
|
}
|
|
135
|
+
function normalizeRadioGroup(elements) {
|
|
136
|
+
let map = null;
|
|
137
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
138
|
+
const element = elements[i];
|
|
139
|
+
if (!(element instanceof HTMLInputElement)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!isUngroupedRadio(element)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!map) {
|
|
146
|
+
map = /* @__PURE__ */ new Map();
|
|
147
|
+
}
|
|
148
|
+
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
149
|
+
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
150
|
+
if (group) {
|
|
151
|
+
group[group.length] = element;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!map) {
|
|
155
|
+
return elements;
|
|
156
|
+
}
|
|
157
|
+
const placeholder = /* @__PURE__ */ new Set();
|
|
158
|
+
for (const group of map.values()) {
|
|
159
|
+
placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
|
|
160
|
+
}
|
|
161
|
+
return elements.filter((element) => {
|
|
162
|
+
if (isUngroupedRadio(element)) {
|
|
163
|
+
return placeholder.has(element);
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function sortByTabIndex(elements) {
|
|
169
|
+
const ordered = [];
|
|
170
|
+
const natural = [];
|
|
171
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
172
|
+
const element = elements[i];
|
|
173
|
+
if (!element) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const target = getTabIndex(element) > 0 ? ordered : natural;
|
|
177
|
+
target[target.length] = element;
|
|
178
|
+
}
|
|
179
|
+
ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
|
|
180
|
+
let count = 0;
|
|
181
|
+
const sorted = new Array(ordered.length + natural.length);
|
|
182
|
+
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
183
|
+
sorted[count++] = ordered[i];
|
|
184
|
+
}
|
|
185
|
+
for (let i = 0, l = natural.length; i < l; i++) {
|
|
186
|
+
sorted[count++] = natural[i];
|
|
187
|
+
}
|
|
188
|
+
return sorted;
|
|
189
|
+
}
|
|
137
190
|
function containsDeep(container, element) {
|
|
138
191
|
let current = element;
|
|
139
192
|
while (current) {
|
|
@@ -151,6 +204,16 @@ function getActiveElement() {
|
|
|
151
204
|
}
|
|
152
205
|
return current;
|
|
153
206
|
}
|
|
207
|
+
function getChildren(node) {
|
|
208
|
+
const elements = [];
|
|
209
|
+
for (let child = node.firstElementChild; child; child = child.nextElementSibling) {
|
|
210
|
+
elements[elements.length] = child;
|
|
211
|
+
}
|
|
212
|
+
return elements;
|
|
213
|
+
}
|
|
214
|
+
function getTabIndex(element) {
|
|
215
|
+
return "tabIndex" in element ? Number(element.tabIndex) : 0;
|
|
216
|
+
}
|
|
154
217
|
function isDisabled(element) {
|
|
155
218
|
return "disabled" in element && !!element.disabled;
|
|
156
219
|
}
|
|
@@ -190,67 +253,12 @@ function isFormControl(element) {
|
|
|
190
253
|
function isUngroupedRadio(element) {
|
|
191
254
|
return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
192
255
|
}
|
|
193
|
-
function normalizeRadioGroup(elements) {
|
|
194
|
-
let map = null;
|
|
195
|
-
for (let i = 0, l = elements.length; i < l; i++) {
|
|
196
|
-
const element = elements[i];
|
|
197
|
-
if (!(element instanceof HTMLInputElement)) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (!isUngroupedRadio(element)) {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (!map) {
|
|
204
|
-
map = /* @__PURE__ */ new Map();
|
|
205
|
-
}
|
|
206
|
-
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
207
|
-
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
208
|
-
if (group) {
|
|
209
|
-
group[group.length] = element;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (!map) {
|
|
213
|
-
return elements;
|
|
214
|
-
}
|
|
215
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
216
|
-
for (const group of map.values()) {
|
|
217
|
-
placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
|
|
218
|
-
}
|
|
219
|
-
return elements.filter((element) => {
|
|
220
|
-
if (isUngroupedRadio(element)) {
|
|
221
|
-
return placeholder.has(element);
|
|
222
|
-
}
|
|
223
|
-
return true;
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
function sortByTabIndex(elements) {
|
|
227
|
-
const ordered = [];
|
|
228
|
-
const natural = [];
|
|
229
|
-
for (let i = 0, l = elements.length; i < l; i++) {
|
|
230
|
-
const element = elements[i];
|
|
231
|
-
if (!element) {
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
const target = element.tabIndex > 0 ? ordered : natural;
|
|
235
|
-
target[target.length] = element;
|
|
236
|
-
}
|
|
237
|
-
ordered.sort((a, b) => a.tabIndex - b.tabIndex);
|
|
238
|
-
let count = 0;
|
|
239
|
-
const sorted = new Array(ordered.length + natural.length);
|
|
240
|
-
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
241
|
-
sorted[count++] = ordered[i];
|
|
242
|
-
}
|
|
243
|
-
for (let i = 0, l = natural.length; i < l; i++) {
|
|
244
|
-
sorted[count++] = natural[i];
|
|
245
|
-
}
|
|
246
|
-
return sorted;
|
|
247
|
-
}
|
|
248
256
|
/**
|
|
249
257
|
* Power Focusable
|
|
250
258
|
* High-precision focus management utility with shadow DOM support.
|
|
251
259
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
252
260
|
*
|
|
253
|
-
* @version 2.
|
|
261
|
+
* @version 2.2.0
|
|
254
262
|
* @author Yusuke Kamiyamane
|
|
255
263
|
* @license MIT
|
|
256
264
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.d.cts
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
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.2.0
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
10
10
|
* @see {@link https://github.com/y14e/power-focusable}
|
|
11
11
|
*/
|
|
12
12
|
interface PowerFocusableOptions {
|
|
13
|
-
readonly active?:
|
|
13
|
+
readonly active?: Element | null;
|
|
14
14
|
readonly composed?: boolean;
|
|
15
15
|
readonly wrap?: boolean;
|
|
16
16
|
}
|
|
17
|
-
declare function getFocusables(container?:
|
|
18
|
-
declare function getNextFocusable(container?:
|
|
19
|
-
declare function getPreviousFocusable(container?:
|
|
20
|
-
declare function hasFocusable(container?:
|
|
21
|
-
declare function isFocusable(element:
|
|
17
|
+
declare function getFocusables(container?: Element, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): Element[];
|
|
18
|
+
declare function getNextFocusable(container?: Element, options?: PowerFocusableOptions): Element | null;
|
|
19
|
+
declare function getPreviousFocusable(container?: Element, options?: PowerFocusableOptions): Element | null;
|
|
20
|
+
declare function hasFocusable(container?: Element, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
|
|
21
|
+
declare function isFocusable(element: Element): boolean;
|
|
22
22
|
|
|
23
23
|
export { type PowerFocusableOptions, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
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.2.0
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
10
10
|
* @see {@link https://github.com/y14e/power-focusable}
|
|
11
11
|
*/
|
|
12
12
|
interface PowerFocusableOptions {
|
|
13
|
-
readonly active?:
|
|
13
|
+
readonly active?: Element | null;
|
|
14
14
|
readonly composed?: boolean;
|
|
15
15
|
readonly wrap?: boolean;
|
|
16
16
|
}
|
|
17
|
-
declare function getFocusables(container?:
|
|
18
|
-
declare function getNextFocusable(container?:
|
|
19
|
-
declare function getPreviousFocusable(container?:
|
|
20
|
-
declare function hasFocusable(container?:
|
|
21
|
-
declare function isFocusable(element:
|
|
17
|
+
declare function getFocusables(container?: Element, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): Element[];
|
|
18
|
+
declare function getNextFocusable(container?: Element, options?: PowerFocusableOptions): Element | null;
|
|
19
|
+
declare function getPreviousFocusable(container?: Element, options?: PowerFocusableOptions): Element | null;
|
|
20
|
+
declare function hasFocusable(container?: Element, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
|
|
21
|
+
declare function isFocusable(element: Element): boolean;
|
|
22
22
|
|
|
23
23
|
export { type PowerFocusableOptions, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
3
|
function getFocusables(container = document.body, options = {}) {
|
|
4
|
-
if (!(container instanceof
|
|
4
|
+
if (!(container instanceof Element)) {
|
|
5
5
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
6
6
|
container = document.body;
|
|
7
7
|
}
|
|
@@ -9,32 +9,12 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
9
9
|
const elements = [];
|
|
10
10
|
if (composed) {
|
|
11
11
|
let traverse2 = function(node) {
|
|
12
|
-
if (node instanceof
|
|
12
|
+
if (node instanceof Element) {
|
|
13
13
|
if (isFocusable(node)) {
|
|
14
14
|
elements[elements.length] = node;
|
|
15
15
|
}
|
|
16
|
-
const shadow = node.shadowRoot;
|
|
17
|
-
if (shadow && shadow.mode === "open") {
|
|
18
|
-
traverse2(shadow);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
if (node instanceof HTMLSlotElement) {
|
|
22
|
-
const assigneds = node.assignedElements({ flatten: true });
|
|
23
|
-
if (assigneds.length) {
|
|
24
|
-
for (let i = 0, l = assigneds.length; i < l; i++) {
|
|
25
|
-
const assigned = assigneds[i];
|
|
26
|
-
if (!assigned) {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
traverse2(assigned);
|
|
30
|
-
}
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (!(node instanceof Element || node instanceof ShadowRoot)) {
|
|
35
|
-
return;
|
|
36
16
|
}
|
|
37
|
-
const children = node
|
|
17
|
+
const children = getComposedChildren(node);
|
|
38
18
|
for (let i = 0, l = children.length; i < l; i++) {
|
|
39
19
|
const child = children[i];
|
|
40
20
|
if (!child) {
|
|
@@ -48,7 +28,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
48
28
|
const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
49
29
|
for (let i = 0, l = candidates.length; i < l; i++) {
|
|
50
30
|
const candidate = candidates[i];
|
|
51
|
-
if (!(candidate instanceof
|
|
31
|
+
if (!(candidate instanceof Element)) {
|
|
52
32
|
continue;
|
|
53
33
|
}
|
|
54
34
|
if (isFocusable(candidate)) {
|
|
@@ -59,35 +39,35 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
59
39
|
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
60
40
|
}
|
|
61
41
|
function getNextFocusable(container = document.body, options = {}) {
|
|
62
|
-
if (!(container instanceof
|
|
42
|
+
if (!(container instanceof Element)) {
|
|
63
43
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
64
44
|
container = document.body;
|
|
65
45
|
}
|
|
66
46
|
return getRelativeFocusable(container, 1, options);
|
|
67
47
|
}
|
|
68
48
|
function getPreviousFocusable(container = document.body, options = {}) {
|
|
69
|
-
if (!(container instanceof
|
|
49
|
+
if (!(container instanceof Element)) {
|
|
70
50
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
71
51
|
container = document.body;
|
|
72
52
|
}
|
|
73
53
|
return getRelativeFocusable(container, -1, options);
|
|
74
54
|
}
|
|
75
55
|
function hasFocusable(container = document.body, options = {}) {
|
|
76
|
-
if (!(container instanceof
|
|
56
|
+
if (!(container instanceof Element)) {
|
|
77
57
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
78
58
|
container = document.body;
|
|
79
59
|
}
|
|
80
60
|
return !!getFocusables(container, options).length;
|
|
81
61
|
}
|
|
82
62
|
function isFocusable(element) {
|
|
83
|
-
if (!(element instanceof
|
|
63
|
+
if (!(element instanceof Element)) {
|
|
84
64
|
console.warn("Invalid element");
|
|
85
65
|
return false;
|
|
86
66
|
}
|
|
87
|
-
if (element.hidden || element.inert) {
|
|
67
|
+
if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
|
|
88
68
|
return false;
|
|
89
69
|
}
|
|
90
|
-
if (element
|
|
70
|
+
if (getTabIndex(element) < 0) {
|
|
91
71
|
return false;
|
|
92
72
|
}
|
|
93
73
|
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
@@ -105,6 +85,24 @@ function isFocusable(element) {
|
|
|
105
85
|
}
|
|
106
86
|
return true;
|
|
107
87
|
}
|
|
88
|
+
function getComposedChildren(node) {
|
|
89
|
+
if (node instanceof ShadowRoot) {
|
|
90
|
+
return getChildren(node);
|
|
91
|
+
}
|
|
92
|
+
if (!(node instanceof Element)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
if (node instanceof HTMLSlotElement) {
|
|
96
|
+
const assigned = node.assignedElements({ flatten: true });
|
|
97
|
+
if (assigned.length) {
|
|
98
|
+
return assigned;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
|
|
102
|
+
return getChildren(node.shadowRoot);
|
|
103
|
+
}
|
|
104
|
+
return getChildren(node);
|
|
105
|
+
}
|
|
108
106
|
function getRelativeFocusable(container, offset, options) {
|
|
109
107
|
const {
|
|
110
108
|
active = getActiveElement(),
|
|
@@ -119,7 +117,7 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
119
117
|
if (!active || !containsDeep(container, active)) {
|
|
120
118
|
return null;
|
|
121
119
|
}
|
|
122
|
-
if (!(active instanceof
|
|
120
|
+
if (!(active instanceof Element)) {
|
|
123
121
|
return null;
|
|
124
122
|
}
|
|
125
123
|
const currentIndex = focusables.indexOf(active);
|
|
@@ -132,6 +130,61 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
132
130
|
}
|
|
133
131
|
return focusables[(offsetIndex + length) % length] ?? null;
|
|
134
132
|
}
|
|
133
|
+
function normalizeRadioGroup(elements) {
|
|
134
|
+
let map = null;
|
|
135
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
136
|
+
const element = elements[i];
|
|
137
|
+
if (!(element instanceof HTMLInputElement)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!isUngroupedRadio(element)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!map) {
|
|
144
|
+
map = /* @__PURE__ */ new Map();
|
|
145
|
+
}
|
|
146
|
+
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
147
|
+
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
148
|
+
if (group) {
|
|
149
|
+
group[group.length] = element;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!map) {
|
|
153
|
+
return elements;
|
|
154
|
+
}
|
|
155
|
+
const placeholder = /* @__PURE__ */ new Set();
|
|
156
|
+
for (const group of map.values()) {
|
|
157
|
+
placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
|
|
158
|
+
}
|
|
159
|
+
return elements.filter((element) => {
|
|
160
|
+
if (isUngroupedRadio(element)) {
|
|
161
|
+
return placeholder.has(element);
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function sortByTabIndex(elements) {
|
|
167
|
+
const ordered = [];
|
|
168
|
+
const natural = [];
|
|
169
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
170
|
+
const element = elements[i];
|
|
171
|
+
if (!element) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const target = getTabIndex(element) > 0 ? ordered : natural;
|
|
175
|
+
target[target.length] = element;
|
|
176
|
+
}
|
|
177
|
+
ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
|
|
178
|
+
let count = 0;
|
|
179
|
+
const sorted = new Array(ordered.length + natural.length);
|
|
180
|
+
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
181
|
+
sorted[count++] = ordered[i];
|
|
182
|
+
}
|
|
183
|
+
for (let i = 0, l = natural.length; i < l; i++) {
|
|
184
|
+
sorted[count++] = natural[i];
|
|
185
|
+
}
|
|
186
|
+
return sorted;
|
|
187
|
+
}
|
|
135
188
|
function containsDeep(container, element) {
|
|
136
189
|
let current = element;
|
|
137
190
|
while (current) {
|
|
@@ -149,6 +202,16 @@ function getActiveElement() {
|
|
|
149
202
|
}
|
|
150
203
|
return current;
|
|
151
204
|
}
|
|
205
|
+
function getChildren(node) {
|
|
206
|
+
const elements = [];
|
|
207
|
+
for (let child = node.firstElementChild; child; child = child.nextElementSibling) {
|
|
208
|
+
elements[elements.length] = child;
|
|
209
|
+
}
|
|
210
|
+
return elements;
|
|
211
|
+
}
|
|
212
|
+
function getTabIndex(element) {
|
|
213
|
+
return "tabIndex" in element ? Number(element.tabIndex) : 0;
|
|
214
|
+
}
|
|
152
215
|
function isDisabled(element) {
|
|
153
216
|
return "disabled" in element && !!element.disabled;
|
|
154
217
|
}
|
|
@@ -188,67 +251,12 @@ function isFormControl(element) {
|
|
|
188
251
|
function isUngroupedRadio(element) {
|
|
189
252
|
return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
190
253
|
}
|
|
191
|
-
function normalizeRadioGroup(elements) {
|
|
192
|
-
let map = null;
|
|
193
|
-
for (let i = 0, l = elements.length; i < l; i++) {
|
|
194
|
-
const element = elements[i];
|
|
195
|
-
if (!(element instanceof HTMLInputElement)) {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (!isUngroupedRadio(element)) {
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (!map) {
|
|
202
|
-
map = /* @__PURE__ */ new Map();
|
|
203
|
-
}
|
|
204
|
-
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
205
|
-
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
206
|
-
if (group) {
|
|
207
|
-
group[group.length] = element;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (!map) {
|
|
211
|
-
return elements;
|
|
212
|
-
}
|
|
213
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
214
|
-
for (const group of map.values()) {
|
|
215
|
-
placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
|
|
216
|
-
}
|
|
217
|
-
return elements.filter((element) => {
|
|
218
|
-
if (isUngroupedRadio(element)) {
|
|
219
|
-
return placeholder.has(element);
|
|
220
|
-
}
|
|
221
|
-
return true;
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
function sortByTabIndex(elements) {
|
|
225
|
-
const ordered = [];
|
|
226
|
-
const natural = [];
|
|
227
|
-
for (let i = 0, l = elements.length; i < l; i++) {
|
|
228
|
-
const element = elements[i];
|
|
229
|
-
if (!element) {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
const target = element.tabIndex > 0 ? ordered : natural;
|
|
233
|
-
target[target.length] = element;
|
|
234
|
-
}
|
|
235
|
-
ordered.sort((a, b) => a.tabIndex - b.tabIndex);
|
|
236
|
-
let count = 0;
|
|
237
|
-
const sorted = new Array(ordered.length + natural.length);
|
|
238
|
-
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
239
|
-
sorted[count++] = ordered[i];
|
|
240
|
-
}
|
|
241
|
-
for (let i = 0, l = natural.length; i < l; i++) {
|
|
242
|
-
sorted[count++] = natural[i];
|
|
243
|
-
}
|
|
244
|
-
return sorted;
|
|
245
|
-
}
|
|
246
254
|
/**
|
|
247
255
|
* Power Focusable
|
|
248
256
|
* High-precision focus management utility with shadow DOM support.
|
|
249
257
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
250
258
|
*
|
|
251
|
-
* @version 2.
|
|
259
|
+
* @version 2.2.0
|
|
252
260
|
* @author Yusuke Kamiyamane
|
|
253
261
|
* @license MIT
|
|
254
262
|
* @copyright Copyright (c) Yusuke Kamiyamane
|