@y14e/portal 1.0.6 → 1.1.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/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,53 @@ 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.#focusables];
385
+ if (!focusables.length) {
385
386
  event.preventDefault();
386
387
  (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
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
399
  (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
398
400
  }
399
401
  };
402
+ #update() {
403
+ const current = new Set(
404
+ getFocusables(this.#host, { composed: true })
405
+ );
406
+ this.#focusables.forEach((focusable) => {
407
+ if (current.has(focusable)) {
408
+ return;
409
+ }
410
+ if (focusable.isConnected) {
411
+ const index = this.#tabIndexes.get(focusable);
412
+ if (index == null) {
413
+ focusable.removeAttribute("tabindex");
414
+ } else {
415
+ focusable.setAttribute("tabindex", index);
416
+ }
417
+ }
418
+ this.#focusables.delete(focusable);
419
+ this.#tabIndexes.delete(focusable);
420
+ });
421
+ const active = getActiveElement2();
422
+ current.forEach((c) => {
423
+ if (!this.#tabIndexes.has(c)) {
424
+ this.#tabIndexes.set(c, c.getAttribute("tabindex"));
425
+ }
426
+ c.setAttribute("tabindex", c === active ? "0" : "-1");
427
+ this.#focusables.add(c);
428
+ });
429
+ }
400
430
  #createSentinel() {
401
431
  const sentinel = document.createElement("span");
402
432
  sentinel.setAttribute("aria-hidden", "true");
@@ -405,12 +435,6 @@ var Portal = class {
405
435
  sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
406
436
  return sentinel;
407
437
  }
408
- #getFocusables() {
409
- return getFocusables(this.#host, {
410
- composed: true,
411
- include: (element) => this.#tabIndexes.has(element)
412
- });
413
- }
414
438
  #moveFocus(direction) {
415
439
  const options = {
416
440
  anchor: direction === "previous" ? this.#entranceSentinel : this.#exitSentinel,
@@ -445,7 +469,7 @@ function getActiveElement2() {
445
469
  * Lightweight DOM portal (teleport) utility with fully focus management.
446
470
  * Designed for accessible dialogs, menus, overlays, popovers.
447
471
  *
448
- * @version 1.0.6
472
+ * @version 1.1.0
449
473
  * @author Yusuke Kamiyamane
450
474
  * @license MIT
451
475
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -467,5 +491,4 @@ power-focusable/dist/index.js:
467
491
  *)
468
492
  */
469
493
 
470
- exports.Portal = Portal;
471
494
  exports.createPortal = createPortal;
package/dist/index.d.cts CHANGED
@@ -3,17 +3,12 @@
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.0.6
6
+ * @version 1.1.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
12
  declare function createPortal(host: Element, container?: HTMLElement): () => void;
13
- declare class Portal {
14
- #private;
15
- constructor(host: Element, container: Element);
16
- destroy(): void;
17
- }
18
13
 
19
- export { Portal, createPortal };
14
+ export { createPortal };
package/dist/index.d.ts CHANGED
@@ -3,17 +3,12 @@
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.0.6
6
+ * @version 1.1.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
12
  declare function createPortal(host: Element, container?: HTMLElement): () => void;
13
- declare class Portal {
14
- #private;
15
- constructor(host: Element, container: Element);
16
- destroy(): void;
17
- }
18
13
 
19
- export { Portal, createPortal };
14
+ export { createPortal };
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,53 @@ 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.#focusables];
383
+ if (!focusables.length) {
383
384
  event.preventDefault();
384
385
  (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
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
397
  (event.shiftKey ? this.#entranceSentinel : this.#exitSentinel).focus();
396
398
  }
397
399
  };
400
+ #update() {
401
+ const current = new Set(
402
+ getFocusables(this.#host, { composed: true })
403
+ );
404
+ this.#focusables.forEach((focusable) => {
405
+ if (current.has(focusable)) {
406
+ return;
407
+ }
408
+ if (focusable.isConnected) {
409
+ const index = this.#tabIndexes.get(focusable);
410
+ if (index == null) {
411
+ focusable.removeAttribute("tabindex");
412
+ } else {
413
+ focusable.setAttribute("tabindex", index);
414
+ }
415
+ }
416
+ this.#focusables.delete(focusable);
417
+ this.#tabIndexes.delete(focusable);
418
+ });
419
+ const active = getActiveElement2();
420
+ current.forEach((c) => {
421
+ if (!this.#tabIndexes.has(c)) {
422
+ this.#tabIndexes.set(c, c.getAttribute("tabindex"));
423
+ }
424
+ c.setAttribute("tabindex", c === active ? "0" : "-1");
425
+ this.#focusables.add(c);
426
+ });
427
+ }
398
428
  #createSentinel() {
399
429
  const sentinel = document.createElement("span");
400
430
  sentinel.setAttribute("aria-hidden", "true");
@@ -403,12 +433,6 @@ var Portal = class {
403
433
  sentinel.style.cssText += VISUALLY_HIDDEN_CSS;
404
434
  return sentinel;
405
435
  }
406
- #getFocusables() {
407
- return getFocusables(this.#host, {
408
- composed: true,
409
- include: (element) => this.#tabIndexes.has(element)
410
- });
411
- }
412
436
  #moveFocus(direction) {
413
437
  const options = {
414
438
  anchor: direction === "previous" ? this.#entranceSentinel : this.#exitSentinel,
@@ -443,7 +467,7 @@ function getActiveElement2() {
443
467
  * Lightweight DOM portal (teleport) utility with fully focus management.
444
468
  * Designed for accessible dialogs, menus, overlays, popovers.
445
469
  *
446
- * @version 1.0.6
470
+ * @version 1.1.0
447
471
  * @author Yusuke Kamiyamane
448
472
  * @license MIT
449
473
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -465,4 +489,4 @@ power-focusable/dist/index.js:
465
489
  *)
466
490
  */
467
491
 
468
- export { Portal, createPortal };
492
+ export { createPortal };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/portal",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Lightweight DOM portal (teleport) utility with fully focus management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",