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 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
- // Starting from a specific element
83
+ // Specifies the starting element
53
84
  getNextFocusable(container, { active: document.querySelector('.button') });
54
85
 
55
- // Wrap to the first element if necessary
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
- // Starting from a specific element
103
+ // Specifies the starting element
70
104
  getPreviousFocusable(container, { active: document.querySelector('.button') });
71
105
 
72
- // Wrap to the last element if necessary
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
- var cache = /* @__PURE__ */ new WeakMap();
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
- function hasShadow(root) {
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
- function walk(node) {
31
- if (node instanceof HTMLElement) {
32
- if (isFocusable(node)) {
33
- elements.push(node);
34
- }
35
- const shadow = node.shadowRoot;
36
- if (shadow && shadow.mode === "open") {
37
- walk(shadow);
38
- }
39
- } else if (node instanceof HTMLSlotElement) {
40
- const elements2 = node.assignedElements({ flatten: true });
41
- if (elements2.length > 0) {
42
- elements2.forEach((element) => {
43
- walk(element);
44
- });
45
- return;
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
- node.childNodes.forEach((child) => {
49
- walk(child);
50
- });
51
- }
52
- walk(container);
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 result;
74
+ return elements2;
87
75
  }
76
+ const placeholder = /* @__PURE__ */ new Set();
88
77
  for (const group of map.values()) {
89
- const enabled = group.filter((radio) => isFocusable(radio));
90
- if (enabled.length === 0) {
91
- continue;
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 result;
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 focusables = getFocusables(container);
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 1.0.0
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 1.0.0
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 1.0.0
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
- var cache = /* @__PURE__ */ new WeakMap();
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
- function hasShadow(root) {
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
- function walk(node) {
29
- if (node instanceof HTMLElement) {
30
- if (isFocusable(node)) {
31
- elements.push(node);
32
- }
33
- const shadow = node.shadowRoot;
34
- if (shadow && shadow.mode === "open") {
35
- walk(shadow);
36
- }
37
- } else if (node instanceof HTMLSlotElement) {
38
- const elements2 = node.assignedElements({ flatten: true });
39
- if (elements2.length > 0) {
40
- elements2.forEach((element) => {
41
- walk(element);
42
- });
43
- return;
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
- node.childNodes.forEach((child) => {
47
- walk(child);
48
- });
49
- }
50
- walk(container);
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 result;
72
+ return elements2;
85
73
  }
74
+ const placeholder = /* @__PURE__ */ new Set();
86
75
  for (const group of map.values()) {
87
- const enabled = group.filter((radio) => isFocusable(radio));
88
- if (enabled.length === 0) {
89
- continue;
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 result;
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 focusables = getFocusables(container);
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 1.0.0
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": "1.0.0",
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",