@y14e/portal 1.2.7 → 1.2.9

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.9';
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.9';
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.9/+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.9';
24
21
  ```
25
22
 
26
23
  ## 📦 APIs
package/dist/index.cjs CHANGED
@@ -34,24 +34,45 @@ 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
- console.warn("Invalid composed. Fallback: false.");
45
+ console.warn("Invalid composed option. Fallback: false.");
40
46
  composed = false;
41
47
  }
42
48
  if (typeof filter !== "undefined" && typeof filter !== "function") {
43
- console.warn("Invalid filter. Fallback: no filter function (undefined).");
49
+ console.warn(
50
+ "Invalid filter function. Fallback: no filter function (undefined)."
51
+ );
44
52
  filter = void 0;
45
53
  }
46
54
  if (typeof include !== "undefined" && typeof include !== "function") {
47
- console.warn("Invalid include. Fallback: no include function (undefined).");
55
+ console.warn(
56
+ "Invalid include function. Fallback: no include function (undefined)."
57
+ );
48
58
  include = void 0;
49
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
+ }
50
68
  const elements = [];
51
69
  if (composed || include) {
52
70
  let traverse2 = function(node) {
53
71
  if (node instanceof Element) {
54
- if (isFocusable(node) || include?.(node)) {
72
+ if (isFocusable(node, {
73
+ skipNegativeTabIndexCheck,
74
+ skipVisibilityCheck
75
+ }) || include?.(node)) {
55
76
  elements[elements.length] = node;
56
77
  }
57
78
  }
@@ -72,7 +93,10 @@ function getFocusables(container = document.body, options = {}) {
72
93
  if (!(candidate instanceof Element)) {
73
94
  continue;
74
95
  }
75
- if (isFocusable(candidate)) {
96
+ if (isFocusable(candidate, {
97
+ skipNegativeTabIndexCheck,
98
+ skipVisibilityCheck
99
+ })) {
76
100
  elements[elements.length] = candidate;
77
101
  }
78
102
  }
@@ -86,24 +110,35 @@ function getNextFocusable(container = document.body, options = {}) {
86
110
  function getPreviousFocusable(container = document.body, options = {}) {
87
111
  return getRelativeFocusable(container, -1, options);
88
112
  }
89
- function isFocusable(element) {
113
+ function isFocusable(element, options = {}) {
90
114
  if (!(element instanceof Element)) {
91
115
  console.warn("Invalid element");
92
116
  return false;
93
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
+ }
94
127
  if (element.hasAttribute("hidden") || isInert(element)) {
95
128
  return false;
96
129
  }
97
- if (getTabIndex(element) < 0) {
130
+ if (!skipNegativeTabIndexCheck && getTabIndex(element) < 0) {
98
131
  return false;
99
132
  }
100
- if (!element.matches(FOCUSABLE_SELECTOR)) {
133
+ if (!element.matches(
134
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
135
+ )) {
101
136
  return false;
102
137
  }
103
138
  if (isDisabledDeep(element)) {
104
139
  return false;
105
140
  }
106
- if (!element.checkVisibility({
141
+ if (!skipVisibilityCheck && !element.checkVisibility({
107
142
  contentVisibilityAuto: true,
108
143
  opacityProperty: true,
109
144
  visibilityProperty: true
@@ -122,6 +157,8 @@ function getRelativeFocusable(container, offset, options) {
122
157
  composed = false,
123
158
  filter,
124
159
  include,
160
+ skipNegativeTabIndexCheck = false,
161
+ skipVisibilityCheck = false,
125
162
  wrap = false
126
163
  } = options;
127
164
  if (!(anchor instanceof Element)) {
@@ -139,22 +176,41 @@ function getRelativeFocusable(container, offset, options) {
139
176
  return null;
140
177
  }
141
178
  if (typeof composed !== "boolean") {
142
- console.warn("Invalid composed. Fallback: false.");
179
+ console.warn("Invalid composed option. Fallback: false.");
143
180
  composed = false;
144
181
  }
145
182
  if (typeof filter !== "undefined" && typeof filter !== "function") {
146
- console.warn("Invalid filter. Fallback: no filter function (undefined).");
183
+ console.warn(
184
+ "Invalid filter function. Fallback: no filter function (undefined)."
185
+ );
147
186
  filter = void 0;
148
187
  }
149
188
  if (typeof include !== "undefined" && typeof include !== "function") {
150
- console.warn("Invalid include. Fallback: no include function (undefined).");
189
+ console.warn(
190
+ "Invalid include function. Fallback: no include function (undefined)."
191
+ );
151
192
  include = void 0;
152
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
+ }
153
202
  if (typeof wrap !== "boolean") {
154
- console.warn("Invalid wrap. Fallback: false.");
203
+ console.warn("Invalid wrap option. Fallback: false.");
155
204
  wrap = false;
156
205
  }
157
- 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);
158
214
  const { length } = focusables;
159
215
  if (!length) {
160
216
  return null;
@@ -392,19 +448,21 @@ var Portal = class {
392
448
  if (current === this.#entranceSentinel) {
393
449
  if (this.#host.contains(before)) {
394
450
  this.#moveFocus("previous");
395
- } else {
396
- this.#update();
397
- const first = [...this.#focusables][0];
398
- first && focusElement(first);
451
+ return;
399
452
  }
400
- } 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) {
401
459
  if (this.#host.contains(before)) {
402
460
  this.#moveFocus("next");
403
- } else {
404
- this.#update();
405
- const last = [...this.#focusables].at(-1);
406
- last && focusElement(last);
461
+ return;
407
462
  }
463
+ this.#update();
464
+ const last = [...this.#focusables].at(-1);
465
+ last && focusElement(last);
408
466
  }
409
467
  };
410
468
  #onKeyDown = (event) => {
@@ -443,9 +501,7 @@ var Portal = class {
443
501
  if (current.has(focusable)) {
444
502
  continue;
445
503
  }
446
- if (focusable.isConnected) {
447
- restoreAttributes([focusable]);
448
- }
504
+ focusable.isConnected && restoreAttributes([focusable]);
449
505
  this.#focusables.delete(focusable);
450
506
  }
451
507
  for (const focusable of current) {
@@ -510,7 +566,7 @@ function getActiveElement2() {
510
566
  * Lightweight DOM portal (teleport) utility with fully focus management.
511
567
  * Designed for accessible dialogs, menus, overlays, popovers.
512
568
  *
513
- * @version 1.2.7
569
+ * @version 1.2.9
514
570
  * @author Yusuke Kamiyamane
515
571
  * @license MIT
516
572
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -522,7 +578,7 @@ function getActiveElement2() {
522
578
  (**
523
579
  * Attributes Utils
524
580
  *
525
- * @version 1.0.5
581
+ * @version 1.1.0
526
582
  * @author Yusuke Kamiyamane
527
583
  * @license MIT
528
584
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -535,7 +591,7 @@ power-focusable/dist/index.js:
535
591
  * High-precision focus management utility with full composed tree support.
536
592
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
537
593
  *
538
- * @version 4.1.7
594
+ * @version 4.3.1
539
595
  * @author Yusuke Kamiyamane
540
596
  * @license MIT
541
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.7
6
+ * @version 1.2.9
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.7
6
+ * @version 1.2.9
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.js CHANGED
@@ -32,24 +32,45 @@ 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
- console.warn("Invalid composed. Fallback: false.");
43
+ console.warn("Invalid composed option. Fallback: false.");
38
44
  composed = false;
39
45
  }
40
46
  if (typeof filter !== "undefined" && typeof filter !== "function") {
41
- console.warn("Invalid filter. Fallback: no filter function (undefined).");
47
+ console.warn(
48
+ "Invalid filter function. Fallback: no filter function (undefined)."
49
+ );
42
50
  filter = void 0;
43
51
  }
44
52
  if (typeof include !== "undefined" && typeof include !== "function") {
45
- console.warn("Invalid include. Fallback: no include function (undefined).");
53
+ console.warn(
54
+ "Invalid include function. Fallback: no include function (undefined)."
55
+ );
46
56
  include = void 0;
47
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
+ }
48
66
  const elements = [];
49
67
  if (composed || include) {
50
68
  let traverse2 = function(node) {
51
69
  if (node instanceof Element) {
52
- if (isFocusable(node) || include?.(node)) {
70
+ if (isFocusable(node, {
71
+ skipNegativeTabIndexCheck,
72
+ skipVisibilityCheck
73
+ }) || include?.(node)) {
53
74
  elements[elements.length] = node;
54
75
  }
55
76
  }
@@ -70,7 +91,10 @@ function getFocusables(container = document.body, options = {}) {
70
91
  if (!(candidate instanceof Element)) {
71
92
  continue;
72
93
  }
73
- if (isFocusable(candidate)) {
94
+ if (isFocusable(candidate, {
95
+ skipNegativeTabIndexCheck,
96
+ skipVisibilityCheck
97
+ })) {
74
98
  elements[elements.length] = candidate;
75
99
  }
76
100
  }
@@ -84,24 +108,35 @@ function getNextFocusable(container = document.body, options = {}) {
84
108
  function getPreviousFocusable(container = document.body, options = {}) {
85
109
  return getRelativeFocusable(container, -1, options);
86
110
  }
87
- function isFocusable(element) {
111
+ function isFocusable(element, options = {}) {
88
112
  if (!(element instanceof Element)) {
89
113
  console.warn("Invalid element");
90
114
  return false;
91
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
+ }
92
125
  if (element.hasAttribute("hidden") || isInert(element)) {
93
126
  return false;
94
127
  }
95
- if (getTabIndex(element) < 0) {
128
+ if (!skipNegativeTabIndexCheck && getTabIndex(element) < 0) {
96
129
  return false;
97
130
  }
98
- if (!element.matches(FOCUSABLE_SELECTOR)) {
131
+ if (!element.matches(
132
+ skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR.replace(/(,\s*)?\[tabindex="-1"\]/g, "") : FOCUSABLE_SELECTOR
133
+ )) {
99
134
  return false;
100
135
  }
101
136
  if (isDisabledDeep(element)) {
102
137
  return false;
103
138
  }
104
- if (!element.checkVisibility({
139
+ if (!skipVisibilityCheck && !element.checkVisibility({
105
140
  contentVisibilityAuto: true,
106
141
  opacityProperty: true,
107
142
  visibilityProperty: true
@@ -120,6 +155,8 @@ function getRelativeFocusable(container, offset, options) {
120
155
  composed = false,
121
156
  filter,
122
157
  include,
158
+ skipNegativeTabIndexCheck = false,
159
+ skipVisibilityCheck = false,
123
160
  wrap = false
124
161
  } = options;
125
162
  if (!(anchor instanceof Element)) {
@@ -137,22 +174,41 @@ function getRelativeFocusable(container, offset, options) {
137
174
  return null;
138
175
  }
139
176
  if (typeof composed !== "boolean") {
140
- console.warn("Invalid composed. Fallback: false.");
177
+ console.warn("Invalid composed option. Fallback: false.");
141
178
  composed = false;
142
179
  }
143
180
  if (typeof filter !== "undefined" && typeof filter !== "function") {
144
- console.warn("Invalid filter. Fallback: no filter function (undefined).");
181
+ console.warn(
182
+ "Invalid filter function. Fallback: no filter function (undefined)."
183
+ );
145
184
  filter = void 0;
146
185
  }
147
186
  if (typeof include !== "undefined" && typeof include !== "function") {
148
- console.warn("Invalid include. Fallback: no include function (undefined).");
187
+ console.warn(
188
+ "Invalid include function. Fallback: no include function (undefined)."
189
+ );
149
190
  include = void 0;
150
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
+ }
151
200
  if (typeof wrap !== "boolean") {
152
- console.warn("Invalid wrap. Fallback: false.");
201
+ console.warn("Invalid wrap option. Fallback: false.");
153
202
  wrap = false;
154
203
  }
155
- 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);
156
212
  const { length } = focusables;
157
213
  if (!length) {
158
214
  return null;
@@ -390,19 +446,21 @@ var Portal = class {
390
446
  if (current === this.#entranceSentinel) {
391
447
  if (this.#host.contains(before)) {
392
448
  this.#moveFocus("previous");
393
- } else {
394
- this.#update();
395
- const first = [...this.#focusables][0];
396
- first && focusElement(first);
449
+ return;
397
450
  }
398
- } 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) {
399
457
  if (this.#host.contains(before)) {
400
458
  this.#moveFocus("next");
401
- } else {
402
- this.#update();
403
- const last = [...this.#focusables].at(-1);
404
- last && focusElement(last);
459
+ return;
405
460
  }
461
+ this.#update();
462
+ const last = [...this.#focusables].at(-1);
463
+ last && focusElement(last);
406
464
  }
407
465
  };
408
466
  #onKeyDown = (event) => {
@@ -441,9 +499,7 @@ var Portal = class {
441
499
  if (current.has(focusable)) {
442
500
  continue;
443
501
  }
444
- if (focusable.isConnected) {
445
- restoreAttributes([focusable]);
446
- }
502
+ focusable.isConnected && restoreAttributes([focusable]);
447
503
  this.#focusables.delete(focusable);
448
504
  }
449
505
  for (const focusable of current) {
@@ -508,7 +564,7 @@ function getActiveElement2() {
508
564
  * Lightweight DOM portal (teleport) utility with fully focus management.
509
565
  * Designed for accessible dialogs, menus, overlays, popovers.
510
566
  *
511
- * @version 1.2.7
567
+ * @version 1.2.9
512
568
  * @author Yusuke Kamiyamane
513
569
  * @license MIT
514
570
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -520,7 +576,7 @@ function getActiveElement2() {
520
576
  (**
521
577
  * Attributes Utils
522
578
  *
523
- * @version 1.0.5
579
+ * @version 1.1.0
524
580
  * @author Yusuke Kamiyamane
525
581
  * @license MIT
526
582
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -533,7 +589,7 @@ power-focusable/dist/index.js:
533
589
  * High-precision focus management utility with full composed tree support.
534
590
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
535
591
  *
536
- * @version 4.1.7
592
+ * @version 4.3.1
537
593
  * @author Yusuke Kamiyamane
538
594
  * @license MIT
539
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.7",
3
+ "version": "1.2.9",
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.0",
51
51
  "bun-types": "latest",
52
- "power-focusable": "^4.1.7",
52
+ "power-focusable": "^4.3.1",
53
53
  "tsup": "^8.0.0",
54
54
  "typescript": "^5.6.0"
55
55
  },