@y14e/portal 0.0.2 → 0.0.4
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 +4 -4
- package/dist/index.cjs +35 -33
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -33
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Portal
|
|
2
2
|
|
|
3
|
-
Lightweight DOM portal (teleport) utility with fully focus management. Designed for accessible dialogs, menus, overlays, popovers
|
|
3
|
+
Lightweight DOM portal (teleport) utility with fully focus management. Designed for accessible dialogs, menus, overlays, popovers.
|
|
4
4
|
|
|
5
5
|
> [!NOTE]
|
|
6
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).
|
|
@@ -18,7 +18,7 @@ import { createPortal } from '@y14e/portal';
|
|
|
18
18
|
// CDNs
|
|
19
19
|
import { createPortal } from 'https://esm.sh/@y14e/portal'
|
|
20
20
|
// or
|
|
21
|
-
import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal
|
|
21
|
+
import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal/+esm';
|
|
22
22
|
// or
|
|
23
23
|
import { createPortal } from 'https://unpkg.com/@y14e/portal/dist/index.js';
|
|
24
24
|
```
|
|
@@ -27,11 +27,11 @@ import { createPortal } from 'https://unpkg.com/@y14e/portal/dist/index.js';
|
|
|
27
27
|
|
|
28
28
|
### `createPortal`
|
|
29
29
|
|
|
30
|
-
Creates a portal and preserves keyboard focus
|
|
30
|
+
Creates a portal and preserves keyboard focus order between the original DOM and the portal.
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
33
|
const portal = createPortal(source, container);
|
|
34
|
-
// => { element: Element
|
|
34
|
+
// => { element: Element; cleanup: () => void }
|
|
35
35
|
//
|
|
36
36
|
// source: Element
|
|
37
37
|
// container (optional): Element (default: <body>)
|
package/dist/index.cjs
CHANGED
|
@@ -7,12 +7,12 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
7
7
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
8
8
|
container = document.body;
|
|
9
9
|
}
|
|
10
|
-
const { composed = false, filter = () => true } = options;
|
|
10
|
+
const { composed = false, filter = () => true, include } = options;
|
|
11
11
|
const elements = [];
|
|
12
|
-
if (composed) {
|
|
12
|
+
if (composed || typeof include === "function") {
|
|
13
13
|
let traverse2 = function(node) {
|
|
14
14
|
if (node instanceof Element) {
|
|
15
|
-
if (isFocusable(node)) {
|
|
15
|
+
if (isFocusable(node) || include?.(node)) {
|
|
16
16
|
elements[elements.length] = node;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -85,9 +85,10 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
85
85
|
anchor = getActiveElement(),
|
|
86
86
|
composed = false,
|
|
87
87
|
filter = () => true,
|
|
88
|
+
include,
|
|
88
89
|
wrap = false
|
|
89
90
|
} = options;
|
|
90
|
-
const focusables = getFocusables(container, { composed, filter });
|
|
91
|
+
const focusables = getFocusables(container, { composed, filter, include });
|
|
91
92
|
const { length } = focusables;
|
|
92
93
|
if (!length) {
|
|
93
94
|
return null;
|
|
@@ -270,18 +271,18 @@ function createPortal(source, container = document.body) {
|
|
|
270
271
|
var Portal = class {
|
|
271
272
|
#source;
|
|
272
273
|
#container;
|
|
273
|
-
#
|
|
274
|
+
#target;
|
|
274
275
|
#entranceSentinel;
|
|
275
276
|
#exitSentinel;
|
|
276
277
|
#tabIndexes = /* @__PURE__ */ new WeakMap();
|
|
277
278
|
#controller = null;
|
|
278
279
|
#isDestroyed = false;
|
|
279
|
-
constructor(source, container
|
|
280
|
+
constructor(source, container) {
|
|
280
281
|
this.#source = source;
|
|
281
282
|
this.#container = container;
|
|
282
|
-
this.#
|
|
283
|
-
this.#
|
|
284
|
-
this.#
|
|
283
|
+
this.#target = document.createElement("div");
|
|
284
|
+
this.#target.setAttribute("data-portal", "");
|
|
285
|
+
this.#target.setAttribute("tabindex", "-1");
|
|
285
286
|
this.#entranceSentinel = this.#createSentinel();
|
|
286
287
|
this.#exitSentinel = this.#createSentinel();
|
|
287
288
|
this.#initialize();
|
|
@@ -294,6 +295,9 @@ var Portal = class {
|
|
|
294
295
|
this.#controller?.abort();
|
|
295
296
|
this.#controller = null;
|
|
296
297
|
this.#getFocusables().forEach((focusable) => {
|
|
298
|
+
if (!this.#tabIndexes.has(focusable)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
297
301
|
const index = this.#tabIndexes.get(focusable);
|
|
298
302
|
if (index === null) {
|
|
299
303
|
focusable.removeAttribute("tabindex");
|
|
@@ -302,25 +306,23 @@ var Portal = class {
|
|
|
302
306
|
}
|
|
303
307
|
});
|
|
304
308
|
this.#exitSentinel.after(this.#source);
|
|
305
|
-
this.#
|
|
309
|
+
this.#target.remove();
|
|
306
310
|
this.#entranceSentinel.remove();
|
|
307
311
|
this.#exitSentinel.remove();
|
|
308
312
|
}
|
|
309
313
|
getElement() {
|
|
310
|
-
return this.#
|
|
314
|
+
return this.#target;
|
|
311
315
|
}
|
|
312
316
|
#initialize() {
|
|
313
317
|
this.#source.before(this.#entranceSentinel);
|
|
314
318
|
this.#entranceSentinel.after(this.#exitSentinel);
|
|
315
|
-
this.#
|
|
316
|
-
this.#container.append(this.#
|
|
317
|
-
getFocusables(
|
|
318
|
-
(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
);
|
|
319
|
+
this.#target.append(this.#source);
|
|
320
|
+
this.#container.append(this.#target);
|
|
321
|
+
this.#getFocusables().forEach((focusable) => {
|
|
322
|
+
const index = focusable.getAttribute("tabindex")?.trim();
|
|
323
|
+
this.#tabIndexes.set(focusable, index === null ? null : Number(index));
|
|
324
|
+
focusable.setAttribute("tabindex", "-1");
|
|
325
|
+
});
|
|
324
326
|
this.#controller = new AbortController();
|
|
325
327
|
const { signal } = this.#controller;
|
|
326
328
|
document.addEventListener("focusin", this.#onFocusIn, {
|
|
@@ -338,19 +340,18 @@ var Portal = class {
|
|
|
338
340
|
if (!(before instanceof Element)) {
|
|
339
341
|
return;
|
|
340
342
|
}
|
|
341
|
-
const focusables = this.#getFocusables();
|
|
342
343
|
if (current === this.#entranceSentinel) {
|
|
343
344
|
if (this.#source.contains(before)) {
|
|
344
345
|
this.#focusOutside("backward");
|
|
345
346
|
} else {
|
|
346
|
-
const first =
|
|
347
|
+
const first = this.#getFocusables()[0];
|
|
347
348
|
first && focus(first);
|
|
348
349
|
}
|
|
349
350
|
} else if (current === this.#exitSentinel) {
|
|
350
351
|
if (this.#source.contains(before)) {
|
|
351
352
|
this.#focusOutside("forward");
|
|
352
353
|
} else {
|
|
353
|
-
const last =
|
|
354
|
+
const last = this.#getFocusables().at(-1);
|
|
354
355
|
last && focus(last);
|
|
355
356
|
}
|
|
356
357
|
}
|
|
@@ -366,17 +367,16 @@ var Portal = class {
|
|
|
366
367
|
if (!this.#source.contains(active)) {
|
|
367
368
|
return;
|
|
368
369
|
}
|
|
369
|
-
|
|
370
|
-
if (!focusables.length) {
|
|
370
|
+
if (!this.#getFocusables().length) {
|
|
371
371
|
event.preventDefault();
|
|
372
372
|
(event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
|
|
373
373
|
}
|
|
374
|
-
const index =
|
|
374
|
+
const index = this.#getFocusables().indexOf(active);
|
|
375
375
|
if (index === -1) {
|
|
376
376
|
return;
|
|
377
377
|
}
|
|
378
378
|
event.preventDefault();
|
|
379
|
-
const focusable =
|
|
379
|
+
const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
|
|
380
380
|
if (focusable) {
|
|
381
381
|
focus(focusable);
|
|
382
382
|
} else {
|
|
@@ -399,7 +399,10 @@ var Portal = class {
|
|
|
399
399
|
focusable && focus(focusable);
|
|
400
400
|
}
|
|
401
401
|
#getFocusables() {
|
|
402
|
-
return getFocusables(this.#source, {
|
|
402
|
+
return getFocusables(this.#source, {
|
|
403
|
+
composed: true,
|
|
404
|
+
include: (element) => this.#tabIndexes.has(element)
|
|
405
|
+
});
|
|
403
406
|
}
|
|
404
407
|
};
|
|
405
408
|
function focus(element) {
|
|
@@ -415,9 +418,9 @@ function getActiveElement2() {
|
|
|
415
418
|
/**
|
|
416
419
|
* Portal
|
|
417
420
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
418
|
-
* Designed for accessible dialogs, menus, overlays, popovers
|
|
421
|
+
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
419
422
|
*
|
|
420
|
-
* @version 0.0.
|
|
423
|
+
* @version 0.0.4
|
|
421
424
|
* @author Yusuke Kamiyamane
|
|
422
425
|
* @license MIT
|
|
423
426
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -429,10 +432,9 @@ power-focusable/dist/index.js:
|
|
|
429
432
|
(**
|
|
430
433
|
* Power Focusable
|
|
431
434
|
* High-precision focus management utility with full composed tree support.
|
|
432
|
-
* Handles complex focus rules including tabindex ordering, radio groups, inert
|
|
433
|
-
* and shadow DOM.
|
|
435
|
+
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
434
436
|
*
|
|
435
|
-
* @version 4.0
|
|
437
|
+
* @version 4.1.0
|
|
436
438
|
* @author Yusuke Kamiyamane
|
|
437
439
|
* @license MIT
|
|
438
440
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Portal
|
|
3
3
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
4
|
-
* Designed for accessible dialogs, menus, overlays, popovers
|
|
4
|
+
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
5
5
|
*
|
|
6
|
-
* @version 0.0.
|
|
6
|
+
* @version 0.0.4
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -15,7 +15,7 @@ declare function createPortal(source: Element, container?: HTMLElement): {
|
|
|
15
15
|
};
|
|
16
16
|
declare class Portal {
|
|
17
17
|
#private;
|
|
18
|
-
constructor(source: Element, container
|
|
18
|
+
constructor(source: Element, container: Element);
|
|
19
19
|
destroy(): void;
|
|
20
20
|
getElement(): HTMLElement;
|
|
21
21
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Portal
|
|
3
3
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
4
|
-
* Designed for accessible dialogs, menus, overlays, popovers
|
|
4
|
+
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
5
5
|
*
|
|
6
|
-
* @version 0.0.
|
|
6
|
+
* @version 0.0.4
|
|
7
7
|
* @author Yusuke Kamiyamane
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -15,7 +15,7 @@ declare function createPortal(source: Element, container?: HTMLElement): {
|
|
|
15
15
|
};
|
|
16
16
|
declare class Portal {
|
|
17
17
|
#private;
|
|
18
|
-
constructor(source: Element, container
|
|
18
|
+
constructor(source: Element, container: Element);
|
|
19
19
|
destroy(): void;
|
|
20
20
|
getElement(): HTMLElement;
|
|
21
21
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,12 +5,12 @@ function getFocusables(container = document.body, options = {}) {
|
|
|
5
5
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
6
6
|
container = document.body;
|
|
7
7
|
}
|
|
8
|
-
const { composed = false, filter = () => true } = options;
|
|
8
|
+
const { composed = false, filter = () => true, include } = options;
|
|
9
9
|
const elements = [];
|
|
10
|
-
if (composed) {
|
|
10
|
+
if (composed || typeof include === "function") {
|
|
11
11
|
let traverse2 = function(node) {
|
|
12
12
|
if (node instanceof Element) {
|
|
13
|
-
if (isFocusable(node)) {
|
|
13
|
+
if (isFocusable(node) || include?.(node)) {
|
|
14
14
|
elements[elements.length] = node;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -83,9 +83,10 @@ function getRelativeFocusable(container, offset, options) {
|
|
|
83
83
|
anchor = getActiveElement(),
|
|
84
84
|
composed = false,
|
|
85
85
|
filter = () => true,
|
|
86
|
+
include,
|
|
86
87
|
wrap = false
|
|
87
88
|
} = options;
|
|
88
|
-
const focusables = getFocusables(container, { composed, filter });
|
|
89
|
+
const focusables = getFocusables(container, { composed, filter, include });
|
|
89
90
|
const { length } = focusables;
|
|
90
91
|
if (!length) {
|
|
91
92
|
return null;
|
|
@@ -268,18 +269,18 @@ function createPortal(source, container = document.body) {
|
|
|
268
269
|
var Portal = class {
|
|
269
270
|
#source;
|
|
270
271
|
#container;
|
|
271
|
-
#
|
|
272
|
+
#target;
|
|
272
273
|
#entranceSentinel;
|
|
273
274
|
#exitSentinel;
|
|
274
275
|
#tabIndexes = /* @__PURE__ */ new WeakMap();
|
|
275
276
|
#controller = null;
|
|
276
277
|
#isDestroyed = false;
|
|
277
|
-
constructor(source, container
|
|
278
|
+
constructor(source, container) {
|
|
278
279
|
this.#source = source;
|
|
279
280
|
this.#container = container;
|
|
280
|
-
this.#
|
|
281
|
-
this.#
|
|
282
|
-
this.#
|
|
281
|
+
this.#target = document.createElement("div");
|
|
282
|
+
this.#target.setAttribute("data-portal", "");
|
|
283
|
+
this.#target.setAttribute("tabindex", "-1");
|
|
283
284
|
this.#entranceSentinel = this.#createSentinel();
|
|
284
285
|
this.#exitSentinel = this.#createSentinel();
|
|
285
286
|
this.#initialize();
|
|
@@ -292,6 +293,9 @@ var Portal = class {
|
|
|
292
293
|
this.#controller?.abort();
|
|
293
294
|
this.#controller = null;
|
|
294
295
|
this.#getFocusables().forEach((focusable) => {
|
|
296
|
+
if (!this.#tabIndexes.has(focusable)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
295
299
|
const index = this.#tabIndexes.get(focusable);
|
|
296
300
|
if (index === null) {
|
|
297
301
|
focusable.removeAttribute("tabindex");
|
|
@@ -300,25 +304,23 @@ var Portal = class {
|
|
|
300
304
|
}
|
|
301
305
|
});
|
|
302
306
|
this.#exitSentinel.after(this.#source);
|
|
303
|
-
this.#
|
|
307
|
+
this.#target.remove();
|
|
304
308
|
this.#entranceSentinel.remove();
|
|
305
309
|
this.#exitSentinel.remove();
|
|
306
310
|
}
|
|
307
311
|
getElement() {
|
|
308
|
-
return this.#
|
|
312
|
+
return this.#target;
|
|
309
313
|
}
|
|
310
314
|
#initialize() {
|
|
311
315
|
this.#source.before(this.#entranceSentinel);
|
|
312
316
|
this.#entranceSentinel.after(this.#exitSentinel);
|
|
313
|
-
this.#
|
|
314
|
-
this.#container.append(this.#
|
|
315
|
-
getFocusables(
|
|
316
|
-
(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
);
|
|
317
|
+
this.#target.append(this.#source);
|
|
318
|
+
this.#container.append(this.#target);
|
|
319
|
+
this.#getFocusables().forEach((focusable) => {
|
|
320
|
+
const index = focusable.getAttribute("tabindex")?.trim();
|
|
321
|
+
this.#tabIndexes.set(focusable, index === null ? null : Number(index));
|
|
322
|
+
focusable.setAttribute("tabindex", "-1");
|
|
323
|
+
});
|
|
322
324
|
this.#controller = new AbortController();
|
|
323
325
|
const { signal } = this.#controller;
|
|
324
326
|
document.addEventListener("focusin", this.#onFocusIn, {
|
|
@@ -336,19 +338,18 @@ var Portal = class {
|
|
|
336
338
|
if (!(before instanceof Element)) {
|
|
337
339
|
return;
|
|
338
340
|
}
|
|
339
|
-
const focusables = this.#getFocusables();
|
|
340
341
|
if (current === this.#entranceSentinel) {
|
|
341
342
|
if (this.#source.contains(before)) {
|
|
342
343
|
this.#focusOutside("backward");
|
|
343
344
|
} else {
|
|
344
|
-
const first =
|
|
345
|
+
const first = this.#getFocusables()[0];
|
|
345
346
|
first && focus(first);
|
|
346
347
|
}
|
|
347
348
|
} else if (current === this.#exitSentinel) {
|
|
348
349
|
if (this.#source.contains(before)) {
|
|
349
350
|
this.#focusOutside("forward");
|
|
350
351
|
} else {
|
|
351
|
-
const last =
|
|
352
|
+
const last = this.#getFocusables().at(-1);
|
|
352
353
|
last && focus(last);
|
|
353
354
|
}
|
|
354
355
|
}
|
|
@@ -364,17 +365,16 @@ var Portal = class {
|
|
|
364
365
|
if (!this.#source.contains(active)) {
|
|
365
366
|
return;
|
|
366
367
|
}
|
|
367
|
-
|
|
368
|
-
if (!focusables.length) {
|
|
368
|
+
if (!this.#getFocusables().length) {
|
|
369
369
|
event.preventDefault();
|
|
370
370
|
(event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
|
|
371
371
|
}
|
|
372
|
-
const index =
|
|
372
|
+
const index = this.#getFocusables().indexOf(active);
|
|
373
373
|
if (index === -1) {
|
|
374
374
|
return;
|
|
375
375
|
}
|
|
376
376
|
event.preventDefault();
|
|
377
|
-
const focusable =
|
|
377
|
+
const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
|
|
378
378
|
if (focusable) {
|
|
379
379
|
focus(focusable);
|
|
380
380
|
} else {
|
|
@@ -397,7 +397,10 @@ var Portal = class {
|
|
|
397
397
|
focusable && focus(focusable);
|
|
398
398
|
}
|
|
399
399
|
#getFocusables() {
|
|
400
|
-
return getFocusables(this.#source, {
|
|
400
|
+
return getFocusables(this.#source, {
|
|
401
|
+
composed: true,
|
|
402
|
+
include: (element) => this.#tabIndexes.has(element)
|
|
403
|
+
});
|
|
401
404
|
}
|
|
402
405
|
};
|
|
403
406
|
function focus(element) {
|
|
@@ -413,9 +416,9 @@ function getActiveElement2() {
|
|
|
413
416
|
/**
|
|
414
417
|
* Portal
|
|
415
418
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
416
|
-
* Designed for accessible dialogs, menus, overlays, popovers
|
|
419
|
+
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
417
420
|
*
|
|
418
|
-
* @version 0.0.
|
|
421
|
+
* @version 0.0.4
|
|
419
422
|
* @author Yusuke Kamiyamane
|
|
420
423
|
* @license MIT
|
|
421
424
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -427,10 +430,9 @@ power-focusable/dist/index.js:
|
|
|
427
430
|
(**
|
|
428
431
|
* Power Focusable
|
|
429
432
|
* High-precision focus management utility with full composed tree support.
|
|
430
|
-
* Handles complex focus rules including tabindex ordering, radio groups, inert
|
|
431
|
-
* and shadow DOM.
|
|
433
|
+
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
432
434
|
*
|
|
433
|
-
* @version 4.0
|
|
435
|
+
* @version 4.1.0
|
|
434
436
|
* @author Yusuke Kamiyamane
|
|
435
437
|
* @license MIT
|
|
436
438
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@y14e/portal",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Lightweight DOM portal (teleport) utility with fully focus management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"homepage": "https://github.com/y14e/portal#readme",
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"bun-types": "latest",
|
|
51
|
-
"power-focusable": "^4.0
|
|
51
|
+
"power-focusable": "^4.1.0",
|
|
52
52
|
"tsup": "^8.0.0",
|
|
53
53
|
"typescript": "^5.6.0"
|
|
54
54
|
},
|