power-focusable 2.2.1 → 3.0.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
@@ -1,6 +1,6 @@
1
1
  # Power Focusable
2
2
 
3
- High-precision focus management utility with shadow DOM support. Handles complex focus rules including tabindex ordering, radio groups, etc.
3
+ High-precision focus management utility with full composed tree support. Handles complex focus rules including tabindex ordering, radio groups, inert, and shadow DOM.
4
4
 
5
5
  > [!NOTE]
6
6
  > Supports shadow DOM traversal via the composed tree. Only open shadow roots are included; closed shadow roots are not accessible.
@@ -14,10 +14,12 @@ npm i power-focusable
14
14
  ```ts
15
15
  // npm
16
16
  import {
17
+ createFocusTrap,
17
18
  getFocusables,
18
19
  getNextFocusable,
19
20
  getPreviousFocusable,
20
21
  hasFocusable,
22
+ inertOutside,
21
23
  isFocusable,
22
24
  } from 'power-focusable';
23
25
 
@@ -59,6 +61,17 @@ Used by `getNextFocusable` and `getPreviousFocusable`.
59
61
 
60
62
  ## 📦 APIs
61
63
 
64
+ ### `createFocusTrap`
65
+
66
+ Creates a keyboard focus trap within the container. Automatically focuses the container itself when possible; otherwise focuses the first available focusable element.
67
+
68
+ ```ts
69
+ const cleanup = createFocusTrap(container);
70
+ // => () => void
71
+ //
72
+ // container: Element
73
+ ```
74
+
62
75
  ### `getFocusables`
63
76
 
64
77
  Returns all focusable elements within the container.
@@ -128,6 +141,17 @@ hasFocusable(container);
128
141
  hasFocusable(container, { composed: true });
129
142
  ```
130
143
 
144
+ ### `inertOutside`
145
+
146
+ Temporarily applies `inert` to all elements outside the target element. Useful for modals, dialogs, and overlays.
147
+
148
+ ```ts
149
+ const cleanup = inertOutside(element);
150
+ // => () => void
151
+ //
152
+ // element: Element
153
+ ```
154
+
131
155
  ### `isFocusable`
132
156
 
133
157
  Returns whether the given element is focusable.
package/dist/index.cjs CHANGED
@@ -2,6 +2,45 @@
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
+ function createFocusTrap(container) {
6
+ if (!(container instanceof Element)) {
7
+ throw new Error("Invalid container element");
8
+ }
9
+ focus(container);
10
+ if (getActiveElement() !== container) {
11
+ const first = getFocusables(container, { composed: true })[0];
12
+ if (first) {
13
+ focus(first);
14
+ }
15
+ }
16
+ function onKeyDown(event) {
17
+ const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
18
+ if (key !== "Tab" || altKey || ctrlKey || metaKey) {
19
+ return;
20
+ }
21
+ if (!event.composedPath().includes(container)) {
22
+ return;
23
+ }
24
+ const focusable = getRelativeFocusable(container, shiftKey ? -1 : 1, {
25
+ composed: true,
26
+ wrap: true
27
+ });
28
+ if (!focusable) {
29
+ return;
30
+ }
31
+ event.preventDefault();
32
+ focus(focusable);
33
+ }
34
+ let controller = new AbortController();
35
+ document.addEventListener("keydown", onKeyDown, {
36
+ capture: true,
37
+ signal: controller.signal
38
+ });
39
+ return () => {
40
+ controller?.abort();
41
+ controller = null;
42
+ };
43
+ }
5
44
  function getFocusables(container = document.body, options = {}) {
6
45
  if (!(container instanceof Element)) {
7
46
  console.warn("Invalid container element. Fallback: <body> element.");
@@ -61,12 +100,43 @@ function hasFocusable(container = document.body, options = {}) {
61
100
  }
62
101
  return !!getFocusables(container, options).length;
63
102
  }
103
+ function inertOutside(element) {
104
+ if (!(element instanceof Element)) {
105
+ console.warn("Invalid element");
106
+ return () => {
107
+ };
108
+ }
109
+ function traverse(node, callback) {
110
+ const parent = getComposedParent(node);
111
+ if (!parent) {
112
+ return;
113
+ }
114
+ for (const sibling of getComposedSiblings(node)) {
115
+ callback(sibling);
116
+ }
117
+ traverse(parent, callback);
118
+ }
119
+ const elements = [];
120
+ traverse(element, (node) => {
121
+ if (!(node instanceof Element)) {
122
+ return;
123
+ }
124
+ if (applyInert(node)) {
125
+ elements.push(node);
126
+ }
127
+ });
128
+ return () => {
129
+ elements.forEach((element2) => {
130
+ restoreInert(element2);
131
+ });
132
+ };
133
+ }
64
134
  function isFocusable(element) {
65
135
  if (!(element instanceof Element)) {
66
136
  console.warn("Invalid element");
67
137
  return false;
68
138
  }
69
- if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
139
+ if (element.hasAttribute("hidden") || isInert(element)) {
70
140
  return false;
71
141
  }
72
142
  if (getTabIndex(element) < 0) {
@@ -87,24 +157,6 @@ function isFocusable(element) {
87
157
  }
88
158
  return true;
89
159
  }
90
- function getComposedChildren(node) {
91
- if (node instanceof ShadowRoot) {
92
- return getChildren(node);
93
- }
94
- if (!(node instanceof Element)) {
95
- return [];
96
- }
97
- if (node instanceof HTMLSlotElement) {
98
- const assigned = node.assignedElements({ flatten: true });
99
- if (assigned.length) {
100
- return assigned;
101
- }
102
- }
103
- if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
104
- return getChildren(node.shadowRoot);
105
- }
106
- return getChildren(node);
107
- }
108
160
  function getRelativeFocusable(container, offset, options) {
109
161
  const {
110
162
  active = getActiveElement(),
@@ -116,7 +168,7 @@ function getRelativeFocusable(container, offset, options) {
116
168
  if (!length) {
117
169
  return null;
118
170
  }
119
- if (!active || !containsDeep(container, active)) {
171
+ if (!active || !containsComposed(container, active)) {
120
172
  return null;
121
173
  }
122
174
  if (!(active instanceof Element)) {
@@ -132,6 +184,35 @@ function getRelativeFocusable(container, offset, options) {
132
184
  }
133
185
  return focusables[(offsetIndex + length) % length] ?? null;
134
186
  }
187
+ function isDisabledDeep(element) {
188
+ let current = element;
189
+ while (current) {
190
+ if (current instanceof ShadowRoot) {
191
+ if (current.mode !== "open") {
192
+ return false;
193
+ }
194
+ current = current.host;
195
+ continue;
196
+ }
197
+ if (!(current instanceof Element)) {
198
+ current = current.parentNode;
199
+ continue;
200
+ }
201
+ if (current === element && isFormControl(current) && isDisabled(current)) {
202
+ return true;
203
+ }
204
+ if (current.hasAttribute("inert")) {
205
+ return true;
206
+ }
207
+ if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
208
+ if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
209
+ return true;
210
+ }
211
+ }
212
+ current = current.parentNode;
213
+ }
214
+ return false;
215
+ }
135
216
  function normalizeRadioGroup(elements) {
136
217
  let map = null;
137
218
  for (let i = 0, l = elements.length; i < l; i++) {
@@ -187,7 +268,7 @@ function sortByTabIndex(elements) {
187
268
  }
188
269
  return sorted;
189
270
  }
190
- function containsDeep(container, element) {
271
+ function containsComposed(container, element) {
191
272
  let current = element;
192
273
  while (current) {
193
274
  if (current === container) {
@@ -197,6 +278,75 @@ function containsDeep(container, element) {
197
278
  }
198
279
  return false;
199
280
  }
281
+ function getComposedChildren(node) {
282
+ if (node instanceof ShadowRoot) {
283
+ return getChildren(node);
284
+ }
285
+ if (!(node instanceof Element)) {
286
+ return [];
287
+ }
288
+ if (node instanceof HTMLSlotElement) {
289
+ const assigned = node.assignedElements({ flatten: true });
290
+ if (assigned.length) {
291
+ return assigned;
292
+ }
293
+ }
294
+ if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
295
+ return getChildren(node.shadowRoot);
296
+ }
297
+ return getChildren(node);
298
+ }
299
+ function getComposedParent(node) {
300
+ if (node instanceof Element && node.assignedSlot) {
301
+ return node.assignedSlot;
302
+ }
303
+ const parent = node.parentNode;
304
+ if (parent instanceof ShadowRoot) {
305
+ return parent.host;
306
+ }
307
+ return parent instanceof Element ? parent : null;
308
+ }
309
+ function getComposedSiblings(node) {
310
+ if (node.assignedSlot) {
311
+ return [...node.assignedSlot.children].filter(
312
+ (child) => child instanceof Element && child !== node
313
+ );
314
+ }
315
+ const parent = getComposedParent(node);
316
+ if (!parent) {
317
+ return [];
318
+ }
319
+ return getSiblings(node);
320
+ }
321
+ var inertRefCounts = /* @__PURE__ */ new WeakMap();
322
+ function applyInert(element) {
323
+ if (isInert(element) && !inertRefCounts.has(element)) {
324
+ return false;
325
+ }
326
+ const count = inertRefCounts.get(element) ?? 0;
327
+ inertRefCounts.set(element, count + 1);
328
+ if (count === 0) {
329
+ setInert(element, true);
330
+ }
331
+ return true;
332
+ }
333
+ function restoreInert(element) {
334
+ const count = inertRefCounts.get(element);
335
+ if (!count) {
336
+ return;
337
+ }
338
+ if (count === 1) {
339
+ inertRefCounts.delete(element);
340
+ setInert(element, false);
341
+ return;
342
+ }
343
+ inertRefCounts.set(element, count - 1);
344
+ }
345
+ function focus(element) {
346
+ if ("focus" in element && typeof element.focus === "function") {
347
+ element.focus();
348
+ }
349
+ }
200
350
  function getActiveElement() {
201
351
  let current = document.activeElement;
202
352
  while (current?.shadowRoot?.activeElement) {
@@ -211,62 +361,59 @@ function getChildren(node) {
211
361
  }
212
362
  return elements;
213
363
  }
364
+ function getSiblings(node) {
365
+ const parent = getComposedParent(node);
366
+ if (!parent) {
367
+ return [];
368
+ }
369
+ const elements = [];
370
+ for (let child = parent.firstElementChild; child; child = child.nextElementSibling) {
371
+ if (child !== node) {
372
+ elements[elements.length] = child;
373
+ }
374
+ }
375
+ return elements;
376
+ }
214
377
  function getTabIndex(element) {
215
378
  return "tabIndex" in element ? Number(element.tabIndex) : 0;
216
379
  }
217
380
  function isDisabled(element) {
218
381
  return "disabled" in element && !!element.disabled;
219
382
  }
220
- function isDisabledDeep(element) {
221
- let current = element;
222
- while (current) {
223
- if (current instanceof ShadowRoot) {
224
- if (current.mode !== "open") {
225
- return false;
226
- }
227
- current = current.host;
228
- continue;
229
- }
230
- if (!(current instanceof Element)) {
231
- current = current.parentNode;
232
- continue;
233
- }
234
- if (current === element && isFormControl(current) && isDisabled(current)) {
235
- return true;
236
- }
237
- if (current.hasAttribute("inert")) {
238
- return true;
239
- }
240
- if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
241
- if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
242
- return true;
243
- }
244
- }
245
- current = current.parentNode;
246
- }
247
- return false;
248
- }
249
383
  function isFormControl(element) {
250
384
  const name = element.tagName;
251
385
  return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
252
386
  }
387
+ function isInert(element) {
388
+ return "inert" in element && !!element.inert;
389
+ }
253
390
  function isUngroupedRadio(element) {
254
391
  return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
255
392
  }
393
+ function setInert(element, boolean) {
394
+ if (boolean) {
395
+ element.setAttribute("inert", "");
396
+ } else {
397
+ element.removeAttribute("inert");
398
+ }
399
+ }
256
400
  /**
257
401
  * Power Focusable
258
- * High-precision focus management utility with shadow DOM support.
259
- * Handles complex focus rules including tabindex ordering, radio groups, etc.
402
+ * High-precision focus management utility with full composed tree support.
403
+ * Handles complex focus rules including tabindex ordering, radio groups, inert,
404
+ * and shadow DOM.
260
405
  *
261
- * @version 2.2.1
406
+ * @version 3.0.1
262
407
  * @author Yusuke Kamiyamane
263
408
  * @license MIT
264
409
  * @copyright Copyright (c) Yusuke Kamiyamane
265
410
  * @see {@link https://github.com/y14e/power-focusable}
266
411
  */
267
412
 
413
+ exports.createFocusTrap = createFocusTrap;
268
414
  exports.getFocusables = getFocusables;
269
415
  exports.getNextFocusable = getNextFocusable;
270
416
  exports.getPreviousFocusable = getPreviousFocusable;
271
417
  exports.hasFocusable = hasFocusable;
418
+ exports.inertOutside = inertOutside;
272
419
  exports.isFocusable = isFocusable;
package/dist/index.d.cts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Power Focusable
3
- * High-precision focus management utility with shadow DOM support.
4
- * Handles complex focus rules including tabindex ordering, radio groups, etc.
3
+ * High-precision focus management utility with full composed tree support.
4
+ * Handles complex focus rules including tabindex ordering, radio groups, inert,
5
+ * and shadow DOM.
5
6
  *
6
- * @version 2.2.1
7
+ * @version 3.0.1
7
8
  * @author Yusuke Kamiyamane
8
9
  * @license MIT
9
10
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -14,10 +15,12 @@ interface PowerFocusableOptions {
14
15
  readonly composed?: boolean;
15
16
  readonly wrap?: boolean;
16
17
  }
18
+ declare function createFocusTrap(container: Element): () => void;
17
19
  declare function getFocusables(container?: Element, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): Element[];
18
20
  declare function getNextFocusable(container?: Element, options?: PowerFocusableOptions): Element | null;
19
21
  declare function getPreviousFocusable(container?: Element, options?: PowerFocusableOptions): Element | null;
20
22
  declare function hasFocusable(container?: Element, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
23
+ declare function inertOutside(element: Element): () => void;
21
24
  declare function isFocusable(element: Element): boolean;
22
25
 
23
- export { type PowerFocusableOptions, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
26
+ export { type PowerFocusableOptions, createFocusTrap, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, inertOutside, isFocusable };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Power Focusable
3
- * High-precision focus management utility with shadow DOM support.
4
- * Handles complex focus rules including tabindex ordering, radio groups, etc.
3
+ * High-precision focus management utility with full composed tree support.
4
+ * Handles complex focus rules including tabindex ordering, radio groups, inert,
5
+ * and shadow DOM.
5
6
  *
6
- * @version 2.2.1
7
+ * @version 3.0.1
7
8
  * @author Yusuke Kamiyamane
8
9
  * @license MIT
9
10
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -14,10 +15,12 @@ interface PowerFocusableOptions {
14
15
  readonly composed?: boolean;
15
16
  readonly wrap?: boolean;
16
17
  }
18
+ declare function createFocusTrap(container: Element): () => void;
17
19
  declare function getFocusables(container?: Element, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): Element[];
18
20
  declare function getNextFocusable(container?: Element, options?: PowerFocusableOptions): Element | null;
19
21
  declare function getPreviousFocusable(container?: Element, options?: PowerFocusableOptions): Element | null;
20
22
  declare function hasFocusable(container?: Element, options?: Omit<PowerFocusableOptions, 'active' | 'wrap'>): boolean;
23
+ declare function inertOutside(element: Element): () => void;
21
24
  declare function isFocusable(element: Element): boolean;
22
25
 
23
- export { type PowerFocusableOptions, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
26
+ export { type PowerFocusableOptions, createFocusTrap, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, inertOutside, isFocusable };
package/dist/index.js CHANGED
@@ -1,5 +1,44 @@
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
+ function createFocusTrap(container) {
4
+ if (!(container instanceof Element)) {
5
+ throw new Error("Invalid container element");
6
+ }
7
+ focus(container);
8
+ if (getActiveElement() !== container) {
9
+ const first = getFocusables(container, { composed: true })[0];
10
+ if (first) {
11
+ focus(first);
12
+ }
13
+ }
14
+ function onKeyDown(event) {
15
+ const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
16
+ if (key !== "Tab" || altKey || ctrlKey || metaKey) {
17
+ return;
18
+ }
19
+ if (!event.composedPath().includes(container)) {
20
+ return;
21
+ }
22
+ const focusable = getRelativeFocusable(container, shiftKey ? -1 : 1, {
23
+ composed: true,
24
+ wrap: true
25
+ });
26
+ if (!focusable) {
27
+ return;
28
+ }
29
+ event.preventDefault();
30
+ focus(focusable);
31
+ }
32
+ let controller = new AbortController();
33
+ document.addEventListener("keydown", onKeyDown, {
34
+ capture: true,
35
+ signal: controller.signal
36
+ });
37
+ return () => {
38
+ controller?.abort();
39
+ controller = null;
40
+ };
41
+ }
3
42
  function getFocusables(container = document.body, options = {}) {
4
43
  if (!(container instanceof Element)) {
5
44
  console.warn("Invalid container element. Fallback: <body> element.");
@@ -59,12 +98,43 @@ function hasFocusable(container = document.body, options = {}) {
59
98
  }
60
99
  return !!getFocusables(container, options).length;
61
100
  }
101
+ function inertOutside(element) {
102
+ if (!(element instanceof Element)) {
103
+ console.warn("Invalid element");
104
+ return () => {
105
+ };
106
+ }
107
+ function traverse(node, callback) {
108
+ const parent = getComposedParent(node);
109
+ if (!parent) {
110
+ return;
111
+ }
112
+ for (const sibling of getComposedSiblings(node)) {
113
+ callback(sibling);
114
+ }
115
+ traverse(parent, callback);
116
+ }
117
+ const elements = [];
118
+ traverse(element, (node) => {
119
+ if (!(node instanceof Element)) {
120
+ return;
121
+ }
122
+ if (applyInert(node)) {
123
+ elements.push(node);
124
+ }
125
+ });
126
+ return () => {
127
+ elements.forEach((element2) => {
128
+ restoreInert(element2);
129
+ });
130
+ };
131
+ }
62
132
  function isFocusable(element) {
63
133
  if (!(element instanceof Element)) {
64
134
  console.warn("Invalid element");
65
135
  return false;
66
136
  }
67
- if (element.hasAttribute("hidden") || element.hasAttribute("inert")) {
137
+ if (element.hasAttribute("hidden") || isInert(element)) {
68
138
  return false;
69
139
  }
70
140
  if (getTabIndex(element) < 0) {
@@ -85,24 +155,6 @@ function isFocusable(element) {
85
155
  }
86
156
  return true;
87
157
  }
88
- function getComposedChildren(node) {
89
- if (node instanceof ShadowRoot) {
90
- return getChildren(node);
91
- }
92
- if (!(node instanceof Element)) {
93
- return [];
94
- }
95
- if (node instanceof HTMLSlotElement) {
96
- const assigned = node.assignedElements({ flatten: true });
97
- if (assigned.length) {
98
- return assigned;
99
- }
100
- }
101
- if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
102
- return getChildren(node.shadowRoot);
103
- }
104
- return getChildren(node);
105
- }
106
158
  function getRelativeFocusable(container, offset, options) {
107
159
  const {
108
160
  active = getActiveElement(),
@@ -114,7 +166,7 @@ function getRelativeFocusable(container, offset, options) {
114
166
  if (!length) {
115
167
  return null;
116
168
  }
117
- if (!active || !containsDeep(container, active)) {
169
+ if (!active || !containsComposed(container, active)) {
118
170
  return null;
119
171
  }
120
172
  if (!(active instanceof Element)) {
@@ -130,6 +182,35 @@ function getRelativeFocusable(container, offset, options) {
130
182
  }
131
183
  return focusables[(offsetIndex + length) % length] ?? null;
132
184
  }
185
+ function isDisabledDeep(element) {
186
+ let current = element;
187
+ while (current) {
188
+ if (current instanceof ShadowRoot) {
189
+ if (current.mode !== "open") {
190
+ return false;
191
+ }
192
+ current = current.host;
193
+ continue;
194
+ }
195
+ if (!(current instanceof Element)) {
196
+ current = current.parentNode;
197
+ continue;
198
+ }
199
+ if (current === element && isFormControl(current) && isDisabled(current)) {
200
+ return true;
201
+ }
202
+ if (current.hasAttribute("inert")) {
203
+ return true;
204
+ }
205
+ if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
206
+ if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
207
+ return true;
208
+ }
209
+ }
210
+ current = current.parentNode;
211
+ }
212
+ return false;
213
+ }
133
214
  function normalizeRadioGroup(elements) {
134
215
  let map = null;
135
216
  for (let i = 0, l = elements.length; i < l; i++) {
@@ -185,7 +266,7 @@ function sortByTabIndex(elements) {
185
266
  }
186
267
  return sorted;
187
268
  }
188
- function containsDeep(container, element) {
269
+ function containsComposed(container, element) {
189
270
  let current = element;
190
271
  while (current) {
191
272
  if (current === container) {
@@ -195,6 +276,75 @@ function containsDeep(container, element) {
195
276
  }
196
277
  return false;
197
278
  }
279
+ function getComposedChildren(node) {
280
+ if (node instanceof ShadowRoot) {
281
+ return getChildren(node);
282
+ }
283
+ if (!(node instanceof Element)) {
284
+ return [];
285
+ }
286
+ if (node instanceof HTMLSlotElement) {
287
+ const assigned = node.assignedElements({ flatten: true });
288
+ if (assigned.length) {
289
+ return assigned;
290
+ }
291
+ }
292
+ if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
293
+ return getChildren(node.shadowRoot);
294
+ }
295
+ return getChildren(node);
296
+ }
297
+ function getComposedParent(node) {
298
+ if (node instanceof Element && node.assignedSlot) {
299
+ return node.assignedSlot;
300
+ }
301
+ const parent = node.parentNode;
302
+ if (parent instanceof ShadowRoot) {
303
+ return parent.host;
304
+ }
305
+ return parent instanceof Element ? parent : null;
306
+ }
307
+ function getComposedSiblings(node) {
308
+ if (node.assignedSlot) {
309
+ return [...node.assignedSlot.children].filter(
310
+ (child) => child instanceof Element && child !== node
311
+ );
312
+ }
313
+ const parent = getComposedParent(node);
314
+ if (!parent) {
315
+ return [];
316
+ }
317
+ return getSiblings(node);
318
+ }
319
+ var inertRefCounts = /* @__PURE__ */ new WeakMap();
320
+ function applyInert(element) {
321
+ if (isInert(element) && !inertRefCounts.has(element)) {
322
+ return false;
323
+ }
324
+ const count = inertRefCounts.get(element) ?? 0;
325
+ inertRefCounts.set(element, count + 1);
326
+ if (count === 0) {
327
+ setInert(element, true);
328
+ }
329
+ return true;
330
+ }
331
+ function restoreInert(element) {
332
+ const count = inertRefCounts.get(element);
333
+ if (!count) {
334
+ return;
335
+ }
336
+ if (count === 1) {
337
+ inertRefCounts.delete(element);
338
+ setInert(element, false);
339
+ return;
340
+ }
341
+ inertRefCounts.set(element, count - 1);
342
+ }
343
+ function focus(element) {
344
+ if ("focus" in element && typeof element.focus === "function") {
345
+ element.focus();
346
+ }
347
+ }
198
348
  function getActiveElement() {
199
349
  let current = document.activeElement;
200
350
  while (current?.shadowRoot?.activeElement) {
@@ -209,58 +359,53 @@ function getChildren(node) {
209
359
  }
210
360
  return elements;
211
361
  }
362
+ function getSiblings(node) {
363
+ const parent = getComposedParent(node);
364
+ if (!parent) {
365
+ return [];
366
+ }
367
+ const elements = [];
368
+ for (let child = parent.firstElementChild; child; child = child.nextElementSibling) {
369
+ if (child !== node) {
370
+ elements[elements.length] = child;
371
+ }
372
+ }
373
+ return elements;
374
+ }
212
375
  function getTabIndex(element) {
213
376
  return "tabIndex" in element ? Number(element.tabIndex) : 0;
214
377
  }
215
378
  function isDisabled(element) {
216
379
  return "disabled" in element && !!element.disabled;
217
380
  }
218
- function isDisabledDeep(element) {
219
- let current = element;
220
- while (current) {
221
- if (current instanceof ShadowRoot) {
222
- if (current.mode !== "open") {
223
- return false;
224
- }
225
- current = current.host;
226
- continue;
227
- }
228
- if (!(current instanceof Element)) {
229
- current = current.parentNode;
230
- continue;
231
- }
232
- if (current === element && isFormControl(current) && isDisabled(current)) {
233
- return true;
234
- }
235
- if (current.hasAttribute("inert")) {
236
- return true;
237
- }
238
- if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
239
- if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
240
- return true;
241
- }
242
- }
243
- current = current.parentNode;
244
- }
245
- return false;
246
- }
247
381
  function isFormControl(element) {
248
382
  const name = element.tagName;
249
383
  return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
250
384
  }
385
+ function isInert(element) {
386
+ return "inert" in element && !!element.inert;
387
+ }
251
388
  function isUngroupedRadio(element) {
252
389
  return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
253
390
  }
391
+ function setInert(element, boolean) {
392
+ if (boolean) {
393
+ element.setAttribute("inert", "");
394
+ } else {
395
+ element.removeAttribute("inert");
396
+ }
397
+ }
254
398
  /**
255
399
  * Power Focusable
256
- * High-precision focus management utility with shadow DOM support.
257
- * Handles complex focus rules including tabindex ordering, radio groups, etc.
400
+ * High-precision focus management utility with full composed tree support.
401
+ * Handles complex focus rules including tabindex ordering, radio groups, inert,
402
+ * and shadow DOM.
258
403
  *
259
- * @version 2.2.1
404
+ * @version 3.0.1
260
405
  * @author Yusuke Kamiyamane
261
406
  * @license MIT
262
407
  * @copyright Copyright (c) Yusuke Kamiyamane
263
408
  * @see {@link https://github.com/y14e/power-focusable}
264
409
  */
265
410
 
266
- export { getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, isFocusable };
411
+ export { createFocusTrap, getFocusables, getNextFocusable, getPreviousFocusable, hasFocusable, inertOutside, isFocusable };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "power-focusable",
3
- "version": "2.2.1",
4
- "description": "High-precision focus management utility with shadow DOM support",
3
+ "version": "3.0.1",
4
+ "description": "High-precision focus management utility with full composed tree support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",