@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 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 portal = createPortal(source, container);
34
- // => { element: Element; cleanup: () => void }
33
+ const cleanup = createPortal(host, container);
34
+ // => () => void
35
35
  //
36
- // source: Element
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(source, container = document.body) {
258
- if (!(source instanceof Element)) {
259
- throw new Error("Invalid source element");
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(source, container);
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
- #source;
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(source, container) {
281
- this.#source = source;
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 === null) {
295
+ if (index == null) {
303
296
  focusable.removeAttribute("tabindex");
304
297
  } else {
305
- focusable.setAttribute("tabindex", String(index));
298
+ focusable.setAttribute("tabindex", index);
306
299
  }
307
300
  });
308
- this.#exitSentinel.after(this.#source);
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.#source.before(this.#entranceSentinel);
307
+ this.#host.before(this.#entranceSentinel);
318
308
  this.#entranceSentinel.after(this.#exitSentinel);
319
- this.#target.append(this.#source);
320
- this.#container.append(this.#target);
309
+ this.#container.append(this.#host);
321
310
  this.#getFocusables().forEach((focusable) => {
322
- const index = focusable.getAttribute("tabindex")?.trim();
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.#source.contains(before)) {
345
- this.#focusOutside("backward");
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.#source.contains(before)) {
352
- this.#focusOutside("forward");
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.#source.contains(active)) {
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.#source, {
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.4
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.4
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(source: Element, container?: HTMLElement): {
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(source: Element, container: Element);
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.4
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(source: Element, container?: HTMLElement): {
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(source: Element, container: Element);
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(source, container = document.body) {
256
- if (!(source instanceof Element)) {
257
- throw new Error("Invalid source element");
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(source, container);
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
- #source;
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(source, container) {
279
- this.#source = source;
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 === null) {
293
+ if (index == null) {
301
294
  focusable.removeAttribute("tabindex");
302
295
  } else {
303
- focusable.setAttribute("tabindex", String(index));
296
+ focusable.setAttribute("tabindex", index);
304
297
  }
305
298
  });
306
- this.#exitSentinel.after(this.#source);
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.#source.before(this.#entranceSentinel);
305
+ this.#host.before(this.#entranceSentinel);
316
306
  this.#entranceSentinel.after(this.#exitSentinel);
317
- this.#target.append(this.#source);
318
- this.#container.append(this.#target);
307
+ this.#container.append(this.#host);
319
308
  this.#getFocusables().forEach((focusable) => {
320
- const index = focusable.getAttribute("tabindex")?.trim();
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.#source.contains(before)) {
343
- this.#focusOutside("backward");
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.#source.contains(before)) {
350
- this.#focusOutside("forward");
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.#source.contains(active)) {
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.#source, {
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.4
410
+ * @version 1.0.0
422
411
  * @author Yusuke Kamiyamane
423
412
  * @license MIT
424
413
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/portal",
3
- "version": "0.0.4",
3
+ "version": "1.0.0",
4
4
  "description": "Lightweight DOM portal (teleport) utility with fully focus management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",