power-focusable 2.1.13 → 2.2.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
@@ -33,9 +33,9 @@ import { ... } 'https://unpkg.com/power-focusable/dist/index.js';
33
33
 
34
34
  ```ts
35
35
  interface PowerFocusableOptions {
36
- active?: HTMLElement | null; // default: document.activeElement
37
- composed?: boolean; // default: false
38
- wrap?: boolean; // default: false
36
+ active?: Element | null; // default: document.activeElement
37
+ composed?: boolean; // default: false
38
+ wrap?: boolean; // default: false
39
39
  }
40
40
  ```
41
41
 
@@ -65,9 +65,9 @@ Returns all focusable elements within the container.
65
65
 
66
66
  ```ts
67
67
  getFocusables(container);
68
- // => HTMLElement[]
68
+ // => Element[]
69
69
  //
70
- // container (optional): HTMLElement (default: <body>)
70
+ // container (optional): Element (default: <body>)
71
71
 
72
72
  // Traverses the composed tree (including shadow DOM; slower)
73
73
  getFocusables(container, { composed: true });
@@ -79,9 +79,9 @@ Returns the next focusable element within the container, starting from `document
79
79
 
80
80
  ```ts
81
81
  getNextFocusable(container);
82
- // => HTMLElement | null
82
+ // => Element | null
83
83
  //
84
- // container (optional): HTMLElement (default: <body>)
84
+ // container (optional): Element (default: <body>)
85
85
 
86
86
  // Specifies the starting element
87
87
  getNextFocusable(container, { active: document.querySelector('.button') });
@@ -99,9 +99,9 @@ Returns the previous focusable element within the container, starting from `docu
99
99
 
100
100
  ```ts
101
101
  getPreviousFocusable(container);
102
- // => HTMLElement | null
102
+ // => Element | null
103
103
  //
104
- // container (optional): HTMLElement (default: <body>)
104
+ // container (optional): Element (default: <body>)
105
105
 
106
106
  // Specifies the starting element
107
107
  getPreviousFocusable(container, { active: document.querySelector('.button') });
@@ -122,7 +122,7 @@ Returns whether the container contains at least one focusable element.
122
122
  hasFocusable(container);
123
123
  // => boolean
124
124
  //
125
- // container (optional): HTMLElement (default: <body>)
125
+ // container (optional): Element (default: <body>)
126
126
 
127
127
  // Traverses the composed tree (including shadow DOM; slower)
128
128
  hasFocusable(container, { composed: true });
@@ -136,6 +136,6 @@ Returns whether the given element is focusable.
136
136
  isFocusable(element);
137
137
  // => boolean
138
138
  //
139
- // element: HTMLElement
139
+ // element: Element
140
140
 
141
141
  ```
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 HTMLElement)) {
6
+ if (!(container instanceof Element)) {
7
7
  console.warn("Invalid container element. Fallback: <body> element.");
8
8
  container = document.body;
9
9
  }
@@ -11,32 +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 HTMLElement) {
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
- }
36
- if (!(node instanceof Element || node instanceof ShadowRoot)) {
37
- return;
38
18
  }
39
- const children = node.children;
19
+ const children = getComposedChildren(node);
40
20
  for (let i = 0, l = children.length; i < l; i++) {
41
21
  const child = children[i];
42
22
  if (!child) {
@@ -50,7 +30,7 @@ function getFocusables(container = document.body, options = {}) {
50
30
  const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
51
31
  for (let i = 0, l = candidates.length; i < l; i++) {
52
32
  const candidate = candidates[i];
53
- if (!(candidate instanceof HTMLElement)) {
33
+ if (!(candidate instanceof Element)) {
54
34
  continue;
55
35
  }
56
36
  if (isFocusable(candidate)) {
@@ -61,35 +41,35 @@ function getFocusables(container = document.body, options = {}) {
61
41
  return normalizeRadioGroup(sortByTabIndex(elements));
62
42
  }
63
43
  function getNextFocusable(container = document.body, options = {}) {
64
- if (!(container instanceof HTMLElement)) {
44
+ if (!(container instanceof Element)) {
65
45
  console.warn("Invalid container element. Fallback: <body> element.");
66
46
  container = document.body;
67
47
  }
68
48
  return getRelativeFocusable(container, 1, options);
69
49
  }
70
50
  function getPreviousFocusable(container = document.body, options = {}) {
71
- if (!(container instanceof HTMLElement)) {
51
+ if (!(container instanceof Element)) {
72
52
  console.warn("Invalid container element. Fallback: <body> element.");
73
53
  container = document.body;
74
54
  }
75
55
  return getRelativeFocusable(container, -1, options);
76
56
  }
77
57
  function hasFocusable(container = document.body, options = {}) {
78
- if (!(container instanceof HTMLElement)) {
58
+ if (!(container instanceof Element)) {
79
59
  console.warn("Invalid container element. Fallback: <body> element.");
80
60
  container = document.body;
81
61
  }
82
62
  return !!getFocusables(container, options).length;
83
63
  }
84
64
  function isFocusable(element) {
85
- if (!(element instanceof HTMLElement)) {
65
+ if (!(element instanceof Element)) {
86
66
  console.warn("Invalid element");
87
67
  return false;
88
68
  }
89
- if (element.hidden || element.inert) {
69
+ if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
90
70
  return false;
91
71
  }
92
- if (element.tabIndex < 0) {
72
+ if (getTabIndex(element) < 0) {
93
73
  return false;
94
74
  }
95
75
  if (!element.matches(FOCUSABLE_SELECTOR)) {
@@ -107,6 +87,24 @@ function isFocusable(element) {
107
87
  }
108
88
  return true;
109
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
+ }
110
108
  function getRelativeFocusable(container, offset, options) {
111
109
  const {
112
110
  active = getActiveElement(),
@@ -121,7 +119,7 @@ function getRelativeFocusable(container, offset, options) {
121
119
  if (!active || !containsDeep(container, active)) {
122
120
  return null;
123
121
  }
124
- if (!(active instanceof HTMLElement)) {
122
+ if (!(active instanceof Element)) {
125
123
  return null;
126
124
  }
127
125
  const currentIndex = focusables.indexOf(active);
@@ -134,6 +132,61 @@ function getRelativeFocusable(container, offset, options) {
134
132
  }
135
133
  return focusables[(offsetIndex + length) % length] ?? null;
136
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
+ }
137
190
  function containsDeep(container, element) {
138
191
  let current = element;
139
192
  while (current) {
@@ -151,6 +204,16 @@ function getActiveElement() {
151
204
  }
152
205
  return current;
153
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
+ }
154
217
  function isDisabled(element) {
155
218
  return "disabled" in element && !!element.disabled;
156
219
  }
@@ -190,67 +253,12 @@ function isFormControl(element) {
190
253
  function isUngroupedRadio(element) {
191
254
  return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
192
255
  }
193
- function normalizeRadioGroup(elements) {
194
- let map = null;
195
- for (let i = 0, l = elements.length; i < l; i++) {
196
- const element = elements[i];
197
- if (!(element instanceof HTMLInputElement)) {
198
- continue;
199
- }
200
- if (!isUngroupedRadio(element)) {
201
- continue;
202
- }
203
- if (!map) {
204
- map = /* @__PURE__ */ new Map();
205
- }
206
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
207
- const group = map.get(key) ?? map.set(key, []).get(key);
208
- if (group) {
209
- group[group.length] = element;
210
- }
211
- }
212
- if (!map) {
213
- return elements;
214
- }
215
- const placeholder = /* @__PURE__ */ new Set();
216
- for (const group of map.values()) {
217
- placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
218
- }
219
- return elements.filter((element) => {
220
- if (isUngroupedRadio(element)) {
221
- return placeholder.has(element);
222
- }
223
- return true;
224
- });
225
- }
226
- function sortByTabIndex(elements) {
227
- const ordered = [];
228
- const natural = [];
229
- for (let i = 0, l = elements.length; i < l; i++) {
230
- const element = elements[i];
231
- if (!element) {
232
- continue;
233
- }
234
- const target = element.tabIndex > 0 ? ordered : natural;
235
- target[target.length] = element;
236
- }
237
- ordered.sort((a, b) => a.tabIndex - b.tabIndex);
238
- let count = 0;
239
- const sorted = new Array(ordered.length + natural.length);
240
- for (let i = 0, l = ordered.length; i < l; i++) {
241
- sorted[count++] = ordered[i];
242
- }
243
- for (let i = 0, l = natural.length; i < l; i++) {
244
- sorted[count++] = natural[i];
245
- }
246
- return sorted;
247
- }
248
256
  /**
249
257
  * Power Focusable
250
258
  * High-precision focus management utility with shadow DOM support.
251
259
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
252
260
  *
253
- * @version 2.1.13
261
+ * @version 2.2.1
254
262
  * @author Yusuke Kamiyamane
255
263
  * @license MIT
256
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.1.13
6
+ * @version 2.2.1
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?: HTMLElement | null;
13
+ readonly active?: Element | null;
14
14
  readonly composed?: boolean;
15
15
  readonly wrap?: boolean;
16
16
  }
17
- declare function getFocusables(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): HTMLElement[];
18
- declare function getNextFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
19
- declare function getPreviousFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
20
- declare function hasFocusable(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
21
- declare function isFocusable(element: HTMLElement): boolean;
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.1.13
6
+ * @version 2.2.1
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?: HTMLElement | null;
13
+ readonly active?: Element | null;
14
14
  readonly composed?: boolean;
15
15
  readonly wrap?: boolean;
16
16
  }
17
- declare function getFocusables(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): HTMLElement[];
18
- declare function getNextFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
19
- declare function getPreviousFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
20
- declare function hasFocusable(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
21
- declare function isFocusable(element: HTMLElement): boolean;
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 HTMLElement)) {
4
+ if (!(container instanceof Element)) {
5
5
  console.warn("Invalid container element. Fallback: <body> element.");
6
6
  container = document.body;
7
7
  }
@@ -9,32 +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 HTMLElement) {
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
- }
34
- if (!(node instanceof Element || node instanceof ShadowRoot)) {
35
- return;
36
16
  }
37
- const children = node.children;
17
+ const children = getComposedChildren(node);
38
18
  for (let i = 0, l = children.length; i < l; i++) {
39
19
  const child = children[i];
40
20
  if (!child) {
@@ -48,7 +28,7 @@ function getFocusables(container = document.body, options = {}) {
48
28
  const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
49
29
  for (let i = 0, l = candidates.length; i < l; i++) {
50
30
  const candidate = candidates[i];
51
- if (!(candidate instanceof HTMLElement)) {
31
+ if (!(candidate instanceof Element)) {
52
32
  continue;
53
33
  }
54
34
  if (isFocusable(candidate)) {
@@ -59,35 +39,35 @@ function getFocusables(container = document.body, options = {}) {
59
39
  return normalizeRadioGroup(sortByTabIndex(elements));
60
40
  }
61
41
  function getNextFocusable(container = document.body, options = {}) {
62
- if (!(container instanceof HTMLElement)) {
42
+ if (!(container instanceof Element)) {
63
43
  console.warn("Invalid container element. Fallback: <body> element.");
64
44
  container = document.body;
65
45
  }
66
46
  return getRelativeFocusable(container, 1, options);
67
47
  }
68
48
  function getPreviousFocusable(container = document.body, options = {}) {
69
- if (!(container instanceof HTMLElement)) {
49
+ if (!(container instanceof Element)) {
70
50
  console.warn("Invalid container element. Fallback: <body> element.");
71
51
  container = document.body;
72
52
  }
73
53
  return getRelativeFocusable(container, -1, options);
74
54
  }
75
55
  function hasFocusable(container = document.body, options = {}) {
76
- if (!(container instanceof HTMLElement)) {
56
+ if (!(container instanceof Element)) {
77
57
  console.warn("Invalid container element. Fallback: <body> element.");
78
58
  container = document.body;
79
59
  }
80
60
  return !!getFocusables(container, options).length;
81
61
  }
82
62
  function isFocusable(element) {
83
- if (!(element instanceof HTMLElement)) {
63
+ if (!(element instanceof Element)) {
84
64
  console.warn("Invalid element");
85
65
  return false;
86
66
  }
87
- if (element.hidden || element.inert) {
67
+ if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
88
68
  return false;
89
69
  }
90
- if (element.tabIndex < 0) {
70
+ if (getTabIndex(element) < 0) {
91
71
  return false;
92
72
  }
93
73
  if (!element.matches(FOCUSABLE_SELECTOR)) {
@@ -105,6 +85,24 @@ function isFocusable(element) {
105
85
  }
106
86
  return true;
107
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
+ }
108
106
  function getRelativeFocusable(container, offset, options) {
109
107
  const {
110
108
  active = getActiveElement(),
@@ -119,7 +117,7 @@ function getRelativeFocusable(container, offset, options) {
119
117
  if (!active || !containsDeep(container, active)) {
120
118
  return null;
121
119
  }
122
- if (!(active instanceof HTMLElement)) {
120
+ if (!(active instanceof Element)) {
123
121
  return null;
124
122
  }
125
123
  const currentIndex = focusables.indexOf(active);
@@ -132,6 +130,61 @@ function getRelativeFocusable(container, offset, options) {
132
130
  }
133
131
  return focusables[(offsetIndex + length) % length] ?? null;
134
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
+ }
135
188
  function containsDeep(container, element) {
136
189
  let current = element;
137
190
  while (current) {
@@ -149,6 +202,16 @@ function getActiveElement() {
149
202
  }
150
203
  return current;
151
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
+ }
152
215
  function isDisabled(element) {
153
216
  return "disabled" in element && !!element.disabled;
154
217
  }
@@ -188,67 +251,12 @@ function isFormControl(element) {
188
251
  function isUngroupedRadio(element) {
189
252
  return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
190
253
  }
191
- function normalizeRadioGroup(elements) {
192
- let map = null;
193
- for (let i = 0, l = elements.length; i < l; i++) {
194
- const element = elements[i];
195
- if (!(element instanceof HTMLInputElement)) {
196
- continue;
197
- }
198
- if (!isUngroupedRadio(element)) {
199
- continue;
200
- }
201
- if (!map) {
202
- map = /* @__PURE__ */ new Map();
203
- }
204
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
205
- const group = map.get(key) ?? map.set(key, []).get(key);
206
- if (group) {
207
- group[group.length] = element;
208
- }
209
- }
210
- if (!map) {
211
- return elements;
212
- }
213
- const placeholder = /* @__PURE__ */ new Set();
214
- for (const group of map.values()) {
215
- placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
216
- }
217
- return elements.filter((element) => {
218
- if (isUngroupedRadio(element)) {
219
- return placeholder.has(element);
220
- }
221
- return true;
222
- });
223
- }
224
- function sortByTabIndex(elements) {
225
- const ordered = [];
226
- const natural = [];
227
- for (let i = 0, l = elements.length; i < l; i++) {
228
- const element = elements[i];
229
- if (!element) {
230
- continue;
231
- }
232
- const target = element.tabIndex > 0 ? ordered : natural;
233
- target[target.length] = element;
234
- }
235
- ordered.sort((a, b) => a.tabIndex - b.tabIndex);
236
- let count = 0;
237
- const sorted = new Array(ordered.length + natural.length);
238
- for (let i = 0, l = ordered.length; i < l; i++) {
239
- sorted[count++] = ordered[i];
240
- }
241
- for (let i = 0, l = natural.length; i < l; i++) {
242
- sorted[count++] = natural[i];
243
- }
244
- return sorted;
245
- }
246
254
  /**
247
255
  * Power Focusable
248
256
  * High-precision focus management utility with shadow DOM support.
249
257
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
250
258
  *
251
- * @version 2.1.13
259
+ * @version 2.2.1
252
260
  * @author Yusuke Kamiyamane
253
261
  * @license MIT
254
262
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-focusable",
3
- "version": "2.1.13",
3
+ "version": "2.2.1",
4
4
  "description": "High-precision focus management utility with shadow DOM support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",