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 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; // default: document.activeElement
39
- composed?: boolean; // default: false
40
- wrap?: boolean; // default: false
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
- if (first) {
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
- if (applyInert(node)) {
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
- if (count === 0) {
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
- setInert(element, false);
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
- if ("focus" in element && typeof element.focus === "function") {
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.0.2
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.0.2
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.0.2
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
- if (first) {
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
- if (applyInert(node)) {
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
- if (count === 0) {
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
- setInert(element, false);
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
- if ("focus" in element && typeof element.focus === "function") {
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.0.2
399
+ * @version 3.1.1
414
400
  * @author Yusuke Kamiyamane
415
401
  * @license MIT
416
402
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-focusable",
3
- "version": "3.0.2",
3
+ "version": "3.1.1",
4
4
  "description": "High-precision focus management utility with full composed tree support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",