@y14e/portal 0.1.0 → 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 -3
- package/dist/index.cjs +35 -36
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,9 +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 cleanup = createPortal(
|
|
33
|
+
const cleanup = createPortal(host, container);
|
|
34
34
|
// => () => void
|
|
35
35
|
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// host: Element
|
|
37
|
+
// container (optional): Element (default: <body>)
|
|
38
38
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -254,28 +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
|
-
if (!(
|
|
262
|
-
console.warn("Invalid
|
|
263
|
-
|
|
261
|
+
if (!(container instanceof Element)) {
|
|
262
|
+
console.warn("Invalid container element. Fallback: <body> element.");
|
|
263
|
+
container = document.body;
|
|
264
264
|
}
|
|
265
|
-
const portal = new Portal(
|
|
265
|
+
const portal = new Portal(host, container);
|
|
266
266
|
return () => portal.destroy();
|
|
267
267
|
}
|
|
268
268
|
var Portal = class {
|
|
269
|
-
#
|
|
270
|
-
#
|
|
269
|
+
#host;
|
|
270
|
+
#container;
|
|
271
271
|
#entranceSentinel;
|
|
272
272
|
#exitSentinel;
|
|
273
273
|
#tabIndexes = /* @__PURE__ */ new WeakMap();
|
|
274
274
|
#controller = null;
|
|
275
275
|
#isDestroyed = false;
|
|
276
|
-
constructor(
|
|
277
|
-
this.#
|
|
278
|
-
this.#
|
|
276
|
+
constructor(host, container) {
|
|
277
|
+
this.#host = host;
|
|
278
|
+
this.#container = container;
|
|
279
279
|
this.#entranceSentinel = this.#createSentinel();
|
|
280
280
|
this.#exitSentinel = this.#createSentinel();
|
|
281
281
|
this.#initialize();
|
|
@@ -292,24 +292,23 @@ var Portal = class {
|
|
|
292
292
|
return;
|
|
293
293
|
}
|
|
294
294
|
const index = this.#tabIndexes.get(focusable);
|
|
295
|
-
if (index
|
|
295
|
+
if (index == null) {
|
|
296
296
|
focusable.removeAttribute("tabindex");
|
|
297
297
|
} else {
|
|
298
|
-
focusable.setAttribute("tabindex",
|
|
298
|
+
focusable.setAttribute("tabindex", index);
|
|
299
299
|
}
|
|
300
300
|
});
|
|
301
|
-
this.#exitSentinel.after(this.#
|
|
301
|
+
this.#exitSentinel.after(this.#host);
|
|
302
302
|
this.#entranceSentinel.remove();
|
|
303
303
|
this.#exitSentinel.remove();
|
|
304
|
-
this.#
|
|
304
|
+
this.#host.removeAttribute("data-portal");
|
|
305
305
|
}
|
|
306
306
|
#initialize() {
|
|
307
|
-
this.#
|
|
307
|
+
this.#host.before(this.#entranceSentinel);
|
|
308
308
|
this.#entranceSentinel.after(this.#exitSentinel);
|
|
309
|
-
this.#
|
|
309
|
+
this.#container.append(this.#host);
|
|
310
310
|
this.#getFocusables().forEach((focusable) => {
|
|
311
|
-
|
|
312
|
-
this.#tabIndexes.set(focusable, index === null ? null : Number(index));
|
|
311
|
+
this.#tabIndexes.set(focusable, focusable.getAttribute("tabindex"));
|
|
313
312
|
focusable.setAttribute("tabindex", "-1");
|
|
314
313
|
});
|
|
315
314
|
this.#controller = new AbortController();
|
|
@@ -322,7 +321,7 @@ var Portal = class {
|
|
|
322
321
|
capture: true,
|
|
323
322
|
signal
|
|
324
323
|
});
|
|
325
|
-
this.#
|
|
324
|
+
this.#host.setAttribute("data-portal", "");
|
|
326
325
|
}
|
|
327
326
|
#onFocusIn = (event) => {
|
|
328
327
|
const current = event.target;
|
|
@@ -331,15 +330,15 @@ var Portal = class {
|
|
|
331
330
|
return;
|
|
332
331
|
}
|
|
333
332
|
if (current === this.#entranceSentinel) {
|
|
334
|
-
if (this.#
|
|
335
|
-
this.#
|
|
333
|
+
if (this.#host.contains(before)) {
|
|
334
|
+
this.#moveFocusOutside("previous");
|
|
336
335
|
} else {
|
|
337
336
|
const first = this.#getFocusables()[0];
|
|
338
337
|
first && focus(first);
|
|
339
338
|
}
|
|
340
339
|
} else if (current === this.#exitSentinel) {
|
|
341
|
-
if (this.#
|
|
342
|
-
this.#
|
|
340
|
+
if (this.#host.contains(before)) {
|
|
341
|
+
this.#moveFocusOutside("next");
|
|
343
342
|
} else {
|
|
344
343
|
const last = this.#getFocusables().at(-1);
|
|
345
344
|
last && focus(last);
|
|
@@ -354,7 +353,7 @@ var Portal = class {
|
|
|
354
353
|
if (!(active instanceof Element)) {
|
|
355
354
|
return;
|
|
356
355
|
}
|
|
357
|
-
if (!this.#
|
|
356
|
+
if (!this.#host.contains(active)) {
|
|
358
357
|
return;
|
|
359
358
|
}
|
|
360
359
|
if (!this.#getFocusables().length) {
|
|
@@ -380,20 +379,20 @@ var Portal = class {
|
|
|
380
379
|
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
381
380
|
return sentinel;
|
|
382
381
|
}
|
|
383
|
-
#focusOutside(direction) {
|
|
384
|
-
const options = {
|
|
385
|
-
anchor: direction === "backward" ? this.#entranceSentinel : this.#exitSentinel,
|
|
386
|
-
composed: true
|
|
387
|
-
};
|
|
388
|
-
const focusable = direction === "backward" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
389
|
-
focusable && focus(focusable);
|
|
390
|
-
}
|
|
391
382
|
#getFocusables() {
|
|
392
|
-
return getFocusables(this.#
|
|
383
|
+
return getFocusables(this.#host, {
|
|
393
384
|
composed: true,
|
|
394
385
|
include: (element) => this.#tabIndexes.has(element)
|
|
395
386
|
});
|
|
396
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
|
+
}
|
|
397
396
|
};
|
|
398
397
|
function focus(element) {
|
|
399
398
|
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
@@ -410,7 +409,7 @@ function getActiveElement2() {
|
|
|
410
409
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
411
410
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
412
411
|
*
|
|
413
|
-
* @version
|
|
412
|
+
* @version 1.0.0
|
|
414
413
|
* @author Yusuke Kamiyamane
|
|
415
414
|
* @license MIT
|
|
416
415
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
package/dist/index.d.cts
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
4
4
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
5
5
|
*
|
|
6
|
-
* @version
|
|
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(
|
|
12
|
+
declare function createPortal(host: Element, container?: HTMLElement): () => void;
|
|
13
13
|
declare class Portal {
|
|
14
14
|
#private;
|
|
15
|
-
constructor(
|
|
15
|
+
constructor(host: Element, container: Element);
|
|
16
16
|
destroy(): void;
|
|
17
17
|
}
|
|
18
18
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
4
4
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
5
5
|
*
|
|
6
|
-
* @version
|
|
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(
|
|
12
|
+
declare function createPortal(host: Element, container?: HTMLElement): () => void;
|
|
13
13
|
declare class Portal {
|
|
14
14
|
#private;
|
|
15
|
-
constructor(
|
|
15
|
+
constructor(host: Element, container: Element);
|
|
16
16
|
destroy(): void;
|
|
17
17
|
}
|
|
18
18
|
|
package/dist/index.js
CHANGED
|
@@ -252,28 +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
|
-
if (!(
|
|
260
|
-
console.warn("Invalid
|
|
261
|
-
|
|
259
|
+
if (!(container instanceof Element)) {
|
|
260
|
+
console.warn("Invalid container element. Fallback: <body> element.");
|
|
261
|
+
container = document.body;
|
|
262
262
|
}
|
|
263
|
-
const portal = new Portal(
|
|
263
|
+
const portal = new Portal(host, container);
|
|
264
264
|
return () => portal.destroy();
|
|
265
265
|
}
|
|
266
266
|
var Portal = class {
|
|
267
|
-
#
|
|
268
|
-
#
|
|
267
|
+
#host;
|
|
268
|
+
#container;
|
|
269
269
|
#entranceSentinel;
|
|
270
270
|
#exitSentinel;
|
|
271
271
|
#tabIndexes = /* @__PURE__ */ new WeakMap();
|
|
272
272
|
#controller = null;
|
|
273
273
|
#isDestroyed = false;
|
|
274
|
-
constructor(
|
|
275
|
-
this.#
|
|
276
|
-
this.#
|
|
274
|
+
constructor(host, container) {
|
|
275
|
+
this.#host = host;
|
|
276
|
+
this.#container = container;
|
|
277
277
|
this.#entranceSentinel = this.#createSentinel();
|
|
278
278
|
this.#exitSentinel = this.#createSentinel();
|
|
279
279
|
this.#initialize();
|
|
@@ -290,24 +290,23 @@ var Portal = class {
|
|
|
290
290
|
return;
|
|
291
291
|
}
|
|
292
292
|
const index = this.#tabIndexes.get(focusable);
|
|
293
|
-
if (index
|
|
293
|
+
if (index == null) {
|
|
294
294
|
focusable.removeAttribute("tabindex");
|
|
295
295
|
} else {
|
|
296
|
-
focusable.setAttribute("tabindex",
|
|
296
|
+
focusable.setAttribute("tabindex", index);
|
|
297
297
|
}
|
|
298
298
|
});
|
|
299
|
-
this.#exitSentinel.after(this.#
|
|
299
|
+
this.#exitSentinel.after(this.#host);
|
|
300
300
|
this.#entranceSentinel.remove();
|
|
301
301
|
this.#exitSentinel.remove();
|
|
302
|
-
this.#
|
|
302
|
+
this.#host.removeAttribute("data-portal");
|
|
303
303
|
}
|
|
304
304
|
#initialize() {
|
|
305
|
-
this.#
|
|
305
|
+
this.#host.before(this.#entranceSentinel);
|
|
306
306
|
this.#entranceSentinel.after(this.#exitSentinel);
|
|
307
|
-
this.#
|
|
307
|
+
this.#container.append(this.#host);
|
|
308
308
|
this.#getFocusables().forEach((focusable) => {
|
|
309
|
-
|
|
310
|
-
this.#tabIndexes.set(focusable, index === null ? null : Number(index));
|
|
309
|
+
this.#tabIndexes.set(focusable, focusable.getAttribute("tabindex"));
|
|
311
310
|
focusable.setAttribute("tabindex", "-1");
|
|
312
311
|
});
|
|
313
312
|
this.#controller = new AbortController();
|
|
@@ -320,7 +319,7 @@ var Portal = class {
|
|
|
320
319
|
capture: true,
|
|
321
320
|
signal
|
|
322
321
|
});
|
|
323
|
-
this.#
|
|
322
|
+
this.#host.setAttribute("data-portal", "");
|
|
324
323
|
}
|
|
325
324
|
#onFocusIn = (event) => {
|
|
326
325
|
const current = event.target;
|
|
@@ -329,15 +328,15 @@ var Portal = class {
|
|
|
329
328
|
return;
|
|
330
329
|
}
|
|
331
330
|
if (current === this.#entranceSentinel) {
|
|
332
|
-
if (this.#
|
|
333
|
-
this.#
|
|
331
|
+
if (this.#host.contains(before)) {
|
|
332
|
+
this.#moveFocusOutside("previous");
|
|
334
333
|
} else {
|
|
335
334
|
const first = this.#getFocusables()[0];
|
|
336
335
|
first && focus(first);
|
|
337
336
|
}
|
|
338
337
|
} else if (current === this.#exitSentinel) {
|
|
339
|
-
if (this.#
|
|
340
|
-
this.#
|
|
338
|
+
if (this.#host.contains(before)) {
|
|
339
|
+
this.#moveFocusOutside("next");
|
|
341
340
|
} else {
|
|
342
341
|
const last = this.#getFocusables().at(-1);
|
|
343
342
|
last && focus(last);
|
|
@@ -352,7 +351,7 @@ var Portal = class {
|
|
|
352
351
|
if (!(active instanceof Element)) {
|
|
353
352
|
return;
|
|
354
353
|
}
|
|
355
|
-
if (!this.#
|
|
354
|
+
if (!this.#host.contains(active)) {
|
|
356
355
|
return;
|
|
357
356
|
}
|
|
358
357
|
if (!this.#getFocusables().length) {
|
|
@@ -378,20 +377,20 @@ var Portal = class {
|
|
|
378
377
|
sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
|
|
379
378
|
return sentinel;
|
|
380
379
|
}
|
|
381
|
-
#focusOutside(direction) {
|
|
382
|
-
const options = {
|
|
383
|
-
anchor: direction === "backward" ? this.#entranceSentinel : this.#exitSentinel,
|
|
384
|
-
composed: true
|
|
385
|
-
};
|
|
386
|
-
const focusable = direction === "backward" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
|
|
387
|
-
focusable && focus(focusable);
|
|
388
|
-
}
|
|
389
380
|
#getFocusables() {
|
|
390
|
-
return getFocusables(this.#
|
|
381
|
+
return getFocusables(this.#host, {
|
|
391
382
|
composed: true,
|
|
392
383
|
include: (element) => this.#tabIndexes.has(element)
|
|
393
384
|
});
|
|
394
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
|
+
}
|
|
395
394
|
};
|
|
396
395
|
function focus(element) {
|
|
397
396
|
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
@@ -408,7 +407,7 @@ function getActiveElement2() {
|
|
|
408
407
|
* Lightweight DOM portal (teleport) utility with fully focus management.
|
|
409
408
|
* Designed for accessible dialogs, menus, overlays, popovers.
|
|
410
409
|
*
|
|
411
|
-
* @version
|
|
410
|
+
* @version 1.0.0
|
|
412
411
|
* @author Yusuke Kamiyamane
|
|
413
412
|
* @license MIT
|
|
414
413
|
* @copyright Copyright (c) Yusuke Kamiyamane
|