@y14e/portal 1.2.8 → 1.2.10

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
@@ -2,9 +2,6 @@
2
2
 
3
3
  Lightweight DOM portal (teleport) utility with fully focus management. Designed for accessible dialogs, menus, overlays, popovers.
4
4
 
5
- > [!NOTE]
6
- > Focus traversal works across portals using invisible sentinels and composed-tree-aware focus detection powered by [Power Focusable](https://github.com/y14e/power-focusable).
7
-
8
5
  ## Install
9
6
 
10
7
  ```bash
@@ -13,14 +10,14 @@ npm i @y14e/portal
13
10
 
14
11
  ```ts
15
12
  // npm
16
- import { createPortal } from '@y14e/portal';
13
+ import { createPortal } from '@y14e/portal@1.2.10';
17
14
 
18
15
  // CDNs
19
- import { createPortal } from 'https://esm.sh/@y14e/portal'
16
+ import { createPortal } from 'https://esm.sh/@y14e/portal@1.2.10';
20
17
  // or
21
- import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal/+esm';
18
+ import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal@1.2.10/+esm';
22
19
  // or
23
- import { createPortal } from 'https://unpkg.com/@y14e/portal/dist/index.js';
20
+ import { createPortal } from 'https://esm.unpkg.com/@y14e/portal@1.2.10';
24
21
  ```
25
22
 
26
23
  ## 📦 APIs
package/dist/index.cjs CHANGED
@@ -34,7 +34,13 @@ function getFocusables(container = document.body, options = {}) {
34
34
  console.warn("Invalid container element. Fallback: <body> element.");
35
35
  container = document.body;
36
36
  }
37
- let { composed = false, filter, include } = options;
37
+ let {
38
+ composed = false,
39
+ filter,
40
+ include,
41
+ skipNegativeTabIndexCheck = false,
42
+ skipVisibilityCheck = false
43
+ } = options;
38
44
  if (typeof composed !== "boolean") {
39
45
  console.warn("Invalid composed option. Fallback: false.");
40
46
  composed = false;
@@ -51,11 +57,22 @@ function getFocusables(container = document.body, options = {}) {
51
57
  );
52
58
  include = void 0;
53
59
  }
60
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
61
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
62
+ skipNegativeTabIndexCheck = false;
63
+ }
64
+ if (typeof skipVisibilityCheck !== "boolean") {
65
+ console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
66
+ skipVisibilityCheck = false;
67
+ }
54
68
  const elements = [];
55
69
  if (composed || include) {
56
70
  let traverse2 = function(node) {
57
71
  if (node instanceof Element) {
58
- if (isFocusable(node) || include?.(node)) {
72
+ if (isFocusable(node, {
73
+ skipNegativeTabIndexCheck,
74
+ skipVisibilityCheck
75
+ }) || include?.(node)) {
59
76
  elements[elements.length] = node;
60
77
  }
61
78
  }
@@ -76,7 +93,10 @@ function getFocusables(container = document.body, options = {}) {
76
93
  if (!(candidate instanceof Element)) {
77
94
  continue;
78
95
  }
79
- if (isFocusable(candidate)) {
96
+ if (isFocusable(candidate, {
97
+ skipNegativeTabIndexCheck,
98
+ skipVisibilityCheck
99
+ })) {
80
100
  elements[elements.length] = candidate;
81
101
  }
82
102
  }
@@ -90,24 +110,35 @@ function getNextFocusable(container = document.body, options = {}) {
90
110
  function getPreviousFocusable(container = document.body, options = {}) {
91
111
  return getRelativeFocusable(container, -1, options);
92
112
  }
93
- function isFocusable(element) {
113
+ function isFocusable(element, options = {}) {
94
114
  if (!(element instanceof Element)) {
95
115
  console.warn("Invalid element");
96
116
  return false;
97
117
  }
118
+ let { skipNegativeTabIndexCheck = false, skipVisibilityCheck = false } = options;
119
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
120
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
121
+ skipNegativeTabIndexCheck = false;
122
+ }
123
+ if (typeof skipVisibilityCheck !== "boolean") {
124
+ console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
125
+ skipVisibilityCheck = false;
126
+ }
98
127
  if (element.hasAttribute("hidden") || isInert(element)) {
99
128
  return false;
100
129
  }
101
- if (getTabIndex(element) < 0) {
130
+ if (!skipNegativeTabIndexCheck && getTabIndex(element) < 0) {
102
131
  return false;
103
132
  }
104
- if (!element.matches(FOCUSABLE_SELECTOR)) {
133
+ if (!element.matches(
134
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
135
+ )) {
105
136
  return false;
106
137
  }
107
138
  if (isDisabledDeep(element)) {
108
139
  return false;
109
140
  }
110
- if (!element.checkVisibility({
141
+ if (!skipVisibilityCheck && !element.checkVisibility({
111
142
  contentVisibilityAuto: true,
112
143
  opacityProperty: true,
113
144
  visibilityProperty: true
@@ -126,6 +157,8 @@ function getRelativeFocusable(container, offset, options) {
126
157
  composed = false,
127
158
  filter,
128
159
  include,
160
+ skipNegativeTabIndexCheck = false,
161
+ skipVisibilityCheck = false,
129
162
  wrap = false
130
163
  } = options;
131
164
  if (!(anchor instanceof Element)) {
@@ -158,11 +191,26 @@ function getRelativeFocusable(container, offset, options) {
158
191
  );
159
192
  include = void 0;
160
193
  }
194
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
195
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
196
+ skipNegativeTabIndexCheck = false;
197
+ }
198
+ if (typeof skipVisibilityCheck !== "boolean") {
199
+ console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
200
+ skipVisibilityCheck = false;
201
+ }
161
202
  if (typeof wrap !== "boolean") {
162
203
  console.warn("Invalid wrap option. Fallback: false.");
163
204
  wrap = false;
164
205
  }
165
- const focusables = getFocusables(container, { composed, filter, include });
206
+ const settings = { composed, skipNegativeTabIndexCheck, skipVisibilityCheck };
207
+ if (filter !== void 0) {
208
+ Object.assign(settings, { filter });
209
+ }
210
+ if (include !== void 0) {
211
+ Object.assign(settings, { include });
212
+ }
213
+ const focusables = getFocusables(container, settings);
166
214
  const { length } = focusables;
167
215
  if (!length) {
168
216
  return null;
@@ -400,19 +448,21 @@ var Portal = class {
400
448
  if (current === this.#entranceSentinel) {
401
449
  if (this.#host.contains(before)) {
402
450
  this.#moveFocus("previous");
403
- } else {
404
- this.#update();
405
- const first = [...this.#focusables][0];
406
- first && focusElement(first);
451
+ return;
407
452
  }
408
- } else if (current === this.#exitSentinel) {
453
+ this.#update();
454
+ const first = [...this.#focusables][0];
455
+ first && focusElement(first);
456
+ return;
457
+ }
458
+ if (current === this.#exitSentinel) {
409
459
  if (this.#host.contains(before)) {
410
460
  this.#moveFocus("next");
411
- } else {
412
- this.#update();
413
- const last = [...this.#focusables].at(-1);
414
- last && focusElement(last);
461
+ return;
415
462
  }
463
+ this.#update();
464
+ const last = [...this.#focusables].at(-1);
465
+ last && focusElement(last);
416
466
  }
417
467
  };
418
468
  #onKeyDown = (event) => {
@@ -516,7 +566,7 @@ function getActiveElement2() {
516
566
  * Lightweight DOM portal (teleport) utility with fully focus management.
517
567
  * Designed for accessible dialogs, menus, overlays, popovers.
518
568
  *
519
- * @version 1.2.8
569
+ * @version 1.2.10
520
570
  * @author Yusuke Kamiyamane
521
571
  * @license MIT
522
572
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -528,7 +578,7 @@ function getActiveElement2() {
528
578
  (**
529
579
  * Attributes Utils
530
580
  *
531
- * @version 1.0.5
581
+ * @version 1.1.1
532
582
  * @author Yusuke Kamiyamane
533
583
  * @license MIT
534
584
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -541,7 +591,7 @@ power-focusable/dist/index.js:
541
591
  * High-precision focus management utility with full composed tree support.
542
592
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
543
593
  *
544
- * @version 4.1.8
594
+ * @version 4.3.2
545
595
  * @author Yusuke Kamiyamane
546
596
  * @license MIT
547
597
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Lightweight DOM portal (teleport) utility with fully focus management.
4
4
  * Designed for accessible dialogs, menus, overlays, popovers.
5
5
  *
6
- * @version 1.2.8
6
+ * @version 1.2.10
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
  * Lightweight DOM portal (teleport) utility with fully focus management.
4
4
  * Designed for accessible dialogs, menus, overlays, popovers.
5
5
  *
6
- * @version 1.2.8
6
+ * @version 1.2.10
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.js CHANGED
@@ -32,7 +32,13 @@ function getFocusables(container = document.body, options = {}) {
32
32
  console.warn("Invalid container element. Fallback: <body> element.");
33
33
  container = document.body;
34
34
  }
35
- let { composed = false, filter, include } = options;
35
+ let {
36
+ composed = false,
37
+ filter,
38
+ include,
39
+ skipNegativeTabIndexCheck = false,
40
+ skipVisibilityCheck = false
41
+ } = options;
36
42
  if (typeof composed !== "boolean") {
37
43
  console.warn("Invalid composed option. Fallback: false.");
38
44
  composed = false;
@@ -49,11 +55,22 @@ function getFocusables(container = document.body, options = {}) {
49
55
  );
50
56
  include = void 0;
51
57
  }
58
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
59
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
60
+ skipNegativeTabIndexCheck = false;
61
+ }
62
+ if (typeof skipVisibilityCheck !== "boolean") {
63
+ console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
64
+ skipVisibilityCheck = false;
65
+ }
52
66
  const elements = [];
53
67
  if (composed || include) {
54
68
  let traverse2 = function(node) {
55
69
  if (node instanceof Element) {
56
- if (isFocusable(node) || include?.(node)) {
70
+ if (isFocusable(node, {
71
+ skipNegativeTabIndexCheck,
72
+ skipVisibilityCheck
73
+ }) || include?.(node)) {
57
74
  elements[elements.length] = node;
58
75
  }
59
76
  }
@@ -74,7 +91,10 @@ function getFocusables(container = document.body, options = {}) {
74
91
  if (!(candidate instanceof Element)) {
75
92
  continue;
76
93
  }
77
- if (isFocusable(candidate)) {
94
+ if (isFocusable(candidate, {
95
+ skipNegativeTabIndexCheck,
96
+ skipVisibilityCheck
97
+ })) {
78
98
  elements[elements.length] = candidate;
79
99
  }
80
100
  }
@@ -88,24 +108,35 @@ function getNextFocusable(container = document.body, options = {}) {
88
108
  function getPreviousFocusable(container = document.body, options = {}) {
89
109
  return getRelativeFocusable(container, -1, options);
90
110
  }
91
- function isFocusable(element) {
111
+ function isFocusable(element, options = {}) {
92
112
  if (!(element instanceof Element)) {
93
113
  console.warn("Invalid element");
94
114
  return false;
95
115
  }
116
+ let { skipNegativeTabIndexCheck = false, skipVisibilityCheck = false } = options;
117
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
118
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
119
+ skipNegativeTabIndexCheck = false;
120
+ }
121
+ if (typeof skipVisibilityCheck !== "boolean") {
122
+ console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
123
+ skipVisibilityCheck = false;
124
+ }
96
125
  if (element.hasAttribute("hidden") || isInert(element)) {
97
126
  return false;
98
127
  }
99
- if (getTabIndex(element) < 0) {
128
+ if (!skipNegativeTabIndexCheck && getTabIndex(element) < 0) {
100
129
  return false;
101
130
  }
102
- if (!element.matches(FOCUSABLE_SELECTOR)) {
131
+ if (!element.matches(
132
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
133
+ )) {
103
134
  return false;
104
135
  }
105
136
  if (isDisabledDeep(element)) {
106
137
  return false;
107
138
  }
108
- if (!element.checkVisibility({
139
+ if (!skipVisibilityCheck && !element.checkVisibility({
109
140
  contentVisibilityAuto: true,
110
141
  opacityProperty: true,
111
142
  visibilityProperty: true
@@ -124,6 +155,8 @@ function getRelativeFocusable(container, offset, options) {
124
155
  composed = false,
125
156
  filter,
126
157
  include,
158
+ skipNegativeTabIndexCheck = false,
159
+ skipVisibilityCheck = false,
127
160
  wrap = false
128
161
  } = options;
129
162
  if (!(anchor instanceof Element)) {
@@ -156,11 +189,26 @@ function getRelativeFocusable(container, offset, options) {
156
189
  );
157
190
  include = void 0;
158
191
  }
192
+ if (typeof skipNegativeTabIndexCheck !== "boolean") {
193
+ console.warn("Invalid skipNegativeTabIndexCheck option. Fallback: false.");
194
+ skipNegativeTabIndexCheck = false;
195
+ }
196
+ if (typeof skipVisibilityCheck !== "boolean") {
197
+ console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
198
+ skipVisibilityCheck = false;
199
+ }
159
200
  if (typeof wrap !== "boolean") {
160
201
  console.warn("Invalid wrap option. Fallback: false.");
161
202
  wrap = false;
162
203
  }
163
- const focusables = getFocusables(container, { composed, filter, include });
204
+ const settings = { composed, skipNegativeTabIndexCheck, skipVisibilityCheck };
205
+ if (filter !== void 0) {
206
+ Object.assign(settings, { filter });
207
+ }
208
+ if (include !== void 0) {
209
+ Object.assign(settings, { include });
210
+ }
211
+ const focusables = getFocusables(container, settings);
164
212
  const { length } = focusables;
165
213
  if (!length) {
166
214
  return null;
@@ -398,19 +446,21 @@ var Portal = class {
398
446
  if (current === this.#entranceSentinel) {
399
447
  if (this.#host.contains(before)) {
400
448
  this.#moveFocus("previous");
401
- } else {
402
- this.#update();
403
- const first = [...this.#focusables][0];
404
- first && focusElement(first);
449
+ return;
405
450
  }
406
- } else if (current === this.#exitSentinel) {
451
+ this.#update();
452
+ const first = [...this.#focusables][0];
453
+ first && focusElement(first);
454
+ return;
455
+ }
456
+ if (current === this.#exitSentinel) {
407
457
  if (this.#host.contains(before)) {
408
458
  this.#moveFocus("next");
409
- } else {
410
- this.#update();
411
- const last = [...this.#focusables].at(-1);
412
- last && focusElement(last);
459
+ return;
413
460
  }
461
+ this.#update();
462
+ const last = [...this.#focusables].at(-1);
463
+ last && focusElement(last);
414
464
  }
415
465
  };
416
466
  #onKeyDown = (event) => {
@@ -514,7 +564,7 @@ function getActiveElement2() {
514
564
  * Lightweight DOM portal (teleport) utility with fully focus management.
515
565
  * Designed for accessible dialogs, menus, overlays, popovers.
516
566
  *
517
- * @version 1.2.8
567
+ * @version 1.2.10
518
568
  * @author Yusuke Kamiyamane
519
569
  * @license MIT
520
570
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -526,7 +576,7 @@ function getActiveElement2() {
526
576
  (**
527
577
  * Attributes Utils
528
578
  *
529
- * @version 1.0.5
579
+ * @version 1.1.1
530
580
  * @author Yusuke Kamiyamane
531
581
  * @license MIT
532
582
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -539,7 +589,7 @@ power-focusable/dist/index.js:
539
589
  * High-precision focus management utility with full composed tree support.
540
590
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
541
591
  *
542
- * @version 4.1.8
592
+ * @version 4.3.2
543
593
  * @author Yusuke Kamiyamane
544
594
  * @license MIT
545
595
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/portal",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
4
4
  "description": "Lightweight DOM portal (teleport) utility with fully focus management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -47,9 +47,9 @@
47
47
  },
48
48
  "homepage": "https://github.com/y14e/portal#readme",
49
49
  "devDependencies": {
50
- "@y14e/attributes-utils": "^1.0.5",
50
+ "@y14e/attributes-utils": "^1.1.1",
51
51
  "bun-types": "latest",
52
- "power-focusable": "^4.1.8",
52
+ "power-focusable": "^4.3.2",
53
53
  "tsup": "^8.0.0",
54
54
  "typescript": "^5.6.0"
55
55
  },