power-focusable 2.1.12 → 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 -84
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +95 -84
- package/package.json +5 -2
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,29 +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
18
|
}
|
|
36
|
-
const children = node
|
|
19
|
+
const children = getComposedChildren(node);
|
|
37
20
|
for (let i = 0, l = children.length; i < l; i++) {
|
|
38
21
|
const child = children[i];
|
|
39
22
|
if (!child) {
|
|
@@ -47,7 +30,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
47
30
|
const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
48
31
|
for (let i = 0, l = candidates.length; i < l; i++) {
|
|
49
32
|
const candidate = candidates[i];
|
|
50
|
-
if (!(candidate instanceof
|
|
33
|
+
if (!(candidate instanceof Element)) {
|
|
51
34
|
continue;
|
|
52
35
|
}
|
|
53
36
|
if (isFocusable(candidate)) {
|
|
@@ -58,35 +41,35 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
58
41
|
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
59
42
|
}
|
|
60
43
|
function getNextFocusable(container = document.body, options = {}) {
|
|
61
|
-
if (!(container instanceof
|
|
44
|
+
if (!(container instanceof Element)) {
|
|
62
45
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
63
46
|
container = document.body;
|
|
64
47
|
}
|
|
65
48
|
return getRelativeFocusable(container, 1, options);
|
|
66
49
|
}
|
|
67
50
|
function getPreviousFocusable(container = document.body, options = {}) {
|
|
68
|
-
if (!(container instanceof
|
|
51
|
+
if (!(container instanceof Element)) {
|
|
69
52
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
70
53
|
container = document.body;
|
|
71
54
|
}
|
|
72
55
|
return getRelativeFocusable(container, -1, options);
|
|
73
56
|
}
|
|
74
57
|
function hasFocusable(container = document.body, options = {}) {
|
|
75
|
-
if (!(container instanceof
|
|
58
|
+
if (!(container instanceof Element)) {
|
|
76
59
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
77
60
|
container = document.body;
|
|
78
61
|
}
|
|
79
62
|
return !!getFocusables(container, options).length;
|
|
80
63
|
}
|
|
81
64
|
function isFocusable(element) {
|
|
82
|
-
if (!(element instanceof
|
|
65
|
+
if (!(element instanceof Element)) {
|
|
83
66
|
console.warn("Invalid element");
|
|
84
67
|
return false;
|
|
85
68
|
}
|
|
86
|
-
if (element.hidden || element.inert) {
|
|
69
|
+
if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
|
|
87
70
|
return false;
|
|
88
71
|
}
|
|
89
|
-
if (element
|
|
72
|
+
if (getTabIndex(element) < 0) {
|
|
90
73
|
return false;
|
|
91
74
|
}
|
|
92
75
|
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
@@ -104,6 +87,24 @@ function isFocusable(element) {
|
|
|
104
87
|
}
|
|
105
88
|
return true;
|
|
106
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
|
+
}
|
|
107
108
|
function getRelativeFocusable(container, offset, options) {
|
|
108
109
|
const {
|
|
109
110
|
active = getActiveElement(),
|
|
@@ -118,7 +119,7 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
118
119
|
if (!active || !containsDeep(container, active)) {
|
|
119
120
|
return null;
|
|
120
121
|
}
|
|
121
|
-
if (!(active instanceof
|
|
122
|
+
if (!(active instanceof Element)) {
|
|
122
123
|
return null;
|
|
123
124
|
}
|
|
124
125
|
const currentIndex = focusables.indexOf(active);
|
|
@@ -131,6 +132,61 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
131
132
|
}
|
|
132
133
|
return focusables[(offsetIndex + length) % length] ?? null;
|
|
133
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
|
+
}
|
|
134
190
|
function containsDeep(container, element) {
|
|
135
191
|
let current = element;
|
|
136
192
|
while (current) {
|
|
@@ -148,6 +204,16 @@ function getActiveElement() {
|
|
|
148
204
|
}
|
|
149
205
|
return current;
|
|
150
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
|
+
}
|
|
151
217
|
function isDisabled(element) {
|
|
152
218
|
return "disabled" in element && !!element.disabled;
|
|
153
219
|
}
|
|
@@ -187,67 +253,12 @@ function isFormControl(element) {
|
|
|
187
253
|
function isUngroupedRadio(element) {
|
|
188
254
|
return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
189
255
|
}
|
|
190
|
-
function normalizeRadioGroup(elements) {
|
|
191
|
-
let map = null;
|
|
192
|
-
for (let i = 0, l = elements.length; i < l; i++) {
|
|
193
|
-
const element = elements[i];
|
|
194
|
-
if (!(element instanceof HTMLInputElement)) {
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (!isUngroupedRadio(element)) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (!map) {
|
|
201
|
-
map = /* @__PURE__ */ new Map();
|
|
202
|
-
}
|
|
203
|
-
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
204
|
-
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
205
|
-
if (group) {
|
|
206
|
-
group[group.length] = element;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (!map) {
|
|
210
|
-
return elements;
|
|
211
|
-
}
|
|
212
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
213
|
-
for (const group of map.values()) {
|
|
214
|
-
placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
|
|
215
|
-
}
|
|
216
|
-
return elements.filter((element) => {
|
|
217
|
-
if (isUngroupedRadio(element)) {
|
|
218
|
-
return placeholder.has(element);
|
|
219
|
-
}
|
|
220
|
-
return true;
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
function sortByTabIndex(elements) {
|
|
224
|
-
const ordered = [];
|
|
225
|
-
const natural = [];
|
|
226
|
-
for (let i = 0, l = elements.length; i < l; i++) {
|
|
227
|
-
const element = elements[i];
|
|
228
|
-
if (!element) {
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const target = element.tabIndex > 0 ? ordered : natural;
|
|
232
|
-
target[target.length] = element;
|
|
233
|
-
}
|
|
234
|
-
ordered.sort((a, b) => a.tabIndex - b.tabIndex);
|
|
235
|
-
let count = 0;
|
|
236
|
-
const sorted = new Array(ordered.length + natural.length);
|
|
237
|
-
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
238
|
-
sorted[count++] = ordered[i];
|
|
239
|
-
}
|
|
240
|
-
for (let i = 0, l = natural.length; i < l; i++) {
|
|
241
|
-
sorted[count++] = natural[i];
|
|
242
|
-
}
|
|
243
|
-
return sorted;
|
|
244
|
-
}
|
|
245
256
|
/**
|
|
246
257
|
* Power Focusable
|
|
247
258
|
* High-precision focus management utility with shadow DOM support.
|
|
248
259
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
249
260
|
*
|
|
250
|
-
* @version 2.
|
|
261
|
+
* @version 2.2.0
|
|
251
262
|
* @author Yusuke Kamiyamane
|
|
252
263
|
* @license MIT
|
|
253
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,29 +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
16
|
}
|
|
34
|
-
const children = node
|
|
17
|
+
const children = getComposedChildren(node);
|
|
35
18
|
for (let i = 0, l = children.length; i < l; i++) {
|
|
36
19
|
const child = children[i];
|
|
37
20
|
if (!child) {
|
|
@@ -45,7 +28,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
45
28
|
const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
46
29
|
for (let i = 0, l = candidates.length; i < l; i++) {
|
|
47
30
|
const candidate = candidates[i];
|
|
48
|
-
if (!(candidate instanceof
|
|
31
|
+
if (!(candidate instanceof Element)) {
|
|
49
32
|
continue;
|
|
50
33
|
}
|
|
51
34
|
if (isFocusable(candidate)) {
|
|
@@ -56,35 +39,35 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
56
39
|
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
57
40
|
}
|
|
58
41
|
function getNextFocusable(container = document.body, options = {}) {
|
|
59
|
-
if (!(container instanceof
|
|
42
|
+
if (!(container instanceof Element)) {
|
|
60
43
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
61
44
|
container = document.body;
|
|
62
45
|
}
|
|
63
46
|
return getRelativeFocusable(container, 1, options);
|
|
64
47
|
}
|
|
65
48
|
function getPreviousFocusable(container = document.body, options = {}) {
|
|
66
|
-
if (!(container instanceof
|
|
49
|
+
if (!(container instanceof Element)) {
|
|
67
50
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
68
51
|
container = document.body;
|
|
69
52
|
}
|
|
70
53
|
return getRelativeFocusable(container, -1, options);
|
|
71
54
|
}
|
|
72
55
|
function hasFocusable(container = document.body, options = {}) {
|
|
73
|
-
if (!(container instanceof
|
|
56
|
+
if (!(container instanceof Element)) {
|
|
74
57
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
75
58
|
container = document.body;
|
|
76
59
|
}
|
|
77
60
|
return !!getFocusables(container, options).length;
|
|
78
61
|
}
|
|
79
62
|
function isFocusable(element) {
|
|
80
|
-
if (!(element instanceof
|
|
63
|
+
if (!(element instanceof Element)) {
|
|
81
64
|
console.warn("Invalid element");
|
|
82
65
|
return false;
|
|
83
66
|
}
|
|
84
|
-
if (element.hidden || element.inert) {
|
|
67
|
+
if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
|
|
85
68
|
return false;
|
|
86
69
|
}
|
|
87
|
-
if (element
|
|
70
|
+
if (getTabIndex(element) < 0) {
|
|
88
71
|
return false;
|
|
89
72
|
}
|
|
90
73
|
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
@@ -102,6 +85,24 @@ function isFocusable(element) {
|
|
|
102
85
|
}
|
|
103
86
|
return true;
|
|
104
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
|
+
}
|
|
105
106
|
function getRelativeFocusable(container, offset, options) {
|
|
106
107
|
const {
|
|
107
108
|
active = getActiveElement(),
|
|
@@ -116,7 +117,7 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
116
117
|
if (!active || !containsDeep(container, active)) {
|
|
117
118
|
return null;
|
|
118
119
|
}
|
|
119
|
-
if (!(active instanceof
|
|
120
|
+
if (!(active instanceof Element)) {
|
|
120
121
|
return null;
|
|
121
122
|
}
|
|
122
123
|
const currentIndex = focusables.indexOf(active);
|
|
@@ -129,6 +130,61 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
129
130
|
}
|
|
130
131
|
return focusables[(offsetIndex + length) % length] ?? null;
|
|
131
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
|
+
}
|
|
132
188
|
function containsDeep(container, element) {
|
|
133
189
|
let current = element;
|
|
134
190
|
while (current) {
|
|
@@ -146,6 +202,16 @@ function getActiveElement() {
|
|
|
146
202
|
}
|
|
147
203
|
return current;
|
|
148
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
|
+
}
|
|
149
215
|
function isDisabled(element) {
|
|
150
216
|
return "disabled" in element && !!element.disabled;
|
|
151
217
|
}
|
|
@@ -185,67 +251,12 @@ function isFormControl(element) {
|
|
|
185
251
|
function isUngroupedRadio(element) {
|
|
186
252
|
return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
187
253
|
}
|
|
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)) {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (!isUngroupedRadio(element)) {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (!map) {
|
|
199
|
-
map = /* @__PURE__ */ new Map();
|
|
200
|
-
}
|
|
201
|
-
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
202
|
-
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
203
|
-
if (group) {
|
|
204
|
-
group[group.length] = element;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (!map) {
|
|
208
|
-
return elements;
|
|
209
|
-
}
|
|
210
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
211
|
-
for (const group of map.values()) {
|
|
212
|
-
placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
|
|
213
|
-
}
|
|
214
|
-
return elements.filter((element) => {
|
|
215
|
-
if (isUngroupedRadio(element)) {
|
|
216
|
-
return placeholder.has(element);
|
|
217
|
-
}
|
|
218
|
-
return true;
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
function sortByTabIndex(elements) {
|
|
222
|
-
const ordered = [];
|
|
223
|
-
const natural = [];
|
|
224
|
-
for (let i = 0, l = elements.length; i < l; i++) {
|
|
225
|
-
const element = elements[i];
|
|
226
|
-
if (!element) {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
const target = element.tabIndex > 0 ? ordered : natural;
|
|
230
|
-
target[target.length] = element;
|
|
231
|
-
}
|
|
232
|
-
ordered.sort((a, b) => a.tabIndex - b.tabIndex);
|
|
233
|
-
let count = 0;
|
|
234
|
-
const sorted = new Array(ordered.length + natural.length);
|
|
235
|
-
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
236
|
-
sorted[count++] = ordered[i];
|
|
237
|
-
}
|
|
238
|
-
for (let i = 0, l = natural.length; i < l; i++) {
|
|
239
|
-
sorted[count++] = natural[i];
|
|
240
|
-
}
|
|
241
|
-
return sorted;
|
|
242
|
-
}
|
|
243
254
|
/**
|
|
244
255
|
* Power Focusable
|
|
245
256
|
* High-precision focus management utility with shadow DOM support.
|
|
246
257
|
* Handles complex focus rules including tabindex ordering, radio groups, etc.
|
|
247
258
|
*
|
|
248
|
-
* @version 2.
|
|
259
|
+
* @version 2.2.0
|
|
249
260
|
* @author Yusuke Kamiyamane
|
|
250
261
|
* @license MIT
|
|
251
262
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "power-focusable",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "High-precision focus management utility with shadow DOM support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsup",
|
|
23
|
+
"test": "vitest run",
|
|
23
24
|
"prepublishOnly": "npm run build",
|
|
24
25
|
"lint": "tsc --noEmit"
|
|
25
26
|
},
|
|
@@ -46,8 +47,10 @@
|
|
|
46
47
|
"homepage": "https://github.com/y14e/power-focusable#readme",
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"bun-types": "latest",
|
|
50
|
+
"happy-dom": "^20.9.0",
|
|
49
51
|
"tsup": "^8.0.0",
|
|
50
|
-
"typescript": "^5.6.0"
|
|
52
|
+
"typescript": "^5.6.0",
|
|
53
|
+
"vitest": "^4.1.5"
|
|
51
54
|
},
|
|
52
55
|
"engines": {
|
|
53
56
|
"node": ">=18"
|