power-focusable 2.0.2 → 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/README.md CHANGED
@@ -67,7 +67,7 @@ Returns all focusable elements within the container.
67
67
  getFocusables(container);
68
68
  // => HTMLElement[]
69
69
  //
70
- // container (optional): HTMLElement (default: document.body)
70
+ // container (optional): HTMLElement (default: <body>)
71
71
 
72
72
  // Traverses the composed tree (including shadow DOM; slower)
73
73
  getFocusables(container, { composed: true });
@@ -81,7 +81,7 @@ Returns the next focusable element within the container, starting from `document
81
81
  getNextFocusable(container);
82
82
  // => HTMLElement | null
83
83
  //
84
- // container (optional): HTMLElement (default: document.body)
84
+ // container (optional): HTMLElement (default: <body>)
85
85
 
86
86
  // Specifies the starting element
87
87
  getNextFocusable(container, { active: document.querySelector('.button') });
@@ -89,7 +89,7 @@ getNextFocusable(container, { active: document.querySelector('.button') });
89
89
  // Traverses the composed tree (including shadow DOM; slower)
90
90
  getNextFocusable(container, { composed: true });
91
91
 
92
- // Wraps around to the first element when reaching the end.
92
+ // Wraps around to the first element when reaching the end
93
93
  getNextFocusable(container, { wrap: true });
94
94
  ```
95
95
 
@@ -101,7 +101,7 @@ Returns the previous focusable element within the container, starting from `docu
101
101
  getPreviousFocusable(container);
102
102
  // => HTMLElement | null
103
103
  //
104
- // container (optional): HTMLElement (default: document.body)
104
+ // container (optional): HTMLElement (default: <body>)
105
105
 
106
106
  // Specifies the starting element
107
107
  getPreviousFocusable(container, { active: document.querySelector('.button') });
@@ -109,7 +109,7 @@ getPreviousFocusable(container, { active: document.querySelector('.button') });
109
109
  // Traverses the composed tree (including shadow DOM; slower)
110
110
  getPreviousFocusable(container, { composed: true });
111
111
 
112
- //Wraps around to the last element when reaching the end.
112
+ //Wraps around to the last element when reaching the end
113
113
  getPreviousFocusable(container, { wrap: true });
114
114
 
115
115
  ```
@@ -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: document.body)
125
+ // container (optional): HTMLElement (default: <body>)
126
126
  ```
127
127
 
128
128
  ### `isFocusable`
package/dist/index.cjs CHANGED
@@ -13,7 +13,7 @@ function getFocusables(container = document.body, options = {}) {
13
13
  let walk2 = function(node) {
14
14
  if (node instanceof HTMLElement) {
15
15
  if (isFocusable(node)) {
16
- elements.push(node);
16
+ elements[elements.length] = node;
17
17
  }
18
18
  const shadow = node.shadowRoot;
19
19
  if (shadow && shadow.mode === "open") {
@@ -22,74 +22,28 @@ function getFocusables(container = document.body, options = {}) {
22
22
  } else if (node instanceof HTMLSlotElement) {
23
23
  const assigned = node.assignedElements({ flatten: true });
24
24
  if (assigned.length > 0) {
25
- assigned.forEach((a) => {
26
- walk2(a);
27
- });
25
+ for (let i = 0, l = assigned.length; i < l; i++) {
26
+ walk2(assigned[i]);
27
+ }
28
28
  return;
29
29
  }
30
30
  }
31
- node.childNodes.forEach((child) => {
32
- walk2(child);
33
- });
31
+ const children = node.childNodes;
32
+ for (let i = 0, l = children.length; i < l; i++) {
33
+ walk2(children[i]);
34
+ }
34
35
  };
35
36
  walk2(container);
36
37
  } else {
37
- elements.push(
38
- ...[
39
- ...container.querySelectorAll(FOCUSABLE_SELECTOR)
40
- ].filter(isFocusable)
41
- );
42
- }
43
- const cache = /* @__PURE__ */ new WeakMap();
44
- function sort(elements2) {
45
- const ordered = [];
46
- const natural = [];
47
- function getTabIndex(element) {
48
- const cached = cache.get(element);
49
- if (cached !== void 0) {
50
- return cached;
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;
51
43
  }
52
- const number = Number(element.getAttribute("tabindex"));
53
- cache.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 normalizeRadioGroup(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
44
  }
76
- const placeholder = /* @__PURE__ */ new Set();
77
- for (const radios of map.values()) {
78
- if (radios.length > 0) {
79
- const enabled = radios.filter((radio) => isFocusable(radio));
80
- if (enabled.length > 0) {
81
- placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
82
- }
83
- }
84
- }
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
45
  }
92
- return normalizeRadioGroup(sort(elements));
46
+ return normalizeRadioGroup(sortByTabIndex(elements));
93
47
  }
94
48
  function getNextFocusable(container = document.body, options = {}) {
95
49
  if (!(container instanceof HTMLElement)) {
@@ -117,65 +71,42 @@ function isFocusable(element) {
117
71
  console.warn("Invalid element");
118
72
  return false;
119
73
  }
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
- }
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;
143
81
  }
82
+ }
83
+ if (!element.matches(FOCUSABLE_SELECTOR)) {
144
84
  return false;
145
85
  }
146
- return element.matches(FOCUSABLE_SELECTOR) && !isDisabledDeep(element) && element.checkVisibility({
86
+ if (isDisabledDeep(element)) {
87
+ return false;
88
+ }
89
+ if (!element.checkVisibility({
147
90
  contentVisibilityAuto: true,
148
91
  opacityProperty: true,
149
92
  visibilityProperty: true
150
- });
93
+ })) {
94
+ return false;
95
+ }
96
+ return true;
151
97
  }
152
98
  function getRelativeFocusable(container, offset = 0, options) {
153
- const { active, composed = false, wrap = false } = options;
99
+ const { active: a = null, composed = false, wrap = false } = options;
154
100
  const focusables = getFocusables(container, { composed });
155
101
  const { length } = focusables;
156
102
  if (length === 0) {
157
103
  return null;
158
104
  }
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)) {
105
+ const active = a ?? getActiveElement();
106
+ if (active === null || !containsDeep(container, active)) {
176
107
  return null;
177
108
  }
178
- const currentIndex = focusables.indexOf(current);
109
+ const currentIndex = focusables.indexOf(active);
179
110
  if (currentIndex === -1) {
180
111
  return null;
181
112
  }
@@ -185,12 +116,132 @@ function getRelativeFocusable(container, offset = 0, options) {
185
116
  }
186
117
  return focusables[(offsetIndex + length) % length] ?? null;
187
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
+ }
188
239
  /**
189
240
  * Power Focusable
190
241
  * High-precision focus management utility with shadow DOM support.
191
242
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
192
243
  *
193
- * @version 2.0.2
244
+ * @version 2.1.0
194
245
  * @author Yusuke Kamiyamane
195
246
  * @license MIT
196
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.2
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.2
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
@@ -11,7 +11,7 @@ function getFocusables(container = document.body, options = {}) {
11
11
  let walk2 = function(node) {
12
12
  if (node instanceof HTMLElement) {
13
13
  if (isFocusable(node)) {
14
- elements.push(node);
14
+ elements[elements.length] = node;
15
15
  }
16
16
  const shadow = node.shadowRoot;
17
17
  if (shadow && shadow.mode === "open") {
@@ -20,74 +20,28 @@ function getFocusables(container = document.body, options = {}) {
20
20
  } else if (node instanceof HTMLSlotElement) {
21
21
  const assigned = node.assignedElements({ flatten: true });
22
22
  if (assigned.length > 0) {
23
- assigned.forEach((a) => {
24
- walk2(a);
25
- });
23
+ for (let i = 0, l = assigned.length; i < l; i++) {
24
+ walk2(assigned[i]);
25
+ }
26
26
  return;
27
27
  }
28
28
  }
29
- node.childNodes.forEach((child) => {
30
- walk2(child);
31
- });
29
+ const children = node.childNodes;
30
+ for (let i = 0, l = children.length; i < l; i++) {
31
+ walk2(children[i]);
32
+ }
32
33
  };
33
34
  walk2(container);
34
35
  } else {
35
- elements.push(
36
- ...[
37
- ...container.querySelectorAll(FOCUSABLE_SELECTOR)
38
- ].filter(isFocusable)
39
- );
40
- }
41
- const cache = /* @__PURE__ */ new WeakMap();
42
- function sort(elements2) {
43
- const ordered = [];
44
- const natural = [];
45
- function getTabIndex(element) {
46
- const cached = cache.get(element);
47
- if (cached !== void 0) {
48
- return cached;
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;
49
41
  }
50
- const number = Number(element.getAttribute("tabindex"));
51
- cache.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 normalizeRadioGroup(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
42
  }
74
- const placeholder = /* @__PURE__ */ new Set();
75
- for (const radios of map.values()) {
76
- if (radios.length > 0) {
77
- const enabled = radios.filter((radio) => isFocusable(radio));
78
- if (enabled.length > 0) {
79
- placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
80
- }
81
- }
82
- }
83
- return elements2.filter((element) => {
84
- if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
85
- return placeholder.has(element);
86
- }
87
- return true;
88
- });
89
43
  }
90
- return normalizeRadioGroup(sort(elements));
44
+ return normalizeRadioGroup(sortByTabIndex(elements));
91
45
  }
92
46
  function getNextFocusable(container = document.body, options = {}) {
93
47
  if (!(container instanceof HTMLElement)) {
@@ -115,65 +69,42 @@ function isFocusable(element) {
115
69
  console.warn("Invalid element");
116
70
  return false;
117
71
  }
118
- function isDisabledDeep(element2) {
119
- function isFormControl(element3) {
120
- return /^(BUTTON|INPUT|SELECT|TEXTAREA)$/.test(element3.tagName);
121
- }
122
- function isDisabled(element3) {
123
- return "disabled" in element3 && element3.disabled;
124
- }
125
- for (let current = element2; current; current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode) {
126
- if (!(current instanceof Element)) {
127
- continue;
128
- }
129
- if (current === element2 && isFormControl(current) && isDisabled(current)) {
130
- return true;
131
- }
132
- if (current.matches("[inert]")) {
133
- return true;
134
- }
135
- if (isFormControl(element2) && current.tagName === "FIELDSET" && isDisabled(current)) {
136
- if (current.querySelector(":scope > legend:first-of-type")?.contains(element2)) {
137
- continue;
138
- }
139
- return true;
140
- }
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;
141
79
  }
80
+ }
81
+ if (!element.matches(FOCUSABLE_SELECTOR)) {
142
82
  return false;
143
83
  }
144
- return element.matches(FOCUSABLE_SELECTOR) && !isDisabledDeep(element) && element.checkVisibility({
84
+ if (isDisabledDeep(element)) {
85
+ return false;
86
+ }
87
+ if (!element.checkVisibility({
145
88
  contentVisibilityAuto: true,
146
89
  opacityProperty: true,
147
90
  visibilityProperty: true
148
- });
91
+ })) {
92
+ return false;
93
+ }
94
+ return true;
149
95
  }
150
96
  function getRelativeFocusable(container, offset = 0, options) {
151
- const { active, composed = false, wrap = false } = options;
97
+ const { active: a = null, composed = false, wrap = false } = options;
152
98
  const focusables = getFocusables(container, { composed });
153
99
  const { length } = focusables;
154
100
  if (length === 0) {
155
101
  return null;
156
102
  }
157
- function getActiveElement() {
158
- let active2 = document.activeElement;
159
- while (active2 instanceof HTMLElement && active2.shadowRoot?.activeElement) {
160
- active2 = active2.shadowRoot.activeElement;
161
- }
162
- return active2 instanceof HTMLElement ? active2 : null;
163
- }
164
- const current = active ?? getActiveElement();
165
- function containsDeep(container2, node) {
166
- for (let current2 = node; current2; current2 = !(current2 instanceof ShadowRoot) ? current2.parentNode : current2.mode === "open" ? current2.host : null) {
167
- if (current2 === container2) {
168
- return true;
169
- }
170
- }
171
- return false;
172
- }
173
- if (!current || !containsDeep(container, current)) {
103
+ const active = a ?? getActiveElement();
104
+ if (active === null || !containsDeep(container, active)) {
174
105
  return null;
175
106
  }
176
- const currentIndex = focusables.indexOf(current);
107
+ const currentIndex = focusables.indexOf(active);
177
108
  if (currentIndex === -1) {
178
109
  return null;
179
110
  }
@@ -183,12 +114,132 @@ function getRelativeFocusable(container, offset = 0, options) {
183
114
  }
184
115
  return focusables[(offsetIndex + length) % length] ?? null;
185
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
+ }
186
237
  /**
187
238
  * Power Focusable
188
239
  * High-precision focus management utility with shadow DOM support.
189
240
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
190
241
  *
191
- * @version 2.0.2
242
+ * @version 2.1.0
192
243
  * @author Yusuke Kamiyamane
193
244
  * @license MIT
194
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.2",
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",