power-focusable 2.1.3 → 2.1.5

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
@@ -10,30 +10,31 @@ function getFocusables(container = document.body, options = {}) {
10
10
  const { composed = false } = options;
11
11
  const elements = [];
12
12
  if (composed) {
13
- let walk2 = function(node) {
13
+ let traverse2 = function(node) {
14
14
  if (node instanceof HTMLElement) {
15
15
  if (isFocusable(node)) {
16
16
  elements[elements.length] = node;
17
17
  }
18
18
  const shadow = node.shadowRoot;
19
19
  if (shadow && shadow.mode === "open") {
20
- walk2(shadow);
20
+ traverse2(shadow);
21
21
  }
22
- } else if (node instanceof HTMLSlotElement) {
22
+ }
23
+ if (node instanceof HTMLSlotElement) {
23
24
  const assigned = node.assignedElements({ flatten: true });
24
25
  if (assigned.length) {
25
26
  for (let i = 0, l = assigned.length; i < l; i++) {
26
- walk2(assigned[i]);
27
+ traverse2(assigned[i]);
27
28
  }
28
29
  return;
29
30
  }
30
31
  }
31
32
  const children = node.childNodes;
32
33
  for (let i = 0, l = children.length; i < l; i++) {
33
- walk2(children[i]);
34
+ traverse2(children[i]);
34
35
  }
35
36
  };
36
- walk2(container);
37
+ traverse2(container);
37
38
  } else {
38
39
  const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
39
40
  for (let i = 0, l = candidates.length; i < l; i++) {
@@ -74,11 +75,9 @@ function isFocusable(element) {
74
75
  if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
75
76
  return false;
76
77
  }
77
- const tabIndex = element.getAttribute("tabindex");
78
- if (tabIndex) {
79
- if (Number(tabIndex) < 0) {
80
- return false;
81
- }
78
+ const index = element.getAttribute("tabindex");
79
+ if (index && Number(index) < 0) {
80
+ return false;
82
81
  }
83
82
  if (!element.matches(FOCUSABLE_SELECTOR)) {
84
83
  return false;
@@ -95,7 +94,7 @@ function isFocusable(element) {
95
94
  }
96
95
  return true;
97
96
  }
98
- function getRelativeFocusable(container, offset = 0, options) {
97
+ function getRelativeFocusable(container, offset, options) {
99
98
  const { active: a = null, composed = false, wrap = false } = options;
100
99
  const focusables = getFocusables(container, { composed });
101
100
  const { length } = focusables;
@@ -117,29 +116,21 @@ function getRelativeFocusable(container, offset = 0, options) {
117
116
  return focusables[(offsetIndex + length) % length] ?? null;
118
117
  }
119
118
  function containsDeep(container, element) {
120
- function walk(node) {
121
- if (!node) {
122
- return false;
123
- }
124
- if (node === container) {
119
+ let current = element;
120
+ while (current) {
121
+ if (current === container) {
125
122
  return true;
126
123
  }
127
- if (node instanceof ShadowRoot) {
128
- return node.mode === "open" ? walk(node.host) : false;
129
- }
130
- return walk(node.parentNode);
124
+ current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode;
131
125
  }
132
- return walk(element);
126
+ return false;
133
127
  }
134
128
  function getActiveElement() {
135
- function walk(node) {
136
- if (!node) {
137
- return null;
138
- }
139
- const active = node.shadowRoot?.activeElement;
140
- return active ? walk(active) : node;
129
+ let current = document.activeElement;
130
+ while (current?.shadowRoot?.activeElement) {
131
+ current = current.shadowRoot.activeElement;
141
132
  }
142
- return walk(document.activeElement);
133
+ return current;
143
134
  }
144
135
  var tabIndexCache = /* @__PURE__ */ new WeakMap();
145
136
  function getTabIndex(element) {
@@ -147,71 +138,72 @@ function getTabIndex(element) {
147
138
  if (cached !== void 0) {
148
139
  return cached;
149
140
  }
150
- const number = Number(element.getAttribute("tabindex"));
151
- tabIndexCache.set(element, number);
152
- return number;
141
+ const index = Number(element.getAttribute("tabindex"));
142
+ tabIndexCache.set(element, index);
143
+ return index;
153
144
  }
154
145
  function isDisabled(element) {
155
- return "disabled" in element && element.disabled;
146
+ return "disabled" in element && !!element.disabled;
156
147
  }
157
148
  function isDisabledDeep(element) {
158
- function walk(node) {
159
- if (!node) {
160
- return false;
161
- }
162
- if (node instanceof ShadowRoot) {
163
- return node.mode === "open" ? walk(node.host) : false;
149
+ let current = element;
150
+ while (current) {
151
+ if (current instanceof ShadowRoot) {
152
+ if (current.mode !== "open") {
153
+ return false;
154
+ }
155
+ current = current.host;
156
+ continue;
164
157
  }
165
- if (!(node instanceof Element)) {
166
- return walk(node.parentNode);
158
+ if (!(current instanceof Element)) {
159
+ current = current.parentNode;
160
+ continue;
167
161
  }
168
- if (node === element && isFormControl(node) && isDisabled(node)) {
162
+ if (current === element && isFormControl(current) && isDisabled(current)) {
169
163
  return true;
170
164
  }
171
- if (node.hasAttribute("inert")) {
165
+ if (current.hasAttribute("inert")) {
172
166
  return true;
173
167
  }
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);
168
+ if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
169
+ if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
170
+ return true;
177
171
  }
178
- return true;
179
172
  }
180
- return walk(node.parentNode);
173
+ current = current.parentNode;
181
174
  }
182
- return walk(element);
175
+ return false;
183
176
  }
184
177
  function isFormControl(element) {
185
- const tagName = element.tagName;
186
- return tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
178
+ const name = element.tagName;
179
+ return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
180
+ }
181
+ function isUngroupedRadio(element) {
182
+ return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
187
183
  }
188
184
  function normalizeRadioGroup(elements) {
189
185
  let map = null;
190
186
  for (let i = 0, l = elements.length; i < l; i++) {
191
187
  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;
188
+ if (!isUngroupedRadio(element)) {
189
+ continue;
199
190
  }
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;
200
197
  }
201
198
  if (!map) {
202
199
  return elements;
203
200
  }
204
201
  const placeholder = /* @__PURE__ */ new Set();
205
202
  for (const group of map.values()) {
206
- if (group.length) {
207
- const enabled = group.filter(isFocusable);
208
- if (enabled.length) {
209
- placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
210
- }
211
- }
203
+ placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
212
204
  }
213
205
  return elements.filter((element) => {
214
- if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
206
+ if (isUngroupedRadio(element)) {
215
207
  return placeholder.has(element);
216
208
  }
217
209
  return true;
@@ -241,7 +233,7 @@ function sortByTabIndex(elements) {
241
233
  * High-precision focus management utility with shadow DOM support.
242
234
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
243
235
  *
244
- * @version 2.1.3
236
+ * @version 2.1.5
245
237
  * @author Yusuke Kamiyamane
246
238
  * @license MIT
247
239
  * @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.1.3
6
+ * @version 2.1.5
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.1.3
6
+ * @version 2.1.5
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.js CHANGED
@@ -8,30 +8,31 @@ function getFocusables(container = document.body, options = {}) {
8
8
  const { composed = false } = options;
9
9
  const elements = [];
10
10
  if (composed) {
11
- let walk2 = function(node) {
11
+ let traverse2 = function(node) {
12
12
  if (node instanceof HTMLElement) {
13
13
  if (isFocusable(node)) {
14
14
  elements[elements.length] = node;
15
15
  }
16
16
  const shadow = node.shadowRoot;
17
17
  if (shadow && shadow.mode === "open") {
18
- walk2(shadow);
18
+ traverse2(shadow);
19
19
  }
20
- } else if (node instanceof HTMLSlotElement) {
20
+ }
21
+ if (node instanceof HTMLSlotElement) {
21
22
  const assigned = node.assignedElements({ flatten: true });
22
23
  if (assigned.length) {
23
24
  for (let i = 0, l = assigned.length; i < l; i++) {
24
- walk2(assigned[i]);
25
+ traverse2(assigned[i]);
25
26
  }
26
27
  return;
27
28
  }
28
29
  }
29
30
  const children = node.childNodes;
30
31
  for (let i = 0, l = children.length; i < l; i++) {
31
- walk2(children[i]);
32
+ traverse2(children[i]);
32
33
  }
33
34
  };
34
- walk2(container);
35
+ traverse2(container);
35
36
  } else {
36
37
  const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
37
38
  for (let i = 0, l = candidates.length; i < l; i++) {
@@ -72,11 +73,9 @@ function isFocusable(element) {
72
73
  if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
73
74
  return false;
74
75
  }
75
- const tabIndex = element.getAttribute("tabindex");
76
- if (tabIndex) {
77
- if (Number(tabIndex) < 0) {
78
- return false;
79
- }
76
+ const index = element.getAttribute("tabindex");
77
+ if (index && Number(index) < 0) {
78
+ return false;
80
79
  }
81
80
  if (!element.matches(FOCUSABLE_SELECTOR)) {
82
81
  return false;
@@ -93,7 +92,7 @@ function isFocusable(element) {
93
92
  }
94
93
  return true;
95
94
  }
96
- function getRelativeFocusable(container, offset = 0, options) {
95
+ function getRelativeFocusable(container, offset, options) {
97
96
  const { active: a = null, composed = false, wrap = false } = options;
98
97
  const focusables = getFocusables(container, { composed });
99
98
  const { length } = focusables;
@@ -115,29 +114,21 @@ function getRelativeFocusable(container, offset = 0, options) {
115
114
  return focusables[(offsetIndex + length) % length] ?? null;
116
115
  }
117
116
  function containsDeep(container, element) {
118
- function walk(node) {
119
- if (!node) {
120
- return false;
121
- }
122
- if (node === container) {
117
+ let current = element;
118
+ while (current) {
119
+ if (current === container) {
123
120
  return true;
124
121
  }
125
- if (node instanceof ShadowRoot) {
126
- return node.mode === "open" ? walk(node.host) : false;
127
- }
128
- return walk(node.parentNode);
122
+ current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode;
129
123
  }
130
- return walk(element);
124
+ return false;
131
125
  }
132
126
  function getActiveElement() {
133
- function walk(node) {
134
- if (!node) {
135
- return null;
136
- }
137
- const active = node.shadowRoot?.activeElement;
138
- return active ? walk(active) : node;
127
+ let current = document.activeElement;
128
+ while (current?.shadowRoot?.activeElement) {
129
+ current = current.shadowRoot.activeElement;
139
130
  }
140
- return walk(document.activeElement);
131
+ return current;
141
132
  }
142
133
  var tabIndexCache = /* @__PURE__ */ new WeakMap();
143
134
  function getTabIndex(element) {
@@ -145,71 +136,72 @@ function getTabIndex(element) {
145
136
  if (cached !== void 0) {
146
137
  return cached;
147
138
  }
148
- const number = Number(element.getAttribute("tabindex"));
149
- tabIndexCache.set(element, number);
150
- return number;
139
+ const index = Number(element.getAttribute("tabindex"));
140
+ tabIndexCache.set(element, index);
141
+ return index;
151
142
  }
152
143
  function isDisabled(element) {
153
- return "disabled" in element && element.disabled;
144
+ return "disabled" in element && !!element.disabled;
154
145
  }
155
146
  function isDisabledDeep(element) {
156
- function walk(node) {
157
- if (!node) {
158
- return false;
159
- }
160
- if (node instanceof ShadowRoot) {
161
- return node.mode === "open" ? walk(node.host) : false;
147
+ let current = element;
148
+ while (current) {
149
+ if (current instanceof ShadowRoot) {
150
+ if (current.mode !== "open") {
151
+ return false;
152
+ }
153
+ current = current.host;
154
+ continue;
162
155
  }
163
- if (!(node instanceof Element)) {
164
- return walk(node.parentNode);
156
+ if (!(current instanceof Element)) {
157
+ current = current.parentNode;
158
+ continue;
165
159
  }
166
- if (node === element && isFormControl(node) && isDisabled(node)) {
160
+ if (current === element && isFormControl(current) && isDisabled(current)) {
167
161
  return true;
168
162
  }
169
- if (node.hasAttribute("inert")) {
163
+ if (current.hasAttribute("inert")) {
170
164
  return true;
171
165
  }
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);
166
+ if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
167
+ if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
168
+ return true;
175
169
  }
176
- return true;
177
170
  }
178
- return walk(node.parentNode);
171
+ current = current.parentNode;
179
172
  }
180
- return walk(element);
173
+ return false;
181
174
  }
182
175
  function isFormControl(element) {
183
- const tagName = element.tagName;
184
- return tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
176
+ const name = element.tagName;
177
+ return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
178
+ }
179
+ function isUngroupedRadio(element) {
180
+ return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
185
181
  }
186
182
  function normalizeRadioGroup(elements) {
187
183
  let map = null;
188
184
  for (let i = 0, l = elements.length; i < l; i++) {
189
185
  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;
186
+ if (!isUngroupedRadio(element)) {
187
+ continue;
197
188
  }
189
+ if (!map) {
190
+ map = /* @__PURE__ */ new Map();
191
+ }
192
+ const key = `${element.form?.id ?? "no-form"}::${element.name}`;
193
+ const group = map.get(key) ?? map.set(key, []).get(key);
194
+ group[group.length] = element;
198
195
  }
199
196
  if (!map) {
200
197
  return elements;
201
198
  }
202
199
  const placeholder = /* @__PURE__ */ new Set();
203
200
  for (const group of map.values()) {
204
- if (group.length) {
205
- const enabled = group.filter(isFocusable);
206
- if (enabled.length) {
207
- placeholder.add(enabled.find((radio) => radio.checked) ?? enabled[0]);
208
- }
209
- }
201
+ placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
210
202
  }
211
203
  return elements.filter((element) => {
212
- if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
204
+ if (isUngroupedRadio(element)) {
213
205
  return placeholder.has(element);
214
206
  }
215
207
  return true;
@@ -239,7 +231,7 @@ function sortByTabIndex(elements) {
239
231
  * High-precision focus management utility with shadow DOM support.
240
232
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
241
233
  *
242
- * @version 2.1.3
234
+ * @version 2.1.5
243
235
  * @author Yusuke Kamiyamane
244
236
  * @license MIT
245
237
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-focusable",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "High-precision focus management utility with shadow DOM support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",