@y14e/portal 1.2.31 → 1.3.0
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 +13 -4
- package/dist/index.bundle.cjs +92 -51
- package/dist/index.bundle.js +92 -51
- package/dist/index.cjs +16 -7
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +16 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@ npm i @y14e/portal
|
|
|
13
13
|
import { createPortal } from '@y14e/portal';
|
|
14
14
|
|
|
15
15
|
// CDNs
|
|
16
|
-
import { createPortal } from 'https://esm.sh/@y14e/portal@1.
|
|
16
|
+
import { createPortal } from 'https://esm.sh/@y14e/portal@1.3.0';
|
|
17
17
|
// or
|
|
18
|
-
import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal@1.
|
|
18
|
+
import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal@1.3.0/+esm';
|
|
19
19
|
// or
|
|
20
|
-
import { createPortal } from 'https://esm.unpkg.com/@y14e/portal@1.
|
|
20
|
+
import { createPortal } from 'https://esm.unpkg.com/@y14e/portal@1.3.0';
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## 📦 APIs
|
|
@@ -27,11 +27,20 @@ import { createPortal } from 'https://esm.unpkg.com/@y14e/portal@1.2.31';
|
|
|
27
27
|
Creates a portal and preserves keyboard focus order between the original DOM and the portal.
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
|
-
const cleanup = createPortal(host, container);
|
|
30
|
+
const cleanup = createPortal(host, container, options);
|
|
31
31
|
// => () => void
|
|
32
32
|
//
|
|
33
33
|
// host: Element
|
|
34
34
|
// container (optional): Element (default: <body>)
|
|
35
|
+
// options (optional): PortalOptions
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 🪄 Options
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
interface PortalOptions {
|
|
42
|
+
noInlineStyle: boolean; // default: false
|
|
43
|
+
}
|
|
35
44
|
```
|
|
36
45
|
|
|
37
46
|
## Demo
|
package/dist/index.bundle.cjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// node_modules/@y14e/attributes-utils/dist/index.js
|
|
4
4
|
var snapshots = /* @__PURE__ */ new WeakMap();
|
|
5
|
-
function restoreAttributes(
|
|
6
|
-
for (const element of
|
|
5
|
+
function restoreAttributes(element_or_elements) {
|
|
6
|
+
for (const element of Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]) {
|
|
7
7
|
const snapshot = snapshots.get(element);
|
|
8
8
|
if (!snapshot) {
|
|
9
9
|
continue;
|
|
@@ -14,8 +14,9 @@ function restoreAttributes(elements) {
|
|
|
14
14
|
snapshots.delete(element);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
function saveAttributes(
|
|
18
|
-
|
|
17
|
+
function saveAttributes(element_or_elements, attribute_or_attributes) {
|
|
18
|
+
const attributes = Array.isArray(attribute_or_attributes) ? attribute_or_attributes : [attribute_or_attributes];
|
|
19
|
+
(Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]).forEach((element) => {
|
|
19
20
|
let snapshot = snapshots.get(element);
|
|
20
21
|
if (!snapshot) {
|
|
21
22
|
snapshot = /* @__PURE__ */ new Map();
|
|
@@ -29,6 +30,10 @@ function saveAttributes(elements, attributes) {
|
|
|
29
30
|
|
|
30
31
|
// node_modules/power-focusable/dist/index.js
|
|
31
32
|
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"])`;
|
|
33
|
+
var FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX = FOCUSABLE_SELECTOR.replace(
|
|
34
|
+
/(,\s*)?\[tabindex="-1"\]/g,
|
|
35
|
+
""
|
|
36
|
+
);
|
|
32
37
|
function getFocusables(container = document.body, options = {}) {
|
|
33
38
|
if (!(container instanceof Element)) {
|
|
34
39
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
@@ -65,36 +70,43 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
65
70
|
console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
|
|
66
71
|
skipVisibilityCheck = false;
|
|
67
72
|
}
|
|
68
|
-
const
|
|
73
|
+
const candidates = [];
|
|
69
74
|
if (composed || include) {
|
|
70
75
|
let traverse2 = function(node) {
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
if (isFocusable(node, {
|
|
77
|
+
skipNegativeTabIndexCheck,
|
|
78
|
+
skipVisibilityCheck
|
|
79
|
+
}) || include?.(node)) {
|
|
80
|
+
candidates[candidates.length] = node;
|
|
76
81
|
}
|
|
77
|
-
const
|
|
78
|
-
for (let i = 0, l =
|
|
79
|
-
const child =
|
|
82
|
+
const children2 = composed ? getComposedChildren(node) : getChildren(node);
|
|
83
|
+
for (let i = 0, l = children2.length; i < l; i++) {
|
|
84
|
+
const child = children2[i];
|
|
80
85
|
child && traverse2(child);
|
|
81
86
|
}
|
|
82
87
|
};
|
|
83
|
-
|
|
88
|
+
const children = composed ? getComposedChildren(container) : getChildren(container);
|
|
89
|
+
for (let i = 0, l = children.length; i < l; i++) {
|
|
90
|
+
const child = children[i];
|
|
91
|
+
child && traverse2(child);
|
|
92
|
+
}
|
|
84
93
|
} else {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
const matches = container.querySelectorAll(
|
|
95
|
+
skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX : FOCUSABLE_SELECTOR
|
|
96
|
+
);
|
|
97
|
+
for (let i = 0, l = matches.length; i < l; i++) {
|
|
98
|
+
const matched = matches[i];
|
|
99
|
+
if (matched && isFocusable(matched, {
|
|
89
100
|
skipNegativeTabIndexCheck,
|
|
90
101
|
skipVisibilityCheck
|
|
91
102
|
})) {
|
|
92
|
-
|
|
103
|
+
candidates[candidates.length] = matched;
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
return normalizeRadioGroup(
|
|
108
|
+
sortByTabIndex(filter ? candidates.filter(filter) : candidates)
|
|
109
|
+
);
|
|
98
110
|
}
|
|
99
111
|
function getNextFocusable(container = document.body, options = {}) {
|
|
100
112
|
return getRelativeFocusable(container, 1, options);
|
|
@@ -123,7 +135,7 @@ function isFocusable(element, options = {}) {
|
|
|
123
135
|
return false;
|
|
124
136
|
}
|
|
125
137
|
if (!element.matches(
|
|
126
|
-
skipNegativeTabIndexCheck ?
|
|
138
|
+
skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX : FOCUSABLE_SELECTOR
|
|
127
139
|
)) {
|
|
128
140
|
return false;
|
|
129
141
|
}
|
|
@@ -195,23 +207,23 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
195
207
|
console.warn("Invalid wrap option. Fallback: false.");
|
|
196
208
|
wrap = false;
|
|
197
209
|
}
|
|
198
|
-
const settings = {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
210
|
+
const settings = {
|
|
211
|
+
composed,
|
|
212
|
+
filter,
|
|
213
|
+
include,
|
|
214
|
+
skipNegativeTabIndexCheck,
|
|
215
|
+
skipVisibilityCheck
|
|
216
|
+
};
|
|
205
217
|
const focusables = getFocusables(container, settings);
|
|
206
218
|
const { length } = focusables;
|
|
207
219
|
if (!length) {
|
|
208
220
|
return null;
|
|
209
221
|
}
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
222
|
+
const anchorIndex = focusables.indexOf(anchor);
|
|
223
|
+
if (anchorIndex === -1) {
|
|
212
224
|
return null;
|
|
213
225
|
}
|
|
214
|
-
const offsetIndex =
|
|
226
|
+
const offsetIndex = anchorIndex + offset;
|
|
215
227
|
if ((offsetIndex < 0 || offsetIndex >= length) && !wrap) {
|
|
216
228
|
return null;
|
|
217
229
|
}
|
|
@@ -259,22 +271,42 @@ function normalizeRadioGroup(elements) {
|
|
|
259
271
|
if (!map) {
|
|
260
272
|
map = /* @__PURE__ */ new Map();
|
|
261
273
|
}
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
274
|
+
const root = element.getRootNode();
|
|
275
|
+
let value = map.get(root);
|
|
276
|
+
if (!value) {
|
|
277
|
+
value = /* @__PURE__ */ new Map();
|
|
278
|
+
map.set(root, value);
|
|
279
|
+
}
|
|
280
|
+
let v = value.get(element.form);
|
|
281
|
+
if (!v) {
|
|
282
|
+
v = /* @__PURE__ */ new Map();
|
|
283
|
+
value.set(element.form, v);
|
|
284
|
+
}
|
|
285
|
+
let vv = v.get(element.name);
|
|
286
|
+
if (!vv) {
|
|
287
|
+
vv = [];
|
|
288
|
+
v.set(element.name, vv);
|
|
266
289
|
}
|
|
290
|
+
vv[vv.length] = element;
|
|
267
291
|
}
|
|
268
292
|
if (!map) {
|
|
269
293
|
return elements;
|
|
270
294
|
}
|
|
271
295
|
const placeholder = /* @__PURE__ */ new Set();
|
|
272
|
-
for (const
|
|
273
|
-
|
|
296
|
+
for (const value of map.values()) {
|
|
297
|
+
for (const v of value.values()) {
|
|
298
|
+
for (const vv of v.values()) {
|
|
299
|
+
const radio = vv.find((element) => element.checked) ?? vv[0];
|
|
300
|
+
radio && placeholder.add(radio);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
274
303
|
}
|
|
275
|
-
return elements.filter(
|
|
276
|
-
|
|
277
|
-
|
|
304
|
+
return elements.filter((element) => {
|
|
305
|
+
if (!(element instanceof HTMLInputElement)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return !isUngroupedRadio(element) || placeholder.has(element);
|
|
309
|
+
});
|
|
278
310
|
}
|
|
279
311
|
function sortByTabIndex(elements) {
|
|
280
312
|
const ordered = [];
|
|
@@ -357,12 +389,12 @@ function isInert(element) {
|
|
|
357
389
|
return "inert" in element && !!element.inert;
|
|
358
390
|
}
|
|
359
391
|
function isUngroupedRadio(element) {
|
|
360
|
-
return element
|
|
392
|
+
return element.type === "radio" && !!element.name;
|
|
361
393
|
}
|
|
362
394
|
|
|
363
395
|
// src/index.ts
|
|
364
396
|
var VISUALLY_HIDDEN_CSS = `border: 0; clip: rect(0, 0, 0, 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; user-select: none; white-space: nowrap; width: 1px;`;
|
|
365
|
-
function createPortal(host, container = document.body) {
|
|
397
|
+
function createPortal(host, container = document.body, options = {}) {
|
|
366
398
|
if (!(host instanceof Element)) {
|
|
367
399
|
console.warn("Invalid host element");
|
|
368
400
|
return () => {
|
|
@@ -378,24 +410,31 @@ function createPortal(host, container = document.body) {
|
|
|
378
410
|
return () => {
|
|
379
411
|
};
|
|
380
412
|
}
|
|
381
|
-
const portal = new Portal(host, container);
|
|
413
|
+
const portal = new Portal(host, container, options);
|
|
382
414
|
return () => portal.destroy();
|
|
383
415
|
}
|
|
384
416
|
var Portal = class {
|
|
385
417
|
#host;
|
|
386
418
|
#container;
|
|
419
|
+
#settings;
|
|
387
420
|
#entranceSentinel;
|
|
388
421
|
#exitSentinel;
|
|
389
422
|
#focusables = /* @__PURE__ */ new Set();
|
|
390
423
|
#controller = null;
|
|
391
424
|
#isDestroyed = false;
|
|
392
|
-
constructor(host, container) {
|
|
425
|
+
constructor(host, container, options = {}) {
|
|
393
426
|
this.#host = host;
|
|
394
427
|
if (!(container instanceof Element)) {
|
|
395
428
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
396
429
|
container = document.body;
|
|
397
430
|
}
|
|
398
431
|
this.#container = container;
|
|
432
|
+
let { noInlineStyle = false } = options;
|
|
433
|
+
if (typeof noInlineStyle !== "boolean") {
|
|
434
|
+
console.warn("Invalid noInlineStyle option. Fallback: false.");
|
|
435
|
+
noInlineStyle = false;
|
|
436
|
+
}
|
|
437
|
+
this.#settings = { noInlineStyle };
|
|
399
438
|
this.#entranceSentinel = this.#createSentinel();
|
|
400
439
|
this.#exitSentinel = this.#createSentinel();
|
|
401
440
|
this.#initialize();
|
|
@@ -483,14 +522,14 @@ var Portal = class {
|
|
|
483
522
|
]);
|
|
484
523
|
for (const focusable of this.#focusables) {
|
|
485
524
|
if (!current.has(focusable)) {
|
|
486
|
-
|
|
525
|
+
restoreAttributes(focusable);
|
|
487
526
|
this.#focusables.delete(focusable);
|
|
488
527
|
}
|
|
489
528
|
}
|
|
490
529
|
for (const focusable of current) {
|
|
491
530
|
if (!this.#focusables.has(focusable)) {
|
|
492
531
|
this.#focusables.add(focusable);
|
|
493
|
-
saveAttributes(
|
|
532
|
+
saveAttributes(focusable, "tabindex");
|
|
494
533
|
focusable.setAttribute("tabindex", "-1");
|
|
495
534
|
}
|
|
496
535
|
}
|
|
@@ -500,7 +539,9 @@ var Portal = class {
|
|
|
500
539
|
sentinel.setAttribute("aria-hidden", "true");
|
|
501
540
|
sentinel.setAttribute("data-portal-sentinel", "");
|
|
502
541
|
sentinel.setAttribute("tabindex", "0");
|
|
503
|
-
|
|
542
|
+
if (!this.#settings.noInlineStyle) {
|
|
543
|
+
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
544
|
+
}
|
|
504
545
|
return sentinel;
|
|
505
546
|
}
|
|
506
547
|
#focusSentinel(isPrevious) {
|
|
@@ -536,7 +577,7 @@ function containsComposed2(container, element) {
|
|
|
536
577
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
537
578
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
538
579
|
*
|
|
539
|
-
* @version 1.
|
|
580
|
+
* @version 1.3.0
|
|
540
581
|
* @author Yusuke Kamiyamane
|
|
541
582
|
* @license MIT
|
|
542
583
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -548,7 +589,7 @@ function containsComposed2(container, element) {
|
|
|
548
589
|
(**
|
|
549
590
|
* Attributes Utils
|
|
550
591
|
*
|
|
551
|
-
* @version 1.1.
|
|
592
|
+
* @version 1.1.4
|
|
552
593
|
* @author Yusuke Kamiyamane
|
|
553
594
|
* @license MIT
|
|
554
595
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -561,7 +602,7 @@ power-focusable/dist/index.js:
|
|
|
561
602
|
* High-precision focus management utility with full composed tree support.
|
|
562
603
|
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
563
604
|
*
|
|
564
|
-
* @version 4.3.
|
|
605
|
+
* @version 4.3.21
|
|
565
606
|
* @author Yusuke Kamiyamane
|
|
566
607
|
* @license MIT
|
|
567
608
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.bundle.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// node_modules/@y14e/attributes-utils/dist/index.js
|
|
2
2
|
var snapshots = /* @__PURE__ */ new WeakMap();
|
|
3
|
-
function restoreAttributes(
|
|
4
|
-
for (const element of
|
|
3
|
+
function restoreAttributes(element_or_elements) {
|
|
4
|
+
for (const element of Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]) {
|
|
5
5
|
const snapshot = snapshots.get(element);
|
|
6
6
|
if (!snapshot) {
|
|
7
7
|
continue;
|
|
@@ -12,8 +12,9 @@ function restoreAttributes(elements) {
|
|
|
12
12
|
snapshots.delete(element);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
function saveAttributes(
|
|
16
|
-
|
|
15
|
+
function saveAttributes(element_or_elements, attribute_or_attributes) {
|
|
16
|
+
const attributes = Array.isArray(attribute_or_attributes) ? attribute_or_attributes : [attribute_or_attributes];
|
|
17
|
+
(Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]).forEach((element) => {
|
|
17
18
|
let snapshot = snapshots.get(element);
|
|
18
19
|
if (!snapshot) {
|
|
19
20
|
snapshot = /* @__PURE__ */ new Map();
|
|
@@ -27,6 +28,10 @@ function saveAttributes(elements, attributes) {
|
|
|
27
28
|
|
|
28
29
|
// node_modules/power-focusable/dist/index.js
|
|
29
30
|
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"])`;
|
|
31
|
+
var FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX = FOCUSABLE_SELECTOR.replace(
|
|
32
|
+
/(,\s*)?\[tabindex="-1"\]/g,
|
|
33
|
+
""
|
|
34
|
+
);
|
|
30
35
|
function getFocusables(container = document.body, options = {}) {
|
|
31
36
|
if (!(container instanceof Element)) {
|
|
32
37
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
@@ -63,36 +68,43 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
63
68
|
console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
|
|
64
69
|
skipVisibilityCheck = false;
|
|
65
70
|
}
|
|
66
|
-
const
|
|
71
|
+
const candidates = [];
|
|
67
72
|
if (composed || include) {
|
|
68
73
|
let traverse2 = function(node) {
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
if (isFocusable(node, {
|
|
75
|
+
skipNegativeTabIndexCheck,
|
|
76
|
+
skipVisibilityCheck
|
|
77
|
+
}) || include?.(node)) {
|
|
78
|
+
candidates[candidates.length] = node;
|
|
74
79
|
}
|
|
75
|
-
const
|
|
76
|
-
for (let i = 0, l =
|
|
77
|
-
const child =
|
|
80
|
+
const children2 = composed ? getComposedChildren(node) : getChildren(node);
|
|
81
|
+
for (let i = 0, l = children2.length; i < l; i++) {
|
|
82
|
+
const child = children2[i];
|
|
78
83
|
child && traverse2(child);
|
|
79
84
|
}
|
|
80
85
|
};
|
|
81
|
-
|
|
86
|
+
const children = composed ? getComposedChildren(container) : getChildren(container);
|
|
87
|
+
for (let i = 0, l = children.length; i < l; i++) {
|
|
88
|
+
const child = children[i];
|
|
89
|
+
child && traverse2(child);
|
|
90
|
+
}
|
|
82
91
|
} else {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
const matches = container.querySelectorAll(
|
|
93
|
+
skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX : FOCUSABLE_SELECTOR
|
|
94
|
+
);
|
|
95
|
+
for (let i = 0, l = matches.length; i < l; i++) {
|
|
96
|
+
const matched = matches[i];
|
|
97
|
+
if (matched && isFocusable(matched, {
|
|
87
98
|
skipNegativeTabIndexCheck,
|
|
88
99
|
skipVisibilityCheck
|
|
89
100
|
})) {
|
|
90
|
-
|
|
101
|
+
candidates[candidates.length] = matched;
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
return normalizeRadioGroup(
|
|
106
|
+
sortByTabIndex(filter ? candidates.filter(filter) : candidates)
|
|
107
|
+
);
|
|
96
108
|
}
|
|
97
109
|
function getNextFocusable(container = document.body, options = {}) {
|
|
98
110
|
return getRelativeFocusable(container, 1, options);
|
|
@@ -121,7 +133,7 @@ function isFocusable(element, options = {}) {
|
|
|
121
133
|
return false;
|
|
122
134
|
}
|
|
123
135
|
if (!element.matches(
|
|
124
|
-
skipNegativeTabIndexCheck ?
|
|
136
|
+
skipNegativeTabIndexCheck ? FOCUSABLE_SELECTOR_WITH_NEGATIVE_TABINDEX : FOCUSABLE_SELECTOR
|
|
125
137
|
)) {
|
|
126
138
|
return false;
|
|
127
139
|
}
|
|
@@ -193,23 +205,23 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
193
205
|
console.warn("Invalid wrap option. Fallback: false.");
|
|
194
206
|
wrap = false;
|
|
195
207
|
}
|
|
196
|
-
const settings = {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
208
|
+
const settings = {
|
|
209
|
+
composed,
|
|
210
|
+
filter,
|
|
211
|
+
include,
|
|
212
|
+
skipNegativeTabIndexCheck,
|
|
213
|
+
skipVisibilityCheck
|
|
214
|
+
};
|
|
203
215
|
const focusables = getFocusables(container, settings);
|
|
204
216
|
const { length } = focusables;
|
|
205
217
|
if (!length) {
|
|
206
218
|
return null;
|
|
207
219
|
}
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
220
|
+
const anchorIndex = focusables.indexOf(anchor);
|
|
221
|
+
if (anchorIndex === -1) {
|
|
210
222
|
return null;
|
|
211
223
|
}
|
|
212
|
-
const offsetIndex =
|
|
224
|
+
const offsetIndex = anchorIndex + offset;
|
|
213
225
|
if ((offsetIndex < 0 || offsetIndex >= length) && !wrap) {
|
|
214
226
|
return null;
|
|
215
227
|
}
|
|
@@ -257,22 +269,42 @@ function normalizeRadioGroup(elements) {
|
|
|
257
269
|
if (!map) {
|
|
258
270
|
map = /* @__PURE__ */ new Map();
|
|
259
271
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
if (
|
|
263
|
-
|
|
272
|
+
const root = element.getRootNode();
|
|
273
|
+
let value = map.get(root);
|
|
274
|
+
if (!value) {
|
|
275
|
+
value = /* @__PURE__ */ new Map();
|
|
276
|
+
map.set(root, value);
|
|
277
|
+
}
|
|
278
|
+
let v = value.get(element.form);
|
|
279
|
+
if (!v) {
|
|
280
|
+
v = /* @__PURE__ */ new Map();
|
|
281
|
+
value.set(element.form, v);
|
|
282
|
+
}
|
|
283
|
+
let vv = v.get(element.name);
|
|
284
|
+
if (!vv) {
|
|
285
|
+
vv = [];
|
|
286
|
+
v.set(element.name, vv);
|
|
264
287
|
}
|
|
288
|
+
vv[vv.length] = element;
|
|
265
289
|
}
|
|
266
290
|
if (!map) {
|
|
267
291
|
return elements;
|
|
268
292
|
}
|
|
269
293
|
const placeholder = /* @__PURE__ */ new Set();
|
|
270
|
-
for (const
|
|
271
|
-
|
|
294
|
+
for (const value of map.values()) {
|
|
295
|
+
for (const v of value.values()) {
|
|
296
|
+
for (const vv of v.values()) {
|
|
297
|
+
const radio = vv.find((element) => element.checked) ?? vv[0];
|
|
298
|
+
radio && placeholder.add(radio);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
272
301
|
}
|
|
273
|
-
return elements.filter(
|
|
274
|
-
|
|
275
|
-
|
|
302
|
+
return elements.filter((element) => {
|
|
303
|
+
if (!(element instanceof HTMLInputElement)) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
return !isUngroupedRadio(element) || placeholder.has(element);
|
|
307
|
+
});
|
|
276
308
|
}
|
|
277
309
|
function sortByTabIndex(elements) {
|
|
278
310
|
const ordered = [];
|
|
@@ -355,12 +387,12 @@ function isInert(element) {
|
|
|
355
387
|
return "inert" in element && !!element.inert;
|
|
356
388
|
}
|
|
357
389
|
function isUngroupedRadio(element) {
|
|
358
|
-
return element
|
|
390
|
+
return element.type === "radio" && !!element.name;
|
|
359
391
|
}
|
|
360
392
|
|
|
361
393
|
// src/index.ts
|
|
362
394
|
var VISUALLY_HIDDEN_CSS = `border: 0; clip: rect(0, 0, 0, 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; user-select: none; white-space: nowrap; width: 1px;`;
|
|
363
|
-
function createPortal(host, container = document.body) {
|
|
395
|
+
function createPortal(host, container = document.body, options = {}) {
|
|
364
396
|
if (!(host instanceof Element)) {
|
|
365
397
|
console.warn("Invalid host element");
|
|
366
398
|
return () => {
|
|
@@ -376,24 +408,31 @@ function createPortal(host, container = document.body) {
|
|
|
376
408
|
return () => {
|
|
377
409
|
};
|
|
378
410
|
}
|
|
379
|
-
const portal = new Portal(host, container);
|
|
411
|
+
const portal = new Portal(host, container, options);
|
|
380
412
|
return () => portal.destroy();
|
|
381
413
|
}
|
|
382
414
|
var Portal = class {
|
|
383
415
|
#host;
|
|
384
416
|
#container;
|
|
417
|
+
#settings;
|
|
385
418
|
#entranceSentinel;
|
|
386
419
|
#exitSentinel;
|
|
387
420
|
#focusables = /* @__PURE__ */ new Set();
|
|
388
421
|
#controller = null;
|
|
389
422
|
#isDestroyed = false;
|
|
390
|
-
constructor(host, container) {
|
|
423
|
+
constructor(host, container, options = {}) {
|
|
391
424
|
this.#host = host;
|
|
392
425
|
if (!(container instanceof Element)) {
|
|
393
426
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
394
427
|
container = document.body;
|
|
395
428
|
}
|
|
396
429
|
this.#container = container;
|
|
430
|
+
let { noInlineStyle = false } = options;
|
|
431
|
+
if (typeof noInlineStyle !== "boolean") {
|
|
432
|
+
console.warn("Invalid noInlineStyle option. Fallback: false.");
|
|
433
|
+
noInlineStyle = false;
|
|
434
|
+
}
|
|
435
|
+
this.#settings = { noInlineStyle };
|
|
397
436
|
this.#entranceSentinel = this.#createSentinel();
|
|
398
437
|
this.#exitSentinel = this.#createSentinel();
|
|
399
438
|
this.#initialize();
|
|
@@ -481,14 +520,14 @@ var Portal = class {
|
|
|
481
520
|
]);
|
|
482
521
|
for (const focusable of this.#focusables) {
|
|
483
522
|
if (!current.has(focusable)) {
|
|
484
|
-
|
|
523
|
+
restoreAttributes(focusable);
|
|
485
524
|
this.#focusables.delete(focusable);
|
|
486
525
|
}
|
|
487
526
|
}
|
|
488
527
|
for (const focusable of current) {
|
|
489
528
|
if (!this.#focusables.has(focusable)) {
|
|
490
529
|
this.#focusables.add(focusable);
|
|
491
|
-
saveAttributes(
|
|
530
|
+
saveAttributes(focusable, "tabindex");
|
|
492
531
|
focusable.setAttribute("tabindex", "-1");
|
|
493
532
|
}
|
|
494
533
|
}
|
|
@@ -498,7 +537,9 @@ var Portal = class {
|
|
|
498
537
|
sentinel.setAttribute("aria-hidden", "true");
|
|
499
538
|
sentinel.setAttribute("data-portal-sentinel", "");
|
|
500
539
|
sentinel.setAttribute("tabindex", "0");
|
|
501
|
-
|
|
540
|
+
if (!this.#settings.noInlineStyle) {
|
|
541
|
+
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
542
|
+
}
|
|
502
543
|
return sentinel;
|
|
503
544
|
}
|
|
504
545
|
#focusSentinel(isPrevious) {
|
|
@@ -534,7 +575,7 @@ function containsComposed2(container, element) {
|
|
|
534
575
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
535
576
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
536
577
|
*
|
|
537
|
-
* @version 1.
|
|
578
|
+
* @version 1.3.0
|
|
538
579
|
* @author Yusuke Kamiyamane
|
|
539
580
|
* @license MIT
|
|
540
581
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -546,7 +587,7 @@ function containsComposed2(container, element) {
|
|
|
546
587
|
(**
|
|
547
588
|
* Attributes Utils
|
|
548
589
|
*
|
|
549
|
-
* @version 1.1.
|
|
590
|
+
* @version 1.1.4
|
|
550
591
|
* @author Yusuke Kamiyamane
|
|
551
592
|
* @license MIT
|
|
552
593
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -559,7 +600,7 @@ power-focusable/dist/index.js:
|
|
|
559
600
|
* High-precision focus management utility with full composed tree support.
|
|
560
601
|
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
561
602
|
*
|
|
562
|
-
* @version 4.3.
|
|
603
|
+
* @version 4.3.21
|
|
563
604
|
* @author Yusuke Kamiyamane
|
|
564
605
|
* @license MIT
|
|
565
606
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.cjs
CHANGED
|
@@ -5,7 +5,7 @@ var powerFocusable = require('power-focusable');
|
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
7
|
var VISUALLY_HIDDEN_CSS = `border: 0; clip: rect(0, 0, 0, 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; user-select: none; white-space: nowrap; width: 1px;`;
|
|
8
|
-
function createPortal(host, container = document.body) {
|
|
8
|
+
function createPortal(host, container = document.body, options = {}) {
|
|
9
9
|
if (!(host instanceof Element)) {
|
|
10
10
|
console.warn("Invalid host element");
|
|
11
11
|
return () => {
|
|
@@ -21,24 +21,31 @@ function createPortal(host, container = document.body) {
|
|
|
21
21
|
return () => {
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
const portal = new Portal(host, container);
|
|
24
|
+
const portal = new Portal(host, container, options);
|
|
25
25
|
return () => portal.destroy();
|
|
26
26
|
}
|
|
27
27
|
var Portal = class {
|
|
28
28
|
#host;
|
|
29
29
|
#container;
|
|
30
|
+
#settings;
|
|
30
31
|
#entranceSentinel;
|
|
31
32
|
#exitSentinel;
|
|
32
33
|
#focusables = /* @__PURE__ */ new Set();
|
|
33
34
|
#controller = null;
|
|
34
35
|
#isDestroyed = false;
|
|
35
|
-
constructor(host, container) {
|
|
36
|
+
constructor(host, container, options = {}) {
|
|
36
37
|
this.#host = host;
|
|
37
38
|
if (!(container instanceof Element)) {
|
|
38
39
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
39
40
|
container = document.body;
|
|
40
41
|
}
|
|
41
42
|
this.#container = container;
|
|
43
|
+
let { noInlineStyle = false } = options;
|
|
44
|
+
if (typeof noInlineStyle !== "boolean") {
|
|
45
|
+
console.warn("Invalid noInlineStyle option. Fallback: false.");
|
|
46
|
+
noInlineStyle = false;
|
|
47
|
+
}
|
|
48
|
+
this.#settings = { noInlineStyle };
|
|
42
49
|
this.#entranceSentinel = this.#createSentinel();
|
|
43
50
|
this.#exitSentinel = this.#createSentinel();
|
|
44
51
|
this.#initialize();
|
|
@@ -126,14 +133,14 @@ var Portal = class {
|
|
|
126
133
|
]);
|
|
127
134
|
for (const focusable of this.#focusables) {
|
|
128
135
|
if (!current.has(focusable)) {
|
|
129
|
-
|
|
136
|
+
attributesUtils.restoreAttributes(focusable);
|
|
130
137
|
this.#focusables.delete(focusable);
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
for (const focusable of current) {
|
|
134
141
|
if (!this.#focusables.has(focusable)) {
|
|
135
142
|
this.#focusables.add(focusable);
|
|
136
|
-
attributesUtils.saveAttributes(
|
|
143
|
+
attributesUtils.saveAttributes(focusable, "tabindex");
|
|
137
144
|
focusable.setAttribute("tabindex", "-1");
|
|
138
145
|
}
|
|
139
146
|
}
|
|
@@ -143,7 +150,9 @@ var Portal = class {
|
|
|
143
150
|
sentinel.setAttribute("aria-hidden", "true");
|
|
144
151
|
sentinel.setAttribute("data-portal-sentinel", "");
|
|
145
152
|
sentinel.setAttribute("tabindex", "0");
|
|
146
|
-
|
|
153
|
+
if (!this.#settings.noInlineStyle) {
|
|
154
|
+
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
155
|
+
}
|
|
147
156
|
return sentinel;
|
|
148
157
|
}
|
|
149
158
|
#focusSentinel(isPrevious) {
|
|
@@ -179,7 +188,7 @@ function containsComposed(container, element) {
|
|
|
179
188
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
180
189
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
181
190
|
*
|
|
182
|
-
* @version 1.
|
|
191
|
+
* @version 1.3.0
|
|
183
192
|
* @author Yusuke Kamiyamane
|
|
184
193
|
* @license MIT
|
|
185
194
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.d.cts
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
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.
|
|
6
|
+
* @version 1.3.0
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
10
10
|
* @see {@link https://github.com/y14e/portal}
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
interface PortalOptions {
|
|
13
|
+
noInlineStyle: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function createPortal(host: Element, container?: HTMLElement, options?: Partial<PortalOptions>): () => void;
|
|
13
16
|
|
|
14
|
-
export { createPortal };
|
|
17
|
+
export { type PortalOptions, createPortal };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
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.
|
|
6
|
+
* @version 1.3.0
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
10
10
|
* @see {@link https://github.com/y14e/portal}
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
interface PortalOptions {
|
|
13
|
+
noInlineStyle: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function createPortal(host: Element, container?: HTMLElement, options?: Partial<PortalOptions>): () => void;
|
|
13
16
|
|
|
14
|
-
export { createPortal };
|
|
17
|
+
export { type PortalOptions, createPortal };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { focusElement, getActiveElement, getFocusables, getPreviousFocusable, ge
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
var VISUALLY_HIDDEN_CSS = `border: 0; clip: rect(0, 0, 0, 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; user-select: none; white-space: nowrap; width: 1px;`;
|
|
6
|
-
function createPortal(host, container = document.body) {
|
|
6
|
+
function createPortal(host, container = document.body, options = {}) {
|
|
7
7
|
if (!(host instanceof Element)) {
|
|
8
8
|
console.warn("Invalid host element");
|
|
9
9
|
return () => {
|
|
@@ -19,24 +19,31 @@ function createPortal(host, container = document.body) {
|
|
|
19
19
|
return () => {
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
-
const portal = new Portal(host, container);
|
|
22
|
+
const portal = new Portal(host, container, options);
|
|
23
23
|
return () => portal.destroy();
|
|
24
24
|
}
|
|
25
25
|
var Portal = class {
|
|
26
26
|
#host;
|
|
27
27
|
#container;
|
|
28
|
+
#settings;
|
|
28
29
|
#entranceSentinel;
|
|
29
30
|
#exitSentinel;
|
|
30
31
|
#focusables = /* @__PURE__ */ new Set();
|
|
31
32
|
#controller = null;
|
|
32
33
|
#isDestroyed = false;
|
|
33
|
-
constructor(host, container) {
|
|
34
|
+
constructor(host, container, options = {}) {
|
|
34
35
|
this.#host = host;
|
|
35
36
|
if (!(container instanceof Element)) {
|
|
36
37
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
37
38
|
container = document.body;
|
|
38
39
|
}
|
|
39
40
|
this.#container = container;
|
|
41
|
+
let { noInlineStyle = false } = options;
|
|
42
|
+
if (typeof noInlineStyle !== "boolean") {
|
|
43
|
+
console.warn("Invalid noInlineStyle option. Fallback: false.");
|
|
44
|
+
noInlineStyle = false;
|
|
45
|
+
}
|
|
46
|
+
this.#settings = { noInlineStyle };
|
|
40
47
|
this.#entranceSentinel = this.#createSentinel();
|
|
41
48
|
this.#exitSentinel = this.#createSentinel();
|
|
42
49
|
this.#initialize();
|
|
@@ -124,14 +131,14 @@ var Portal = class {
|
|
|
124
131
|
]);
|
|
125
132
|
for (const focusable of this.#focusables) {
|
|
126
133
|
if (!current.has(focusable)) {
|
|
127
|
-
|
|
134
|
+
restoreAttributes(focusable);
|
|
128
135
|
this.#focusables.delete(focusable);
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
for (const focusable of current) {
|
|
132
139
|
if (!this.#focusables.has(focusable)) {
|
|
133
140
|
this.#focusables.add(focusable);
|
|
134
|
-
saveAttributes(
|
|
141
|
+
saveAttributes(focusable, "tabindex");
|
|
135
142
|
focusable.setAttribute("tabindex", "-1");
|
|
136
143
|
}
|
|
137
144
|
}
|
|
@@ -141,7 +148,9 @@ var Portal = class {
|
|
|
141
148
|
sentinel.setAttribute("aria-hidden", "true");
|
|
142
149
|
sentinel.setAttribute("data-portal-sentinel", "");
|
|
143
150
|
sentinel.setAttribute("tabindex", "0");
|
|
144
|
-
|
|
151
|
+
if (!this.#settings.noInlineStyle) {
|
|
152
|
+
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
153
|
+
}
|
|
145
154
|
return sentinel;
|
|
146
155
|
}
|
|
147
156
|
#focusSentinel(isPrevious) {
|
|
@@ -177,7 +186,7 @@ function containsComposed(container, element) {
|
|
|
177
186
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
178
187
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
179
188
|
*
|
|
180
|
-
* @version 1.
|
|
189
|
+
* @version 1.3.0
|
|
181
190
|
* @author Yusuke Kamiyamane
|
|
182
191
|
* @license MIT
|
|
183
192
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@y14e/portal",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Lightweight DOM portal (teleport) utility with fully focus management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"node": ">=18"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@y14e/attributes-utils": "^1.1.
|
|
59
|
-
"power-focusable": "^4.3.
|
|
58
|
+
"@y14e/attributes-utils": "^1.1.4",
|
|
59
|
+
"power-focusable": "^4.3.21"
|
|
60
60
|
}
|
|
61
61
|
}
|