power-focusable 2.1.4 → 2.1.6

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++) {
@@ -75,10 +76,8 @@ function isFocusable(element) {
75
76
  return false;
76
77
  }
77
78
  const index = element.getAttribute("tabindex");
78
- if (index) {
79
- if (Number(index) < 0) {
80
- return false;
81
- }
79
+ if (index && Number(index) < 0) {
80
+ return false;
82
81
  }
83
82
  if (!element.matches(FOCUSABLE_SELECTOR)) {
84
83
  return false;
@@ -96,13 +95,16 @@ function isFocusable(element) {
96
95
  return true;
97
96
  }
98
97
  function getRelativeFocusable(container, offset, options) {
99
- const { active: a = null, composed = false, wrap = false } = options;
98
+ const {
99
+ active = getActiveElement(),
100
+ composed = false,
101
+ wrap = false
102
+ } = options;
100
103
  const focusables = getFocusables(container, { composed });
101
104
  const { length } = focusables;
102
105
  if (!length) {
103
106
  return null;
104
107
  }
105
- const active = a ?? getActiveElement();
106
108
  if (!active || !containsDeep(container, active)) {
107
109
  return null;
108
110
  }
@@ -117,29 +119,21 @@ function getRelativeFocusable(container, offset, options) {
117
119
  return focusables[(offsetIndex + length) % length] ?? null;
118
120
  }
119
121
  function containsDeep(container, element) {
120
- function walk(node) {
121
- if (!node) {
122
- return false;
123
- }
124
- if (node === container) {
122
+ let current = element;
123
+ while (current) {
124
+ if (current === container) {
125
125
  return true;
126
126
  }
127
- if (node instanceof ShadowRoot) {
128
- return node.mode === "open" ? walk(node.host) : false;
129
- }
130
- return walk(node.parentNode);
127
+ current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode;
131
128
  }
132
- return walk(element);
129
+ return false;
133
130
  }
134
131
  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;
132
+ let current = document.activeElement;
133
+ while (current?.shadowRoot?.activeElement) {
134
+ current = current.shadowRoot.activeElement;
141
135
  }
142
- return walk(document.activeElement);
136
+ return current;
143
137
  }
144
138
  var tabIndexCache = /* @__PURE__ */ new WeakMap();
145
139
  function getTabIndex(element) {
@@ -152,44 +146,49 @@ function getTabIndex(element) {
152
146
  return index;
153
147
  }
154
148
  function isDisabled(element) {
155
- return "disabled" in element && element.disabled;
149
+ return "disabled" in element && !!element.disabled;
156
150
  }
157
151
  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;
152
+ let current = element;
153
+ while (current) {
154
+ if (current instanceof ShadowRoot) {
155
+ if (current.mode !== "open") {
156
+ return false;
157
+ }
158
+ current = current.host;
159
+ continue;
164
160
  }
165
- if (!(node instanceof Element)) {
166
- return walk(node.parentNode);
161
+ if (!(current instanceof Element)) {
162
+ current = current.parentNode;
163
+ continue;
167
164
  }
168
- if (node === element && isFormControl(node) && isDisabled(node)) {
165
+ if (current === element && isFormControl(current) && isDisabled(current)) {
169
166
  return true;
170
167
  }
171
- if (node.hasAttribute("inert")) {
168
+ if (current.hasAttribute("inert")) {
172
169
  return true;
173
170
  }
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);
171
+ if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
172
+ if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
173
+ return true;
177
174
  }
178
- return true;
179
175
  }
180
- return walk(node.parentNode);
176
+ current = current.parentNode;
181
177
  }
182
- return walk(element);
178
+ return false;
183
179
  }
184
180
  function isFormControl(element) {
185
- const tagName = element.tagName;
186
- return tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
181
+ const name = element.tagName;
182
+ return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
183
+ }
184
+ function isUngroupedRadio(element) {
185
+ return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
187
186
  }
188
187
  function normalizeRadioGroup(elements) {
189
188
  let map = null;
190
189
  for (let i = 0, l = elements.length; i < l; i++) {
191
190
  const element = elements[i];
192
- if (!(element instanceof HTMLInputElement) || element.type !== "radio" || !element.name) {
191
+ if (!isUngroupedRadio(element)) {
193
192
  continue;
194
193
  }
195
194
  if (!map) {
@@ -207,7 +206,7 @@ function normalizeRadioGroup(elements) {
207
206
  placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
208
207
  }
209
208
  return elements.filter((element) => {
210
- if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
209
+ if (isUngroupedRadio(element)) {
211
210
  return placeholder.has(element);
212
211
  }
213
212
  return true;
@@ -237,7 +236,7 @@ function sortByTabIndex(elements) {
237
236
  * High-precision focus management utility with shadow DOM support.
238
237
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
239
238
  *
240
- * @version 2.1.4
239
+ * @version 2.1.6
241
240
  * @author Yusuke Kamiyamane
242
241
  * @license MIT
243
242
  * @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.4
6
+ * @version 2.1.6
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.4
6
+ * @version 2.1.6
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++) {
@@ -73,10 +74,8 @@ function isFocusable(element) {
73
74
  return false;
74
75
  }
75
76
  const index = element.getAttribute("tabindex");
76
- if (index) {
77
- if (Number(index) < 0) {
78
- return false;
79
- }
77
+ if (index && Number(index) < 0) {
78
+ return false;
80
79
  }
81
80
  if (!element.matches(FOCUSABLE_SELECTOR)) {
82
81
  return false;
@@ -94,13 +93,16 @@ function isFocusable(element) {
94
93
  return true;
95
94
  }
96
95
  function getRelativeFocusable(container, offset, options) {
97
- const { active: a = null, composed = false, wrap = false } = options;
96
+ const {
97
+ active = getActiveElement(),
98
+ composed = false,
99
+ wrap = false
100
+ } = options;
98
101
  const focusables = getFocusables(container, { composed });
99
102
  const { length } = focusables;
100
103
  if (!length) {
101
104
  return null;
102
105
  }
103
- const active = a ?? getActiveElement();
104
106
  if (!active || !containsDeep(container, active)) {
105
107
  return null;
106
108
  }
@@ -115,29 +117,21 @@ function getRelativeFocusable(container, offset, options) {
115
117
  return focusables[(offsetIndex + length) % length] ?? null;
116
118
  }
117
119
  function containsDeep(container, element) {
118
- function walk(node) {
119
- if (!node) {
120
- return false;
121
- }
122
- if (node === container) {
120
+ let current = element;
121
+ while (current) {
122
+ if (current === container) {
123
123
  return true;
124
124
  }
125
- if (node instanceof ShadowRoot) {
126
- return node.mode === "open" ? walk(node.host) : false;
127
- }
128
- return walk(node.parentNode);
125
+ current = current instanceof ShadowRoot ? current.mode === "open" ? current.host : null : current.parentNode;
129
126
  }
130
- return walk(element);
127
+ return false;
131
128
  }
132
129
  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;
130
+ let current = document.activeElement;
131
+ while (current?.shadowRoot?.activeElement) {
132
+ current = current.shadowRoot.activeElement;
139
133
  }
140
- return walk(document.activeElement);
134
+ return current;
141
135
  }
142
136
  var tabIndexCache = /* @__PURE__ */ new WeakMap();
143
137
  function getTabIndex(element) {
@@ -150,44 +144,49 @@ function getTabIndex(element) {
150
144
  return index;
151
145
  }
152
146
  function isDisabled(element) {
153
- return "disabled" in element && element.disabled;
147
+ return "disabled" in element && !!element.disabled;
154
148
  }
155
149
  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;
150
+ let current = element;
151
+ while (current) {
152
+ if (current instanceof ShadowRoot) {
153
+ if (current.mode !== "open") {
154
+ return false;
155
+ }
156
+ current = current.host;
157
+ continue;
162
158
  }
163
- if (!(node instanceof Element)) {
164
- return walk(node.parentNode);
159
+ if (!(current instanceof Element)) {
160
+ current = current.parentNode;
161
+ continue;
165
162
  }
166
- if (node === element && isFormControl(node) && isDisabled(node)) {
163
+ if (current === element && isFormControl(current) && isDisabled(current)) {
167
164
  return true;
168
165
  }
169
- if (node.hasAttribute("inert")) {
166
+ if (current.hasAttribute("inert")) {
170
167
  return true;
171
168
  }
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);
169
+ if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
170
+ if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
171
+ return true;
175
172
  }
176
- return true;
177
173
  }
178
- return walk(node.parentNode);
174
+ current = current.parentNode;
179
175
  }
180
- return walk(element);
176
+ return false;
181
177
  }
182
178
  function isFormControl(element) {
183
- const tagName = element.tagName;
184
- return tagName === "BUTTON" || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
179
+ const name = element.tagName;
180
+ return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
181
+ }
182
+ function isUngroupedRadio(element) {
183
+ return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
185
184
  }
186
185
  function normalizeRadioGroup(elements) {
187
186
  let map = null;
188
187
  for (let i = 0, l = elements.length; i < l; i++) {
189
188
  const element = elements[i];
190
- if (!(element instanceof HTMLInputElement) || element.type !== "radio" || !element.name) {
189
+ if (!isUngroupedRadio(element)) {
191
190
  continue;
192
191
  }
193
192
  if (!map) {
@@ -205,7 +204,7 @@ function normalizeRadioGroup(elements) {
205
204
  placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
206
205
  }
207
206
  return elements.filter((element) => {
208
- if (element instanceof HTMLInputElement && element.type === "radio" && element.name) {
207
+ if (isUngroupedRadio(element)) {
209
208
  return placeholder.has(element);
210
209
  }
211
210
  return true;
@@ -235,7 +234,7 @@ function sortByTabIndex(elements) {
235
234
  * High-precision focus management utility with shadow DOM support.
236
235
  * Handles complex focus rules including tabindex ordering, radio groups, etc.
237
236
  *
238
- * @version 2.1.4
237
+ * @version 2.1.6
239
238
  * @author Yusuke Kamiyamane
240
239
  * @license MIT
241
240
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-focusable",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "High-precision focus management utility with shadow DOM support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",