@y14e/portal 0.0.4 → 1.0.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 +3 -9
- package/dist/index.cjs +31 -42
- package/dist/index.d.cts +3 -7
- package/dist/index.d.ts +3 -7
- package/dist/index.js +31 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,15 +30,9 @@ import { createPortal } from 'https://unpkg.com/@y14e/portal/dist/index.js';
|
|
|
30
30
|
Creates a portal and preserves keyboard focus order between the original DOM and the portal.
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
const
|
|
34
|
-
// =>
|
|
33
|
+
const cleanup = createPortal(host, container);
|
|
34
|
+
// => () => void
|
|
35
35
|
//
|
|
36
|
-
//
|
|
36
|
+
// host: Element
|
|
37
37
|
// container (optional): Element (default: <body>)
|
|
38
|
-
|
|
39
|
-
// Element
|
|
40
|
-
console.log(portal.element);
|
|
41
|
-
|
|
42
|
-
// Cleanup
|
|
43
|
-
portal.cleanup();
|
|
44
38
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -254,35 +254,28 @@ function isUngroupedRadio(element) {
|
|
|
254
254
|
|
|
255
255
|
// src/index.ts
|
|
256
256
|
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;`;
|
|
257
|
-
function createPortal(
|
|
258
|
-
if (!(
|
|
259
|
-
throw new Error("Invalid
|
|
257
|
+
function createPortal(host, container = document.body) {
|
|
258
|
+
if (!(host instanceof Element)) {
|
|
259
|
+
throw new Error("Invalid host element");
|
|
260
260
|
}
|
|
261
261
|
if (!(container instanceof Element)) {
|
|
262
262
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
263
263
|
container = document.body;
|
|
264
264
|
}
|
|
265
|
-
const portal = new Portal(
|
|
266
|
-
return
|
|
267
|
-
element: portal.getElement(),
|
|
268
|
-
cleanup: () => portal.destroy()
|
|
269
|
-
};
|
|
265
|
+
const portal = new Portal(host, container);
|
|
266
|
+
return () => portal.destroy();
|
|
270
267
|
}
|
|
271
268
|
var Portal = class {
|
|
272
|
-
#
|
|
269
|
+
#host;
|
|
273
270
|
#container;
|
|
274
|
-
#target;
|
|
275
271
|
#entranceSentinel;
|
|
276
272
|
#exitSentinel;
|
|
277
273
|
#tabIndexes = /* @__PURE__ */ new WeakMap();
|
|
278
274
|
#controller = null;
|
|
279
275
|
#isDestroyed = false;
|
|
280
|
-
constructor(
|
|
281
|
-
this.#
|
|
276
|
+
constructor(host, container) {
|
|
277
|
+
this.#host = host;
|
|
282
278
|
this.#container = container;
|
|
283
|
-
this.#target = document.createElement("div");
|
|
284
|
-
this.#target.setAttribute("data-portal", "");
|
|
285
|
-
this.#target.setAttribute("tabindex", "-1");
|
|
286
279
|
this.#entranceSentinel = this.#createSentinel();
|
|
287
280
|
this.#exitSentinel = this.#createSentinel();
|
|
288
281
|
this.#initialize();
|
|
@@ -299,28 +292,23 @@ var Portal = class {
|
|
|
299
292
|
return;
|
|
300
293
|
}
|
|
301
294
|
const index = this.#tabIndexes.get(focusable);
|
|
302
|
-
if (index
|
|
295
|
+
if (index == null) {
|
|
303
296
|
focusable.removeAttribute("tabindex");
|
|
304
297
|
} else {
|
|
305
|
-
focusable.setAttribute("tabindex",
|
|
298
|
+
focusable.setAttribute("tabindex", index);
|
|
306
299
|
}
|
|
307
300
|
});
|
|
308
|
-
this.#exitSentinel.after(this.#
|
|
309
|
-
this.#target.remove();
|
|
301
|
+
this.#exitSentinel.after(this.#host);
|
|
310
302
|
this.#entranceSentinel.remove();
|
|
311
303
|
this.#exitSentinel.remove();
|
|
312
|
-
|
|
313
|
-
getElement() {
|
|
314
|
-
return this.#target;
|
|
304
|
+
this.#host.removeAttribute("data-portal");
|
|
315
305
|
}
|
|
316
306
|
#initialize() {
|
|
317
|
-
this.#
|
|
307
|
+
this.#host.before(this.#entranceSentinel);
|
|
318
308
|
this.#entranceSentinel.after(this.#exitSentinel);
|
|
319
|
-
this.#
|
|
320
|
-
this.#container.append(this.#target);
|
|
309
|
+
this.#container.append(this.#host);
|
|
321
310
|
this.#getFocusables().forEach((focusable) => {
|
|
322
|
-
|
|
323
|
-
this.#tabIndexes.set(focusable, index === null ? null : Number(index));
|
|
311
|
+
this.#tabIndexes.set(focusable, focusable.getAttribute("tabindex"));
|
|
324
312
|
focusable.setAttribute("tabindex", "-1");
|
|
325
313
|
});
|
|
326
314
|
this.#controller = new AbortController();
|
|
@@ -333,6 +321,7 @@ var Portal = class {
|
|
|
333
321
|
capture: true,
|
|
334
322
|
signal
|
|
335
323
|
});
|
|
324
|
+
this.#host.setAttribute("data-portal", "");
|
|
336
325
|
}
|
|
337
326
|
#onFocusIn = (event) => {
|
|
338
327
|
const current = event.target;
|
|
@@ -341,15 +330,15 @@ var Portal = class {
|
|
|
341
330
|
return;
|
|
342
331
|
}
|
|
343
332
|
if (current === this.#entranceSentinel) {
|
|
344
|
-
if (this.#
|
|
345
|
-
this.#
|
|
333
|
+
if (this.#host.contains(before)) {
|
|
334
|
+
this.#moveFocusOutside("previous");
|
|
346
335
|
} else {
|
|
347
336
|
const first = this.#getFocusables()[0];
|
|
348
337
|
first && focus(first);
|
|
349
338
|
}
|
|
350
339
|
} else if (current === this.#exitSentinel) {
|
|
351
|
-
if (this.#
|
|
352
|
-
this.#
|
|
340
|
+
if (this.#host.contains(before)) {
|
|
341
|
+
this.#moveFocusOutside("next");
|
|
353
342
|
} else {
|
|
354
343
|
const last = this.#getFocusables().at(-1);
|
|
355
344
|
last && focus(last);
|
|
@@ -364,7 +353,7 @@ var Portal = class {
|
|
|
364
353
|
if (!(active instanceof Element)) {
|
|
365
354
|
return;
|
|
366
355
|
}
|
|
367
|
-
if (!this.#
|
|
356
|
+
if (!this.#host.contains(active)) {
|
|
368
357
|
return;
|
|
369
358
|
}
|
|
370
359
|
if (!this.#getFocusables().length) {
|
|
@@ -390,20 +379,20 @@ var Portal = class {
|
|
|
390
379
|
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
391
380
|
return sentinel;
|
|
392
381
|
}
|
|
393
|
-
#focusOutside(direction) {
|
|
394
|
-
const options = {
|
|
395
|
-
anchor: direction === "backward" ? this.#entranceSentinel : this.#exitSentinel,
|
|
396
|
-
composed: true
|
|
397
|
-
};
|
|
398
|
-
const focusable = direction === "backward" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
399
|
-
focusable && focus(focusable);
|
|
400
|
-
}
|
|
401
382
|
#getFocusables() {
|
|
402
|
-
return getFocusables(this.#
|
|
383
|
+
return getFocusables(this.#host, {
|
|
403
384
|
composed: true,
|
|
404
385
|
include: (element) => this.#tabIndexes.has(element)
|
|
405
386
|
});
|
|
406
387
|
}
|
|
388
|
+
#moveFocusOutside(direction) {
|
|
389
|
+
const options = {
|
|
390
|
+
anchor: direction === "previous" ? this.#entranceSentinel : this.#exitSentinel,
|
|
391
|
+
composed: true
|
|
392
|
+
};
|
|
393
|
+
const focusable = direction === "previous" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
394
|
+
focusable && focus(focusable);
|
|
395
|
+
}
|
|
407
396
|
};
|
|
408
397
|
function focus(element) {
|
|
409
398
|
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
@@ -420,7 +409,7 @@ function getActiveElement2() {
|
|
|
420
409
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
421
410
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
422
411
|
*
|
|
423
|
-
* @version 0.0
|
|
412
|
+
* @version 1.0.0
|
|
424
413
|
* @author Yusuke Kamiyamane
|
|
425
414
|
* @license MIT
|
|
426
415
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.d.cts
CHANGED
|
@@ -3,21 +3,17 @@
|
|
|
3
3
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
4
4
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
5
5
|
*
|
|
6
|
-
* @version 0.0
|
|
6
|
+
* @version 1.0.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
|
-
declare function createPortal(
|
|
13
|
-
element: Element;
|
|
14
|
-
cleanup: () => void;
|
|
15
|
-
};
|
|
12
|
+
declare function createPortal(host: Element, container?: HTMLElement): () => void;
|
|
16
13
|
declare class Portal {
|
|
17
14
|
#private;
|
|
18
|
-
constructor(
|
|
15
|
+
constructor(host: Element, container: Element);
|
|
19
16
|
destroy(): void;
|
|
20
|
-
getElement(): HTMLElement;
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
export { Portal, createPortal };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,21 +3,17 @@
|
|
|
3
3
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
4
4
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
5
5
|
*
|
|
6
|
-
* @version 0.0
|
|
6
|
+
* @version 1.0.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
|
-
declare function createPortal(
|
|
13
|
-
element: Element;
|
|
14
|
-
cleanup: () => void;
|
|
15
|
-
};
|
|
12
|
+
declare function createPortal(host: Element, container?: HTMLElement): () => void;
|
|
16
13
|
declare class Portal {
|
|
17
14
|
#private;
|
|
18
|
-
constructor(
|
|
15
|
+
constructor(host: Element, container: Element);
|
|
19
16
|
destroy(): void;
|
|
20
|
-
getElement(): HTMLElement;
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
export { Portal, createPortal };
|
package/dist/index.js
CHANGED
|
@@ -252,35 +252,28 @@ function isUngroupedRadio(element) {
|
|
|
252
252
|
|
|
253
253
|
// src/index.ts
|
|
254
254
|
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;`;
|
|
255
|
-
function createPortal(
|
|
256
|
-
if (!(
|
|
257
|
-
throw new Error("Invalid
|
|
255
|
+
function createPortal(host, container = document.body) {
|
|
256
|
+
if (!(host instanceof Element)) {
|
|
257
|
+
throw new Error("Invalid host element");
|
|
258
258
|
}
|
|
259
259
|
if (!(container instanceof Element)) {
|
|
260
260
|
console.warn("Invalid container element. Fallback: <body> element.");
|
|
261
261
|
container = document.body;
|
|
262
262
|
}
|
|
263
|
-
const portal = new Portal(
|
|
264
|
-
return
|
|
265
|
-
element: portal.getElement(),
|
|
266
|
-
cleanup: () => portal.destroy()
|
|
267
|
-
};
|
|
263
|
+
const portal = new Portal(host, container);
|
|
264
|
+
return () => portal.destroy();
|
|
268
265
|
}
|
|
269
266
|
var Portal = class {
|
|
270
|
-
#
|
|
267
|
+
#host;
|
|
271
268
|
#container;
|
|
272
|
-
#target;
|
|
273
269
|
#entranceSentinel;
|
|
274
270
|
#exitSentinel;
|
|
275
271
|
#tabIndexes = /* @__PURE__ */ new WeakMap();
|
|
276
272
|
#controller = null;
|
|
277
273
|
#isDestroyed = false;
|
|
278
|
-
constructor(
|
|
279
|
-
this.#
|
|
274
|
+
constructor(host, container) {
|
|
275
|
+
this.#host = host;
|
|
280
276
|
this.#container = container;
|
|
281
|
-
this.#target = document.createElement("div");
|
|
282
|
-
this.#target.setAttribute("data-portal", "");
|
|
283
|
-
this.#target.setAttribute("tabindex", "-1");
|
|
284
277
|
this.#entranceSentinel = this.#createSentinel();
|
|
285
278
|
this.#exitSentinel = this.#createSentinel();
|
|
286
279
|
this.#initialize();
|
|
@@ -297,28 +290,23 @@ var Portal = class {
|
|
|
297
290
|
return;
|
|
298
291
|
}
|
|
299
292
|
const index = this.#tabIndexes.get(focusable);
|
|
300
|
-
if (index
|
|
293
|
+
if (index == null) {
|
|
301
294
|
focusable.removeAttribute("tabindex");
|
|
302
295
|
} else {
|
|
303
|
-
focusable.setAttribute("tabindex",
|
|
296
|
+
focusable.setAttribute("tabindex", index);
|
|
304
297
|
}
|
|
305
298
|
});
|
|
306
|
-
this.#exitSentinel.after(this.#
|
|
307
|
-
this.#target.remove();
|
|
299
|
+
this.#exitSentinel.after(this.#host);
|
|
308
300
|
this.#entranceSentinel.remove();
|
|
309
301
|
this.#exitSentinel.remove();
|
|
310
|
-
|
|
311
|
-
getElement() {
|
|
312
|
-
return this.#target;
|
|
302
|
+
this.#host.removeAttribute("data-portal");
|
|
313
303
|
}
|
|
314
304
|
#initialize() {
|
|
315
|
-
this.#
|
|
305
|
+
this.#host.before(this.#entranceSentinel);
|
|
316
306
|
this.#entranceSentinel.after(this.#exitSentinel);
|
|
317
|
-
this.#
|
|
318
|
-
this.#container.append(this.#target);
|
|
307
|
+
this.#container.append(this.#host);
|
|
319
308
|
this.#getFocusables().forEach((focusable) => {
|
|
320
|
-
|
|
321
|
-
this.#tabIndexes.set(focusable, index === null ? null : Number(index));
|
|
309
|
+
this.#tabIndexes.set(focusable, focusable.getAttribute("tabindex"));
|
|
322
310
|
focusable.setAttribute("tabindex", "-1");
|
|
323
311
|
});
|
|
324
312
|
this.#controller = new AbortController();
|
|
@@ -331,6 +319,7 @@ var Portal = class {
|
|
|
331
319
|
capture: true,
|
|
332
320
|
signal
|
|
333
321
|
});
|
|
322
|
+
this.#host.setAttribute("data-portal", "");
|
|
334
323
|
}
|
|
335
324
|
#onFocusIn = (event) => {
|
|
336
325
|
const current = event.target;
|
|
@@ -339,15 +328,15 @@ var Portal = class {
|
|
|
339
328
|
return;
|
|
340
329
|
}
|
|
341
330
|
if (current === this.#entranceSentinel) {
|
|
342
|
-
if (this.#
|
|
343
|
-
this.#
|
|
331
|
+
if (this.#host.contains(before)) {
|
|
332
|
+
this.#moveFocusOutside("previous");
|
|
344
333
|
} else {
|
|
345
334
|
const first = this.#getFocusables()[0];
|
|
346
335
|
first && focus(first);
|
|
347
336
|
}
|
|
348
337
|
} else if (current === this.#exitSentinel) {
|
|
349
|
-
if (this.#
|
|
350
|
-
this.#
|
|
338
|
+
if (this.#host.contains(before)) {
|
|
339
|
+
this.#moveFocusOutside("next");
|
|
351
340
|
} else {
|
|
352
341
|
const last = this.#getFocusables().at(-1);
|
|
353
342
|
last && focus(last);
|
|
@@ -362,7 +351,7 @@ var Portal = class {
|
|
|
362
351
|
if (!(active instanceof Element)) {
|
|
363
352
|
return;
|
|
364
353
|
}
|
|
365
|
-
if (!this.#
|
|
354
|
+
if (!this.#host.contains(active)) {
|
|
366
355
|
return;
|
|
367
356
|
}
|
|
368
357
|
if (!this.#getFocusables().length) {
|
|
@@ -388,20 +377,20 @@ var Portal = class {
|
|
|
388
377
|
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
389
378
|
return sentinel;
|
|
390
379
|
}
|
|
391
|
-
#focusOutside(direction) {
|
|
392
|
-
const options = {
|
|
393
|
-
anchor: direction === "backward" ? this.#entranceSentinel : this.#exitSentinel,
|
|
394
|
-
composed: true
|
|
395
|
-
};
|
|
396
|
-
const focusable = direction === "backward" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
397
|
-
focusable && focus(focusable);
|
|
398
|
-
}
|
|
399
380
|
#getFocusables() {
|
|
400
|
-
return getFocusables(this.#
|
|
381
|
+
return getFocusables(this.#host, {
|
|
401
382
|
composed: true,
|
|
402
383
|
include: (element) => this.#tabIndexes.has(element)
|
|
403
384
|
});
|
|
404
385
|
}
|
|
386
|
+
#moveFocusOutside(direction) {
|
|
387
|
+
const options = {
|
|
388
|
+
anchor: direction === "previous" ? this.#entranceSentinel : this.#exitSentinel,
|
|
389
|
+
composed: true
|
|
390
|
+
};
|
|
391
|
+
const focusable = direction === "previous" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
392
|
+
focusable && focus(focusable);
|
|
393
|
+
}
|
|
405
394
|
};
|
|
406
395
|
function focus(element) {
|
|
407
396
|
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
@@ -418,7 +407,7 @@ function getActiveElement2() {
|
|
|
418
407
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
419
408
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
420
409
|
*
|
|
421
|
-
* @version 0.0
|
|
410
|
+
* @version 1.0.0
|
|
422
411
|
* @author Yusuke Kamiyamane
|
|
423
412
|
* @license MIT
|
|
424
413
|
* @copyright Copyright (c) Yusuke Kamiyamane
|