@y14e/portal 1.1.1 → 1.2.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,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
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 = /* @__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");
@@ -408,7 +439,9 @@ var Portal = class {
408
439
  #getFocusables() {
409
440
  return getFocusables(this.#host, {
410
441
  composed: true,
411
- include: (element) => this.#tabIndexes.has(element)
442
+ include: (element) => {
443
+ return this.#focusables.has(element);
444
+ }
412
445
  });
413
446
  }
414
447
  #moveFocus(direction) {
@@ -445,7 +478,7 @@ function getActiveElement2() {
445
478
  * Lightweight DOM portal (teleport) utility with fully focus management.
446
479
  * Designed for accessible dialogs, menus, overlays, popovers.
447
480
  *
448
- * @version 1.1.1
481
+ * @version 1.2.0
449
482
  * @author Yusuke Kamiyamane
450
483
  * @license MIT
451
484
  * @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.0
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.0
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
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 = /* @__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");
@@ -406,7 +437,9 @@ var Portal = class {
406
437
  #getFocusables() {
407
438
  return getFocusables(this.#host, {
408
439
  composed: true,
409
- include: (element) => this.#tabIndexes.has(element)
440
+ include: (element) => {
441
+ return this.#focusables.has(element);
442
+ }
410
443
  });
411
444
  }
412
445
  #moveFocus(direction) {
@@ -443,7 +476,7 @@ function getActiveElement2() {
443
476
  * Lightweight DOM portal (teleport) utility with fully focus management.
444
477
  * Designed for accessible dialogs, menus, overlays, popovers.
445
478
  *
446
- * @version 1.1.1
479
+ * @version 1.2.0
447
480
  * @author Yusuke Kamiyamane
448
481
  * @license MIT
449
482
  * @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.0",
4
4
  "description": "Lightweight DOM portal (teleport) utility with fully focus management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",