power-focusable 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,7 +49,7 @@ Used by `getNextFocusable` and `getPreviousFocusable`.
49
49
 
50
50
  If `true`, traverses the composed tree (including shadow DOM; slower)
51
51
 
52
- Used by `getFocusables`, `getNextFocusable`, and `getPreviousFocusable`.
52
+ Used by `getFocusables`, `getNextFocusable`, `getPreviousFocusable`, and `hasFocusables`.
53
53
 
54
54
  ### `wrap`
55
55
 
@@ -123,6 +123,9 @@ hasFocusable(container);
123
123
  // => boolean
124
124
  //
125
125
  // container (optional): HTMLElement (default: <body>)
126
+
127
+ // Traverses the composed tree (including shadow DOM; slower)
128
+ hasFocusable(container, { composed: true });
126
129
  ```
127
130
 
128
131
  ### `isFocusable`
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)) {
@@ -107,77 +59,54 @@ function getPreviousFocusable(container = document.body, options = {}) {
107
59
  }
108
60
  return getRelativeFocusable(container, -1, options);
109
61
  }
110
- function hasFocusable(container = document.body) {
62
+ function hasFocusable(container = document.body, options = {}) {
111
63
  if (!(container instanceof HTMLElement)) {
112
64
  console.warn("Invalid container element. Fallback: <body> element.");
113
65
  container = document.body;
114
66
  }
115
- return getFocusables(container).length > 0;
67
+ return getFocusables(container, options).length > 0;
116
68
  }
117
69
  function isFocusable(element) {
118
70
  if (!(element instanceof HTMLElement)) {
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.1
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.1
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -17,7 +17,7 @@ interface PowerFocusableOptions {
17
17
  declare function getFocusables(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): HTMLElement[];
18
18
  declare function getNextFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
19
19
  declare function getPreviousFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
20
- declare function hasFocusable(container?: HTMLElement): boolean;
20
+ declare function hasFocusable(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
21
21
  declare function isFocusable(element: HTMLElement): boolean;
22
22
 
23
23
  export { type PowerFocusableOptions, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
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.1
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -17,7 +17,7 @@ interface PowerFocusableOptions {
17
17
  declare function getFocusables(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): HTMLElement[];
18
18
  declare function getNextFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
19
19
  declare function getPreviousFocusable(container?: HTMLElement, options?: PowerFocusableOptions): HTMLElement | null;
20
- declare function hasFocusable(container?: HTMLElement): boolean;
20
+ declare function hasFocusable(container?: HTMLElement, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
21
21
  declare function isFocusable(element: HTMLElement): boolean;
22
22
 
23
23
  export { type PowerFocusableOptions, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
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)) {
@@ -105,77 +57,54 @@ function getPreviousFocusable(container = document.body, options = {}) {
105
57
  }
106
58
  return getRelativeFocusable(container, -1, options);
107
59
  }
108
- function hasFocusable(container = document.body) {
60
+ function hasFocusable(container = document.body, options = {}) {
109
61
  if (!(container instanceof HTMLElement)) {
110
62
  console.warn("Invalid container element. Fallback: <body> element.");
111
63
  container = document.body;
112
64
  }
113
- return getFocusables(container).length > 0;
65
+ return getFocusables(container, options).length > 0;
114
66
  }
115
67
  function isFocusable(element) {
116
68
  if (!(element instanceof HTMLElement)) {
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.1
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.1",
4
4
  "description": "High-precision focus management utility with shadow DOM support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",