@y14e/portal 0.1.0 → 1.0.1

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,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(source, target);
33
+ const cleanup = createPortal(host, container);
34
34
  // => () => void
35
35
  //
36
- // source: Element
37
- // target (optional): Element (default: <body>)
36
+ // host: Element
37
+ // container (optional): Element (default: <body>)
38
38
  ```
package/dist/index.cjs CHANGED
@@ -7,9 +7,18 @@ 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, include } = options;
10
+ const { composed = false } = options;
11
+ let { filter, include } = options;
12
+ if (filter && typeof filter !== "function") {
13
+ console.warn("Invalid filter function. Fallback: undefined.");
14
+ filter = void 0;
15
+ }
16
+ if (include && typeof include !== "function") {
17
+ console.warn("Invalid include function. Fallback: undefined.");
18
+ include = void 0;
19
+ }
11
20
  const elements = [];
12
- if (composed || typeof include === "function") {
21
+ if (composed || include) {
13
22
  let traverse2 = function(node) {
14
23
  if (node instanceof Element) {
15
24
  if (isFocusable(node) || include?.(node)) {
@@ -38,7 +47,8 @@ function getFocusables(container = document.body, options = {}) {
38
47
  }
39
48
  }
40
49
  }
41
- return normalizeRadioGroup(sortByTabIndex(elements)).filter(filter);
50
+ const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
51
+ return filter ? unfiltered.filter(filter) : unfiltered;
42
52
  }
43
53
  function getNextFocusable(container = document.body, options = {}) {
44
54
  if (!(container instanceof Element)) {
@@ -84,7 +94,7 @@ function getRelativeFocusable(container, offset, options) {
84
94
  const {
85
95
  anchor = getActiveElement(),
86
96
  composed = false,
87
- filter = () => true,
97
+ filter,
88
98
  include,
89
99
  wrap = false
90
100
  } = options;
@@ -254,28 +264,28 @@ function isUngroupedRadio(element) {
254
264
 
255
265
  // src/index.ts
256
266
  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, target = document.body) {
258
- if (!(source instanceof Element)) {
259
- throw new Error("Invalid source element");
267
+ function createPortal(host, container = document.body) {
268
+ if (!(host instanceof Element)) {
269
+ throw new Error("Invalid host element");
260
270
  }
261
- if (!(target instanceof Element)) {
262
- console.warn("Invalid target element. Fallback: <body> element.");
263
- target = document.body;
271
+ if (!(container instanceof Element)) {
272
+ console.warn("Invalid container element. Fallback: <body> element.");
273
+ container = document.body;
264
274
  }
265
- const portal = new Portal(source, target);
275
+ const portal = new Portal(host, container);
266
276
  return () => portal.destroy();
267
277
  }
268
278
  var Portal = class {
269
- #source;
270
- #target;
279
+ #host;
280
+ #container;
271
281
  #entranceSentinel;
272
282
  #exitSentinel;
273
283
  #tabIndexes = /* @__PURE__ */ new WeakMap();
274
284
  #controller = null;
275
285
  #isDestroyed = false;
276
- constructor(source, target) {
277
- this.#source = source;
278
- this.#target = target;
286
+ constructor(host, container) {
287
+ this.#host = host;
288
+ this.#container = container;
279
289
  this.#entranceSentinel = this.#createSentinel();
280
290
  this.#exitSentinel = this.#createSentinel();
281
291
  this.#initialize();
@@ -292,24 +302,23 @@ var Portal = class {
292
302
  return;
293
303
  }
294
304
  const index = this.#tabIndexes.get(focusable);
295
- if (index === null) {
305
+ if (index == null) {
296
306
  focusable.removeAttribute("tabindex");
297
307
  } else {
298
- focusable.setAttribute("tabindex", String(index));
308
+ focusable.setAttribute("tabindex", index);
299
309
  }
300
310
  });
301
- this.#exitSentinel.after(this.#source);
311
+ this.#exitSentinel.after(this.#host);
302
312
  this.#entranceSentinel.remove();
303
313
  this.#exitSentinel.remove();
304
- this.#source.removeAttribute("data-portal");
314
+ this.#host.removeAttribute("data-portal");
305
315
  }
306
316
  #initialize() {
307
- this.#source.before(this.#entranceSentinel);
317
+ this.#host.before(this.#entranceSentinel);
308
318
  this.#entranceSentinel.after(this.#exitSentinel);
309
- this.#target.append(this.#source);
319
+ this.#container.append(this.#host);
310
320
  this.#getFocusables().forEach((focusable) => {
311
- const index = focusable.getAttribute("tabindex")?.trim();
312
- this.#tabIndexes.set(focusable, index === null ? null : Number(index));
321
+ this.#tabIndexes.set(focusable, focusable.getAttribute("tabindex"));
313
322
  focusable.setAttribute("tabindex", "-1");
314
323
  });
315
324
  this.#controller = new AbortController();
@@ -322,7 +331,7 @@ var Portal = class {
322
331
  capture: true,
323
332
  signal
324
333
  });
325
- this.#source.setAttribute("data-portal", "");
334
+ this.#host.setAttribute("data-portal", "");
326
335
  }
327
336
  #onFocusIn = (event) => {
328
337
  const current = event.target;
@@ -331,15 +340,15 @@ var Portal = class {
331
340
  return;
332
341
  }
333
342
  if (current === this.#entranceSentinel) {
334
- if (this.#source.contains(before)) {
335
- this.#focusOutside("backward");
343
+ if (this.#host.contains(before)) {
344
+ this.#moveFocusOutside("previous");
336
345
  } else {
337
346
  const first = this.#getFocusables()[0];
338
347
  first && focus(first);
339
348
  }
340
349
  } else if (current === this.#exitSentinel) {
341
- if (this.#source.contains(before)) {
342
- this.#focusOutside("forward");
350
+ if (this.#host.contains(before)) {
351
+ this.#moveFocusOutside("next");
343
352
  } else {
344
353
  const last = this.#getFocusables().at(-1);
345
354
  last && focus(last);
@@ -354,7 +363,7 @@ var Portal = class {
354
363
  if (!(active instanceof Element)) {
355
364
  return;
356
365
  }
357
- if (!this.#source.contains(active)) {
366
+ if (!this.#host.contains(active)) {
358
367
  return;
359
368
  }
360
369
  if (!this.#getFocusables().length) {
@@ -380,20 +389,20 @@ var Portal = class {
380
389
  sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
381
390
  return sentinel;
382
391
  }
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
392
  #getFocusables() {
392
- return getFocusables(this.#source, {
393
+ return getFocusables(this.#host, {
393
394
  composed: true,
394
395
  include: (element) => this.#tabIndexes.has(element)
395
396
  });
396
397
  }
398
+ #moveFocusOutside(direction) {
399
+ const options = {
400
+ anchor: direction === "previous" ? this.#entranceSentinel : this.#exitSentinel,
401
+ composed: true
402
+ };
403
+ const focusable = direction === "previous" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
404
+ focusable && focus(focusable);
405
+ }
397
406
  };
398
407
  function focus(element) {
399
408
  "focus" in element && typeof element.focus === "function" && element.focus();
@@ -410,7 +419,7 @@ function getActiveElement2() {
410
419
  * Lightweight DOM portal (teleport) utility with fully focus management.
411
420
  * Designed for accessible dialogs, menus, overlays, popovers.
412
421
  *
413
- * @version 0.1.0
422
+ * @version 1.0.1
414
423
  * @author Yusuke Kamiyamane
415
424
  * @license MIT
416
425
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -424,7 +433,7 @@ power-focusable/dist/index.js:
424
433
  * High-precision focus management utility with full composed tree support.
425
434
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
426
435
  *
427
- * @version 4.1.0
436
+ * @version 4.1.2
428
437
  * @author Yusuke Kamiyamane
429
438
  * @license MIT
430
439
  * @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 0.1.0
6
+ * @version 1.0.1
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, target?: HTMLElement): () => void;
12
+ declare function createPortal(host: Element, container?: HTMLElement): () => void;
13
13
  declare class Portal {
14
14
  #private;
15
- constructor(source: Element, target: Element);
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 0.1.0
6
+ * @version 1.0.1
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, target?: HTMLElement): () => void;
12
+ declare function createPortal(host: Element, container?: HTMLElement): () => void;
13
13
  declare class Portal {
14
14
  #private;
15
- constructor(source: Element, target: Element);
15
+ constructor(host: Element, container: Element);
16
16
  destroy(): void;
17
17
  }
18
18
 
package/dist/index.js CHANGED
@@ -5,9 +5,18 @@ 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, include } = options;
8
+ const { composed = false } = options;
9
+ let { filter, include } = options;
10
+ if (filter && typeof filter !== "function") {
11
+ console.warn("Invalid filter function. Fallback: undefined.");
12
+ filter = void 0;
13
+ }
14
+ if (include && typeof include !== "function") {
15
+ console.warn("Invalid include function. Fallback: undefined.");
16
+ include = void 0;
17
+ }
9
18
  const elements = [];
10
- if (composed || typeof include === "function") {
19
+ if (composed || include) {
11
20
  let traverse2 = function(node) {
12
21
  if (node instanceof Element) {
13
22
  if (isFocusable(node) || include?.(node)) {
@@ -36,7 +45,8 @@ function getFocusables(container = document.body, options = {}) {
36
45
  }
37
46
  }
38
47
  }
39
- return normalizeRadioGroup(sortByTabIndex(elements)).filter(filter);
48
+ const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
49
+ return filter ? unfiltered.filter(filter) : unfiltered;
40
50
  }
41
51
  function getNextFocusable(container = document.body, options = {}) {
42
52
  if (!(container instanceof Element)) {
@@ -82,7 +92,7 @@ function getRelativeFocusable(container, offset, options) {
82
92
  const {
83
93
  anchor = getActiveElement(),
84
94
  composed = false,
85
- filter = () => true,
95
+ filter,
86
96
  include,
87
97
  wrap = false
88
98
  } = options;
@@ -252,28 +262,28 @@ function isUngroupedRadio(element) {
252
262
 
253
263
  // src/index.ts
254
264
  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, target = document.body) {
256
- if (!(source instanceof Element)) {
257
- throw new Error("Invalid source element");
265
+ function createPortal(host, container = document.body) {
266
+ if (!(host instanceof Element)) {
267
+ throw new Error("Invalid host element");
258
268
  }
259
- if (!(target instanceof Element)) {
260
- console.warn("Invalid target element. Fallback: <body> element.");
261
- target = document.body;
269
+ if (!(container instanceof Element)) {
270
+ console.warn("Invalid container element. Fallback: <body> element.");
271
+ container = document.body;
262
272
  }
263
- const portal = new Portal(source, target);
273
+ const portal = new Portal(host, container);
264
274
  return () => portal.destroy();
265
275
  }
266
276
  var Portal = class {
267
- #source;
268
- #target;
277
+ #host;
278
+ #container;
269
279
  #entranceSentinel;
270
280
  #exitSentinel;
271
281
  #tabIndexes = /* @__PURE__ */ new WeakMap();
272
282
  #controller = null;
273
283
  #isDestroyed = false;
274
- constructor(source, target) {
275
- this.#source = source;
276
- this.#target = target;
284
+ constructor(host, container) {
285
+ this.#host = host;
286
+ this.#container = container;
277
287
  this.#entranceSentinel = this.#createSentinel();
278
288
  this.#exitSentinel = this.#createSentinel();
279
289
  this.#initialize();
@@ -290,24 +300,23 @@ var Portal = class {
290
300
  return;
291
301
  }
292
302
  const index = this.#tabIndexes.get(focusable);
293
- if (index === null) {
303
+ if (index == null) {
294
304
  focusable.removeAttribute("tabindex");
295
305
  } else {
296
- focusable.setAttribute("tabindex", String(index));
306
+ focusable.setAttribute("tabindex", index);
297
307
  }
298
308
  });
299
- this.#exitSentinel.after(this.#source);
309
+ this.#exitSentinel.after(this.#host);
300
310
  this.#entranceSentinel.remove();
301
311
  this.#exitSentinel.remove();
302
- this.#source.removeAttribute("data-portal");
312
+ this.#host.removeAttribute("data-portal");
303
313
  }
304
314
  #initialize() {
305
- this.#source.before(this.#entranceSentinel);
315
+ this.#host.before(this.#entranceSentinel);
306
316
  this.#entranceSentinel.after(this.#exitSentinel);
307
- this.#target.append(this.#source);
317
+ this.#container.append(this.#host);
308
318
  this.#getFocusables().forEach((focusable) => {
309
- const index = focusable.getAttribute("tabindex")?.trim();
310
- this.#tabIndexes.set(focusable, index === null ? null : Number(index));
319
+ this.#tabIndexes.set(focusable, focusable.getAttribute("tabindex"));
311
320
  focusable.setAttribute("tabindex", "-1");
312
321
  });
313
322
  this.#controller = new AbortController();
@@ -320,7 +329,7 @@ var Portal = class {
320
329
  capture: true,
321
330
  signal
322
331
  });
323
- this.#source.setAttribute("data-portal", "");
332
+ this.#host.setAttribute("data-portal", "");
324
333
  }
325
334
  #onFocusIn = (event) => {
326
335
  const current = event.target;
@@ -329,15 +338,15 @@ var Portal = class {
329
338
  return;
330
339
  }
331
340
  if (current === this.#entranceSentinel) {
332
- if (this.#source.contains(before)) {
333
- this.#focusOutside("backward");
341
+ if (this.#host.contains(before)) {
342
+ this.#moveFocusOutside("previous");
334
343
  } else {
335
344
  const first = this.#getFocusables()[0];
336
345
  first && focus(first);
337
346
  }
338
347
  } else if (current === this.#exitSentinel) {
339
- if (this.#source.contains(before)) {
340
- this.#focusOutside("forward");
348
+ if (this.#host.contains(before)) {
349
+ this.#moveFocusOutside("next");
341
350
  } else {
342
351
  const last = this.#getFocusables().at(-1);
343
352
  last && focus(last);
@@ -352,7 +361,7 @@ var Portal = class {
352
361
  if (!(active instanceof Element)) {
353
362
  return;
354
363
  }
355
- if (!this.#source.contains(active)) {
364
+ if (!this.#host.contains(active)) {
356
365
  return;
357
366
  }
358
367
  if (!this.#getFocusables().length) {
@@ -378,20 +387,20 @@ var Portal = class {
378
387
  sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
379
388
  return sentinel;
380
389
  }
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
390
  #getFocusables() {
390
- return getFocusables(this.#source, {
391
+ return getFocusables(this.#host, {
391
392
  composed: true,
392
393
  include: (element) => this.#tabIndexes.has(element)
393
394
  });
394
395
  }
396
+ #moveFocusOutside(direction) {
397
+ const options = {
398
+ anchor: direction === "previous" ? this.#entranceSentinel : this.#exitSentinel,
399
+ composed: true
400
+ };
401
+ const focusable = direction === "previous" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
402
+ focusable && focus(focusable);
403
+ }
395
404
  };
396
405
  function focus(element) {
397
406
  "focus" in element && typeof element.focus === "function" && element.focus();
@@ -408,7 +417,7 @@ function getActiveElement2() {
408
417
  * Lightweight DOM portal (teleport) utility with fully focus management.
409
418
  * Designed for accessible dialogs, menus, overlays, popovers.
410
419
  *
411
- * @version 0.1.0
420
+ * @version 1.0.1
412
421
  * @author Yusuke Kamiyamane
413
422
  * @license MIT
414
423
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -422,7 +431,7 @@ power-focusable/dist/index.js:
422
431
  * High-precision focus management utility with full composed tree support.
423
432
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
424
433
  *
425
- * @version 4.1.0
434
+ * @version 4.1.2
426
435
  * @author Yusuke Kamiyamane
427
436
  * @license MIT
428
437
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/portal",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
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.1.0",
51
+ "power-focusable": "^4.1.2",
52
52
  "tsup": "^8.0.0",
53
53
  "typescript": "^5.6.0"
54
54
  },