power-focusable 1.0.0 → 2.0.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 +41 -4
- package/dist/index.cjs +51 -56
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +51 -56
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -26,6 +26,34 @@ import { ... } 'https://cdn.jsdelivr.net/npm/power-focusable/dist/index.js';
|
|
|
26
26
|
import { ... } 'https://unpkg.com/power-focusable/dist/index.js';
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## 🪄 Options
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface PowerFocusableOptions {
|
|
33
|
+
active?: HTMLElement | null; // default: document.activeElement
|
|
34
|
+
composed?: boolean; // default: false
|
|
35
|
+
wrap?: boolean; // default: false
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `active`
|
|
40
|
+
|
|
41
|
+
Specifies the starting element.
|
|
42
|
+
|
|
43
|
+
Used by `getNextFocusable` and `getPreviousFocusable`.
|
|
44
|
+
|
|
45
|
+
### `composed`
|
|
46
|
+
|
|
47
|
+
If `true`, traverses the composed tree (including shadow DOM; slower)
|
|
48
|
+
|
|
49
|
+
Used by `getFocusables`, `getNextFocusable`, and `getPreviousFocusable`.
|
|
50
|
+
|
|
51
|
+
### `wrap`
|
|
52
|
+
|
|
53
|
+
If `true`, wraps around to the first or last element when reaching the end.
|
|
54
|
+
|
|
55
|
+
Used by `getNextFocusable` and `getPreviousFocusable`.
|
|
56
|
+
|
|
29
57
|
## 📦 APIs
|
|
30
58
|
|
|
31
59
|
### `getFocusables`
|
|
@@ -37,6 +65,9 @@ getFocusables(container);
|
|
|
37
65
|
// => HTMLElement[]
|
|
38
66
|
//
|
|
39
67
|
// container (optional): HTMLElement (default: document.body)
|
|
68
|
+
|
|
69
|
+
// Traverses the composed tree (including shadow DOM; slower)
|
|
70
|
+
getFocusables(container, { composed: true });
|
|
40
71
|
```
|
|
41
72
|
|
|
42
73
|
### `getNextFocusable`
|
|
@@ -49,10 +80,13 @@ getNextFocusable(container);
|
|
|
49
80
|
//
|
|
50
81
|
// container (optional): HTMLElement (default: document.body)
|
|
51
82
|
|
|
52
|
-
//
|
|
83
|
+
// Specifies the starting element
|
|
53
84
|
getNextFocusable(container, { active: document.querySelector('.button') });
|
|
54
85
|
|
|
55
|
-
//
|
|
86
|
+
// Traverses the composed tree (including shadow DOM; slower)
|
|
87
|
+
getNextFocusable(container, { composed: true });
|
|
88
|
+
|
|
89
|
+
// Wraps around to the first element when reaching the end.
|
|
56
90
|
getNextFocusable(container, { wrap: true });
|
|
57
91
|
```
|
|
58
92
|
|
|
@@ -66,10 +100,13 @@ getPreviousFocusable(container);
|
|
|
66
100
|
//
|
|
67
101
|
// container (optional): HTMLElement (default: document.body)
|
|
68
102
|
|
|
69
|
-
//
|
|
103
|
+
// Specifies the starting element
|
|
70
104
|
getPreviousFocusable(container, { active: document.querySelector('.button') });
|
|
71
105
|
|
|
72
|
-
//
|
|
106
|
+
// Traverses the composed tree (including shadow DOM; slower)
|
|
107
|
+
getPreviousFocusable(container, { composed: true });
|
|
108
|
+
|
|
109
|
+
//Wraps around to the last element when reaching the end.
|
|
73
110
|
getPreviousFocusable(container, { wrap: true });
|
|
74
111
|
|
|
75
112
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -2,54 +2,45 @@
|
|
|
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
|
-
|
|
6
|
-
function getFocusables(container = document.body) {
|
|
5
|
+
function getFocusables(container = document.body, options = {}) {
|
|
7
6
|
if (!(container instanceof HTMLElement)) {
|
|
8
7
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
9
8
|
container = document.body;
|
|
10
9
|
}
|
|
11
|
-
|
|
12
|
-
let isFound = false;
|
|
13
|
-
function walk2(node) {
|
|
14
|
-
if (isFound) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if (node instanceof HTMLElement && node.shadowRoot && node.shadowRoot.mode === "open") {
|
|
18
|
-
isFound = true;
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
node.childNodes.forEach(walk2);
|
|
22
|
-
}
|
|
23
|
-
walk2(root);
|
|
24
|
-
return isFound;
|
|
25
|
-
}
|
|
26
|
-
if (!container.querySelector(FOCUSABLE_SELECTOR) && !hasShadow(container)) {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
10
|
+
const { composed = false } = options;
|
|
29
11
|
const elements = [];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
12
|
+
if (composed) {
|
|
13
|
+
let walk2 = function(node) {
|
|
14
|
+
if (node instanceof HTMLElement) {
|
|
15
|
+
if (isFocusable(node)) {
|
|
16
|
+
elements.push(node);
|
|
17
|
+
}
|
|
18
|
+
const shadow = node.shadowRoot;
|
|
19
|
+
if (shadow && shadow.mode === "open") {
|
|
20
|
+
walk2(shadow);
|
|
21
|
+
}
|
|
22
|
+
} else if (node instanceof HTMLSlotElement) {
|
|
23
|
+
const assigned = node.assignedElements({ flatten: true });
|
|
24
|
+
if (assigned.length > 0) {
|
|
25
|
+
assigned.forEach((a) => {
|
|
26
|
+
walk2(a);
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
46
30
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
31
|
+
node.childNodes.forEach((child) => {
|
|
32
|
+
walk2(child);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
walk2(container);
|
|
36
|
+
} else {
|
|
37
|
+
elements.push(
|
|
38
|
+
...[
|
|
39
|
+
...container.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
40
|
+
].filter(isFocusable)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
53
44
|
function sort(elements2) {
|
|
54
45
|
const ordered = [];
|
|
55
46
|
const natural = [];
|
|
@@ -70,7 +61,6 @@ function getFocusables(container = document.body) {
|
|
|
70
61
|
}
|
|
71
62
|
function normalizeRadioGroup(elements2) {
|
|
72
63
|
let map = null;
|
|
73
|
-
const result = [];
|
|
74
64
|
for (const element of elements2) {
|
|
75
65
|
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
76
66
|
if (!map) {
|
|
@@ -78,23 +68,28 @@ function getFocusables(container = document.body) {
|
|
|
78
68
|
}
|
|
79
69
|
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
80
70
|
(map.get(key) ?? map.set(key, []).get(key)).push(element);
|
|
81
|
-
} else {
|
|
82
|
-
result.push(element);
|
|
83
71
|
}
|
|
84
72
|
}
|
|
85
73
|
if (!map) {
|
|
86
|
-
return
|
|
74
|
+
return elements2;
|
|
87
75
|
}
|
|
76
|
+
const placeholder = /* @__PURE__ */ new Set();
|
|
88
77
|
for (const group of map.values()) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
92
85
|
}
|
|
93
|
-
result.push(
|
|
94
|
-
enabled.find((radio) => radio.checked) ?? enabled[0]
|
|
95
|
-
);
|
|
96
86
|
}
|
|
97
|
-
return
|
|
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
|
+
});
|
|
98
93
|
}
|
|
99
94
|
return normalizeRadioGroup(sort(elements));
|
|
100
95
|
}
|
|
@@ -157,12 +152,12 @@ function isFocusable(element) {
|
|
|
157
152
|
});
|
|
158
153
|
}
|
|
159
154
|
function getRelativeFocusable(container, offset = 0, options) {
|
|
160
|
-
const
|
|
155
|
+
const { active, composed = false, wrap = false } = options;
|
|
156
|
+
const focusables = getFocusables(container, { composed });
|
|
161
157
|
const { length } = focusables;
|
|
162
158
|
if (length === 0) {
|
|
163
159
|
return null;
|
|
164
160
|
}
|
|
165
|
-
const { active, wrap = false } = options;
|
|
166
161
|
function getActiveElement() {
|
|
167
162
|
let active2 = document.activeElement;
|
|
168
163
|
while (active2 instanceof HTMLElement && active2.shadowRoot?.activeElement) {
|
|
@@ -197,7 +192,7 @@ function getRelativeFocusable(container, offset = 0, options) {
|
|
|
197
192
|
* High-precision focus management utility with shadow DOM support.
|
|
198
193
|
* Handles complex focus rules.
|
|
199
194
|
*
|
|
200
|
-
* @version
|
|
195
|
+
* @version 2.0.0
|
|
201
196
|
* @author Yusuke Kamiyamane
|
|
202
197
|
* @license MIT
|
|
203
198
|
* @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.
|
|
5
5
|
*
|
|
6
|
-
* @version
|
|
6
|
+
* @version 2.0.0
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
interface PowerFocusableOptions {
|
|
13
13
|
readonly active?: HTMLElement | null;
|
|
14
|
+
readonly composed?: boolean;
|
|
14
15
|
readonly wrap?: boolean;
|
|
15
16
|
}
|
|
16
|
-
declare function getFocusables(container?: HTMLElement): HTMLElement[];
|
|
17
|
+
declare function getFocusables(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): HTMLElement[];
|
|
17
18
|
declare function getNextFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
|
|
18
19
|
declare function getPreviousFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
|
|
19
20
|
declare function hasFocusable(container?: HTMLElement): boolean;
|
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.
|
|
5
5
|
*
|
|
6
|
-
* @version
|
|
6
|
+
* @version 2.0.0
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
interface PowerFocusableOptions {
|
|
13
13
|
readonly active?: HTMLElement | null;
|
|
14
|
+
readonly composed?: boolean;
|
|
14
15
|
readonly wrap?: boolean;
|
|
15
16
|
}
|
|
16
|
-
declare function getFocusables(container?: HTMLElement): HTMLElement[];
|
|
17
|
+
declare function getFocusables(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): HTMLElement[];
|
|
17
18
|
declare function getNextFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
|
|
18
19
|
declare function getPreviousFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
|
|
19
20
|
declare function hasFocusable(container?: HTMLElement): boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,53 +1,44 @@
|
|
|
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
|
-
|
|
4
|
-
function getFocusables(container = document.body) {
|
|
3
|
+
function getFocusables(container = document.body, options = {}) {
|
|
5
4
|
if (!(container instanceof HTMLElement)) {
|
|
6
5
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
7
6
|
container = document.body;
|
|
8
7
|
}
|
|
9
|
-
|
|
10
|
-
let isFound = false;
|
|
11
|
-
function walk2(node) {
|
|
12
|
-
if (isFound) {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
if (node instanceof HTMLElement && node.shadowRoot && node.shadowRoot.mode === "open") {
|
|
16
|
-
isFound = true;
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
node.childNodes.forEach(walk2);
|
|
20
|
-
}
|
|
21
|
-
walk2(root);
|
|
22
|
-
return isFound;
|
|
23
|
-
}
|
|
24
|
-
if (!container.querySelector(FOCUSABLE_SELECTOR) && !hasShadow(container)) {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
8
|
+
const { composed = false } = options;
|
|
27
9
|
const elements = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
10
|
+
if (composed) {
|
|
11
|
+
let walk2 = function(node) {
|
|
12
|
+
if (node instanceof HTMLElement) {
|
|
13
|
+
if (isFocusable(node)) {
|
|
14
|
+
elements.push(node);
|
|
15
|
+
}
|
|
16
|
+
const shadow = node.shadowRoot;
|
|
17
|
+
if (shadow && shadow.mode === "open") {
|
|
18
|
+
walk2(shadow);
|
|
19
|
+
}
|
|
20
|
+
} else if (node instanceof HTMLSlotElement) {
|
|
21
|
+
const assigned = node.assignedElements({ flatten: true });
|
|
22
|
+
if (assigned.length > 0) {
|
|
23
|
+
assigned.forEach((a) => {
|
|
24
|
+
walk2(a);
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
44
28
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
29
|
+
node.childNodes.forEach((child) => {
|
|
30
|
+
walk2(child);
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
walk2(container);
|
|
34
|
+
} else {
|
|
35
|
+
elements.push(
|
|
36
|
+
...[
|
|
37
|
+
...container.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
38
|
+
].filter(isFocusable)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
51
42
|
function sort(elements2) {
|
|
52
43
|
const ordered = [];
|
|
53
44
|
const natural = [];
|
|
@@ -68,7 +59,6 @@ function getFocusables(container = document.body) {
|
|
|
68
59
|
}
|
|
69
60
|
function normalizeRadioGroup(elements2) {
|
|
70
61
|
let map = null;
|
|
71
|
-
const result = [];
|
|
72
62
|
for (const element of elements2) {
|
|
73
63
|
if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
|
|
74
64
|
if (!map) {
|
|
@@ -76,23 +66,28 @@ function getFocusables(container = document.body) {
|
|
|
76
66
|
}
|
|
77
67
|
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
78
68
|
(map.get(key) ?? map.set(key, []).get(key)).push(element);
|
|
79
|
-
} else {
|
|
80
|
-
result.push(element);
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
if (!map) {
|
|
84
|
-
return
|
|
72
|
+
return elements2;
|
|
85
73
|
}
|
|
74
|
+
const placeholder = /* @__PURE__ */ new Set();
|
|
86
75
|
for (const group of map.values()) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
83
|
}
|
|
91
|
-
result.push(
|
|
92
|
-
enabled.find((radio) => radio.checked) ?? enabled[0]
|
|
93
|
-
);
|
|
94
84
|
}
|
|
95
|
-
return
|
|
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
|
+
});
|
|
96
91
|
}
|
|
97
92
|
return normalizeRadioGroup(sort(elements));
|
|
98
93
|
}
|
|
@@ -155,12 +150,12 @@ function isFocusable(element) {
|
|
|
155
150
|
});
|
|
156
151
|
}
|
|
157
152
|
function getRelativeFocusable(container, offset = 0, options) {
|
|
158
|
-
const
|
|
153
|
+
const { active, composed = false, wrap = false } = options;
|
|
154
|
+
const focusables = getFocusables(container, { composed });
|
|
159
155
|
const { length } = focusables;
|
|
160
156
|
if (length === 0) {
|
|
161
157
|
return null;
|
|
162
158
|
}
|
|
163
|
-
const { active, wrap = false } = options;
|
|
164
159
|
function getActiveElement() {
|
|
165
160
|
let active2 = document.activeElement;
|
|
166
161
|
while (active2 instanceof HTMLElement && active2.shadowRoot?.activeElement) {
|
|
@@ -195,7 +190,7 @@ function getRelativeFocusable(container, offset = 0, options) {
|
|
|
195
190
|
* High-precision focus management utility with shadow DOM support.
|
|
196
191
|
* Handles complex focus rules.
|
|
197
192
|
*
|
|
198
|
-
* @version
|
|
193
|
+
* @version 2.0.0
|
|
199
194
|
* @author Yusuke Kamiyamane
|
|
200
195
|
* @license MIT
|
|
201
196
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "power-focusable",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "High-precision focus management utility with shadow DOM support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"focus",
|
|
28
|
+
"focus-management",
|
|
28
29
|
"focusable",
|
|
29
30
|
"shadow-dom",
|
|
30
31
|
"shadowdom",
|