@y14e/portal 0.0.2 → 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 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, and etc.
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).
@@ -18,7 +18,7 @@ import { createPortal } from '@y14e/portal';
18
18
  // CDNs
19
19
  import { createPortal } from 'https://esm.sh/@y14e/portal'
20
20
  // or
21
- import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal/dist/index.js';
21
+ import { createPortal } from 'https://cdn.jsdelivr.net/npm/@y14e/portal/+esm';
22
22
  // or
23
23
  import { createPortal } from 'https://unpkg.com/@y14e/portal/dist/index.js';
24
24
  ```
@@ -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 flow between the original DOM position and the portal.
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, cleanup: () => void }
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,18 +271,18 @@ function createPortal(source, container = document.body) {
270
271
  var Portal = class {
271
272
  #source;
272
273
  #container;
273
- #portal;
274
+ #target;
274
275
  #entranceSentinel;
275
276
  #exitSentinel;
276
277
  #tabIndexes = /* @__PURE__ */ new WeakMap();
277
278
  #controller = null;
278
279
  #isDestroyed = false;
279
- constructor(source, container = document.body) {
280
+ constructor(source, container) {
280
281
  this.#source = source;
281
282
  this.#container = container;
282
- this.#portal = document.createElement("div");
283
- this.#portal.setAttribute("data-portal", "");
284
- this.#portal.setAttribute("tabindex", "-1");
283
+ this.#target = document.createElement("div");
284
+ this.#target.setAttribute("data-portal", "");
285
+ this.#target.setAttribute("tabindex", "-1");
285
286
  this.#entranceSentinel = this.#createSentinel();
286
287
  this.#exitSentinel = this.#createSentinel();
287
288
  this.#initialize();
@@ -294,6 +295,9 @@ var Portal = class {
294
295
  this.#controller?.abort();
295
296
  this.#controller = null;
296
297
  this.#getFocusables().forEach((focusable) => {
298
+ if (!this.#tabIndexes.has(focusable)) {
299
+ return;
300
+ }
297
301
  const index = this.#tabIndexes.get(focusable);
298
302
  if (index === null) {
299
303
  focusable.removeAttribute("tabindex");
@@ -302,25 +306,23 @@ var Portal = class {
302
306
  }
303
307
  });
304
308
  this.#exitSentinel.after(this.#source);
305
- this.#portal.remove();
309
+ this.#target.remove();
306
310
  this.#entranceSentinel.remove();
307
311
  this.#exitSentinel.remove();
308
312
  }
309
313
  getElement() {
310
- return this.#portal;
314
+ return this.#target;
311
315
  }
312
316
  #initialize() {
313
317
  this.#source.before(this.#entranceSentinel);
314
318
  this.#entranceSentinel.after(this.#exitSentinel);
315
- this.#portal.append(this.#source);
316
- this.#container.append(this.#portal);
317
- getFocusables(this.#source, { composed: true }).forEach(
318
- (focusable) => {
319
- const index = focusable.getAttribute("tabindex")?.trim();
320
- this.#tabIndexes.set(focusable, index === null ? null : Number(index));
321
- focusable.setAttribute("tabindex", "-1");
322
- }
323
- );
319
+ this.#target.append(this.#source);
320
+ this.#container.append(this.#target);
321
+ this.#getFocusables().forEach((focusable) => {
322
+ const index = focusable.getAttribute("tabindex")?.trim();
323
+ this.#tabIndexes.set(focusable, index === null ? null : Number(index));
324
+ focusable.setAttribute("tabindex", "-1");
325
+ });
324
326
  this.#controller = new AbortController();
325
327
  const { signal } = this.#controller;
326
328
  document.addEventListener("focusin", this.#onFocusIn, {
@@ -338,19 +340,18 @@ var Portal = class {
338
340
  if (!(before instanceof Element)) {
339
341
  return;
340
342
  }
341
- const focusables = this.#getFocusables();
342
343
  if (current === this.#entranceSentinel) {
343
344
  if (this.#source.contains(before)) {
344
345
  this.#focusOutside("backward");
345
346
  } else {
346
- const first = focusables[0];
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 = focusables.at(-1);
354
+ const last = this.#getFocusables().at(-1);
354
355
  last && focus(last);
355
356
  }
356
357
  }
@@ -366,17 +367,16 @@ var Portal = class {
366
367
  if (!this.#source.contains(active)) {
367
368
  return;
368
369
  }
369
- const focusables = this.#getFocusables();
370
- if (!focusables.length) {
370
+ if (!this.#getFocusables().length) {
371
371
  event.preventDefault();
372
372
  (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
373
373
  }
374
- const index = focusables.indexOf(active);
374
+ const index = this.#getFocusables().indexOf(active);
375
375
  if (index === -1) {
376
376
  return;
377
377
  }
378
378
  event.preventDefault();
379
- const focusable = focusables[index + (event.shiftKey ? -1 : 1)];
379
+ const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
380
380
  if (focusable) {
381
381
  focus(focusable);
382
382
  } else {
@@ -399,7 +399,10 @@ var Portal = class {
399
399
  focusable && focus(focusable);
400
400
  }
401
401
  #getFocusables() {
402
- return getFocusables(this.#source, { composed: true });
402
+ return getFocusables(this.#source, {
403
+ composed: true,
404
+ include: (element) => this.#tabIndexes.has(element)
405
+ });
403
406
  }
404
407
  };
405
408
  function focus(element) {
@@ -415,9 +418,9 @@ function getActiveElement2() {
415
418
  /**
416
419
  * Portal
417
420
  * Lightweight DOM portal (teleport) utility with fully focus management.
418
- * Designed for accessible dialogs, menus, overlays, popovers, and etc.
421
+ * Designed for accessible dialogs, menus, overlays, popovers.
419
422
  *
420
- * @version 0.0.2
423
+ * @version 0.0.4
421
424
  * @author Yusuke Kamiyamane
422
425
  * @license MIT
423
426
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -429,10 +432,9 @@ power-focusable/dist/index.js:
429
432
  (**
430
433
  * Power Focusable
431
434
  * High-precision focus management utility with full composed tree support.
432
- * Handles complex focus rules including tabindex ordering, radio groups, inert,
433
- * and shadow DOM.
435
+ * Handles complex focus rules including tabindex ordering, radio groups, inert.
434
436
  *
435
- * @version 4.0.2
437
+ * @version 4.1.0
436
438
  * @author Yusuke Kamiyamane
437
439
  * @license MIT
438
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, and etc.
4
+ * Designed for accessible dialogs, menus, overlays, popovers.
5
5
  *
6
- * @version 0.0.2
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?: HTMLElement);
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, and etc.
4
+ * Designed for accessible dialogs, menus, overlays, popovers.
5
5
  *
6
- * @version 0.0.2
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?: HTMLElement);
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,18 +269,18 @@ function createPortal(source, container = document.body) {
268
269
  var Portal = class {
269
270
  #source;
270
271
  #container;
271
- #portal;
272
+ #target;
272
273
  #entranceSentinel;
273
274
  #exitSentinel;
274
275
  #tabIndexes = /* @__PURE__ */ new WeakMap();
275
276
  #controller = null;
276
277
  #isDestroyed = false;
277
- constructor(source, container = document.body) {
278
+ constructor(source, container) {
278
279
  this.#source = source;
279
280
  this.#container = container;
280
- this.#portal = document.createElement("div");
281
- this.#portal.setAttribute("data-portal", "");
282
- this.#portal.setAttribute("tabindex", "-1");
281
+ this.#target = document.createElement("div");
282
+ this.#target.setAttribute("data-portal", "");
283
+ this.#target.setAttribute("tabindex", "-1");
283
284
  this.#entranceSentinel = this.#createSentinel();
284
285
  this.#exitSentinel = this.#createSentinel();
285
286
  this.#initialize();
@@ -292,6 +293,9 @@ var Portal = class {
292
293
  this.#controller?.abort();
293
294
  this.#controller = null;
294
295
  this.#getFocusables().forEach((focusable) => {
296
+ if (!this.#tabIndexes.has(focusable)) {
297
+ return;
298
+ }
295
299
  const index = this.#tabIndexes.get(focusable);
296
300
  if (index === null) {
297
301
  focusable.removeAttribute("tabindex");
@@ -300,25 +304,23 @@ var Portal = class {
300
304
  }
301
305
  });
302
306
  this.#exitSentinel.after(this.#source);
303
- this.#portal.remove();
307
+ this.#target.remove();
304
308
  this.#entranceSentinel.remove();
305
309
  this.#exitSentinel.remove();
306
310
  }
307
311
  getElement() {
308
- return this.#portal;
312
+ return this.#target;
309
313
  }
310
314
  #initialize() {
311
315
  this.#source.before(this.#entranceSentinel);
312
316
  this.#entranceSentinel.after(this.#exitSentinel);
313
- this.#portal.append(this.#source);
314
- this.#container.append(this.#portal);
315
- getFocusables(this.#source, { composed: true }).forEach(
316
- (focusable) => {
317
- const index = focusable.getAttribute("tabindex")?.trim();
318
- this.#tabIndexes.set(focusable, index === null ? null : Number(index));
319
- focusable.setAttribute("tabindex", "-1");
320
- }
321
- );
317
+ this.#target.append(this.#source);
318
+ this.#container.append(this.#target);
319
+ this.#getFocusables().forEach((focusable) => {
320
+ const index = focusable.getAttribute("tabindex")?.trim();
321
+ this.#tabIndexes.set(focusable, index === null ? null : Number(index));
322
+ focusable.setAttribute("tabindex", "-1");
323
+ });
322
324
  this.#controller = new AbortController();
323
325
  const { signal } = this.#controller;
324
326
  document.addEventListener("focusin", this.#onFocusIn, {
@@ -336,19 +338,18 @@ var Portal = class {
336
338
  if (!(before instanceof Element)) {
337
339
  return;
338
340
  }
339
- const focusables = this.#getFocusables();
340
341
  if (current === this.#entranceSentinel) {
341
342
  if (this.#source.contains(before)) {
342
343
  this.#focusOutside("backward");
343
344
  } else {
344
- const first = focusables[0];
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 = focusables.at(-1);
352
+ const last = this.#getFocusables().at(-1);
352
353
  last && focus(last);
353
354
  }
354
355
  }
@@ -364,17 +365,16 @@ var Portal = class {
364
365
  if (!this.#source.contains(active)) {
365
366
  return;
366
367
  }
367
- const focusables = this.#getFocusables();
368
- if (!focusables.length) {
368
+ if (!this.#getFocusables().length) {
369
369
  event.preventDefault();
370
370
  (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
371
371
  }
372
- const index = focusables.indexOf(active);
372
+ const index = this.#getFocusables().indexOf(active);
373
373
  if (index === -1) {
374
374
  return;
375
375
  }
376
376
  event.preventDefault();
377
- const focusable = focusables[index + (event.shiftKey ? -1 : 1)];
377
+ const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
378
378
  if (focusable) {
379
379
  focus(focusable);
380
380
  } else {
@@ -397,7 +397,10 @@ var Portal = class {
397
397
  focusable && focus(focusable);
398
398
  }
399
399
  #getFocusables() {
400
- return getFocusables(this.#source, { composed: true });
400
+ return getFocusables(this.#source, {
401
+ composed: true,
402
+ include: (element) => this.#tabIndexes.has(element)
403
+ });
401
404
  }
402
405
  };
403
406
  function focus(element) {
@@ -413,9 +416,9 @@ function getActiveElement2() {
413
416
  /**
414
417
  * Portal
415
418
  * Lightweight DOM portal (teleport) utility with fully focus management.
416
- * Designed for accessible dialogs, menus, overlays, popovers, and etc.
419
+ * Designed for accessible dialogs, menus, overlays, popovers.
417
420
  *
418
- * @version 0.0.2
421
+ * @version 0.0.4
419
422
  * @author Yusuke Kamiyamane
420
423
  * @license MIT
421
424
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -427,10 +430,9 @@ power-focusable/dist/index.js:
427
430
  (**
428
431
  * Power Focusable
429
432
  * High-precision focus management utility with full composed tree support.
430
- * Handles complex focus rules including tabindex ordering, radio groups, inert,
431
- * and shadow DOM.
433
+ * Handles complex focus rules including tabindex ordering, radio groups, inert.
432
434
  *
433
- * @version 4.0.2
435
+ * @version 4.1.0
434
436
  * @author Yusuke Kamiyamane
435
437
  * @license MIT
436
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.2",
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.2",
51
+ "power-focusable": "^4.1.0",
52
52
  "tsup": "^8.0.0",
53
53
  "typescript": "^5.6.0"
54
54
  },