power-focusable 4.3.10 → 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,25 +24,27 @@ import {
24
24
  } from 'power-focusable';
25
25
 
26
26
  // CDNs
27
- import { ... } 'https://esm.sh/power-focusable@4.3.10';
27
+ import { ... } 'https://esm.sh/power-focusable@4.3.12';
28
28
  // or
29
- import { ... } 'https://cdn.jsdelivr.net/npm/power-focusable@4.3.10/+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.10';
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: active element
38
+ anchor: Element | null; // default: DocumentOrShadowRoot.activeElement
39
39
  composed: boolean; // default: false
40
- filter: (element: Element) => boolean;
41
- include: (element: Element) => boolean;
40
+ filter: PowerFocusableFunction; // default: undefined
41
+ include: PowerFocusableFunction; // default: undefined
42
42
  skipNegativeTabIndexCheck: boolean; // default: false
43
43
  skipVisibilityCheck: boolean; // default: false
44
44
  wrap: boolean; // default: false
45
45
  }
46
+
47
+ type PowerFocusableFunction = ((element: Element) => boolean) | undefined;
46
48
  ```
47
49
 
48
50
  ### `anchor`
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
  }
@@ -248,9 +258,13 @@ function getRelativeFocusable(container, offset, options) {
248
258
  console.warn("Invalid wrap option. Fallback: false.");
249
259
  wrap = false;
250
260
  }
251
- const settings = { composed, skipNegativeTabIndexCheck, skipVisibilityCheck };
252
- filter && Object.assign(settings, { filter });
253
- include && Object.assign(settings, { include });
261
+ const settings = {
262
+ composed,
263
+ filter,
264
+ include,
265
+ skipNegativeTabIndexCheck,
266
+ skipVisibilityCheck
267
+ };
254
268
  const focusables = getFocusables(container, settings);
255
269
  const { length } = focusables;
256
270
  if (!length) {
@@ -296,7 +310,7 @@ function isDisabledDeep(element) {
296
310
  return false;
297
311
  }
298
312
  function normalizeRadioGroup(elements) {
299
- let map = null;
313
+ let rootMap = null;
300
314
  for (let i = 0, l = elements.length; i < l; i++) {
301
315
  const element = elements[i];
302
316
  if (!(element instanceof HTMLInputElement)) {
@@ -305,25 +319,45 @@ function normalizeRadioGroup(elements) {
305
319
  if (!isUngroupedRadio(element)) {
306
320
  continue;
307
321
  }
308
- if (!map) {
309
- map = /* @__PURE__ */ new Map();
322
+ if (!rootMap) {
323
+ rootMap = /* @__PURE__ */ new Map();
310
324
  }
311
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
312
- const group = map.get(key) ?? map.set(key, []).get(key);
313
- if (group) {
314
- group[group.length] = element;
325
+ const root = element.getRootNode();
326
+ let formMap = rootMap.get(root);
327
+ if (!formMap) {
328
+ formMap = /* @__PURE__ */ new Map();
329
+ rootMap.set(root, formMap);
315
330
  }
331
+ let nameMap = formMap.get(element.form);
332
+ if (!nameMap) {
333
+ nameMap = /* @__PURE__ */ new Map();
334
+ formMap.set(element.form, nameMap);
335
+ }
336
+ let group = nameMap.get(element.name);
337
+ if (!group) {
338
+ group = [];
339
+ nameMap.set(element.name, group);
340
+ }
341
+ group[group.length] = element;
316
342
  }
317
- if (!map) {
343
+ if (!rootMap) {
318
344
  return elements;
319
345
  }
320
346
  const placeholder = /* @__PURE__ */ new Set();
321
- for (const group of map.values()) {
322
- 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
+ }
323
354
  }
324
- return elements.filter(
325
- (element) => isUngroupedRadio(element) ? placeholder.has(element) : true
326
- );
355
+ return elements.filter((element) => {
356
+ if (!(element instanceof HTMLInputElement)) {
357
+ return true;
358
+ }
359
+ return !isUngroupedRadio(element) || placeholder.has(element);
360
+ });
327
361
  }
328
362
  function sortByTabIndex(elements) {
329
363
  const ordered = [];
@@ -384,19 +418,19 @@ function getComposedParent(node) {
384
418
  }
385
419
  }
386
420
  function getComposedSiblings(node) {
387
- if (node.assignedSlot) {
388
- const siblings = node.assignedSlot.children;
389
- const filtered = [];
390
- for (let i = 0, l = siblings.length; i < l; i++) {
391
- const sibling = siblings[i];
392
- if (sibling && sibling !== node) {
393
- filtered[filtered.length] = sibling;
394
- }
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;
395
431
  }
396
- return filtered;
397
- } else {
398
- return getComposedParent(node) ? getSiblings(node) : [];
399
432
  }
433
+ return filtered;
400
434
  }
401
435
  var inertRefCounts = /* @__PURE__ */ new WeakMap();
402
436
  function applyInert(element) {
@@ -438,19 +472,6 @@ function getChildren(node) {
438
472
  }
439
473
  return elements;
440
474
  }
441
- function getSiblings(node) {
442
- const parent = getComposedParent(node);
443
- if (!parent) {
444
- return [];
445
- }
446
- const elements = [];
447
- for (let child = parent.firstElementChild; child; child = child.nextElementSibling) {
448
- if (child !== node) {
449
- elements[elements.length] = child;
450
- }
451
- }
452
- return elements;
453
- }
454
475
  function getTabIndex(element) {
455
476
  return "tabIndex" in element ? Number(element.tabIndex) : 0;
456
477
  }
@@ -465,14 +486,14 @@ function isInert(element) {
465
486
  return "inert" in element && !!element.inert;
466
487
  }
467
488
  function isUngroupedRadio(element) {
468
- return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
489
+ return element.type === "radio" && !!element.name;
469
490
  }
470
491
  /**
471
492
  * Power Focusable
472
493
  * High-precision focus management utility with full composed tree support.
473
494
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
474
495
  *
475
- * @version 4.3.10
496
+ * @version 4.3.12
476
497
  * @author Yusuke Kamiyamane
477
498
  * @license MIT
478
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.10
6
+ * @version 4.3.12
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -12,12 +12,13 @@
12
12
  interface PowerFocusableOptions {
13
13
  anchor: Element | null;
14
14
  composed: boolean;
15
- filter: (element: Element) => boolean;
16
- include: (element: Element) => boolean;
15
+ filter: PowerFocusableFunction;
16
+ include: PowerFocusableFunction;
17
17
  skipNegativeTabIndexCheck: boolean;
18
18
  skipVisibilityCheck: boolean;
19
19
  wrap: boolean;
20
20
  }
21
+ type PowerFocusableFunction = ((element: Element) => boolean) | undefined;
21
22
  declare function createFocusTrap(container?: Element): () => void;
22
23
  declare function getFocusables(container?: Element, options?: Partial<Omit<PowerFocusableOptions, 'anchor' | 'wrap'>>): Element[];
23
24
  declare function getNextFocusable(container?: Element, options?: Partial<PowerFocusableOptions>): Element | null;
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.10
6
+ * @version 4.3.12
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -12,12 +12,13 @@
12
12
  interface PowerFocusableOptions {
13
13
  anchor: Element | null;
14
14
  composed: boolean;
15
- filter: (element: Element) => boolean;
16
- include: (element: Element) => boolean;
15
+ filter: PowerFocusableFunction;
16
+ include: PowerFocusableFunction;
17
17
  skipNegativeTabIndexCheck: boolean;
18
18
  skipVisibilityCheck: boolean;
19
19
  wrap: boolean;
20
20
  }
21
+ type PowerFocusableFunction = ((element: Element) => boolean) | undefined;
21
22
  declare function createFocusTrap(container?: Element): () => void;
22
23
  declare function getFocusables(container?: Element, options?: Partial<Omit<PowerFocusableOptions, 'anchor' | 'wrap'>>): Element[];
23
24
  declare function getNextFocusable(container?: Element, options?: Partial<PowerFocusableOptions>): Element | null;
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
  }
@@ -246,9 +256,13 @@ function getRelativeFocusable(container, offset, options) {
246
256
  console.warn("Invalid wrap option. Fallback: false.");
247
257
  wrap = false;
248
258
  }
249
- const settings = { composed, skipNegativeTabIndexCheck, skipVisibilityCheck };
250
- filter && Object.assign(settings, { filter });
251
- include && Object.assign(settings, { include });
259
+ const settings = {
260
+ composed,
261
+ filter,
262
+ include,
263
+ skipNegativeTabIndexCheck,
264
+ skipVisibilityCheck
265
+ };
252
266
  const focusables = getFocusables(container, settings);
253
267
  const { length } = focusables;
254
268
  if (!length) {
@@ -294,7 +308,7 @@ function isDisabledDeep(element) {
294
308
  return false;
295
309
  }
296
310
  function normalizeRadioGroup(elements) {
297
- let map = null;
311
+ let rootMap = null;
298
312
  for (let i = 0, l = elements.length; i < l; i++) {
299
313
  const element = elements[i];
300
314
  if (!(element instanceof HTMLInputElement)) {
@@ -303,25 +317,45 @@ function normalizeRadioGroup(elements) {
303
317
  if (!isUngroupedRadio(element)) {
304
318
  continue;
305
319
  }
306
- if (!map) {
307
- map = /* @__PURE__ */ new Map();
320
+ if (!rootMap) {
321
+ rootMap = /* @__PURE__ */ new Map();
308
322
  }
309
- const key = `${element.form?.id ?? "no-form"}::${element.name}`;
310
- const group = map.get(key) ?? map.set(key, []).get(key);
311
- if (group) {
312
- group[group.length] = element;
323
+ const root = element.getRootNode();
324
+ let formMap = rootMap.get(root);
325
+ if (!formMap) {
326
+ formMap = /* @__PURE__ */ new Map();
327
+ rootMap.set(root, formMap);
313
328
  }
329
+ let nameMap = formMap.get(element.form);
330
+ if (!nameMap) {
331
+ nameMap = /* @__PURE__ */ new Map();
332
+ formMap.set(element.form, nameMap);
333
+ }
334
+ let group = nameMap.get(element.name);
335
+ if (!group) {
336
+ group = [];
337
+ nameMap.set(element.name, group);
338
+ }
339
+ group[group.length] = element;
314
340
  }
315
- if (!map) {
341
+ if (!rootMap) {
316
342
  return elements;
317
343
  }
318
344
  const placeholder = /* @__PURE__ */ new Set();
319
- for (const group of map.values()) {
320
- 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
+ }
321
352
  }
322
- return elements.filter(
323
- (element) => isUngroupedRadio(element) ? placeholder.has(element) : true
324
- );
353
+ return elements.filter((element) => {
354
+ if (!(element instanceof HTMLInputElement)) {
355
+ return true;
356
+ }
357
+ return !isUngroupedRadio(element) || placeholder.has(element);
358
+ });
325
359
  }
326
360
  function sortByTabIndex(elements) {
327
361
  const ordered = [];
@@ -382,19 +416,19 @@ function getComposedParent(node) {
382
416
  }
383
417
  }
384
418
  function getComposedSiblings(node) {
385
- if (node.assignedSlot) {
386
- const siblings = node.assignedSlot.children;
387
- const filtered = [];
388
- for (let i = 0, l = siblings.length; i < l; i++) {
389
- const sibling = siblings[i];
390
- if (sibling && sibling !== node) {
391
- filtered[filtered.length] = sibling;
392
- }
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;
393
429
  }
394
- return filtered;
395
- } else {
396
- return getComposedParent(node) ? getSiblings(node) : [];
397
430
  }
431
+ return filtered;
398
432
  }
399
433
  var inertRefCounts = /* @__PURE__ */ new WeakMap();
400
434
  function applyInert(element) {
@@ -436,19 +470,6 @@ function getChildren(node) {
436
470
  }
437
471
  return elements;
438
472
  }
439
- function getSiblings(node) {
440
- const parent = getComposedParent(node);
441
- if (!parent) {
442
- return [];
443
- }
444
- const elements = [];
445
- for (let child = parent.firstElementChild; child; child = child.nextElementSibling) {
446
- if (child !== node) {
447
- elements[elements.length] = child;
448
- }
449
- }
450
- return elements;
451
- }
452
473
  function getTabIndex(element) {
453
474
  return "tabIndex" in element ? Number(element.tabIndex) : 0;
454
475
  }
@@ -463,14 +484,14 @@ function isInert(element) {
463
484
  return "inert" in element && !!element.inert;
464
485
  }
465
486
  function isUngroupedRadio(element) {
466
- return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
487
+ return element.type === "radio" && !!element.name;
467
488
  }
468
489
  /**
469
490
  * Power Focusable
470
491
  * High-precision focus management utility with full composed tree support.
471
492
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
472
493
  *
473
- * @version 4.3.10
494
+ * @version 4.3.12
474
495
  * @author Yusuke Kamiyamane
475
496
  * @license MIT
476
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.10",
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",