power-focusable 3.0.2 → 3.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 +24 -3
- package/dist/index.cjs +10 -24
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +10 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,9 +35,10 @@ import { ... } 'https://unpkg.com/power-focusable/dist/index.js';
|
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
37
|
interface PowerFocusableOptions {
|
|
38
|
-
active?: Element | null;
|
|
39
|
-
composed?: boolean;
|
|
40
|
-
|
|
38
|
+
active?: Element | null; // default: document.activeElement
|
|
39
|
+
composed?: boolean; // default: false
|
|
40
|
+
filter?: (element: Element) => boolean; // default: () => true
|
|
41
|
+
wrap?: boolean; // default: false
|
|
41
42
|
}
|
|
42
43
|
```
|
|
43
44
|
|
|
@@ -53,6 +54,14 @@ If `true`, traverses the composed tree (including shadow DOM; slower)
|
|
|
53
54
|
|
|
54
55
|
Used by `getFocusables`, `getNextFocusable`, `getPreviousFocusable`, and `hasFocusable`.
|
|
55
56
|
|
|
57
|
+
### `filter`
|
|
58
|
+
|
|
59
|
+
Custom filter function for excluding elements from focus traversal. Useful for advanced focus management such as portals, virtual focus regions, temporary exclusions, or custom focus scopes.
|
|
60
|
+
|
|
61
|
+
The function should return `true` to include the element, or `false` to exclude it.
|
|
62
|
+
|
|
63
|
+
Used by `getFocusables`, `getNextFocusable`, `getPreviousFocusable`, and `hasFocusable`.
|
|
64
|
+
|
|
56
65
|
### `wrap`
|
|
57
66
|
|
|
58
67
|
If `true`, wraps around to the first or last element when reaching the end.
|
|
@@ -84,6 +93,9 @@ getFocusables(container);
|
|
|
84
93
|
|
|
85
94
|
// Traverses the composed tree (including shadow DOM; slower)
|
|
86
95
|
getFocusables(container, { composed: true });
|
|
96
|
+
|
|
97
|
+
// Uses custom filter function
|
|
98
|
+
getFocusables(container, { filter: (element) => !element.matches('[data-skip-focus]') });
|
|
87
99
|
```
|
|
88
100
|
|
|
89
101
|
### `getNextFocusable`
|
|
@@ -102,6 +114,9 @@ getNextFocusable(container, { active: document.querySelector('.button') });
|
|
|
102
114
|
// Traverses the composed tree (including shadow DOM; slower)
|
|
103
115
|
getNextFocusable(container, { composed: true });
|
|
104
116
|
|
|
117
|
+
// Uses custom filter function
|
|
118
|
+
getNextFocusable(container, { filter: (element) => !element.matches('[data-skip-focus]') });
|
|
119
|
+
|
|
105
120
|
// Wraps around to the first element when reaching the end
|
|
106
121
|
getNextFocusable(container, { wrap: true });
|
|
107
122
|
```
|
|
@@ -122,6 +137,9 @@ getPreviousFocusable(container, { active: document.querySelector('.button') });
|
|
|
122
137
|
// Traverses the composed tree (including shadow DOM; slower)
|
|
123
138
|
getPreviousFocusable(container, { composed: true });
|
|
124
139
|
|
|
140
|
+
// Uses custom filter function
|
|
141
|
+
getPreviousFocusable(container, { filter: (element) => !element.matches('[data-skip-focus]') });
|
|
142
|
+
|
|
125
143
|
//Wraps around to the last element when reaching the end
|
|
126
144
|
getPreviousFocusable(container, { wrap: true });
|
|
127
145
|
|
|
@@ -139,6 +157,9 @@ hasFocusable(container);
|
|
|
139
157
|
|
|
140
158
|
// Traverses the composed tree (including shadow DOM; slower)
|
|
141
159
|
hasFocusable(container, { composed: true });
|
|
160
|
+
|
|
161
|
+
// Uses custom filter function
|
|
162
|
+
hasFocusable(container, { filter: (element) => !element.matches('[data-skip-focus]') });
|
|
142
163
|
```
|
|
143
164
|
|
|
144
165
|
### `inertOutside`
|
package/dist/index.cjs
CHANGED
|
@@ -9,9 +9,7 @@ function createFocusTrap(container) {
|
|
|
9
9
|
focus(container);
|
|
10
10
|
if (getActiveElement() !== container) {
|
|
11
11
|
const first = getFocusables(container, { composed: true })[0];
|
|
12
|
-
|
|
13
|
-
focus(first);
|
|
14
|
-
}
|
|
12
|
+
first && focus(first);
|
|
15
13
|
}
|
|
16
14
|
function onKeyDown(event) {
|
|
17
15
|
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
@@ -46,7 +44,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
46
44
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
47
45
|
container = document.body;
|
|
48
46
|
}
|
|
49
|
-
const { composed = false } = options;
|
|
47
|
+
const { composed = false, filter = () => true } = options;
|
|
50
48
|
const elements = [];
|
|
51
49
|
if (composed) {
|
|
52
50
|
let traverse2 = function(node) {
|
|
@@ -77,7 +75,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
|
-
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
78
|
+
return normalizeRadioGroup(sortByTabIndex(elements)).filter(filter);
|
|
81
79
|
}
|
|
82
80
|
function getNextFocusable(container = document.body, options = {}) {
|
|
83
81
|
if (!(container instanceof Element)) {
|
|
@@ -121,9 +119,7 @@ function inertOutside(element) {
|
|
|
121
119
|
if (!(node instanceof Element)) {
|
|
122
120
|
return;
|
|
123
121
|
}
|
|
124
|
-
|
|
125
|
-
elements.push(node);
|
|
126
|
-
}
|
|
122
|
+
applyInert(node) && elements.push(node);
|
|
127
123
|
});
|
|
128
124
|
return () => {
|
|
129
125
|
elements.forEach((element2) => {
|
|
@@ -161,9 +157,10 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
161
157
|
const {
|
|
162
158
|
active = getActiveElement(),
|
|
163
159
|
composed = false,
|
|
160
|
+
filter = () => true,
|
|
164
161
|
wrap = false
|
|
165
162
|
} = options;
|
|
166
|
-
const focusables = getFocusables(container, { composed });
|
|
163
|
+
const focusables = getFocusables(container, { composed, filter });
|
|
167
164
|
const { length } = focusables;
|
|
168
165
|
if (!length) {
|
|
169
166
|
return null;
|
|
@@ -334,9 +331,7 @@ function applyInert(element) {
|
|
|
334
331
|
}
|
|
335
332
|
const count = inertRefCounts.get(element) ?? 0;
|
|
336
333
|
inertRefCounts.set(element, count + 1);
|
|
337
|
-
|
|
338
|
-
setInert(element, true);
|
|
339
|
-
}
|
|
334
|
+
count === 0 && element.toggleAttribute("inert", true);
|
|
340
335
|
return true;
|
|
341
336
|
}
|
|
342
337
|
function restoreInert(element) {
|
|
@@ -346,15 +341,13 @@ function restoreInert(element) {
|
|
|
346
341
|
}
|
|
347
342
|
if (count === 1) {
|
|
348
343
|
inertRefCounts.delete(element);
|
|
349
|
-
|
|
344
|
+
element.toggleAttribute("inert", false);
|
|
350
345
|
return;
|
|
351
346
|
}
|
|
352
347
|
inertRefCounts.set(element, count - 1);
|
|
353
348
|
}
|
|
354
349
|
function focus(element) {
|
|
355
|
-
|
|
356
|
-
element.focus();
|
|
357
|
-
}
|
|
350
|
+
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
358
351
|
}
|
|
359
352
|
function getActiveElement() {
|
|
360
353
|
let current = document.activeElement;
|
|
@@ -399,20 +392,13 @@ function isInert(element) {
|
|
|
399
392
|
function isUngroupedRadio(element) {
|
|
400
393
|
return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
401
394
|
}
|
|
402
|
-
function setInert(element, boolean) {
|
|
403
|
-
if (boolean) {
|
|
404
|
-
element.setAttribute("inert", "");
|
|
405
|
-
} else {
|
|
406
|
-
element.removeAttribute("inert");
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
395
|
/**
|
|
410
396
|
* Power Focusable
|
|
411
397
|
* High-precision focus management utility with full composed tree support.
|
|
412
398
|
* Handles complex focus rules including tabindex ordering, radio groups, inert,
|
|
413
399
|
* and shadow DOM.
|
|
414
400
|
*
|
|
415
|
-
* @version 3.
|
|
401
|
+
* @version 3.1.1
|
|
416
402
|
* @author Yusuke Kamiyamane
|
|
417
403
|
* @license MIT
|
|
418
404
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.d.cts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles complex focus rules including tabindex ordering, radio groups, inert,
|
|
5
5
|
* and shadow DOM.
|
|
6
6
|
*
|
|
7
|
-
* @version 3.
|
|
7
|
+
* @version 3.1.1
|
|
8
8
|
* @author Yusuke Kamiyamane
|
|
9
9
|
* @license MIT
|
|
10
10
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
interface PowerFocusableOptions {
|
|
14
14
|
readonly active?: Element | null;
|
|
15
15
|
readonly composed?: boolean;
|
|
16
|
+
readonly filter?: (element: Element) => boolean;
|
|
16
17
|
readonly wrap?: boolean;
|
|
17
18
|
}
|
|
18
19
|
declare function createFocusTrap(container: Element): () => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles complex focus rules including tabindex ordering, radio groups, inert,
|
|
5
5
|
* and shadow DOM.
|
|
6
6
|
*
|
|
7
|
-
* @version 3.
|
|
7
|
+
* @version 3.1.1
|
|
8
8
|
* @author Yusuke Kamiyamane
|
|
9
9
|
* @license MIT
|
|
10
10
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
interface PowerFocusableOptions {
|
|
14
14
|
readonly active?: Element | null;
|
|
15
15
|
readonly composed?: boolean;
|
|
16
|
+
readonly filter?: (element: Element) => boolean;
|
|
16
17
|
readonly wrap?: boolean;
|
|
17
18
|
}
|
|
18
19
|
declare function createFocusTrap(container: Element): () => void;
|
package/dist/index.js
CHANGED
|
@@ -7,9 +7,7 @@ function createFocusTrap(container) {
|
|
|
7
7
|
focus(container);
|
|
8
8
|
if (getActiveElement() !== container) {
|
|
9
9
|
const first = getFocusables(container, { composed: true })[0];
|
|
10
|
-
|
|
11
|
-
focus(first);
|
|
12
|
-
}
|
|
10
|
+
first && focus(first);
|
|
13
11
|
}
|
|
14
12
|
function onKeyDown(event) {
|
|
15
13
|
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
@@ -44,7 +42,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
44
42
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
45
43
|
container = document.body;
|
|
46
44
|
}
|
|
47
|
-
const { composed = false } = options;
|
|
45
|
+
const { composed = false, filter = () => true } = options;
|
|
48
46
|
const elements = [];
|
|
49
47
|
if (composed) {
|
|
50
48
|
let traverse2 = function(node) {
|
|
@@ -75,7 +73,7 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
75
73
|
}
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
|
-
return normalizeRadioGroup(sortByTabIndex(elements));
|
|
76
|
+
return normalizeRadioGroup(sortByTabIndex(elements)).filter(filter);
|
|
79
77
|
}
|
|
80
78
|
function getNextFocusable(container = document.body, options = {}) {
|
|
81
79
|
if (!(container instanceof Element)) {
|
|
@@ -119,9 +117,7 @@ function inertOutside(element) {
|
|
|
119
117
|
if (!(node instanceof Element)) {
|
|
120
118
|
return;
|
|
121
119
|
}
|
|
122
|
-
|
|
123
|
-
elements.push(node);
|
|
124
|
-
}
|
|
120
|
+
applyInert(node) && elements.push(node);
|
|
125
121
|
});
|
|
126
122
|
return () => {
|
|
127
123
|
elements.forEach((element2) => {
|
|
@@ -159,9 +155,10 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
159
155
|
const {
|
|
160
156
|
active = getActiveElement(),
|
|
161
157
|
composed = false,
|
|
158
|
+
filter = () => true,
|
|
162
159
|
wrap = false
|
|
163
160
|
} = options;
|
|
164
|
-
const focusables = getFocusables(container, { composed });
|
|
161
|
+
const focusables = getFocusables(container, { composed, filter });
|
|
165
162
|
const { length } = focusables;
|
|
166
163
|
if (!length) {
|
|
167
164
|
return null;
|
|
@@ -332,9 +329,7 @@ function applyInert(element) {
|
|
|
332
329
|
}
|
|
333
330
|
const count = inertRefCounts.get(element) ?? 0;
|
|
334
331
|
inertRefCounts.set(element, count + 1);
|
|
335
|
-
|
|
336
|
-
setInert(element, true);
|
|
337
|
-
}
|
|
332
|
+
count === 0 && element.toggleAttribute("inert", true);
|
|
338
333
|
return true;
|
|
339
334
|
}
|
|
340
335
|
function restoreInert(element) {
|
|
@@ -344,15 +339,13 @@ function restoreInert(element) {
|
|
|
344
339
|
}
|
|
345
340
|
if (count === 1) {
|
|
346
341
|
inertRefCounts.delete(element);
|
|
347
|
-
|
|
342
|
+
element.toggleAttribute("inert", false);
|
|
348
343
|
return;
|
|
349
344
|
}
|
|
350
345
|
inertRefCounts.set(element, count - 1);
|
|
351
346
|
}
|
|
352
347
|
function focus(element) {
|
|
353
|
-
|
|
354
|
-
element.focus();
|
|
355
|
-
}
|
|
348
|
+
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
356
349
|
}
|
|
357
350
|
function getActiveElement() {
|
|
358
351
|
let current = document.activeElement;
|
|
@@ -397,20 +390,13 @@ function isInert(element) {
|
|
|
397
390
|
function isUngroupedRadio(element) {
|
|
398
391
|
return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
399
392
|
}
|
|
400
|
-
function setInert(element, boolean) {
|
|
401
|
-
if (boolean) {
|
|
402
|
-
element.setAttribute("inert", "");
|
|
403
|
-
} else {
|
|
404
|
-
element.removeAttribute("inert");
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
393
|
/**
|
|
408
394
|
* Power Focusable
|
|
409
395
|
* High-precision focus management utility with full composed tree support.
|
|
410
396
|
* Handles complex focus rules including tabindex ordering, radio groups, inert,
|
|
411
397
|
* and shadow DOM.
|
|
412
398
|
*
|
|
413
|
-
* @version 3.
|
|
399
|
+
* @version 3.1.1
|
|
414
400
|
* @author Yusuke Kamiyamane
|
|
415
401
|
* @license MIT
|
|
416
402
|
* @copyright Copyright (c) Yusuke Kamiyamane
|