power-focusable 2.0.3 → 2.1.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/dist/index.cjs CHANGED
@@ -2,7 +2,6 @@
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 tabIndexCache = /* @__PURE__ */ new WeakMap();
6
5
  function getFocusables(container = document.body, options = {}) {
7
6
  if (!(container instanceof HTMLElement)) {
8
7
  console.warn("Invalid container element. Fallback: <body> element.");
@@ -14,7 +13,7 @@ function getFocusables(container = document.body, options = {}) {
14
13
  let walk2 = function(node) {
15
14
  if (node instanceof HTMLElement) {
16
15
  if (isFocusable(node)) {
17
- elements.push(node);
16
+ elements[elements.length] = node;
18
17
  }
19
18
  const shadow = node.shadowRoot;
20
19
  if (shadow && shadow.mode === "open") {
@@ -23,75 +22,28 @@ function getFocusables(container = document.body, options = {}) {
23
22
  } else if (node instanceof HTMLSlotElement) {
24
23
  const assigned = node.assignedElements({ flatten: true });
25
24
  if (assigned.length > 0) {
26
- assigned.forEach((a) => {
27
- walk2(a);
28
- });
25
+ for (let i = 0, l = assigned.length; i < l; i++) {
26
+ walk2(assigned[i]);
27
+ }
29
28
  return;
30
29
  }
31
30
  }
32
- node.childNodes.forEach((child) => {
33
- walk2(child);
34
- });
31
+ const children = node.childNodes;
32
+ for (let i = 0, l = children.length; i < l; i++) {
33
+ walk2(children[i]);
34
+ }
35
35
  };
36
36
  walk2(container);
37
37
  } else {
38
- elements.push(
39
- ...[
40
- ...container.querySelectorAll(FOCUSABLE_SELECTOR)
41
- ].filter(isFocusable)
42
- );
43
- }
44
- function sort(elements2) {
45
- const ordered = [];
46
- const natural = [];
47
- function getTabIndex(element) {
48
- const cached = tabIndexCache.get(element);
49
- if (cached !== void 0) {
50
- return cached;
51
- }
52
- const number = Number(element.getAttribute("tabindex"));
53
- tabIndexCache.set(element, number);
54
- return number;
55
- }
56
- elements2.forEach((element) => {
57
- (getTabIndex(element) > 0 ? ordered : natural).push(element);
58
- });
59
- ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
60
- return [...ordered, ...natural];
61
- }
62
- function normalize(elements2) {
63
- let map = null;
64
- for (const element of elements2) {
65
- if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
66
- if (!map) {
67
- map = /* @__PURE__ */ new Map();
68
- }
69
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
70
- (map.get(key) ?? map.set(key, []).get(key)).push(element);
71
- }
72
- }
73
- if (!map) {
74
- return elements2;
75
- }
76
- const placeholder = /* @__PURE__ */ new Set();
77
- for (const group of map.values()) {
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
- }
38
+ const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
39
+ for (let i = 0, l = candidates.length; i < l; i++) {
40
+ const candidate = candidates[i];
41
+ if (isFocusable(candidate)) {
42
+ elements[elements.length] = candidate;
85
43
  }
86
44
  }
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
- });
93
45
  }
94
- return normalize(sort(elements));
46
+ return normalizeRadioGroup(sortByTabIndex(elements));
95
47
  }
96
48
  function getNextFocusable(container = document.body, options = {}) {
97
49
  if (!(container instanceof HTMLElement)) {
@@ -119,65 +71,42 @@ function isFocusable(element) {
119
71
  console.warn("Invalid element");
120
72
  return false;
121
73
  }
122
- function isDisabledDeep(element2) {
123
- function isFormControl(element3) {
124
- return /^(BUTTON|INPUT|SELECT|TEXTAREA)$/.test(element3.tagName);
125
- }
126
- function isDisabled(element3) {
127
- return "disabled" in element3 && element3.disabled;
128
- }
129
- for (let current = element2; current; current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode) {
130
- if (!(current instanceof Element)) {
131
- continue;
132
- }
133
- if (current === element2 && isFormControl(current) && isDisabled(current)) {
134
- return true;
135
- }
136
- if (current.matches("[inert]")) {
137
- return true;
138
- }
139
- if (isFormControl(element2) && current.tagName === "FIELDSET" && isDisabled(current)) {
140
- if (current.querySelector(":scope > legend:first-of-type")?.contains(element2)) {
141
- continue;
142
- }
143
- return true;
144
- }
74
+ if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
75
+ return false;
76
+ }
77
+ const tabIndex = element.getAttribute("tabindex");
78
+ if (tabIndex !== null) {
79
+ if (Number(tabIndex) < 0) {
80
+ return false;
145
81
  }
82
+ }
83
+ if (!element.matches(FOCUSABLE_SELECTOR)) {
84
+ return false;
85
+ }
86
+ if (isDisabledDeep(element)) {
146
87
  return false;
147
88
  }
148
- return element.matches(FOCUSABLE_SELECTOR) && !isDisabledDeep(element) && element.checkVisibility({
89
+ if (!element.checkVisibility({
149
90
  contentVisibilityAuto: true,
150
91
  opacityProperty: true,
151
92
  visibilityProperty: true
152
- });
93
+ })) {
94
+ return false;
95
+ }
96
+ return true;
153
97
  }
154
98
  function getRelativeFocusable(container, offset = 0, options) {
155
- const { active, composed = false, wrap = false } = options;
99
+ const { active: a = null, composed = false, wrap = false } = options;
156
100
  const focusables = getFocusables(container, { composed });
157
101
  const { length } = focusables;
158
102
  if (length === 0) {
159
103
  return null;
160
104
  }
161
- function getActiveElement() {
162
- let active2 = document.activeElement;
163
- while (active2 instanceof HTMLElement && active2.shadowRoot?.activeElement) {
164
- active2 = active2.shadowRoot.activeElement;
165
- }
166
- return active2 instanceof HTMLElement ? active2 : null;
167
- }
168
- const current = active ?? getActiveElement();
169
- function containsDeep(container2, node) {
170
- for (let current2 = node; current2; current2 = !(current2 instanceof ShadowRoot) ? current2.parentNode : current2.mode === "open" ? current2.host : null) {
171
- if (current2 === container2) {
172
- return true;
173
- }
174
- }
175
- return false;
176
- }
177
- if (!current || !containsDeep(container, current)) {
105
+ const active = a ?? getActiveElement();
106
+ if (active === null || !containsDeep(container, active)) {
178
107
  return null;
179
108
  }
180
- const currentIndex = focusables.indexOf(current);
109
+ const currentIndex = focusables.indexOf(active);
181
110
  if (currentIndex === -1) {
182
111
  return null;
183
112
  }
@@ -187,12 +116,132 @@ function getRelativeFocusable(container, offset = 0, options) {
187
116
  }
188
117
  return focusables[(offsetIndex + length) % length] ?? null;
189
118
  }
119
+ function containsDeep(container, element) {
120
+ function walk(node) {
121
+ if (node === null) {
122
+ return false;
123
+ }
124
+ if (node === container) {
125
+ return true;
126
+ }
127
+ if (node instanceof ShadowRoot) {
128
+ return node.mode === "open" ? walk(node.host) : false;
129
+ }
130
+ return walk(node.parentNode);
131
+ }
132
+ return walk(element);
133
+ }
134
+ function getActiveElement() {
135
+ function walk(node) {
136
+ if (node === null) {
137
+ return null;
138
+ }
139
+ const active = node.shadowRoot?.activeElement;
140
+ return active ? walk(active) : node;
141
+ }
142
+ return walk(document.activeElement);
143
+ }
144
+ var tabIndexCache = /* @__PURE__ */ new WeakMap();
145
+ function getTabIndex(element) {
146
+ const cached = tabIndexCache.get(element);
147
+ if (cached !== void 0) {
148
+ return cached;
149
+ }
150
+ const number = Number(element.getAttribute("tabindex"));
151
+ tabIndexCache.set(element, number);
152
+ return number;
153
+ }
154
+ function isDisabled(element) {
155
+ return "disabled" in element && element.disabled;
156
+ }
157
+ function isDisabledDeep(element) {
158
+ function walk(node) {
159
+ if (node === null) {
160
+ return false;
161
+ }
162
+ if (node instanceof ShadowRoot) {
163
+ return node.mode === "open" ? walk(node.host) : false;
164
+ }
165
+ if (!(node instanceof Element)) {
166
+ return walk(node.parentNode);
167
+ }
168
+ if (node === element && isFormControl(node) && isDisabled(node)) {
169
+ return true;
170
+ }
171
+ if (node.hasAttribute("inert")) {
172
+ return true;
173
+ }
174
+ if (isFormControl(element) && node.tagName === "FIELDSET" && isDisabled(node)) {
175
+ if (node.querySelector(":scope > legend:first-of-type")?.contains(element)) {
176
+ return walk(node.parentNode);
177
+ }
178
+ return true;
179
+ }
180
+ return walk(node.parentNode);
181
+ }
182
+ return walk(element);
183
+ }
184
+ function isFormControl(element) {
185
+ const tagName = element.tagName;
186
+ return tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
187
+ }
188
+ function normalizeRadioGroup(elements) {
189
+ let map = null;
190
+ for (let i = 0, l = elements.length; i < l; i++) {
191
+ const element = elements[i];
192
+ if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
193
+ if (!map) {
194
+ map = /* @__PURE__ */ new Map();
195
+ }
196
+ const key = `${element.form?.id ?? "no-form"}::${element.name}`;
197
+ const group = map.get(key) ?? map.set(key, []).get(key);
198
+ group[group.length] = element;
199
+ }
200
+ }
201
+ if (!map) {
202
+ return elements;
203
+ }
204
+ const placeholder = /* @__PURE__ */ new Set();
205
+ for (const group of map.values()) {
206
+ if (group.length > 0) {
207
+ const enabled = group.filter(isFocusable);
208
+ if (enabled.length > 0) {
209
+ placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
210
+ }
211
+ }
212
+ }
213
+ return elements.filter((element) => {
214
+ if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
215
+ return placeholder.has(element);
216
+ }
217
+ return true;
218
+ });
219
+ }
220
+ function sortByTabIndex(elements) {
221
+ const ordered = [];
222
+ const natural = [];
223
+ for (let i = 0, l = elements.length; i < l; i++) {
224
+ const element = elements[i];
225
+ const target = getTabIndex(element) > 0 ? ordered : natural;
226
+ target[target.length] = element;
227
+ }
228
+ ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
229
+ let count = 0;
230
+ const sorted = new Array(ordered.length + natural.length);
231
+ for (let i = 0, l = ordered.length; i < l; i++) {
232
+ sorted[count++] = ordered[i];
233
+ }
234
+ for (let i = 0, l = natural.length; i < l; i++) {
235
+ sorted[count++] = natural[i];
236
+ }
237
+ return sorted;
238
+ }
190
239
  /**
191
240
  * Power Focusable
192
241
  * High-precision focus management utility with shadow DOM support.
193
242
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
194
243
  *
195
- * @version 2.0.3
244
+ * @version 2.1.0
196
245
  * @author Yusuke Kamiyamane
197
246
  * @license MIT
198
247
  * @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 including tabindex ordering, radio groups, etc.
5
5
  *
6
- * @version 2.0.3
6
+ * @version 2.1.0
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
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 including tabindex ordering, radio groups, etc.
5
5
  *
6
- * @version 2.0.3
6
+ * @version 2.1.0
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
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 tabIndexCache = /* @__PURE__ */ new WeakMap();
4
3
  function getFocusables(container = document.body, options = {}) {
5
4
  if (!(container instanceof HTMLElement)) {
6
5
  console.warn("Invalid container element. Fallback: <body> element.");
@@ -12,7 +11,7 @@ function getFocusables(container = document.body, options = {}) {
12
11
  let walk2 = function(node) {
13
12
  if (node instanceof HTMLElement) {
14
13
  if (isFocusable(node)) {
15
- elements.push(node);
14
+ elements[elements.length] = node;
16
15
  }
17
16
  const shadow = node.shadowRoot;
18
17
  if (shadow && shadow.mode === "open") {
@@ -21,75 +20,28 @@ function getFocusables(container = document.body, options = {}) {
21
20
  } else if (node instanceof HTMLSlotElement) {
22
21
  const assigned = node.assignedElements({ flatten: true });
23
22
  if (assigned.length > 0) {
24
- assigned.forEach((a) => {
25
- walk2(a);
26
- });
23
+ for (let i = 0, l = assigned.length; i < l; i++) {
24
+ walk2(assigned[i]);
25
+ }
27
26
  return;
28
27
  }
29
28
  }
30
- node.childNodes.forEach((child) => {
31
- walk2(child);
32
- });
29
+ const children = node.childNodes;
30
+ for (let i = 0, l = children.length; i < l; i++) {
31
+ walk2(children[i]);
32
+ }
33
33
  };
34
34
  walk2(container);
35
35
  } else {
36
- elements.push(
37
- ...[
38
- ...container.querySelectorAll(FOCUSABLE_SELECTOR)
39
- ].filter(isFocusable)
40
- );
41
- }
42
- function sort(elements2) {
43
- const ordered = [];
44
- const natural = [];
45
- function getTabIndex(element) {
46
- const cached = tabIndexCache.get(element);
47
- if (cached !== void 0) {
48
- return cached;
49
- }
50
- const number = Number(element.getAttribute("tabindex"));
51
- tabIndexCache.set(element, number);
52
- return number;
53
- }
54
- elements2.forEach((element) => {
55
- (getTabIndex(element) > 0 ? ordered : natural).push(element);
56
- });
57
- ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
58
- return [...ordered, ...natural];
59
- }
60
- function normalize(elements2) {
61
- let map = null;
62
- for (const element of elements2) {
63
- if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
64
- if (!map) {
65
- map = /* @__PURE__ */ new Map();
66
- }
67
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
68
- (map.get(key) ?? map.set(key, []).get(key)).push(element);
69
- }
70
- }
71
- if (!map) {
72
- return elements2;
73
- }
74
- const placeholder = /* @__PURE__ */ new Set();
75
- for (const group of map.values()) {
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
- }
36
+ const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
37
+ for (let i = 0, l = candidates.length; i < l; i++) {
38
+ const candidate = candidates[i];
39
+ if (isFocusable(candidate)) {
40
+ elements[elements.length] = candidate;
83
41
  }
84
42
  }
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
- });
91
43
  }
92
- return normalize(sort(elements));
44
+ return normalizeRadioGroup(sortByTabIndex(elements));
93
45
  }
94
46
  function getNextFocusable(container = document.body, options = {}) {
95
47
  if (!(container instanceof HTMLElement)) {
@@ -117,65 +69,42 @@ function isFocusable(element) {
117
69
  console.warn("Invalid element");
118
70
  return false;
119
71
  }
120
- function isDisabledDeep(element2) {
121
- function isFormControl(element3) {
122
- return /^(BUTTON|INPUT|SELECT|TEXTAREA)$/.test(element3.tagName);
123
- }
124
- function isDisabled(element3) {
125
- return "disabled" in element3 && element3.disabled;
126
- }
127
- for (let current = element2; current; current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode) {
128
- if (!(current instanceof Element)) {
129
- continue;
130
- }
131
- if (current === element2 && isFormControl(current) && isDisabled(current)) {
132
- return true;
133
- }
134
- if (current.matches("[inert]")) {
135
- return true;
136
- }
137
- if (isFormControl(element2) && current.tagName === "FIELDSET" && isDisabled(current)) {
138
- if (current.querySelector(":scope > legend:first-of-type")?.contains(element2)) {
139
- continue;
140
- }
141
- return true;
142
- }
72
+ if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
73
+ return false;
74
+ }
75
+ const tabIndex = element.getAttribute("tabindex");
76
+ if (tabIndex !== null) {
77
+ if (Number(tabIndex) < 0) {
78
+ return false;
143
79
  }
80
+ }
81
+ if (!element.matches(FOCUSABLE_SELECTOR)) {
82
+ return false;
83
+ }
84
+ if (isDisabledDeep(element)) {
144
85
  return false;
145
86
  }
146
- return element.matches(FOCUSABLE_SELECTOR) && !isDisabledDeep(element) && element.checkVisibility({
87
+ if (!element.checkVisibility({
147
88
  contentVisibilityAuto: true,
148
89
  opacityProperty: true,
149
90
  visibilityProperty: true
150
- });
91
+ })) {
92
+ return false;
93
+ }
94
+ return true;
151
95
  }
152
96
  function getRelativeFocusable(container, offset = 0, options) {
153
- const { active, composed = false, wrap = false } = options;
97
+ const { active: a = null, composed = false, wrap = false } = options;
154
98
  const focusables = getFocusables(container, { composed });
155
99
  const { length } = focusables;
156
100
  if (length === 0) {
157
101
  return null;
158
102
  }
159
- function getActiveElement() {
160
- let active2 = document.activeElement;
161
- while (active2 instanceof HTMLElement && active2.shadowRoot?.activeElement) {
162
- active2 = active2.shadowRoot.activeElement;
163
- }
164
- return active2 instanceof HTMLElement ? active2 : null;
165
- }
166
- const current = active ?? getActiveElement();
167
- function containsDeep(container2, node) {
168
- for (let current2 = node; current2; current2 = !(current2 instanceof ShadowRoot) ? current2.parentNode : current2.mode === "open" ? current2.host : null) {
169
- if (current2 === container2) {
170
- return true;
171
- }
172
- }
173
- return false;
174
- }
175
- if (!current || !containsDeep(container, current)) {
103
+ const active = a ?? getActiveElement();
104
+ if (active === null || !containsDeep(container, active)) {
176
105
  return null;
177
106
  }
178
- const currentIndex = focusables.indexOf(current);
107
+ const currentIndex = focusables.indexOf(active);
179
108
  if (currentIndex === -1) {
180
109
  return null;
181
110
  }
@@ -185,12 +114,132 @@ function getRelativeFocusable(container, offset = 0, options) {
185
114
  }
186
115
  return focusables[(offsetIndex + length) % length] ?? null;
187
116
  }
117
+ function containsDeep(container, element) {
118
+ function walk(node) {
119
+ if (node === null) {
120
+ return false;
121
+ }
122
+ if (node === container) {
123
+ return true;
124
+ }
125
+ if (node instanceof ShadowRoot) {
126
+ return node.mode === "open" ? walk(node.host) : false;
127
+ }
128
+ return walk(node.parentNode);
129
+ }
130
+ return walk(element);
131
+ }
132
+ function getActiveElement() {
133
+ function walk(node) {
134
+ if (node === null) {
135
+ return null;
136
+ }
137
+ const active = node.shadowRoot?.activeElement;
138
+ return active ? walk(active) : node;
139
+ }
140
+ return walk(document.activeElement);
141
+ }
142
+ var tabIndexCache = /* @__PURE__ */ new WeakMap();
143
+ function getTabIndex(element) {
144
+ const cached = tabIndexCache.get(element);
145
+ if (cached !== void 0) {
146
+ return cached;
147
+ }
148
+ const number = Number(element.getAttribute("tabindex"));
149
+ tabIndexCache.set(element, number);
150
+ return number;
151
+ }
152
+ function isDisabled(element) {
153
+ return "disabled" in element && element.disabled;
154
+ }
155
+ function isDisabledDeep(element) {
156
+ function walk(node) {
157
+ if (node === null) {
158
+ return false;
159
+ }
160
+ if (node instanceof ShadowRoot) {
161
+ return node.mode === "open" ? walk(node.host) : false;
162
+ }
163
+ if (!(node instanceof Element)) {
164
+ return walk(node.parentNode);
165
+ }
166
+ if (node === element && isFormControl(node) && isDisabled(node)) {
167
+ return true;
168
+ }
169
+ if (node.hasAttribute("inert")) {
170
+ return true;
171
+ }
172
+ if (isFormControl(element) && node.tagName === "FIELDSET" && isDisabled(node)) {
173
+ if (node.querySelector(":scope > legend:first-of-type")?.contains(element)) {
174
+ return walk(node.parentNode);
175
+ }
176
+ return true;
177
+ }
178
+ return walk(node.parentNode);
179
+ }
180
+ return walk(element);
181
+ }
182
+ function isFormControl(element) {
183
+ const tagName = element.tagName;
184
+ return tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
185
+ }
186
+ function normalizeRadioGroup(elements) {
187
+ let map = null;
188
+ for (let i = 0, l = elements.length; i < l; i++) {
189
+ const element = elements[i];
190
+ if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
191
+ if (!map) {
192
+ map = /* @__PURE__ */ new Map();
193
+ }
194
+ const key = `${element.form?.id ?? "no-form"}::${element.name}`;
195
+ const group = map.get(key) ?? map.set(key, []).get(key);
196
+ group[group.length] = element;
197
+ }
198
+ }
199
+ if (!map) {
200
+ return elements;
201
+ }
202
+ const placeholder = /* @__PURE__ */ new Set();
203
+ for (const group of map.values()) {
204
+ if (group.length > 0) {
205
+ const enabled = group.filter(isFocusable);
206
+ if (enabled.length > 0) {
207
+ placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
208
+ }
209
+ }
210
+ }
211
+ return elements.filter((element) => {
212
+ if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
213
+ return placeholder.has(element);
214
+ }
215
+ return true;
216
+ });
217
+ }
218
+ function sortByTabIndex(elements) {
219
+ const ordered = [];
220
+ const natural = [];
221
+ for (let i = 0, l = elements.length; i < l; i++) {
222
+ const element = elements[i];
223
+ const target = getTabIndex(element) > 0 ? ordered : natural;
224
+ target[target.length] = element;
225
+ }
226
+ ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
227
+ let count = 0;
228
+ const sorted = new Array(ordered.length + natural.length);
229
+ for (let i = 0, l = ordered.length; i < l; i++) {
230
+ sorted[count++] = ordered[i];
231
+ }
232
+ for (let i = 0, l = natural.length; i < l; i++) {
233
+ sorted[count++] = natural[i];
234
+ }
235
+ return sorted;
236
+ }
188
237
  /**
189
238
  * Power Focusable
190
239
  * High-precision focus management utility with shadow DOM support.
191
240
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
192
241
  *
193
- * @version 2.0.3
242
+ * @version 2.1.0
194
243
  * @author Yusuke Kamiyamane
195
244
  * @license MIT
196
245
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-focusable",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "High-precision focus management utility with shadow DOM support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",