@y14e/tabs 1.5.6 → 1.5.7

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
@@ -10,14 +10,14 @@ npm i @y14e/tabs
10
10
 
11
11
  ```ts
12
12
  // npm
13
- import Tabs from '@y14e/tabs@1.5.6';
13
+ import Tabs from '@y14e/tabs@1.5.7';
14
14
 
15
15
  // CDNs
16
- import Tabs from 'https://esm.sh/@y14e/tabs@1.5.6';
16
+ import Tabs from 'https://esm.sh/@y14e/tabs@1.5.7';
17
17
  // or
18
- import Tabs from 'https://cdn.jsdelivr.net/npm/@y14e/tabs@1.5.6/+esm';
18
+ import Tabs from 'https://cdn.jsdelivr.net/npm/@y14e/tabs@1.5.7/+esm';
19
19
  // or
20
- import Tabs from 'https://esm.unpkg.com/@y14e/tabs@1.5.6';
20
+ import Tabs from 'https://esm.unpkg.com/@y14e/tabs@1.5.7';
21
21
  ```
22
22
 
23
23
  ## Usage
package/dist/index.cjs CHANGED
@@ -13,11 +13,10 @@ function addTokenToAttribute(element, attribute, token, options = {}) {
13
13
  const tokens = value ? parse(value).filter(Boolean) : [];
14
14
  if (caseInsensitive) {
15
15
  const lower = token.toLowerCase();
16
- if (tokens.some((token2) => token2.toLowerCase() === lower)) {
17
- return;
16
+ if (tokens.every((token2) => token2.toLowerCase() !== lower)) {
17
+ tokens.push(token);
18
+ element.setAttribute(attribute, serialize(tokens));
18
19
  }
19
- tokens.push(token);
20
- element.setAttribute(attribute, serialize(tokens));
21
20
  return;
22
21
  }
23
22
  const set = new Set(tokens);
@@ -119,11 +118,10 @@ function addTokenToAttribute2(element, attribute, token, options = {}) {
119
118
  const tokens = value ? parse(value).filter(Boolean) : [];
120
119
  if (caseInsensitive) {
121
120
  const lower = token.toLowerCase();
122
- if (tokens.some((token2) => token2.toLowerCase() === lower)) {
123
- return;
121
+ if (tokens.every((token2) => token2.toLowerCase() !== lower)) {
122
+ tokens.push(token);
123
+ element.setAttribute(attribute, serialize(tokens));
124
124
  }
125
- tokens.push(token);
126
- element.setAttribute(attribute, serialize(tokens));
127
125
  return;
128
126
  }
129
127
  const set = new Set(tokens);
@@ -402,6 +400,8 @@ function createRovingTabIndex(container, options = {}) {
402
400
  let {
403
401
  direction,
404
402
  navigationOnly = false,
403
+ noMemory = false,
404
+ noStart = false,
405
405
  selector,
406
406
  typeahead = false,
407
407
  wrap = false
@@ -414,6 +414,14 @@ function createRovingTabIndex(container, options = {}) {
414
414
  console.warn("Invalid navigationOnly option. Fallback: false.");
415
415
  navigationOnly = false;
416
416
  }
417
+ if (typeof noMemory !== "boolean") {
418
+ console.warn("Invalid noMemory option. Fallback: false.");
419
+ noMemory = false;
420
+ }
421
+ if (typeof noStart !== "boolean") {
422
+ console.warn("Invalid noStart option. Fallback: false.");
423
+ noStart = false;
424
+ }
417
425
  if (typeof selector !== "undefined" && (typeof selector !== "string" || !selector.trim())) {
418
426
  console.warn(
419
427
  "Invalid selector. Fallback: all focusable elements (undefined)."
@@ -428,13 +436,9 @@ function createRovingTabIndex(container, options = {}) {
428
436
  console.warn("Invalid wrap option. Fallback: false.");
429
437
  wrap = false;
430
438
  }
431
- const settings = { navigationOnly, typeahead, wrap };
432
- if (direction) {
433
- Object.assign(settings, { direction });
434
- }
435
- if (selector) {
436
- Object.assign(settings, { selector });
437
- }
439
+ const settings = { navigationOnly, noMemory, noStart, typeahead, wrap };
440
+ direction && Object.assign(settings, { direction });
441
+ selector && Object.assign(settings, { selector });
438
442
  const roving = new RovingTabIndex(container, settings);
439
443
  return () => roving.destroy();
440
444
  }
@@ -467,12 +471,29 @@ var RovingTabIndex = class {
467
471
  #initialize() {
468
472
  this.#update(document.activeElement);
469
473
  this.#controller = new AbortController();
474
+ const { signal } = this.#controller;
475
+ document.addEventListener("focusin", this.#onFocusIn, {
476
+ capture: true,
477
+ signal
478
+ });
470
479
  document.addEventListener("keydown", this.#onKeyDown, {
471
480
  capture: true,
472
- signal: this.#controller.signal
481
+ signal
473
482
  });
474
483
  this.#container.setAttribute("data-roving-tabindex-initialized", "");
475
484
  }
485
+ #onFocusIn = (event) => {
486
+ const { target } = event;
487
+ if (!(target instanceof Element)) {
488
+ return;
489
+ }
490
+ const isFocusable22 = this.#focusables.has(target);
491
+ if (this.#options.noMemory && !isFocusable22) {
492
+ this.#update(null);
493
+ return;
494
+ }
495
+ isFocusable22 && this.#update(target);
496
+ };
476
497
  #onKeyDown = (event) => {
477
498
  if (!event.composedPath().includes(this.#container)) {
478
499
  return;
@@ -534,11 +555,7 @@ var RovingTabIndex = class {
534
555
  }
535
556
  }
536
557
  const focusable = target.at(newIndex);
537
- if (!focusable) {
538
- return;
539
- }
540
- this.#update(focusable);
541
- focusElement(focusable);
558
+ focusable && focusElement(focusable);
542
559
  };
543
560
  #update(active) {
544
561
  const current = new Set(this.#getFocusables());
@@ -553,7 +570,7 @@ var RovingTabIndex = class {
553
570
  index >= 0 && focusables.splice(index, 1);
554
571
  });
555
572
  }
556
- const { navigationOnly } = this.#options;
573
+ const { navigationOnly, noStart, typeahead } = this.#options;
557
574
  for (const focusable of current) {
558
575
  if (this.#focusables.has(focusable)) {
559
576
  continue;
@@ -563,7 +580,7 @@ var RovingTabIndex = class {
563
580
  saveAttributes2([focusable], ["tabindex"]);
564
581
  focusable.setAttribute("tabindex", "-1");
565
582
  }
566
- if (!this.#options.typeahead) {
583
+ if (!typeahead) {
567
584
  continue;
568
585
  }
569
586
  const value = focusable.ariaKeyShortcuts?.trim();
@@ -594,7 +611,7 @@ var RovingTabIndex = class {
594
611
  return;
595
612
  }
596
613
  [...this.#focusables].forEach((focusable, i) => {
597
- focusable.setAttribute("tabindex", i ? "-1" : "0");
614
+ focusable.setAttribute("tabindex", i || noStart ? "-1" : "0");
598
615
  });
599
616
  }
600
617
  #createSelectorFilter() {
@@ -605,7 +622,8 @@ var RovingTabIndex = class {
605
622
  return getFocusables(this.#container, {
606
623
  composed: true,
607
624
  filter: this.#selectorFilter,
608
- skipNegativeTabIndexCheck: !this.#options.navigationOnly
625
+ skipNegativeTabIndexCheck: !this.#options.navigationOnly,
626
+ skipVisibilityCheck: true
609
627
  });
610
628
  }
611
629
  };
@@ -659,6 +677,7 @@ var Tabs = class _Tabs {
659
677
  #animationController = null;
660
678
  #cleanupsRovingTabIndex = [];
661
679
  #animation = null;
680
+ #animationId = 0;
662
681
  #buttons = [];
663
682
  #indicators = [];
664
683
  #isDestroyed = false;
@@ -788,6 +807,7 @@ var Tabs = class _Tabs {
788
807
  p.setAttribute("hidden", isFocusable2(tab2) ? "until-found" : "");
789
808
  }
790
809
  });
810
+ const animationId = ++this.#animationId;
791
811
  this.#animation?.cancel();
792
812
  const { duration, easing } = this.#settings.animation.content;
793
813
  this.#animation = this.#contentElement.animate(
@@ -803,7 +823,9 @@ var Tabs = class _Tabs {
803
823
  }
804
824
  );
805
825
  const cleanup = () => {
806
- this.#animation = null;
826
+ if (animationId === this.#animationId) {
827
+ this.#animation = null;
828
+ }
807
829
  };
808
830
  this.#animationController = new AbortController();
809
831
  const { signal } = this.#animationController;
@@ -814,8 +836,10 @@ var Tabs = class _Tabs {
814
836
  this.#animation.addEventListener(
815
837
  "finish",
816
838
  () => {
817
- this.#onAnimationFinish();
818
- cleanup();
839
+ if (animationId === this.#animationId) {
840
+ this.#onAnimationFinish();
841
+ cleanup();
842
+ }
819
843
  },
820
844
  {
821
845
  once: true,
@@ -1140,7 +1164,7 @@ function isFocusable2(element) {
1140
1164
  * Tabs
1141
1165
  * WAI-ARIA compliant tabs pattern implementation in TypeScript.
1142
1166
  *
1143
- * @version 1.5.6
1167
+ * @version 1.5.7
1144
1168
  * @author Yusuke Kamiyamane
1145
1169
  * @license MIT
1146
1170
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1152,7 +1176,7 @@ function isFocusable2(element) {
1152
1176
  (**
1153
1177
  * Attributes Utils
1154
1178
  *
1155
- * @version 1.1.0
1179
+ * @version 1.1.1
1156
1180
  * @author Yusuke Kamiyamane
1157
1181
  * @license MIT
1158
1182
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1176,7 +1200,7 @@ function isFocusable2(element) {
1176
1200
  * Lightweight roving tabindex utility with fully focus management.
1177
1201
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
1178
1202
  *
1179
- * @version 2.0.4
1203
+ * @version 2.2.0
1180
1204
  * @author Yusuke Kamiyamane
1181
1205
  * @license MIT
1182
1206
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1188,7 +1212,7 @@ function isFocusable2(element) {
1188
1212
  (**
1189
1213
  * Attributes Utils
1190
1214
  *
1191
- * @version 1.1.0
1215
+ * @version 1.1.1
1192
1216
  * @author Yusuke Kamiyamane
1193
1217
  * @license MIT
1194
1218
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1201,7 +1225,7 @@ function isFocusable2(element) {
1201
1225
  * High-precision focus management utility with full composed tree support.
1202
1226
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
1203
1227
  *
1204
- * @version 4.3.1
1228
+ * @version 4.3.2
1205
1229
  * @author Yusuke Kamiyamane
1206
1230
  * @license MIT
1207
1231
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Tabs
3
3
  * WAI-ARIA compliant tabs pattern implementation in TypeScript.
4
4
  *
5
- * @version 1.5.6
5
+ * @version 1.5.7
6
6
  * @author Yusuke Kamiyamane
7
7
  * @license MIT
8
8
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Tabs
3
3
  * WAI-ARIA compliant tabs pattern implementation in TypeScript.
4
4
  *
5
- * @version 1.5.6
5
+ * @version 1.5.7
6
6
  * @author Yusuke Kamiyamane
7
7
  * @license MIT
8
8
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.js CHANGED
@@ -11,11 +11,10 @@ function addTokenToAttribute(element, attribute, token, options = {}) {
11
11
  const tokens = value ? parse(value).filter(Boolean) : [];
12
12
  if (caseInsensitive) {
13
13
  const lower = token.toLowerCase();
14
- if (tokens.some((token2) => token2.toLowerCase() === lower)) {
15
- return;
14
+ if (tokens.every((token2) => token2.toLowerCase() !== lower)) {
15
+ tokens.push(token);
16
+ element.setAttribute(attribute, serialize(tokens));
16
17
  }
17
- tokens.push(token);
18
- element.setAttribute(attribute, serialize(tokens));
19
18
  return;
20
19
  }
21
20
  const set = new Set(tokens);
@@ -117,11 +116,10 @@ function addTokenToAttribute2(element, attribute, token, options = {}) {
117
116
  const tokens = value ? parse(value).filter(Boolean) : [];
118
117
  if (caseInsensitive) {
119
118
  const lower = token.toLowerCase();
120
- if (tokens.some((token2) => token2.toLowerCase() === lower)) {
121
- return;
119
+ if (tokens.every((token2) => token2.toLowerCase() !== lower)) {
120
+ tokens.push(token);
121
+ element.setAttribute(attribute, serialize(tokens));
122
122
  }
123
- tokens.push(token);
124
- element.setAttribute(attribute, serialize(tokens));
125
123
  return;
126
124
  }
127
125
  const set = new Set(tokens);
@@ -400,6 +398,8 @@ function createRovingTabIndex(container, options = {}) {
400
398
  let {
401
399
  direction,
402
400
  navigationOnly = false,
401
+ noMemory = false,
402
+ noStart = false,
403
403
  selector,
404
404
  typeahead = false,
405
405
  wrap = false
@@ -412,6 +412,14 @@ function createRovingTabIndex(container, options = {}) {
412
412
  console.warn("Invalid navigationOnly option. Fallback: false.");
413
413
  navigationOnly = false;
414
414
  }
415
+ if (typeof noMemory !== "boolean") {
416
+ console.warn("Invalid noMemory option. Fallback: false.");
417
+ noMemory = false;
418
+ }
419
+ if (typeof noStart !== "boolean") {
420
+ console.warn("Invalid noStart option. Fallback: false.");
421
+ noStart = false;
422
+ }
415
423
  if (typeof selector !== "undefined" && (typeof selector !== "string" || !selector.trim())) {
416
424
  console.warn(
417
425
  "Invalid selector. Fallback: all focusable elements (undefined)."
@@ -426,13 +434,9 @@ function createRovingTabIndex(container, options = {}) {
426
434
  console.warn("Invalid wrap option. Fallback: false.");
427
435
  wrap = false;
428
436
  }
429
- const settings = { navigationOnly, typeahead, wrap };
430
- if (direction) {
431
- Object.assign(settings, { direction });
432
- }
433
- if (selector) {
434
- Object.assign(settings, { selector });
435
- }
437
+ const settings = { navigationOnly, noMemory, noStart, typeahead, wrap };
438
+ direction && Object.assign(settings, { direction });
439
+ selector && Object.assign(settings, { selector });
436
440
  const roving = new RovingTabIndex(container, settings);
437
441
  return () => roving.destroy();
438
442
  }
@@ -465,12 +469,29 @@ var RovingTabIndex = class {
465
469
  #initialize() {
466
470
  this.#update(document.activeElement);
467
471
  this.#controller = new AbortController();
472
+ const { signal } = this.#controller;
473
+ document.addEventListener("focusin", this.#onFocusIn, {
474
+ capture: true,
475
+ signal
476
+ });
468
477
  document.addEventListener("keydown", this.#onKeyDown, {
469
478
  capture: true,
470
- signal: this.#controller.signal
479
+ signal
471
480
  });
472
481
  this.#container.setAttribute("data-roving-tabindex-initialized", "");
473
482
  }
483
+ #onFocusIn = (event) => {
484
+ const { target } = event;
485
+ if (!(target instanceof Element)) {
486
+ return;
487
+ }
488
+ const isFocusable22 = this.#focusables.has(target);
489
+ if (this.#options.noMemory && !isFocusable22) {
490
+ this.#update(null);
491
+ return;
492
+ }
493
+ isFocusable22 && this.#update(target);
494
+ };
474
495
  #onKeyDown = (event) => {
475
496
  if (!event.composedPath().includes(this.#container)) {
476
497
  return;
@@ -532,11 +553,7 @@ var RovingTabIndex = class {
532
553
  }
533
554
  }
534
555
  const focusable = target.at(newIndex);
535
- if (!focusable) {
536
- return;
537
- }
538
- this.#update(focusable);
539
- focusElement(focusable);
556
+ focusable && focusElement(focusable);
540
557
  };
541
558
  #update(active) {
542
559
  const current = new Set(this.#getFocusables());
@@ -551,7 +568,7 @@ var RovingTabIndex = class {
551
568
  index >= 0 && focusables.splice(index, 1);
552
569
  });
553
570
  }
554
- const { navigationOnly } = this.#options;
571
+ const { navigationOnly, noStart, typeahead } = this.#options;
555
572
  for (const focusable of current) {
556
573
  if (this.#focusables.has(focusable)) {
557
574
  continue;
@@ -561,7 +578,7 @@ var RovingTabIndex = class {
561
578
  saveAttributes2([focusable], ["tabindex"]);
562
579
  focusable.setAttribute("tabindex", "-1");
563
580
  }
564
- if (!this.#options.typeahead) {
581
+ if (!typeahead) {
565
582
  continue;
566
583
  }
567
584
  const value = focusable.ariaKeyShortcuts?.trim();
@@ -592,7 +609,7 @@ var RovingTabIndex = class {
592
609
  return;
593
610
  }
594
611
  [...this.#focusables].forEach((focusable, i) => {
595
- focusable.setAttribute("tabindex", i ? "-1" : "0");
612
+ focusable.setAttribute("tabindex", i || noStart ? "-1" : "0");
596
613
  });
597
614
  }
598
615
  #createSelectorFilter() {
@@ -603,7 +620,8 @@ var RovingTabIndex = class {
603
620
  return getFocusables(this.#container, {
604
621
  composed: true,
605
622
  filter: this.#selectorFilter,
606
- skipNegativeTabIndexCheck: !this.#options.navigationOnly
623
+ skipNegativeTabIndexCheck: !this.#options.navigationOnly,
624
+ skipVisibilityCheck: true
607
625
  });
608
626
  }
609
627
  };
@@ -657,6 +675,7 @@ var Tabs = class _Tabs {
657
675
  #animationController = null;
658
676
  #cleanupsRovingTabIndex = [];
659
677
  #animation = null;
678
+ #animationId = 0;
660
679
  #buttons = [];
661
680
  #indicators = [];
662
681
  #isDestroyed = false;
@@ -786,6 +805,7 @@ var Tabs = class _Tabs {
786
805
  p.setAttribute("hidden", isFocusable2(tab2) ? "until-found" : "");
787
806
  }
788
807
  });
808
+ const animationId = ++this.#animationId;
789
809
  this.#animation?.cancel();
790
810
  const { duration, easing } = this.#settings.animation.content;
791
811
  this.#animation = this.#contentElement.animate(
@@ -801,7 +821,9 @@ var Tabs = class _Tabs {
801
821
  }
802
822
  );
803
823
  const cleanup = () => {
804
- this.#animation = null;
824
+ if (animationId === this.#animationId) {
825
+ this.#animation = null;
826
+ }
805
827
  };
806
828
  this.#animationController = new AbortController();
807
829
  const { signal } = this.#animationController;
@@ -812,8 +834,10 @@ var Tabs = class _Tabs {
812
834
  this.#animation.addEventListener(
813
835
  "finish",
814
836
  () => {
815
- this.#onAnimationFinish();
816
- cleanup();
837
+ if (animationId === this.#animationId) {
838
+ this.#onAnimationFinish();
839
+ cleanup();
840
+ }
817
841
  },
818
842
  {
819
843
  once: true,
@@ -1138,7 +1162,7 @@ function isFocusable2(element) {
1138
1162
  * Tabs
1139
1163
  * WAI-ARIA compliant tabs pattern implementation in TypeScript.
1140
1164
  *
1141
- * @version 1.5.6
1165
+ * @version 1.5.7
1142
1166
  * @author Yusuke Kamiyamane
1143
1167
  * @license MIT
1144
1168
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1150,7 +1174,7 @@ function isFocusable2(element) {
1150
1174
  (**
1151
1175
  * Attributes Utils
1152
1176
  *
1153
- * @version 1.1.0
1177
+ * @version 1.1.1
1154
1178
  * @author Yusuke Kamiyamane
1155
1179
  * @license MIT
1156
1180
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1174,7 +1198,7 @@ function isFocusable2(element) {
1174
1198
  * Lightweight roving tabindex utility with fully focus management.
1175
1199
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
1176
1200
  *
1177
- * @version 2.0.4
1201
+ * @version 2.2.0
1178
1202
  * @author Yusuke Kamiyamane
1179
1203
  * @license MIT
1180
1204
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1186,7 +1210,7 @@ function isFocusable2(element) {
1186
1210
  (**
1187
1211
  * Attributes Utils
1188
1212
  *
1189
- * @version 1.1.0
1213
+ * @version 1.1.1
1190
1214
  * @author Yusuke Kamiyamane
1191
1215
  * @license MIT
1192
1216
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -1199,7 +1223,7 @@ function isFocusable2(element) {
1199
1223
  * High-precision focus management utility with full composed tree support.
1200
1224
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
1201
1225
  *
1202
- * @version 4.3.1
1226
+ * @version 4.3.2
1203
1227
  * @author Yusuke Kamiyamane
1204
1228
  * @license MIT
1205
1229
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/tabs",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "description": "WAI-ARIA compliant tabs pattern implementation in TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -42,9 +42,9 @@
42
42
  },
43
43
  "homepage": "https://github.com/y14e/tabs#readme",
44
44
  "devDependencies": {
45
- "@y14e/attributes-utils": "^1.1.0",
45
+ "@y14e/attributes-utils": "^1.1.1",
46
46
  "@y14e/button": "^1.0.0",
47
- "@y14e/roving-tabindex": "^2.0.4",
47
+ "@y14e/roving-tabindex": "^2.2.0",
48
48
  "bun-types": "latest",
49
49
  "tsup": "^8.0.0",
50
50
  "typescript": "^5.6.0",