@y14e/portal 0.0.3 → 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).
@@ -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,22 +271,20 @@ 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
- #focusables = [];
277
277
  #tabIndexes = /* @__PURE__ */ new WeakMap();
278
278
  #controller = null;
279
279
  #isDestroyed = false;
280
- constructor(source, container = document.body) {
280
+ constructor(source, container) {
281
281
  this.#source = source;
282
282
  this.#container = container;
283
- this.#portal = document.createElement("div");
284
- this.#portal.setAttribute("data-portal", "");
285
- this.#portal.setAttribute("tabindex", "-1");
283
+ this.#target = document.createElement("div");
284
+ this.#target.setAttribute("data-portal", "");
285
+ this.#target.setAttribute("tabindex", "-1");
286
286
  this.#entranceSentinel = this.#createSentinel();
287
287
  this.#exitSentinel = this.#createSentinel();
288
- this.#focusables = getFocusables(this.#source, { composed: true });
289
288
  this.#initialize();
290
289
  }
291
290
  destroy() {
@@ -295,7 +294,10 @@ var Portal = class {
295
294
  this.#isDestroyed = true;
296
295
  this.#controller?.abort();
297
296
  this.#controller = null;
298
- this.#focusables.forEach((focusable) => {
297
+ this.#getFocusables().forEach((focusable) => {
298
+ if (!this.#tabIndexes.has(focusable)) {
299
+ return;
300
+ }
299
301
  const index = this.#tabIndexes.get(focusable);
300
302
  if (index === null) {
301
303
  focusable.removeAttribute("tabindex");
@@ -303,21 +305,20 @@ var Portal = class {
303
305
  focusable.setAttribute("tabindex", String(index));
304
306
  }
305
307
  });
306
- this.#focusables.length = 0;
307
308
  this.#exitSentinel.after(this.#source);
308
- this.#portal.remove();
309
+ this.#target.remove();
309
310
  this.#entranceSentinel.remove();
310
311
  this.#exitSentinel.remove();
311
312
  }
312
313
  getElement() {
313
- return this.#portal;
314
+ return this.#target;
314
315
  }
315
316
  #initialize() {
316
317
  this.#source.before(this.#entranceSentinel);
317
318
  this.#entranceSentinel.after(this.#exitSentinel);
318
- this.#portal.append(this.#source);
319
- this.#container.append(this.#portal);
320
- this.#focusables.forEach((focusable) => {
319
+ this.#target.append(this.#source);
320
+ this.#container.append(this.#target);
321
+ this.#getFocusables().forEach((focusable) => {
321
322
  const index = focusable.getAttribute("tabindex")?.trim();
322
323
  this.#tabIndexes.set(focusable, index === null ? null : Number(index));
323
324
  focusable.setAttribute("tabindex", "-1");
@@ -343,14 +344,14 @@ var Portal = class {
343
344
  if (this.#source.contains(before)) {
344
345
  this.#focusOutside("backward");
345
346
  } else {
346
- const first = this.#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 = this.#focusables.at(-1);
354
+ const last = this.#getFocusables().at(-1);
354
355
  last && focus(last);
355
356
  }
356
357
  }
@@ -366,16 +367,16 @@ var Portal = class {
366
367
  if (!this.#source.contains(active)) {
367
368
  return;
368
369
  }
369
- if (!this.#focusables.length) {
370
+ if (!this.#getFocusables().length) {
370
371
  event.preventDefault();
371
372
  (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
372
373
  }
373
- const index = this.#focusables.indexOf(active);
374
+ const index = this.#getFocusables().indexOf(active);
374
375
  if (index === -1) {
375
376
  return;
376
377
  }
377
378
  event.preventDefault();
378
- const focusable = this.#focusables[index + (event.shiftKey ? -1 : 1)];
379
+ const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
379
380
  if (focusable) {
380
381
  focus(focusable);
381
382
  } else {
@@ -397,6 +398,12 @@ var Portal = class {
397
398
  const focusable = direction === "backward" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
398
399
  focusable && focus(focusable);
399
400
  }
401
+ #getFocusables() {
402
+ return getFocusables(this.#source, {
403
+ composed: true,
404
+ include: (element) => this.#tabIndexes.has(element)
405
+ });
406
+ }
400
407
  };
401
408
  function focus(element) {
402
409
  "focus" in element && typeof element.focus === "function" && element.focus();
@@ -411,9 +418,9 @@ function getActiveElement2() {
411
418
  /**
412
419
  * Portal
413
420
  * Lightweight DOM portal (teleport) utility with fully focus management.
414
- * Designed for accessible dialogs, menus, overlays, popovers, and etc.
421
+ * Designed for accessible dialogs, menus, overlays, popovers.
415
422
  *
416
- * @version 0.0.3
423
+ * @version 0.0.4
417
424
  * @author Yusuke Kamiyamane
418
425
  * @license MIT
419
426
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -425,10 +432,9 @@ power-focusable/dist/index.js:
425
432
  (**
426
433
  * Power Focusable
427
434
  * High-precision focus management utility with full composed tree support.
428
- * Handles complex focus rules including tabindex ordering, radio groups, inert,
429
- * and shadow DOM.
435
+ * Handles complex focus rules including tabindex ordering, radio groups, inert.
430
436
  *
431
- * @version 4.0.2
437
+ * @version 4.1.0
432
438
  * @author Yusuke Kamiyamane
433
439
  * @license MIT
434
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.3
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.3
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,22 +269,20 @@ 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
- #focusables = [];
275
275
  #tabIndexes = /* @__PURE__ */ new WeakMap();
276
276
  #controller = null;
277
277
  #isDestroyed = false;
278
- constructor(source, container = document.body) {
278
+ constructor(source, container) {
279
279
  this.#source = source;
280
280
  this.#container = container;
281
- this.#portal = document.createElement("div");
282
- this.#portal.setAttribute("data-portal", "");
283
- this.#portal.setAttribute("tabindex", "-1");
281
+ this.#target = document.createElement("div");
282
+ this.#target.setAttribute("data-portal", "");
283
+ this.#target.setAttribute("tabindex", "-1");
284
284
  this.#entranceSentinel = this.#createSentinel();
285
285
  this.#exitSentinel = this.#createSentinel();
286
- this.#focusables = getFocusables(this.#source, { composed: true });
287
286
  this.#initialize();
288
287
  }
289
288
  destroy() {
@@ -293,7 +292,10 @@ var Portal = class {
293
292
  this.#isDestroyed = true;
294
293
  this.#controller?.abort();
295
294
  this.#controller = null;
296
- this.#focusables.forEach((focusable) => {
295
+ this.#getFocusables().forEach((focusable) => {
296
+ if (!this.#tabIndexes.has(focusable)) {
297
+ return;
298
+ }
297
299
  const index = this.#tabIndexes.get(focusable);
298
300
  if (index === null) {
299
301
  focusable.removeAttribute("tabindex");
@@ -301,21 +303,20 @@ var Portal = class {
301
303
  focusable.setAttribute("tabindex", String(index));
302
304
  }
303
305
  });
304
- this.#focusables.length = 0;
305
306
  this.#exitSentinel.after(this.#source);
306
- this.#portal.remove();
307
+ this.#target.remove();
307
308
  this.#entranceSentinel.remove();
308
309
  this.#exitSentinel.remove();
309
310
  }
310
311
  getElement() {
311
- return this.#portal;
312
+ return this.#target;
312
313
  }
313
314
  #initialize() {
314
315
  this.#source.before(this.#entranceSentinel);
315
316
  this.#entranceSentinel.after(this.#exitSentinel);
316
- this.#portal.append(this.#source);
317
- this.#container.append(this.#portal);
318
- this.#focusables.forEach((focusable) => {
317
+ this.#target.append(this.#source);
318
+ this.#container.append(this.#target);
319
+ this.#getFocusables().forEach((focusable) => {
319
320
  const index = focusable.getAttribute("tabindex")?.trim();
320
321
  this.#tabIndexes.set(focusable, index === null ? null : Number(index));
321
322
  focusable.setAttribute("tabindex", "-1");
@@ -341,14 +342,14 @@ var Portal = class {
341
342
  if (this.#source.contains(before)) {
342
343
  this.#focusOutside("backward");
343
344
  } else {
344
- const first = this.#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 = this.#focusables.at(-1);
352
+ const last = this.#getFocusables().at(-1);
352
353
  last && focus(last);
353
354
  }
354
355
  }
@@ -364,16 +365,16 @@ var Portal = class {
364
365
  if (!this.#source.contains(active)) {
365
366
  return;
366
367
  }
367
- if (!this.#focusables.length) {
368
+ if (!this.#getFocusables().length) {
368
369
  event.preventDefault();
369
370
  (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
370
371
  }
371
- const index = this.#focusables.indexOf(active);
372
+ const index = this.#getFocusables().indexOf(active);
372
373
  if (index === -1) {
373
374
  return;
374
375
  }
375
376
  event.preventDefault();
376
- const focusable = this.#focusables[index + (event.shiftKey ? -1 : 1)];
377
+ const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
377
378
  if (focusable) {
378
379
  focus(focusable);
379
380
  } else {
@@ -395,6 +396,12 @@ var Portal = class {
395
396
  const focusable = direction === "backward" ? getPreviousFocusable(document.body, options) : getNextFocusable(document.body, options);
396
397
  focusable && focus(focusable);
397
398
  }
399
+ #getFocusables() {
400
+ return getFocusables(this.#source, {
401
+ composed: true,
402
+ include: (element) => this.#tabIndexes.has(element)
403
+ });
404
+ }
398
405
  };
399
406
  function focus(element) {
400
407
  "focus" in element && typeof element.focus === "function" && element.focus();
@@ -409,9 +416,9 @@ function getActiveElement2() {
409
416
  /**
410
417
  * Portal
411
418
  * Lightweight DOM portal (teleport) utility with fully focus management.
412
- * Designed for accessible dialogs, menus, overlays, popovers, and etc.
419
+ * Designed for accessible dialogs, menus, overlays, popovers.
413
420
  *
414
- * @version 0.0.3
421
+ * @version 0.0.4
415
422
  * @author Yusuke Kamiyamane
416
423
  * @license MIT
417
424
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -423,10 +430,9 @@ power-focusable/dist/index.js:
423
430
  (**
424
431
  * Power Focusable
425
432
  * High-precision focus management utility with full composed tree support.
426
- * Handles complex focus rules including tabindex ordering, radio groups, inert,
427
- * and shadow DOM.
433
+ * Handles complex focus rules including tabindex ordering, radio groups, inert.
428
434
  *
429
- * @version 4.0.2
435
+ * @version 4.1.0
430
436
  * @author Yusuke Kamiyamane
431
437
  * @license MIT
432
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.3",
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
  },