@y14e/portal 1.2.32 → 1.3.1
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 +27 -18
- package/dist/index.bundle.js +27 -18
- package/dist/index.cjs +49 -19
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +28 -19
- 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.1';
|
|
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.1/+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.1';
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## 📦 APIs
|
|
@@ -27,11 +27,20 @@ import { createPortal } from 'https://esm.unpkg.com/@y14e/portal@1.2.32';
|
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// node_modules/@y14e/
|
|
3
|
+
// node_modules/@y14e/attribute-util/dist/index.js
|
|
4
4
|
var snapshots = /* @__PURE__ */ new WeakMap();
|
|
5
5
|
function restoreAttributes(element_or_elements) {
|
|
6
6
|
for (const element of Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]) {
|
|
@@ -8,22 +8,22 @@ function restoreAttributes(element_or_elements) {
|
|
|
8
8
|
if (!snapshot) {
|
|
9
9
|
continue;
|
|
10
10
|
}
|
|
11
|
-
for (const [
|
|
12
|
-
value === null ? element.removeAttribute(
|
|
11
|
+
for (const [name, value] of snapshot.entries()) {
|
|
12
|
+
value === null ? element.removeAttribute(name) : element.setAttribute(name, value);
|
|
13
13
|
}
|
|
14
14
|
snapshots.delete(element);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
function saveAttributes(element_or_elements,
|
|
18
|
-
const
|
|
17
|
+
function saveAttributes(element_or_elements, name_or_names) {
|
|
18
|
+
const names = Array.isArray(name_or_names) ? name_or_names : [name_or_names];
|
|
19
19
|
(Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]).forEach((element) => {
|
|
20
20
|
let snapshot = snapshots.get(element);
|
|
21
21
|
if (!snapshot) {
|
|
22
22
|
snapshot = /* @__PURE__ */ new Map();
|
|
23
23
|
snapshots.set(element, snapshot);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
snapshot.set(
|
|
25
|
+
names.forEach((name) => {
|
|
26
|
+
snapshot.set(name, element.getAttribute(name));
|
|
27
27
|
});
|
|
28
28
|
});
|
|
29
29
|
}
|
|
@@ -394,7 +394,7 @@ function isUngroupedRadio(element) {
|
|
|
394
394
|
|
|
395
395
|
// src/index.ts
|
|
396
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;`;
|
|
397
|
-
function createPortal(host, container = document.body) {
|
|
397
|
+
function createPortal(host, container = document.body, options = {}) {
|
|
398
398
|
if (!(host instanceof Element)) {
|
|
399
399
|
console.warn("Invalid host element");
|
|
400
400
|
return () => {
|
|
@@ -410,24 +410,31 @@ function createPortal(host, container = document.body) {
|
|
|
410
410
|
return () => {
|
|
411
411
|
};
|
|
412
412
|
}
|
|
413
|
-
const portal = new Portal(host, container);
|
|
413
|
+
const portal = new Portal(host, container, options);
|
|
414
414
|
return () => portal.destroy();
|
|
415
415
|
}
|
|
416
416
|
var Portal = class {
|
|
417
417
|
#host;
|
|
418
418
|
#container;
|
|
419
|
+
#settings;
|
|
419
420
|
#entranceSentinel;
|
|
420
421
|
#exitSentinel;
|
|
421
422
|
#focusables = /* @__PURE__ */ new Set();
|
|
422
423
|
#controller = null;
|
|
423
424
|
#isDestroyed = false;
|
|
424
|
-
constructor(host, container) {
|
|
425
|
+
constructor(host, container, options = {}) {
|
|
425
426
|
this.#host = host;
|
|
426
427
|
if (!(container instanceof Element)) {
|
|
427
428
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
428
429
|
container = document.body;
|
|
429
430
|
}
|
|
430
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 };
|
|
431
438
|
this.#entranceSentinel = this.#createSentinel();
|
|
432
439
|
this.#exitSentinel = this.#createSentinel();
|
|
433
440
|
this.#initialize();
|
|
@@ -495,7 +502,7 @@ var Portal = class {
|
|
|
495
502
|
return;
|
|
496
503
|
}
|
|
497
504
|
this.#update();
|
|
498
|
-
const focusables = this.#
|
|
505
|
+
const focusables = getFocusables(this.#host);
|
|
499
506
|
if (focusables.length) {
|
|
500
507
|
const index = focusables.indexOf(active);
|
|
501
508
|
if (index >= 0) {
|
|
@@ -532,7 +539,9 @@ var Portal = class {
|
|
|
532
539
|
sentinel.setAttribute("aria-hidden", "true");
|
|
533
540
|
sentinel.setAttribute("data-portal-sentinel", "");
|
|
534
541
|
sentinel.setAttribute("tabindex", "0");
|
|
535
|
-
|
|
542
|
+
if (!this.#settings.noInlineStyle) {
|
|
543
|
+
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
544
|
+
}
|
|
536
545
|
return sentinel;
|
|
537
546
|
}
|
|
538
547
|
#focusSentinel(isPrevious) {
|
|
@@ -568,7 +577,7 @@ function containsComposed2(container, element) {
|
|
|
568
577
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
569
578
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
570
579
|
*
|
|
571
|
-
* @version 1.
|
|
580
|
+
* @version 1.3.1
|
|
572
581
|
* @author Yusuke Kamiyamane
|
|
573
582
|
* @license MIT
|
|
574
583
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -576,15 +585,15 @@ function containsComposed2(container, element) {
|
|
|
576
585
|
*/
|
|
577
586
|
/*! Bundled license information:
|
|
578
587
|
|
|
579
|
-
@y14e/
|
|
588
|
+
@y14e/attribute-util/dist/index.js:
|
|
580
589
|
(**
|
|
581
|
-
*
|
|
590
|
+
* Attribute Util
|
|
582
591
|
*
|
|
583
|
-
* @version
|
|
592
|
+
* @version 2.0.0
|
|
584
593
|
* @author Yusuke Kamiyamane
|
|
585
594
|
* @license MIT
|
|
586
595
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
587
|
-
* @see {@link https://github.com/y14e/
|
|
596
|
+
* @see {@link https://github.com/y14e/attribute-util}
|
|
588
597
|
*)
|
|
589
598
|
|
|
590
599
|
power-focusable/dist/index.js:
|
|
@@ -593,7 +602,7 @@ power-focusable/dist/index.js:
|
|
|
593
602
|
* High-precision focus management utility with full composed tree support.
|
|
594
603
|
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
595
604
|
*
|
|
596
|
-
* @version 4.3.
|
|
605
|
+
* @version 4.3.23
|
|
597
606
|
* @author Yusuke Kamiyamane
|
|
598
607
|
* @license MIT
|
|
599
608
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.bundle.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// node_modules/@y14e/
|
|
1
|
+
// node_modules/@y14e/attribute-util/dist/index.js
|
|
2
2
|
var snapshots = /* @__PURE__ */ new WeakMap();
|
|
3
3
|
function restoreAttributes(element_or_elements) {
|
|
4
4
|
for (const element of Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]) {
|
|
@@ -6,22 +6,22 @@ function restoreAttributes(element_or_elements) {
|
|
|
6
6
|
if (!snapshot) {
|
|
7
7
|
continue;
|
|
8
8
|
}
|
|
9
|
-
for (const [
|
|
10
|
-
value === null ? element.removeAttribute(
|
|
9
|
+
for (const [name, value] of snapshot.entries()) {
|
|
10
|
+
value === null ? element.removeAttribute(name) : element.setAttribute(name, value);
|
|
11
11
|
}
|
|
12
12
|
snapshots.delete(element);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
function saveAttributes(element_or_elements,
|
|
16
|
-
const
|
|
15
|
+
function saveAttributes(element_or_elements, name_or_names) {
|
|
16
|
+
const names = Array.isArray(name_or_names) ? name_or_names : [name_or_names];
|
|
17
17
|
(Array.isArray(element_or_elements) ? element_or_elements : [element_or_elements]).forEach((element) => {
|
|
18
18
|
let snapshot = snapshots.get(element);
|
|
19
19
|
if (!snapshot) {
|
|
20
20
|
snapshot = /* @__PURE__ */ new Map();
|
|
21
21
|
snapshots.set(element, snapshot);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
snapshot.set(
|
|
23
|
+
names.forEach((name) => {
|
|
24
|
+
snapshot.set(name, element.getAttribute(name));
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
27
|
}
|
|
@@ -392,7 +392,7 @@ function isUngroupedRadio(element) {
|
|
|
392
392
|
|
|
393
393
|
// src/index.ts
|
|
394
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;`;
|
|
395
|
-
function createPortal(host, container = document.body) {
|
|
395
|
+
function createPortal(host, container = document.body, options = {}) {
|
|
396
396
|
if (!(host instanceof Element)) {
|
|
397
397
|
console.warn("Invalid host element");
|
|
398
398
|
return () => {
|
|
@@ -408,24 +408,31 @@ function createPortal(host, container = document.body) {
|
|
|
408
408
|
return () => {
|
|
409
409
|
};
|
|
410
410
|
}
|
|
411
|
-
const portal = new Portal(host, container);
|
|
411
|
+
const portal = new Portal(host, container, options);
|
|
412
412
|
return () => portal.destroy();
|
|
413
413
|
}
|
|
414
414
|
var Portal = class {
|
|
415
415
|
#host;
|
|
416
416
|
#container;
|
|
417
|
+
#settings;
|
|
417
418
|
#entranceSentinel;
|
|
418
419
|
#exitSentinel;
|
|
419
420
|
#focusables = /* @__PURE__ */ new Set();
|
|
420
421
|
#controller = null;
|
|
421
422
|
#isDestroyed = false;
|
|
422
|
-
constructor(host, container) {
|
|
423
|
+
constructor(host, container, options = {}) {
|
|
423
424
|
this.#host = host;
|
|
424
425
|
if (!(container instanceof Element)) {
|
|
425
426
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
426
427
|
container = document.body;
|
|
427
428
|
}
|
|
428
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 };
|
|
429
436
|
this.#entranceSentinel = this.#createSentinel();
|
|
430
437
|
this.#exitSentinel = this.#createSentinel();
|
|
431
438
|
this.#initialize();
|
|
@@ -493,7 +500,7 @@ var Portal = class {
|
|
|
493
500
|
return;
|
|
494
501
|
}
|
|
495
502
|
this.#update();
|
|
496
|
-
const focusables = this.#
|
|
503
|
+
const focusables = getFocusables(this.#host);
|
|
497
504
|
if (focusables.length) {
|
|
498
505
|
const index = focusables.indexOf(active);
|
|
499
506
|
if (index >= 0) {
|
|
@@ -530,7 +537,9 @@ var Portal = class {
|
|
|
530
537
|
sentinel.setAttribute("aria-hidden", "true");
|
|
531
538
|
sentinel.setAttribute("data-portal-sentinel", "");
|
|
532
539
|
sentinel.setAttribute("tabindex", "0");
|
|
533
|
-
|
|
540
|
+
if (!this.#settings.noInlineStyle) {
|
|
541
|
+
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
542
|
+
}
|
|
534
543
|
return sentinel;
|
|
535
544
|
}
|
|
536
545
|
#focusSentinel(isPrevious) {
|
|
@@ -566,7 +575,7 @@ function containsComposed2(container, element) {
|
|
|
566
575
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
567
576
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
568
577
|
*
|
|
569
|
-
* @version 1.
|
|
578
|
+
* @version 1.3.1
|
|
570
579
|
* @author Yusuke Kamiyamane
|
|
571
580
|
* @license MIT
|
|
572
581
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -574,15 +583,15 @@ function containsComposed2(container, element) {
|
|
|
574
583
|
*/
|
|
575
584
|
/*! Bundled license information:
|
|
576
585
|
|
|
577
|
-
@y14e/
|
|
586
|
+
@y14e/attribute-util/dist/index.js:
|
|
578
587
|
(**
|
|
579
|
-
*
|
|
588
|
+
* Attribute Util
|
|
580
589
|
*
|
|
581
|
-
* @version
|
|
590
|
+
* @version 2.0.0
|
|
582
591
|
* @author Yusuke Kamiyamane
|
|
583
592
|
* @license MIT
|
|
584
593
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
585
|
-
* @see {@link https://github.com/y14e/
|
|
594
|
+
* @see {@link https://github.com/y14e/attribute-util}
|
|
586
595
|
*)
|
|
587
596
|
|
|
588
597
|
power-focusable/dist/index.js:
|
|
@@ -591,7 +600,7 @@ power-focusable/dist/index.js:
|
|
|
591
600
|
* High-precision focus management utility with full composed tree support.
|
|
592
601
|
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
593
602
|
*
|
|
594
|
-
* @version 4.3.
|
|
603
|
+
* @version 4.3.23
|
|
595
604
|
* @author Yusuke Kamiyamane
|
|
596
605
|
* @license MIT
|
|
597
606
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.cjs
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var util = require('@y14e/attribute-util');
|
|
4
|
+
var pf = require('power-focusable');
|
|
5
|
+
|
|
6
|
+
function _interopNamespace(e) {
|
|
7
|
+
if (e && e.__esModule) return e;
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var util__namespace = /*#__PURE__*/_interopNamespace(util);
|
|
25
|
+
var pf__namespace = /*#__PURE__*/_interopNamespace(pf);
|
|
5
26
|
|
|
6
27
|
// src/index.ts
|
|
7
28
|
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) {
|
|
29
|
+
function createPortal(host, container = document.body, options = {}) {
|
|
9
30
|
if (!(host instanceof Element)) {
|
|
10
31
|
console.warn("Invalid host element");
|
|
11
32
|
return () => {
|
|
@@ -21,24 +42,31 @@ function createPortal(host, container = document.body) {
|
|
|
21
42
|
return () => {
|
|
22
43
|
};
|
|
23
44
|
}
|
|
24
|
-
const portal = new Portal(host, container);
|
|
45
|
+
const portal = new Portal(host, container, options);
|
|
25
46
|
return () => portal.destroy();
|
|
26
47
|
}
|
|
27
48
|
var Portal = class {
|
|
28
49
|
#host;
|
|
29
50
|
#container;
|
|
51
|
+
#settings;
|
|
30
52
|
#entranceSentinel;
|
|
31
53
|
#exitSentinel;
|
|
32
54
|
#focusables = /* @__PURE__ */ new Set();
|
|
33
55
|
#controller = null;
|
|
34
56
|
#isDestroyed = false;
|
|
35
|
-
constructor(host, container) {
|
|
57
|
+
constructor(host, container, options = {}) {
|
|
36
58
|
this.#host = host;
|
|
37
59
|
if (!(container instanceof Element)) {
|
|
38
60
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
39
61
|
container = document.body;
|
|
40
62
|
}
|
|
41
63
|
this.#container = container;
|
|
64
|
+
let { noInlineStyle = false } = options;
|
|
65
|
+
if (typeof noInlineStyle !== "boolean") {
|
|
66
|
+
console.warn("Invalid noInlineStyle option. Fallback: false.");
|
|
67
|
+
noInlineStyle = false;
|
|
68
|
+
}
|
|
69
|
+
this.#settings = { noInlineStyle };
|
|
42
70
|
this.#entranceSentinel = this.#createSentinel();
|
|
43
71
|
this.#exitSentinel = this.#createSentinel();
|
|
44
72
|
this.#initialize();
|
|
@@ -50,7 +78,7 @@ var Portal = class {
|
|
|
50
78
|
this.#isDestroyed = true;
|
|
51
79
|
this.#controller?.abort();
|
|
52
80
|
this.#controller = null;
|
|
53
|
-
|
|
81
|
+
util__namespace.restoreAttributes([...this.#focusables]);
|
|
54
82
|
this.#focusables.clear();
|
|
55
83
|
this.#exitSentinel.after(this.#host);
|
|
56
84
|
this.#entranceSentinel.remove();
|
|
@@ -82,7 +110,7 @@ var Portal = class {
|
|
|
82
110
|
}
|
|
83
111
|
this.#update();
|
|
84
112
|
const first = [...this.#focusables][0];
|
|
85
|
-
first ?
|
|
113
|
+
first ? pf__namespace.focusElement(first) : this.#moveFocus("next");
|
|
86
114
|
} else {
|
|
87
115
|
if (this.#host.contains(previous)) {
|
|
88
116
|
this.#moveFocus("next");
|
|
@@ -90,7 +118,7 @@ var Portal = class {
|
|
|
90
118
|
}
|
|
91
119
|
this.#update();
|
|
92
120
|
const last = [...this.#focusables].at(-1);
|
|
93
|
-
last ?
|
|
121
|
+
last ? pf__namespace.focusElement(last) : this.#moveFocus("previous");
|
|
94
122
|
}
|
|
95
123
|
};
|
|
96
124
|
#onKeyDown = (event) => {
|
|
@@ -101,18 +129,18 @@ var Portal = class {
|
|
|
101
129
|
if (key !== "Tab" || altKey || ctrlKey || metaKey) {
|
|
102
130
|
return;
|
|
103
131
|
}
|
|
104
|
-
const active =
|
|
132
|
+
const active = pf__namespace.getActiveElement();
|
|
105
133
|
if (!(active instanceof Element)) {
|
|
106
134
|
return;
|
|
107
135
|
}
|
|
108
136
|
this.#update();
|
|
109
|
-
const focusables = this.#
|
|
137
|
+
const focusables = pf__namespace.getFocusables(this.#host);
|
|
110
138
|
if (focusables.length) {
|
|
111
139
|
const index = focusables.indexOf(active);
|
|
112
140
|
if (index >= 0) {
|
|
113
141
|
event.preventDefault();
|
|
114
142
|
const focusable = focusables[index + (shiftKey ? -1 : 1)];
|
|
115
|
-
focusable ?
|
|
143
|
+
focusable ? pf__namespace.focusElement(focusable) : this.#focusSentinel(shiftKey);
|
|
116
144
|
}
|
|
117
145
|
} else {
|
|
118
146
|
event.preventDefault();
|
|
@@ -122,18 +150,18 @@ var Portal = class {
|
|
|
122
150
|
#update() {
|
|
123
151
|
const current = /* @__PURE__ */ new Set([
|
|
124
152
|
...this.#getFocusables(),
|
|
125
|
-
...
|
|
153
|
+
...pf__namespace.getFocusables(this.#host, { composed: true })
|
|
126
154
|
]);
|
|
127
155
|
for (const focusable of this.#focusables) {
|
|
128
156
|
if (!current.has(focusable)) {
|
|
129
|
-
|
|
157
|
+
util__namespace.restoreAttributes(focusable);
|
|
130
158
|
this.#focusables.delete(focusable);
|
|
131
159
|
}
|
|
132
160
|
}
|
|
133
161
|
for (const focusable of current) {
|
|
134
162
|
if (!this.#focusables.has(focusable)) {
|
|
135
163
|
this.#focusables.add(focusable);
|
|
136
|
-
|
|
164
|
+
util__namespace.saveAttributes(focusable, "tabindex");
|
|
137
165
|
focusable.setAttribute("tabindex", "-1");
|
|
138
166
|
}
|
|
139
167
|
}
|
|
@@ -143,14 +171,16 @@ var Portal = class {
|
|
|
143
171
|
sentinel.setAttribute("aria-hidden", "true");
|
|
144
172
|
sentinel.setAttribute("data-portal-sentinel", "");
|
|
145
173
|
sentinel.setAttribute("tabindex", "0");
|
|
146
|
-
|
|
174
|
+
if (!this.#settings.noInlineStyle) {
|
|
175
|
+
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
176
|
+
}
|
|
147
177
|
return sentinel;
|
|
148
178
|
}
|
|
149
179
|
#focusSentinel(isPrevious) {
|
|
150
180
|
(isPrevious ? this.#entranceSentinel : this.#exitSentinel).focus();
|
|
151
181
|
}
|
|
152
182
|
#getFocusables() {
|
|
153
|
-
return
|
|
183
|
+
return pf__namespace.getFocusables(this.#host, {
|
|
154
184
|
composed: true,
|
|
155
185
|
include: (element) => this.#focusables.has(element)
|
|
156
186
|
});
|
|
@@ -160,8 +190,8 @@ var Portal = class {
|
|
|
160
190
|
anchor: direction === "previous" ? this.#entranceSentinel : this.#exitSentinel,
|
|
161
191
|
composed: true
|
|
162
192
|
};
|
|
163
|
-
const focusable = direction === "previous" ?
|
|
164
|
-
focusable &&
|
|
193
|
+
const focusable = direction === "previous" ? pf__namespace.getPreviousFocusable(document.body, options) : pf__namespace.getNextFocusable(document.body, options);
|
|
194
|
+
focusable && pf__namespace.focusElement(focusable);
|
|
165
195
|
}
|
|
166
196
|
};
|
|
167
197
|
function containsComposed(container, element) {
|
|
@@ -179,7 +209,7 @@ function containsComposed(container, element) {
|
|
|
179
209
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
180
210
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
181
211
|
*
|
|
182
|
-
* @version 1.
|
|
212
|
+
* @version 1.3.1
|
|
183
213
|
* @author Yusuke Kamiyamane
|
|
184
214
|
* @license MIT
|
|
185
215
|
* @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.1
|
|
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.1
|
|
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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import * as util from '@y14e/attribute-util';
|
|
2
|
+
import * as pf from 'power-focusable';
|
|
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();
|
|
@@ -48,7 +55,7 @@ var Portal = class {
|
|
|
48
55
|
this.#isDestroyed = true;
|
|
49
56
|
this.#controller?.abort();
|
|
50
57
|
this.#controller = null;
|
|
51
|
-
restoreAttributes([...this.#focusables]);
|
|
58
|
+
util.restoreAttributes([...this.#focusables]);
|
|
52
59
|
this.#focusables.clear();
|
|
53
60
|
this.#exitSentinel.after(this.#host);
|
|
54
61
|
this.#entranceSentinel.remove();
|
|
@@ -80,7 +87,7 @@ var Portal = class {
|
|
|
80
87
|
}
|
|
81
88
|
this.#update();
|
|
82
89
|
const first = [...this.#focusables][0];
|
|
83
|
-
first ? focusElement(first) : this.#moveFocus("next");
|
|
90
|
+
first ? pf.focusElement(first) : this.#moveFocus("next");
|
|
84
91
|
} else {
|
|
85
92
|
if (this.#host.contains(previous)) {
|
|
86
93
|
this.#moveFocus("next");
|
|
@@ -88,7 +95,7 @@ var Portal = class {
|
|
|
88
95
|
}
|
|
89
96
|
this.#update();
|
|
90
97
|
const last = [...this.#focusables].at(-1);
|
|
91
|
-
last ? focusElement(last) : this.#moveFocus("previous");
|
|
98
|
+
last ? pf.focusElement(last) : this.#moveFocus("previous");
|
|
92
99
|
}
|
|
93
100
|
};
|
|
94
101
|
#onKeyDown = (event) => {
|
|
@@ -99,18 +106,18 @@ var Portal = class {
|
|
|
99
106
|
if (key !== "Tab" || altKey || ctrlKey || metaKey) {
|
|
100
107
|
return;
|
|
101
108
|
}
|
|
102
|
-
const active = getActiveElement();
|
|
109
|
+
const active = pf.getActiveElement();
|
|
103
110
|
if (!(active instanceof Element)) {
|
|
104
111
|
return;
|
|
105
112
|
}
|
|
106
113
|
this.#update();
|
|
107
|
-
const focusables = this.#
|
|
114
|
+
const focusables = pf.getFocusables(this.#host);
|
|
108
115
|
if (focusables.length) {
|
|
109
116
|
const index = focusables.indexOf(active);
|
|
110
117
|
if (index >= 0) {
|
|
111
118
|
event.preventDefault();
|
|
112
119
|
const focusable = focusables[index + (shiftKey ? -1 : 1)];
|
|
113
|
-
focusable ? focusElement(focusable) : this.#focusSentinel(shiftKey);
|
|
120
|
+
focusable ? pf.focusElement(focusable) : this.#focusSentinel(shiftKey);
|
|
114
121
|
}
|
|
115
122
|
} else {
|
|
116
123
|
event.preventDefault();
|
|
@@ -120,18 +127,18 @@ var Portal = class {
|
|
|
120
127
|
#update() {
|
|
121
128
|
const current = /* @__PURE__ */ new Set([
|
|
122
129
|
...this.#getFocusables(),
|
|
123
|
-
...getFocusables(this.#host, { composed: true })
|
|
130
|
+
...pf.getFocusables(this.#host, { composed: true })
|
|
124
131
|
]);
|
|
125
132
|
for (const focusable of this.#focusables) {
|
|
126
133
|
if (!current.has(focusable)) {
|
|
127
|
-
restoreAttributes(focusable);
|
|
134
|
+
util.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(focusable, "tabindex");
|
|
141
|
+
util.saveAttributes(focusable, "tabindex");
|
|
135
142
|
focusable.setAttribute("tabindex", "-1");
|
|
136
143
|
}
|
|
137
144
|
}
|
|
@@ -141,14 +148,16 @@ 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) {
|
|
148
157
|
(isPrevious ? this.#entranceSentinel : this.#exitSentinel).focus();
|
|
149
158
|
}
|
|
150
159
|
#getFocusables() {
|
|
151
|
-
return getFocusables(this.#host, {
|
|
160
|
+
return pf.getFocusables(this.#host, {
|
|
152
161
|
composed: true,
|
|
153
162
|
include: (element) => this.#focusables.has(element)
|
|
154
163
|
});
|
|
@@ -158,8 +167,8 @@ var Portal = class {
|
|
|
158
167
|
anchor: direction === "previous" ? this.#entranceSentinel : this.#exitSentinel,
|
|
159
168
|
composed: true
|
|
160
169
|
};
|
|
161
|
-
const focusable = direction === "previous" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
162
|
-
focusable && focusElement(focusable);
|
|
170
|
+
const focusable = direction === "previous" ? pf.getPreviousFocusable(document.body, options) : pf.getNextFocusable(document.body, options);
|
|
171
|
+
focusable && pf.focusElement(focusable);
|
|
163
172
|
}
|
|
164
173
|
};
|
|
165
174
|
function containsComposed(container, element) {
|
|
@@ -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.1
|
|
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.1",
|
|
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/
|
|
59
|
-
"power-focusable": "^4.3.
|
|
58
|
+
"@y14e/attribute-util": "^2.0.0",
|
|
59
|
+
"power-focusable": "^4.3.23"
|
|
60
60
|
}
|
|
61
61
|
}
|