@y14e/portal 0.0.3 → 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 +3 -3
- package/dist/index.cjs +34 -28
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +34 -28
- 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).
|
|
@@ -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,22 +271,20 @@ 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
|
-
#focusables = [];
|
|
277
277
|
#tabIndexes = /* @__PURE__ */ new WeakMap();
|
|
278
278
|
#controller = null;
|
|
279
279
|
#isDestroyed = false;
|
|
280
|
-
constructor(source, container
|
|
280
|
+
constructor(source, container) {
|
|
281
281
|
this.#source = source;
|
|
282
282
|
this.#container = container;
|
|
283
|
-
this.#
|
|
284
|
-
this.#
|
|
285
|
-
this.#
|
|
283
|
+
this.#target = document.createElement("div");
|
|
284
|
+
this.#target.setAttribute("data-portal", "");
|
|
285
|
+
this.#target.setAttribute("tabindex", "-1");
|
|
286
286
|
this.#entranceSentinel = this.#createSentinel();
|
|
287
287
|
this.#exitSentinel = this.#createSentinel();
|
|
288
|
-
this.#focusables = getFocusables(this.#source, { composed: true });
|
|
289
288
|
this.#initialize();
|
|
290
289
|
}
|
|
291
290
|
destroy() {
|
|
@@ -295,7 +294,10 @@ var Portal = class {
|
|
|
295
294
|
this.#isDestroyed = true;
|
|
296
295
|
this.#controller?.abort();
|
|
297
296
|
this.#controller = null;
|
|
298
|
-
this.#
|
|
297
|
+
this.#getFocusables().forEach((focusable) => {
|
|
298
|
+
if (!this.#tabIndexes.has(focusable)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
299
301
|
const index = this.#tabIndexes.get(focusable);
|
|
300
302
|
if (index === null) {
|
|
301
303
|
focusable.removeAttribute("tabindex");
|
|
@@ -303,21 +305,20 @@ var Portal = class {
|
|
|
303
305
|
focusable.setAttribute("tabindex", String(index));
|
|
304
306
|
}
|
|
305
307
|
});
|
|
306
|
-
this.#focusables.length = 0;
|
|
307
308
|
this.#exitSentinel.after(this.#source);
|
|
308
|
-
this.#
|
|
309
|
+
this.#target.remove();
|
|
309
310
|
this.#entranceSentinel.remove();
|
|
310
311
|
this.#exitSentinel.remove();
|
|
311
312
|
}
|
|
312
313
|
getElement() {
|
|
313
|
-
return this.#
|
|
314
|
+
return this.#target;
|
|
314
315
|
}
|
|
315
316
|
#initialize() {
|
|
316
317
|
this.#source.before(this.#entranceSentinel);
|
|
317
318
|
this.#entranceSentinel.after(this.#exitSentinel);
|
|
318
|
-
this.#
|
|
319
|
-
this.#container.append(this.#
|
|
320
|
-
this.#
|
|
319
|
+
this.#target.append(this.#source);
|
|
320
|
+
this.#container.append(this.#target);
|
|
321
|
+
this.#getFocusables().forEach((focusable) => {
|
|
321
322
|
const index = focusable.getAttribute("tabindex")?.trim();
|
|
322
323
|
this.#tabIndexes.set(focusable, index === null ? null : Number(index));
|
|
323
324
|
focusable.setAttribute("tabindex", "-1");
|
|
@@ -343,14 +344,14 @@ var Portal = class {
|
|
|
343
344
|
if (this.#source.contains(before)) {
|
|
344
345
|
this.#focusOutside("backward");
|
|
345
346
|
} else {
|
|
346
|
-
const first = this.#
|
|
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 = this.#
|
|
354
|
+
const last = this.#getFocusables().at(-1);
|
|
354
355
|
last && focus(last);
|
|
355
356
|
}
|
|
356
357
|
}
|
|
@@ -366,16 +367,16 @@ var Portal = class {
|
|
|
366
367
|
if (!this.#source.contains(active)) {
|
|
367
368
|
return;
|
|
368
369
|
}
|
|
369
|
-
if (!this.#
|
|
370
|
+
if (!this.#getFocusables().length) {
|
|
370
371
|
event.preventDefault();
|
|
371
372
|
(event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
|
|
372
373
|
}
|
|
373
|
-
const index = this.#
|
|
374
|
+
const index = this.#getFocusables().indexOf(active);
|
|
374
375
|
if (index === -1) {
|
|
375
376
|
return;
|
|
376
377
|
}
|
|
377
378
|
event.preventDefault();
|
|
378
|
-
const focusable = this.#
|
|
379
|
+
const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
|
|
379
380
|
if (focusable) {
|
|
380
381
|
focus(focusable);
|
|
381
382
|
} else {
|
|
@@ -397,6 +398,12 @@ var Portal = class {
|
|
|
397
398
|
const focusable = direction === "backward" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
398
399
|
focusable && focus(focusable);
|
|
399
400
|
}
|
|
401
|
+
#getFocusables() {
|
|
402
|
+
return getFocusables(this.#source, {
|
|
403
|
+
composed: true,
|
|
404
|
+
include: (element) => this.#tabIndexes.has(element)
|
|
405
|
+
});
|
|
406
|
+
}
|
|
400
407
|
};
|
|
401
408
|
function focus(element) {
|
|
402
409
|
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
@@ -411,9 +418,9 @@ function getActiveElement2() {
|
|
|
411
418
|
/**
|
|
412
419
|
* Portal
|
|
413
420
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
414
|
-
* Designed for accessible dialogs, menus, overlays, popovers
|
|
421
|
+
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
415
422
|
*
|
|
416
|
-
* @version 0.0.
|
|
423
|
+
* @version 0.0.4
|
|
417
424
|
* @author Yusuke Kamiyamane
|
|
418
425
|
* @license MIT
|
|
419
426
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -425,10 +432,9 @@ power-focusable/dist/index.js:
|
|
|
425
432
|
(**
|
|
426
433
|
* Power Focusable
|
|
427
434
|
* High-precision focus management utility with full composed tree support.
|
|
428
|
-
* Handles complex focus rules including tabindex ordering, radio groups, inert
|
|
429
|
-
* and shadow DOM.
|
|
435
|
+
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
430
436
|
*
|
|
431
|
-
* @version 4.0
|
|
437
|
+
* @version 4.1.0
|
|
432
438
|
* @author Yusuke Kamiyamane
|
|
433
439
|
* @license MIT
|
|
434
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,22 +269,20 @@ 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
|
-
#focusables = [];
|
|
275
275
|
#tabIndexes = /* @__PURE__ */ new WeakMap();
|
|
276
276
|
#controller = null;
|
|
277
277
|
#isDestroyed = false;
|
|
278
|
-
constructor(source, container
|
|
278
|
+
constructor(source, container) {
|
|
279
279
|
this.#source = source;
|
|
280
280
|
this.#container = container;
|
|
281
|
-
this.#
|
|
282
|
-
this.#
|
|
283
|
-
this.#
|
|
281
|
+
this.#target = document.createElement("div");
|
|
282
|
+
this.#target.setAttribute("data-portal", "");
|
|
283
|
+
this.#target.setAttribute("tabindex", "-1");
|
|
284
284
|
this.#entranceSentinel = this.#createSentinel();
|
|
285
285
|
this.#exitSentinel = this.#createSentinel();
|
|
286
|
-
this.#focusables = getFocusables(this.#source, { composed: true });
|
|
287
286
|
this.#initialize();
|
|
288
287
|
}
|
|
289
288
|
destroy() {
|
|
@@ -293,7 +292,10 @@ var Portal = class {
|
|
|
293
292
|
this.#isDestroyed = true;
|
|
294
293
|
this.#controller?.abort();
|
|
295
294
|
this.#controller = null;
|
|
296
|
-
this.#
|
|
295
|
+
this.#getFocusables().forEach((focusable) => {
|
|
296
|
+
if (!this.#tabIndexes.has(focusable)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
297
299
|
const index = this.#tabIndexes.get(focusable);
|
|
298
300
|
if (index === null) {
|
|
299
301
|
focusable.removeAttribute("tabindex");
|
|
@@ -301,21 +303,20 @@ var Portal = class {
|
|
|
301
303
|
focusable.setAttribute("tabindex", String(index));
|
|
302
304
|
}
|
|
303
305
|
});
|
|
304
|
-
this.#focusables.length = 0;
|
|
305
306
|
this.#exitSentinel.after(this.#source);
|
|
306
|
-
this.#
|
|
307
|
+
this.#target.remove();
|
|
307
308
|
this.#entranceSentinel.remove();
|
|
308
309
|
this.#exitSentinel.remove();
|
|
309
310
|
}
|
|
310
311
|
getElement() {
|
|
311
|
-
return this.#
|
|
312
|
+
return this.#target;
|
|
312
313
|
}
|
|
313
314
|
#initialize() {
|
|
314
315
|
this.#source.before(this.#entranceSentinel);
|
|
315
316
|
this.#entranceSentinel.after(this.#exitSentinel);
|
|
316
|
-
this.#
|
|
317
|
-
this.#container.append(this.#
|
|
318
|
-
this.#
|
|
317
|
+
this.#target.append(this.#source);
|
|
318
|
+
this.#container.append(this.#target);
|
|
319
|
+
this.#getFocusables().forEach((focusable) => {
|
|
319
320
|
const index = focusable.getAttribute("tabindex")?.trim();
|
|
320
321
|
this.#tabIndexes.set(focusable, index === null ? null : Number(index));
|
|
321
322
|
focusable.setAttribute("tabindex", "-1");
|
|
@@ -341,14 +342,14 @@ var Portal = class {
|
|
|
341
342
|
if (this.#source.contains(before)) {
|
|
342
343
|
this.#focusOutside("backward");
|
|
343
344
|
} else {
|
|
344
|
-
const first = this.#
|
|
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 = this.#
|
|
352
|
+
const last = this.#getFocusables().at(-1);
|
|
352
353
|
last && focus(last);
|
|
353
354
|
}
|
|
354
355
|
}
|
|
@@ -364,16 +365,16 @@ var Portal = class {
|
|
|
364
365
|
if (!this.#source.contains(active)) {
|
|
365
366
|
return;
|
|
366
367
|
}
|
|
367
|
-
if (!this.#
|
|
368
|
+
if (!this.#getFocusables().length) {
|
|
368
369
|
event.preventDefault();
|
|
369
370
|
(event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
|
|
370
371
|
}
|
|
371
|
-
const index = this.#
|
|
372
|
+
const index = this.#getFocusables().indexOf(active);
|
|
372
373
|
if (index === -1) {
|
|
373
374
|
return;
|
|
374
375
|
}
|
|
375
376
|
event.preventDefault();
|
|
376
|
-
const focusable = this.#
|
|
377
|
+
const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
|
|
377
378
|
if (focusable) {
|
|
378
379
|
focus(focusable);
|
|
379
380
|
} else {
|
|
@@ -395,6 +396,12 @@ var Portal = class {
|
|
|
395
396
|
const focusable = direction === "backward" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
396
397
|
focusable && focus(focusable);
|
|
397
398
|
}
|
|
399
|
+
#getFocusables() {
|
|
400
|
+
return getFocusables(this.#source, {
|
|
401
|
+
composed: true,
|
|
402
|
+
include: (element) => this.#tabIndexes.has(element)
|
|
403
|
+
});
|
|
404
|
+
}
|
|
398
405
|
};
|
|
399
406
|
function focus(element) {
|
|
400
407
|
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
@@ -409,9 +416,9 @@ function getActiveElement2() {
|
|
|
409
416
|
/**
|
|
410
417
|
* Portal
|
|
411
418
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
412
|
-
* Designed for accessible dialogs, menus, overlays, popovers
|
|
419
|
+
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
413
420
|
*
|
|
414
|
-
* @version 0.0.
|
|
421
|
+
* @version 0.0.4
|
|
415
422
|
* @author Yusuke Kamiyamane
|
|
416
423
|
* @license MIT
|
|
417
424
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -423,10 +430,9 @@ power-focusable/dist/index.js:
|
|
|
423
430
|
(**
|
|
424
431
|
* Power Focusable
|
|
425
432
|
* High-precision focus management utility with full composed tree support.
|
|
426
|
-
* Handles complex focus rules including tabindex ordering, radio groups, inert
|
|
427
|
-
* and shadow DOM.
|
|
433
|
+
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
428
434
|
*
|
|
429
|
-
* @version 4.0
|
|
435
|
+
* @version 4.1.0
|
|
430
436
|
* @author Yusuke Kamiyamane
|
|
431
437
|
* @license MIT
|
|
432
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
|
},
|