power-focusable 4.3.11 → 4.3.12

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
@@ -24,18 +24,18 @@ import {
24
24
  } from 'power-focusable';
25
25
 
26
26
  // CDNs
27
- import { ... } 'https://esm.sh/power-focusable@4.3.11';
27
+ import { ... } 'https://esm.sh/power-focusable@4.3.12';
28
28
  // or
29
- import { ... } 'https://cdn.jsdelivr.net/npm/power-focusable@4.3.11/+esm';
29
+ import { ... } 'https://cdn.jsdelivr.net/npm/power-focusable@4.3.12/+esm';
30
30
  // or
31
- import { ... } 'https://esm.unpkg.com/power-focusable@4.3.11';
31
+ import { ... } 'https://esm.unpkg.com/power-focusable@4.3.12';
32
32
  ```
33
33
 
34
34
  ## 🪄 Options
35
35
 
36
36
  ```ts
37
37
  interface PowerFocusableOptions {
38
- anchor: Element | null; // default: DocumentOrShadowRoot activeElement
38
+ anchor: Element | null; // default: DocumentOrShadowRoot.activeElement
39
39
  composed: boolean; // default: false
40
40
  filter: PowerFocusableFunction; // default: undefined
41
41
  include: PowerFocusableFunction; // default: undefined
package/dist/index.cjs CHANGED
@@ -2,6 +2,10 @@
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 FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX = FOCUSABLE_SELECTOR.replace(
6
+ /(,\s*)?\[tabindex="-1"\]/g,
7
+ ""
8
+ );
5
9
  function createFocusTrap(container = document.body) {
6
10
  if (!(container instanceof Element)) {
7
11
  console.warn("Invalid container element");
@@ -95,21 +99,27 @@ function getFocusables(container = document.body, options = {}) {
95
99
  const elements = [];
96
100
  if (composed || include) {
97
101
  let traverse2 = function(node) {
98
- if (!(node instanceof Element)) {
99
- return;
100
- }
101
- if (isFocusable(node, { skipNegativeTabIndexCheck, skipVisibilityCheck }) || include?.(node)) {
102
+ if (isFocusable(node, {
103
+ skipNegativeTabIndexCheck,
104
+ skipVisibilityCheck
105
+ }) || include?.(node)) {
102
106
  elements[elements.length] = node;
103
107
  }
104
- const children = getComposedChildren(node);
105
- for (let i = 0, l = children.length; i < l; i++) {
106
- const child = children[i];
108
+ const children2 = composed ? getComposedChildren(node) : getChildren(node);
109
+ for (let i = 0, l = children2.length; i < l; i++) {
110
+ const child = children2[i];
107
111
  child && traverse2(child);
108
112
  }
109
113
  };
110
- traverse2(container);
114
+ const children = composed ? getComposedChildren(container) : getChildren(container);
115
+ for (let i = 0, l = children.length; i < l; i++) {
116
+ const child = children[i];
117
+ child && traverse2(child);
118
+ }
111
119
  } else {
112
- const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
120
+ const candidates = container.querySelectorAll(
121
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX : FOCUSABLE_SELECTOR
122
+ );
113
123
  for (let i = 0, l = candidates.length; i < l; i++) {
114
124
  const candidate = candidates[i];
115
125
  if (candidate && isFocusable(candidate, {
@@ -120,8 +130,8 @@ function getFocusables(container = document.body, options = {}) {
120
130
  }
121
131
  }
122
132
  }
123
- const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
124
- return filter ? unfiltered.filter(filter) : unfiltered;
133
+ const filtered = filter ? elements.filter(filter) : elements;
134
+ return normalizeRadioGroup(sortByTabIndex(filtered));
125
135
  }
126
136
  function getNextFocusable(container = document.body, options = {}) {
127
137
  return getRelativeFocusable(container, 1, options);
@@ -176,7 +186,7 @@ function isFocusable(element, options = {}) {
176
186
  return false;
177
187
  }
178
188
  if (!element.matches(
179
- skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
189
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX : FOCUSABLE_SELECTOR
180
190
  )) {
181
191
  return false;
182
192
  }
@@ -300,7 +310,7 @@ function isDisabledDeep(element) {
300
310
  return false;
301
311
  }
302
312
  function normalizeRadioGroup(elements) {
303
- let map = null;
313
+ let rootMap = null;
304
314
  for (let i = 0, l = elements.length; i < l; i++) {
305
315
  const element = elements[i];
306
316
  if (!(element instanceof HTMLInputElement)) {
@@ -309,25 +319,45 @@ function normalizeRadioGroup(elements) {
309
319
  if (!isUngroupedRadio(element)) {
310
320
  continue;
311
321
  }
312
- if (!map) {
313
- map = /* @__PURE__ */ new Map();
322
+ if (!rootMap) {
323
+ rootMap = /* @__PURE__ */ new Map();
324
+ }
325
+ const root = element.getRootNode();
326
+ let formMap = rootMap.get(root);
327
+ if (!formMap) {
328
+ formMap = /* @__PURE__ */ new Map();
329
+ rootMap.set(root, formMap);
330
+ }
331
+ let nameMap = formMap.get(element.form);
332
+ if (!nameMap) {
333
+ nameMap = /* @__PURE__ */ new Map();
334
+ formMap.set(element.form, nameMap);
314
335
  }
315
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
316
- const group = map.get(key) ?? map.set(key, []).get(key);
317
- if (group) {
318
- group[group.length] = element;
336
+ let group = nameMap.get(element.name);
337
+ if (!group) {
338
+ group = [];
339
+ nameMap.set(element.name, group);
319
340
  }
341
+ group[group.length] = element;
320
342
  }
321
- if (!map) {
343
+ if (!rootMap) {
322
344
  return elements;
323
345
  }
324
346
  const placeholder = /* @__PURE__ */ new Set();
325
- for (const group of map.values()) {
326
- placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
347
+ for (const formMap of rootMap.values()) {
348
+ for (const nameMap of formMap.values()) {
349
+ for (const group of nameMap.values()) {
350
+ const radio = group.find((element) => element.checked) ?? group[0];
351
+ radio && placeholder.add(radio);
352
+ }
353
+ }
327
354
  }
328
- return elements.filter(
329
- (element) => isUngroupedRadio(element) ? placeholder.has(element) : true
330
- );
355
+ return elements.filter((element) => {
356
+ if (!(element instanceof HTMLInputElement)) {
357
+ return true;
358
+ }
359
+ return !isUngroupedRadio(element) || placeholder.has(element);
360
+ });
331
361
  }
332
362
  function sortByTabIndex(elements) {
333
363
  const ordered = [];
@@ -388,19 +418,19 @@ function getComposedParent(node) {
388
418
  }
389
419
  }
390
420
  function getComposedSiblings(node) {
391
- if (node.assignedSlot) {
392
- const siblings = node.assignedSlot.children;
393
- const filtered = [];
394
- for (let i = 0, l = siblings.length; i < l; i++) {
395
- const sibling = siblings[i];
396
- if (sibling && sibling !== node) {
397
- filtered[filtered.length] = sibling;
398
- }
421
+ const parent = getComposedParent(node);
422
+ if (!parent) {
423
+ return [];
424
+ }
425
+ const siblings = parent instanceof HTMLSlotElement ? parent.assignedElements({ flatten: true }) : getComposedChildren(parent);
426
+ const filtered = [];
427
+ for (let i = 0, l = siblings.length; i < l; i++) {
428
+ const sibling = siblings[i];
429
+ if (sibling && sibling !== node) {
430
+ filtered[filtered.length] = sibling;
399
431
  }
400
- return filtered;
401
- } else {
402
- return getComposedParent(node) ? getSiblings(node) : [];
403
432
  }
433
+ return filtered;
404
434
  }
405
435
  var inertRefCounts = /* @__PURE__ */ new WeakMap();
406
436
  function applyInert(element) {
@@ -442,19 +472,6 @@ function getChildren(node) {
442
472
  }
443
473
  return elements;
444
474
  }
445
- function getSiblings(node) {
446
- const parent = getComposedParent(node);
447
- if (!parent) {
448
- return [];
449
- }
450
- const elements = [];
451
- for (let child = parent.firstElementChild; child; child = child.nextElementSibling) {
452
- if (child !== node) {
453
- elements[elements.length] = child;
454
- }
455
- }
456
- return elements;
457
- }
458
475
  function getTabIndex(element) {
459
476
  return "tabIndex" in element ? Number(element.tabIndex) : 0;
460
477
  }
@@ -469,14 +486,14 @@ function isInert(element) {
469
486
  return "inert" in element && !!element.inert;
470
487
  }
471
488
  function isUngroupedRadio(element) {
472
- return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
489
+ return element.type === "radio" && !!element.name;
473
490
  }
474
491
  /**
475
492
  * Power Focusable
476
493
  * High-precision focus management utility with full composed tree support.
477
494
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
478
495
  *
479
- * @version 4.3.11
496
+ * @version 4.3.12
480
497
  * @author Yusuke Kamiyamane
481
498
  * @license MIT
482
499
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@
3
3
  * High-precision focus management utility with full composed tree support.
4
4
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
5
5
  *
6
- * @version 4.3.11
6
+ * @version 4.3.12
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 full composed tree support.
4
4
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
5
5
  *
6
- * @version 4.3.11
6
+ * @version 4.3.12
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.js CHANGED
@@ -1,5 +1,9 @@
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 FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX = FOCUSABLE_SELECTOR.replace(
4
+ /(,\s*)?\[tabindex="-1"\]/g,
5
+ ""
6
+ );
3
7
  function createFocusTrap(container = document.body) {
4
8
  if (!(container instanceof Element)) {
5
9
  console.warn("Invalid container element");
@@ -93,21 +97,27 @@ function getFocusables(container = document.body, options = {}) {
93
97
  const elements = [];
94
98
  if (composed || include) {
95
99
  let traverse2 = function(node) {
96
- if (!(node instanceof Element)) {
97
- return;
98
- }
99
- if (isFocusable(node, { skipNegativeTabIndexCheck, skipVisibilityCheck }) || include?.(node)) {
100
+ if (isFocusable(node, {
101
+ skipNegativeTabIndexCheck,
102
+ skipVisibilityCheck
103
+ }) || include?.(node)) {
100
104
  elements[elements.length] = node;
101
105
  }
102
- const children = getComposedChildren(node);
103
- for (let i = 0, l = children.length; i < l; i++) {
104
- const child = children[i];
106
+ const children2 = composed ? getComposedChildren(node) : getChildren(node);
107
+ for (let i = 0, l = children2.length; i < l; i++) {
108
+ const child = children2[i];
105
109
  child && traverse2(child);
106
110
  }
107
111
  };
108
- traverse2(container);
112
+ const children = composed ? getComposedChildren(container) : getChildren(container);
113
+ for (let i = 0, l = children.length; i < l; i++) {
114
+ const child = children[i];
115
+ child && traverse2(child);
116
+ }
109
117
  } else {
110
- const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
118
+ const candidates = container.querySelectorAll(
119
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX : FOCUSABLE_SELECTOR
120
+ );
111
121
  for (let i = 0, l = candidates.length; i < l; i++) {
112
122
  const candidate = candidates[i];
113
123
  if (candidate && isFocusable(candidate, {
@@ -118,8 +128,8 @@ function getFocusables(container = document.body, options = {}) {
118
128
  }
119
129
  }
120
130
  }
121
- const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
122
- return filter ? unfiltered.filter(filter) : unfiltered;
131
+ const filtered = filter ? elements.filter(filter) : elements;
132
+ return normalizeRadioGroup(sortByTabIndex(filtered));
123
133
  }
124
134
  function getNextFocusable(container = document.body, options = {}) {
125
135
  return getRelativeFocusable(container, 1, options);
@@ -174,7 +184,7 @@ function isFocusable(element, options = {}) {
174
184
  return false;
175
185
  }
176
186
  if (!element.matches(
177
- skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
187
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TAB_INDEX : FOCUSABLE_SELECTOR
178
188
  )) {
179
189
  return false;
180
190
  }
@@ -298,7 +308,7 @@ function isDisabledDeep(element) {
298
308
  return false;
299
309
  }
300
310
  function normalizeRadioGroup(elements) {
301
- let map = null;
311
+ let rootMap = null;
302
312
  for (let i = 0, l = elements.length; i < l; i++) {
303
313
  const element = elements[i];
304
314
  if (!(element instanceof HTMLInputElement)) {
@@ -307,25 +317,45 @@ function normalizeRadioGroup(elements) {
307
317
  if (!isUngroupedRadio(element)) {
308
318
  continue;
309
319
  }
310
- if (!map) {
311
- map = /* @__PURE__ */ new Map();
320
+ if (!rootMap) {
321
+ rootMap = /* @__PURE__ */ new Map();
322
+ }
323
+ const root = element.getRootNode();
324
+ let formMap = rootMap.get(root);
325
+ if (!formMap) {
326
+ formMap = /* @__PURE__ */ new Map();
327
+ rootMap.set(root, formMap);
328
+ }
329
+ let nameMap = formMap.get(element.form);
330
+ if (!nameMap) {
331
+ nameMap = /* @__PURE__ */ new Map();
332
+ formMap.set(element.form, nameMap);
312
333
  }
313
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
314
- const group = map.get(key) ?? map.set(key, []).get(key);
315
- if (group) {
316
- group[group.length] = element;
334
+ let group = nameMap.get(element.name);
335
+ if (!group) {
336
+ group = [];
337
+ nameMap.set(element.name, group);
317
338
  }
339
+ group[group.length] = element;
318
340
  }
319
- if (!map) {
341
+ if (!rootMap) {
320
342
  return elements;
321
343
  }
322
344
  const placeholder = /* @__PURE__ */ new Set();
323
- for (const group of map.values()) {
324
- placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
345
+ for (const formMap of rootMap.values()) {
346
+ for (const nameMap of formMap.values()) {
347
+ for (const group of nameMap.values()) {
348
+ const radio = group.find((element) => element.checked) ?? group[0];
349
+ radio && placeholder.add(radio);
350
+ }
351
+ }
325
352
  }
326
- return elements.filter(
327
- (element) => isUngroupedRadio(element) ? placeholder.has(element) : true
328
- );
353
+ return elements.filter((element) => {
354
+ if (!(element instanceof HTMLInputElement)) {
355
+ return true;
356
+ }
357
+ return !isUngroupedRadio(element) || placeholder.has(element);
358
+ });
329
359
  }
330
360
  function sortByTabIndex(elements) {
331
361
  const ordered = [];
@@ -386,19 +416,19 @@ function getComposedParent(node) {
386
416
  }
387
417
  }
388
418
  function getComposedSiblings(node) {
389
- if (node.assignedSlot) {
390
- const siblings = node.assignedSlot.children;
391
- const filtered = [];
392
- for (let i = 0, l = siblings.length; i < l; i++) {
393
- const sibling = siblings[i];
394
- if (sibling && sibling !== node) {
395
- filtered[filtered.length] = sibling;
396
- }
419
+ const parent = getComposedParent(node);
420
+ if (!parent) {
421
+ return [];
422
+ }
423
+ const siblings = parent instanceof HTMLSlotElement ? parent.assignedElements({ flatten: true }) : getComposedChildren(parent);
424
+ const filtered = [];
425
+ for (let i = 0, l = siblings.length; i < l; i++) {
426
+ const sibling = siblings[i];
427
+ if (sibling && sibling !== node) {
428
+ filtered[filtered.length] = sibling;
397
429
  }
398
- return filtered;
399
- } else {
400
- return getComposedParent(node) ? getSiblings(node) : [];
401
430
  }
431
+ return filtered;
402
432
  }
403
433
  var inertRefCounts = /* @__PURE__ */ new WeakMap();
404
434
  function applyInert(element) {
@@ -440,19 +470,6 @@ function getChildren(node) {
440
470
  }
441
471
  return elements;
442
472
  }
443
- function getSiblings(node) {
444
- const parent = getComposedParent(node);
445
- if (!parent) {
446
- return [];
447
- }
448
- const elements = [];
449
- for (let child = parent.firstElementChild; child; child = child.nextElementSibling) {
450
- if (child !== node) {
451
- elements[elements.length] = child;
452
- }
453
- }
454
- return elements;
455
- }
456
473
  function getTabIndex(element) {
457
474
  return "tabIndex" in element ? Number(element.tabIndex) : 0;
458
475
  }
@@ -467,14 +484,14 @@ function isInert(element) {
467
484
  return "inert" in element && !!element.inert;
468
485
  }
469
486
  function isUngroupedRadio(element) {
470
- return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
487
+ return element.type === "radio" && !!element.name;
471
488
  }
472
489
  /**
473
490
  * Power Focusable
474
491
  * High-precision focus management utility with full composed tree support.
475
492
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
476
493
  *
477
- * @version 4.3.11
494
+ * @version 4.3.12
478
495
  * @author Yusuke Kamiyamane
479
496
  * @license MIT
480
497
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-focusable",
3
- "version": "4.3.11",
3
+ "version": "4.3.12",
4
4
  "description": "High-precision focus management utility with full composed tree support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",