@y14e/portal 1.1.1 → 1.2.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
@@ -36,3 +36,7 @@ const cleanup = createPortal(host, container);
36
36
  // host: Element
37
37
  // container (optional): Element (default: <body>)
38
38
  ```
39
+
40
+ ## Demo
41
+
42
+ https://y14e.github.io/portal/
package/dist/index.cjs CHANGED
@@ -295,7 +295,8 @@ var Portal = class {
295
295
  #container;
296
296
  #entranceSentinel;
297
297
  #exitSentinel;
298
- #tabIndexes = /* @__PURE__ */ new WeakMap();
298
+ #focusables = /* @__PURE__ */ new Set();
299
+ #tabIndexes = /* @__PURE__ */ new Map();
299
300
  #controller = null;
300
301
  #isDestroyed = false;
301
302
  constructor(host, container) {
@@ -312,10 +313,7 @@ var Portal = class {
312
313
  this.#isDestroyed = true;
313
314
  this.#controller?.abort();
314
315
  this.#controller = null;
315
- this.#getFocusables().forEach((focusable) => {
316
- if (!this.#tabIndexes.has(focusable)) {
317
- return;
318
- }
316
+ this.#focusables.forEach((focusable) => {
319
317
  const index = this.#tabIndexes.get(focusable);
320
318
  if (index == null) {
321
319
  focusable.removeAttribute("tabindex");
@@ -323,6 +321,8 @@ var Portal = class {
323
321
  focusable.setAttribute("tabindex", index);
324
322
  }
325
323
  });
324
+ this.#focusables.clear();
325
+ this.#tabIndexes.clear();
326
326
  this.#exitSentinel.after(this.#host);
327
327
  this.#entranceSentinel.remove();
328
328
  this.#exitSentinel.remove();
@@ -332,10 +332,7 @@ var Portal = class {
332
332
  this.#host.before(this.#entranceSentinel);
333
333
  this.#entranceSentinel.after(this.#exitSentinel);
334
334
  this.#container.append(this.#host);
335
- this.#getFocusables().forEach((focusable) => {
336
- this.#tabIndexes.set(focusable, focusable.getAttribute("tabindex"));
337
- focusable.setAttribute("tabindex", "-1");
338
- });
335
+ this.#update();
339
336
  this.#controller = new AbortController();
340
337
  const { signal } = this.#controller;
341
338
  document.addEventListener("focusin", this.#onFocusIn, {
@@ -358,14 +355,16 @@ var Portal = class {
358
355
  if (this.#host.contains(before)) {
359
356
  this.#moveFocus("previous");
360
357
  } else {
361
- const first = this.#getFocusables()[0];
358
+ this.#update();
359
+ const first = [...this.#focusables][0];
362
360
  first && focusElement(first);
363
361
  }
364
362
  } else if (current === this.#exitSentinel) {
365
363
  if (this.#host.contains(before)) {
366
364
  this.#moveFocus("next");
367
365
  } else {
368
- const last = this.#getFocusables().at(-1);
366
+ this.#update();
367
+ const last = [...this.#focusables].at(-1);
369
368
  last && focusElement(last);
370
369
  }
371
370
  }
@@ -381,22 +380,54 @@ var Portal = class {
381
380
  if (!this.#host.contains(active)) {
382
381
  return;
383
382
  }
384
- if (!this.#getFocusables().length) {
383
+ this.#update();
384
+ const focusables = this.#getFocusables();
385
+ if (!focusables.length) {
385
386
  event.preventDefault();
386
- (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
387
+ this.#focusSentinel(event.shiftKey);
388
+ return;
387
389
  }
388
- const index = this.#getFocusables().indexOf(active);
390
+ const index = focusables.indexOf(active);
389
391
  if (index === -1) {
390
392
  return;
391
393
  }
392
394
  event.preventDefault();
393
- const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
395
+ const focusable = focusables[index + (event.shiftKey ? -1 : 1)];
394
396
  if (focusable) {
395
397
  focusElement(focusable);
396
398
  } else {
397
- (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
399
+ this.#focusSentinel(event.shiftKey);
398
400
  }
399
401
  };
402
+ #update() {
403
+ const current = /* @__PURE__ */ new Set([
404
+ ...getFocusables(this.#host, { composed: true }),
405
+ ...this.#getFocusables()
406
+ ]);
407
+ this.#focusables.forEach((focusable) => {
408
+ if (current.has(focusable)) {
409
+ return;
410
+ }
411
+ if (focusable.isConnected) {
412
+ const index = this.#tabIndexes.get(focusable);
413
+ if (index == null) {
414
+ focusable.removeAttribute("tabindex");
415
+ } else {
416
+ focusable.setAttribute("tabindex", index);
417
+ }
418
+ }
419
+ this.#focusables.delete(focusable);
420
+ this.#tabIndexes.delete(focusable);
421
+ });
422
+ current.forEach((c) => {
423
+ if (this.#focusables.has(c)) {
424
+ return;
425
+ }
426
+ this.#focusables.add(c);
427
+ this.#tabIndexes.set(c, c.getAttribute("tabindex"));
428
+ c.setAttribute("tabindex", "-1");
429
+ });
430
+ }
400
431
  #createSentinel() {
401
432
  const sentinel = document.createElement("span");
402
433
  sentinel.setAttribute("aria-hidden", "true");
@@ -405,10 +436,17 @@ var Portal = class {
405
436
  sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
406
437
  return sentinel;
407
438
  }
439
+ #focusSentinel(isPrevious) {
440
+ requestAnimationFrame(
441
+ () => (isPrevious ? this.#entranceSentinel : this.#exitSentinel).focus()
442
+ );
443
+ }
408
444
  #getFocusables() {
409
445
  return getFocusables(this.#host, {
410
446
  composed: true,
411
- include: (element) => this.#tabIndexes.has(element)
447
+ include: (element) => {
448
+ return this.#focusables.has(element);
449
+ }
412
450
  });
413
451
  }
414
452
  #moveFocus(direction) {
@@ -445,7 +483,7 @@ function getActiveElement2() {
445
483
  * Lightweight DOM portal (teleport) utility with fully focus management.
446
484
  * Designed for accessible dialogs, menus, overlays, popovers.
447
485
  *
448
- * @version 1.1.1
486
+ * @version 1.2.1
449
487
  * @author Yusuke Kamiyamane
450
488
  * @license MIT
451
489
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Lightweight DOM portal (teleport) utility with fully focus management.
4
4
  * Designed for accessible dialogs, menus, overlays, popovers.
5
5
  *
6
- * @version 1.1.1
6
+ * @version 1.2.1
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Lightweight DOM portal (teleport) utility with fully focus management.
4
4
  * Designed for accessible dialogs, menus, overlays, popovers.
5
5
  *
6
- * @version 1.1.1
6
+ * @version 1.2.1
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.js CHANGED
@@ -293,7 +293,8 @@ var Portal = class {
293
293
  #container;
294
294
  #entranceSentinel;
295
295
  #exitSentinel;
296
- #tabIndexes = /* @__PURE__ */ new WeakMap();
296
+ #focusables = /* @__PURE__ */ new Set();
297
+ #tabIndexes = /* @__PURE__ */ new Map();
297
298
  #controller = null;
298
299
  #isDestroyed = false;
299
300
  constructor(host, container) {
@@ -310,10 +311,7 @@ var Portal = class {
310
311
  this.#isDestroyed = true;
311
312
  this.#controller?.abort();
312
313
  this.#controller = null;
313
- this.#getFocusables().forEach((focusable) => {
314
- if (!this.#tabIndexes.has(focusable)) {
315
- return;
316
- }
314
+ this.#focusables.forEach((focusable) => {
317
315
  const index = this.#tabIndexes.get(focusable);
318
316
  if (index == null) {
319
317
  focusable.removeAttribute("tabindex");
@@ -321,6 +319,8 @@ var Portal = class {
321
319
  focusable.setAttribute("tabindex", index);
322
320
  }
323
321
  });
322
+ this.#focusables.clear();
323
+ this.#tabIndexes.clear();
324
324
  this.#exitSentinel.after(this.#host);
325
325
  this.#entranceSentinel.remove();
326
326
  this.#exitSentinel.remove();
@@ -330,10 +330,7 @@ var Portal = class {
330
330
  this.#host.before(this.#entranceSentinel);
331
331
  this.#entranceSentinel.after(this.#exitSentinel);
332
332
  this.#container.append(this.#host);
333
- this.#getFocusables().forEach((focusable) => {
334
- this.#tabIndexes.set(focusable, focusable.getAttribute("tabindex"));
335
- focusable.setAttribute("tabindex", "-1");
336
- });
333
+ this.#update();
337
334
  this.#controller = new AbortController();
338
335
  const { signal } = this.#controller;
339
336
  document.addEventListener("focusin", this.#onFocusIn, {
@@ -356,14 +353,16 @@ var Portal = class {
356
353
  if (this.#host.contains(before)) {
357
354
  this.#moveFocus("previous");
358
355
  } else {
359
- const first = this.#getFocusables()[0];
356
+ this.#update();
357
+ const first = [...this.#focusables][0];
360
358
  first && focusElement(first);
361
359
  }
362
360
  } else if (current === this.#exitSentinel) {
363
361
  if (this.#host.contains(before)) {
364
362
  this.#moveFocus("next");
365
363
  } else {
366
- const last = this.#getFocusables().at(-1);
364
+ this.#update();
365
+ const last = [...this.#focusables].at(-1);
367
366
  last && focusElement(last);
368
367
  }
369
368
  }
@@ -379,22 +378,54 @@ var Portal = class {
379
378
  if (!this.#host.contains(active)) {
380
379
  return;
381
380
  }
382
- if (!this.#getFocusables().length) {
381
+ this.#update();
382
+ const focusables = this.#getFocusables();
383
+ if (!focusables.length) {
383
384
  event.preventDefault();
384
- (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
385
+ this.#focusSentinel(event.shiftKey);
386
+ return;
385
387
  }
386
- const index = this.#getFocusables().indexOf(active);
388
+ const index = focusables.indexOf(active);
387
389
  if (index === -1) {
388
390
  return;
389
391
  }
390
392
  event.preventDefault();
391
- const focusable = this.#getFocusables()[index + (event.shiftKey ? -1 : 1)];
393
+ const focusable = focusables[index + (event.shiftKey ? -1 : 1)];
392
394
  if (focusable) {
393
395
  focusElement(focusable);
394
396
  } else {
395
- (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
397
+ this.#focusSentinel(event.shiftKey);
396
398
  }
397
399
  };
400
+ #update() {
401
+ const current = /* @__PURE__ */ new Set([
402
+ ...getFocusables(this.#host, { composed: true }),
403
+ ...this.#getFocusables()
404
+ ]);
405
+ this.#focusables.forEach((focusable) => {
406
+ if (current.has(focusable)) {
407
+ return;
408
+ }
409
+ if (focusable.isConnected) {
410
+ const index = this.#tabIndexes.get(focusable);
411
+ if (index == null) {
412
+ focusable.removeAttribute("tabindex");
413
+ } else {
414
+ focusable.setAttribute("tabindex", index);
415
+ }
416
+ }
417
+ this.#focusables.delete(focusable);
418
+ this.#tabIndexes.delete(focusable);
419
+ });
420
+ current.forEach((c) => {
421
+ if (this.#focusables.has(c)) {
422
+ return;
423
+ }
424
+ this.#focusables.add(c);
425
+ this.#tabIndexes.set(c, c.getAttribute("tabindex"));
426
+ c.setAttribute("tabindex", "-1");
427
+ });
428
+ }
398
429
  #createSentinel() {
399
430
  const sentinel = document.createElement("span");
400
431
  sentinel.setAttribute("aria-hidden", "true");
@@ -403,10 +434,17 @@ var Portal = class {
403
434
  sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
404
435
  return sentinel;
405
436
  }
437
+ #focusSentinel(isPrevious) {
438
+ requestAnimationFrame(
439
+ () => (isPrevious ? this.#entranceSentinel : this.#exitSentinel).focus()
440
+ );
441
+ }
406
442
  #getFocusables() {
407
443
  return getFocusables(this.#host, {
408
444
  composed: true,
409
- include: (element) => this.#tabIndexes.has(element)
445
+ include: (element) => {
446
+ return this.#focusables.has(element);
447
+ }
410
448
  });
411
449
  }
412
450
  #moveFocus(direction) {
@@ -443,7 +481,7 @@ function getActiveElement2() {
443
481
  * Lightweight DOM portal (teleport) utility with fully focus management.
444
482
  * Designed for accessible dialogs, menus, overlays, popovers.
445
483
  *
446
- * @version 1.1.1
484
+ * @version 1.2.1
447
485
  * @author Yusuke Kamiyamane
448
486
  * @license MIT
449
487
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/portal",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Lightweight DOM portal (teleport) utility with fully focus management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",