@y14e/tabs 1.3.11 → 1.4.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
@@ -50,6 +50,506 @@ function saveAttributes(elements, attributes) {
50
50
  });
51
51
  }
52
52
 
53
+ // node_modules/@y14e/roving-tabindex/dist/index.js
54
+ var defaultParser2 = (value) => value.split(/\s+/);
55
+ var defaultSerializer2 = (tokens) => tokens.join(" ");
56
+ function addTokenToAttribute2(element, attribute, token, options = {}) {
57
+ const {
58
+ caseInsensitive = false,
59
+ parse = defaultParser2,
60
+ serialize = defaultSerializer2
61
+ } = options;
62
+ const value = element.getAttribute(attribute)?.trim();
63
+ const tokens = value ? parse(value).filter(Boolean) : [];
64
+ if (caseInsensitive) {
65
+ const lower = token.toLowerCase();
66
+ if (tokens.some((token2) => token2.toLowerCase() === lower)) {
67
+ return;
68
+ }
69
+ tokens.push(token);
70
+ element.setAttribute(attribute, serialize(tokens));
71
+ return;
72
+ }
73
+ const set = new Set(tokens);
74
+ set.add(token);
75
+ element.setAttribute(attribute, serialize([...set]));
76
+ }
77
+ var snapshots2 = /* @__PURE__ */ new WeakMap();
78
+ function restoreAttributes2(elements) {
79
+ for (const element of elements) {
80
+ const snapshot = snapshots2.get(element);
81
+ if (!snapshot) {
82
+ continue;
83
+ }
84
+ for (const [attribute, value] of snapshot.entries()) {
85
+ value === null ? element.removeAttribute(attribute) : element.setAttribute(attribute, value);
86
+ }
87
+ snapshots2.delete(element);
88
+ }
89
+ }
90
+ function saveAttributes2(elements, attributes) {
91
+ elements.forEach((element) => {
92
+ let snapshot = snapshots2.get(element);
93
+ if (!snapshot) {
94
+ snapshot = /* @__PURE__ */ new Map();
95
+ snapshots2.set(element, snapshot);
96
+ }
97
+ attributes.forEach((attribute) => {
98
+ snapshot.set(attribute, element.getAttribute(attribute));
99
+ });
100
+ });
101
+ }
102
+ var FOCUSABLE_SELECTOR = `:is(a[href], area[href], button, embed, iframe, input:not([type="hidden" i]), object, select, details > summary:first-of-type, textarea, [contenteditable]:not([contenteditable="false" i]), [controls], [tabindex]):not(:disabled, [hidden], [inert], [tabindex="-1"])`;
103
+ function getFocusables(container = document.body, options = {}) {
104
+ if (!(container instanceof Element)) {
105
+ console.warn("Invalid container element. Fallback: <body> element.");
106
+ container = document.body;
107
+ }
108
+ let {
109
+ composed = false,
110
+ filter,
111
+ include,
112
+ skipVisibilityCheck = false
113
+ } = options;
114
+ if (typeof composed !== "boolean") {
115
+ console.warn("Invalid composed option. Fallback: false.");
116
+ composed = false;
117
+ }
118
+ if (typeof filter !== "undefined" && typeof filter !== "function") {
119
+ console.warn(
120
+ "Invalid filter function. Fallback: no filter function (undefined)."
121
+ );
122
+ filter = void 0;
123
+ }
124
+ if (typeof include !== "undefined" && typeof include !== "function") {
125
+ console.warn(
126
+ "Invalid include function. Fallback: no include function (undefined)."
127
+ );
128
+ include = void 0;
129
+ }
130
+ const elements = [];
131
+ if (composed || include) {
132
+ let traverse2 = function(node) {
133
+ if (node instanceof Element) {
134
+ if (isFocusable(node, { skipVisibilityCheck }) || include?.(node)) {
135
+ elements[elements.length] = node;
136
+ }
137
+ }
138
+ const children = getComposedChildren(node);
139
+ for (let i = 0, l = children.length; i < l; i++) {
140
+ const child = children[i];
141
+ if (!child) {
142
+ continue;
143
+ }
144
+ traverse2(child);
145
+ }
146
+ };
147
+ traverse2(container);
148
+ } else {
149
+ const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
150
+ for (let i = 0, l = candidates.length; i < l; i++) {
151
+ const candidate = candidates[i];
152
+ if (!(candidate instanceof Element)) {
153
+ continue;
154
+ }
155
+ if (isFocusable(candidate, { skipVisibilityCheck })) {
156
+ elements[elements.length] = candidate;
157
+ }
158
+ }
159
+ }
160
+ const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
161
+ return filter ? unfiltered.filter(filter) : unfiltered;
162
+ }
163
+ function isFocusable(element, options = {}) {
164
+ if (!(element instanceof Element)) {
165
+ console.warn("Invalid element");
166
+ return false;
167
+ }
168
+ let { skipVisibilityCheck = false } = options;
169
+ if (typeof skipVisibilityCheck !== "boolean") {
170
+ console.warn("Invalid skipVisibilityCheck option. Fallback: false.");
171
+ skipVisibilityCheck = false;
172
+ }
173
+ if (element.hasAttribute("hidden") || isInert(element)) {
174
+ return false;
175
+ }
176
+ if (getTabIndex(element) < 0) {
177
+ return false;
178
+ }
179
+ if (!element.matches(FOCUSABLE_SELECTOR)) {
180
+ return false;
181
+ }
182
+ if (isDisabledDeep(element)) {
183
+ return false;
184
+ }
185
+ if (!skipVisibilityCheck && !element.checkVisibility({
186
+ contentVisibilityAuto: true,
187
+ opacityProperty: true,
188
+ visibilityProperty: true
189
+ })) {
190
+ return false;
191
+ }
192
+ return true;
193
+ }
194
+ function isDisabledDeep(element) {
195
+ let current = element;
196
+ while (current) {
197
+ if (current instanceof ShadowRoot) {
198
+ if (current.mode !== "open") {
199
+ return false;
200
+ }
201
+ current = current.host;
202
+ continue;
203
+ }
204
+ if (!(current instanceof Element)) {
205
+ current = current.parentNode;
206
+ continue;
207
+ }
208
+ if (current === element && isFormControl(current) && isDisabled(current)) {
209
+ return true;
210
+ }
211
+ if (isInert(current)) {
212
+ return true;
213
+ }
214
+ if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
215
+ if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
216
+ return true;
217
+ }
218
+ }
219
+ current = current.parentNode;
220
+ }
221
+ return false;
222
+ }
223
+ function normalizeRadioGroup(elements) {
224
+ let map = null;
225
+ for (let i = 0, l = elements.length; i < l; i++) {
226
+ const element = elements[i];
227
+ if (!(element instanceof HTMLInputElement)) {
228
+ continue;
229
+ }
230
+ if (!isUngroupedRadio(element)) {
231
+ continue;
232
+ }
233
+ if (!map) {
234
+ map = /* @__PURE__ */ new Map();
235
+ }
236
+ const key = `${element.form?.id ?? "no-form"}::${element.name}`;
237
+ const group = map.get(key) ?? map.set(key, []).get(key);
238
+ if (group) {
239
+ group[group.length] = element;
240
+ }
241
+ }
242
+ if (!map) {
243
+ return elements;
244
+ }
245
+ const placeholder = /* @__PURE__ */ new Set();
246
+ for (const group of map.values()) {
247
+ placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
248
+ }
249
+ return elements.filter((element) => {
250
+ if (isUngroupedRadio(element)) {
251
+ return placeholder.has(element);
252
+ }
253
+ return true;
254
+ });
255
+ }
256
+ function sortByTabIndex(elements) {
257
+ const ordered = [];
258
+ const natural = [];
259
+ for (let i = 0, l = elements.length; i < l; i++) {
260
+ const element = elements[i];
261
+ if (!element) {
262
+ continue;
263
+ }
264
+ const target = getTabIndex(element) > 0 ? ordered : natural;
265
+ target[target.length] = element;
266
+ }
267
+ ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
268
+ let count = 0;
269
+ const sorted = new Array(ordered.length + natural.length);
270
+ for (let i = 0, l = ordered.length; i < l; i++) {
271
+ sorted[count++] = ordered[i];
272
+ }
273
+ for (let i = 0, l = natural.length; i < l; i++) {
274
+ sorted[count++] = natural[i];
275
+ }
276
+ return sorted;
277
+ }
278
+ function getComposedChildren(node) {
279
+ if (node instanceof ShadowRoot) {
280
+ return getChildren(node);
281
+ }
282
+ if (!(node instanceof Element)) {
283
+ return [];
284
+ }
285
+ if (node instanceof HTMLSlotElement) {
286
+ const assigned = node.assignedElements({ flatten: true });
287
+ if (assigned.length) {
288
+ return assigned;
289
+ }
290
+ }
291
+ if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
292
+ return getChildren(node.shadowRoot);
293
+ }
294
+ return getChildren(node);
295
+ }
296
+ function getChildren(node) {
297
+ const elements = [];
298
+ for (let child = node.firstElementChild; child; child = child.nextElementSibling) {
299
+ elements[elements.length] = child;
300
+ }
301
+ return elements;
302
+ }
303
+ function getTabIndex(element) {
304
+ return "tabIndex" in element ? Number(element.tabIndex) : 0;
305
+ }
306
+ function isDisabled(element) {
307
+ return "disabled" in element && !!element.disabled;
308
+ }
309
+ function isFormControl(element) {
310
+ const name = element.tagName;
311
+ return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
312
+ }
313
+ function isInert(element) {
314
+ return "inert" in element && !!element.inert;
315
+ }
316
+ function isUngroupedRadio(element) {
317
+ return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
318
+ }
319
+ function createRovingTabIndex(container, options = {}) {
320
+ if (!(container instanceof Element)) {
321
+ console.warn("Invalid container element");
322
+ return () => {
323
+ };
324
+ }
325
+ let {
326
+ direction,
327
+ navigationOnly = false,
328
+ selector,
329
+ typeahead = false,
330
+ wrap = false
331
+ } = options;
332
+ if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
333
+ console.warn("Invalid direction option. Fallback: both (undefined).");
334
+ direction = void 0;
335
+ }
336
+ if (typeof navigationOnly !== "boolean") {
337
+ console.warn("Invalid navigationOnly option. Fallback: false.");
338
+ navigationOnly = false;
339
+ }
340
+ if (typeof selector !== "undefined" && typeof selector !== "string") {
341
+ console.warn(
342
+ "Invalid selector. Fallback: all focusable elements (undefined)."
343
+ );
344
+ selector = void 0;
345
+ }
346
+ if (typeof typeahead !== "boolean") {
347
+ console.warn("Invalid typeahead option. Fallback: false.");
348
+ typeahead = false;
349
+ }
350
+ if (typeof wrap !== "boolean") {
351
+ console.warn("Invalid wrap option. Fallback: false.");
352
+ wrap = false;
353
+ }
354
+ const roving = new RovingTabIndex(container, {
355
+ direction,
356
+ navigationOnly,
357
+ selector,
358
+ typeahead,
359
+ wrap
360
+ });
361
+ return () => roving.destroy();
362
+ }
363
+ var RovingTabIndex = class {
364
+ #container;
365
+ #options;
366
+ #focusables = /* @__PURE__ */ new Set();
367
+ #focusablesByFirstChar = /* @__PURE__ */ new Map();
368
+ #selectorFilter;
369
+ #controller = null;
370
+ #isDestroyed = false;
371
+ constructor(container, options = {}) {
372
+ this.#container = container;
373
+ this.#options = options;
374
+ this.#selectorFilter = this.#createSelectorFilter();
375
+ this.#initialize();
376
+ }
377
+ destroy() {
378
+ if (this.#isDestroyed) {
379
+ return;
380
+ }
381
+ this.#isDestroyed = true;
382
+ this.#controller?.abort();
383
+ this.#controller = null;
384
+ restoreAttributes2([...this.#focusables]);
385
+ this.#focusables.clear();
386
+ this.#focusablesByFirstChar.clear();
387
+ this.#container.removeAttribute("data-roving-tabindex-initialized");
388
+ }
389
+ #initialize() {
390
+ this.#update(document.activeElement);
391
+ this.#controller = new AbortController();
392
+ document.addEventListener("keydown", this.#onKeyDown, {
393
+ capture: true,
394
+ signal: this.#controller.signal
395
+ });
396
+ this.#container.setAttribute("data-roving-tabindex-initialized", "");
397
+ }
398
+ #onKeyDown = (event) => {
399
+ if (!event.composedPath().includes(this.#container)) {
400
+ return;
401
+ }
402
+ const { key, altKey, ctrlKey, metaKey } = event;
403
+ if (altKey || ctrlKey || metaKey) {
404
+ return;
405
+ }
406
+ const { direction, typeahead, wrap } = this.#options;
407
+ const isBoth = !direction;
408
+ const isHorizontal = direction === "horizontal";
409
+ if (![
410
+ "End",
411
+ "Home",
412
+ ...isBoth ? ["ArrowLeft", "ArrowUp"] : [`Arrow${isHorizontal ? "Left" : "Up"}`],
413
+ ...isBoth ? ["ArrowRight", "ArrowDown"] : [`Arrow${isHorizontal ? "Right" : "Down"}`]
414
+ ].includes(key)) {
415
+ if (!typeahead || !/^\S$/i.test(key) || !this.#focusablesByFirstChar.has(key.toUpperCase())) {
416
+ return;
417
+ }
418
+ }
419
+ const active = getActiveElement();
420
+ if (!(active instanceof HTMLElement)) {
421
+ return;
422
+ }
423
+ const current = this.#getFocusables();
424
+ if (!current.includes(active)) {
425
+ return;
426
+ }
427
+ event.preventDefault();
428
+ event.stopPropagation();
429
+ const currentIndex = current.indexOf(active);
430
+ let rawIndex;
431
+ let newIndex = currentIndex;
432
+ let target = current;
433
+ switch (key) {
434
+ case "End":
435
+ newIndex = -1;
436
+ break;
437
+ case "Home":
438
+ newIndex = 0;
439
+ break;
440
+ case "ArrowLeft":
441
+ case "ArrowUp":
442
+ rawIndex = currentIndex - 1;
443
+ newIndex = wrap ? rawIndex : Math.max(rawIndex, 0);
444
+ break;
445
+ case "ArrowRight":
446
+ case "ArrowDown":
447
+ rawIndex = currentIndex + 1;
448
+ newIndex = wrap ? rawIndex % current.length : Math.min(rawIndex, current.length - 1);
449
+ break;
450
+ default: {
451
+ target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
452
+ const foundIndex = target.findIndex(
453
+ (focusable2) => current.indexOf(focusable2) > currentIndex
454
+ );
455
+ newIndex = foundIndex >= 0 ? foundIndex : 0;
456
+ }
457
+ }
458
+ const focusable = target.at(newIndex);
459
+ if (!focusable) {
460
+ return;
461
+ }
462
+ this.#update(focusable);
463
+ focusElement(focusable);
464
+ };
465
+ #update(active) {
466
+ const current = /* @__PURE__ */ new Set([
467
+ ...this.#getFocusables(),
468
+ ...getFocusables(this.#container, {
469
+ composed: true,
470
+ filter: this.#selectorFilter,
471
+ skipVisibilityCheck: true
472
+ })
473
+ ]);
474
+ for (const focusable of this.#focusables) {
475
+ if (current.has(focusable)) {
476
+ continue;
477
+ }
478
+ focusable.isConnected && restoreAttributes2([focusable]);
479
+ this.#focusables.delete(focusable);
480
+ this.#focusablesByFirstChar.forEach((focusables) => {
481
+ const index = focusables.indexOf(focusable);
482
+ index >= 0 && focusables.splice(index, 1);
483
+ });
484
+ }
485
+ const { navigationOnly } = this.#options;
486
+ for (const focusable of current) {
487
+ if (this.#focusables.has(focusable)) {
488
+ continue;
489
+ }
490
+ this.#focusables.add(focusable);
491
+ if (!navigationOnly) {
492
+ saveAttributes2([focusable], ["tabindex"]);
493
+ focusable.setAttribute("tabindex", "-1");
494
+ }
495
+ if (!this.#options.typeahead) {
496
+ continue;
497
+ }
498
+ const value = focusable.ariaKeyShortcuts?.trim();
499
+ const keys = new Set(
500
+ value ? value.split(/\s+/).filter((key) => /^\S$/i.test(key)).map((key) => key.toUpperCase()) : []
501
+ );
502
+ const char = focusable.textContent?.trim()?.at(0)?.toUpperCase();
503
+ if (char) {
504
+ keys.add(char);
505
+ saveAttributes2([focusable], ["aria-keyshortcuts"]);
506
+ addTokenToAttribute2(focusable, "aria-keyshortcuts", char, {
507
+ caseInsensitive: true
508
+ });
509
+ }
510
+ keys.forEach((key) => {
511
+ const focusables = this.#focusablesByFirstChar.get(key) ?? [];
512
+ focusables.push(focusable);
513
+ this.#focusablesByFirstChar.set(key, focusables);
514
+ });
515
+ }
516
+ if (navigationOnly) {
517
+ return;
518
+ }
519
+ if (active && this.#focusables.has(active)) {
520
+ this.#focusables.forEach((focusable) => {
521
+ focusable.setAttribute("tabindex", focusable === active ? "0" : "-1");
522
+ });
523
+ return;
524
+ }
525
+ [...this.#focusables].forEach((focusable, i) => {
526
+ focusable.setAttribute("tabindex", i ? "-1" : "0");
527
+ });
528
+ }
529
+ #createSelectorFilter() {
530
+ const { selector } = this.#options;
531
+ return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
532
+ }
533
+ #getFocusables() {
534
+ return getFocusables(this.#container, {
535
+ composed: true,
536
+ filter: this.#selectorFilter,
537
+ include: (element) => this.#focusables.has(element),
538
+ skipVisibilityCheck: true
539
+ });
540
+ }
541
+ };
542
+ function focusElement(element) {
543
+ "focus" in element && typeof element.focus === "function" && element.focus();
544
+ }
545
+ function getActiveElement() {
546
+ let current = document.activeElement;
547
+ while (current?.shadowRoot?.activeElement) {
548
+ current = current.shadowRoot.activeElement;
549
+ }
550
+ return current;
551
+ }
552
+
53
553
  // src/index.ts
54
554
  var Tabs = class _Tabs {
55
555
  static defaults = {};
@@ -87,6 +587,7 @@ var Tabs = class _Tabs {
87
587
  #bindings = /* @__PURE__ */ new WeakMap();
88
588
  #eventController = null;
89
589
  #animationController = null;
590
+ #cleanupsRovingTabIndex = [];
90
591
  #animation = null;
91
592
  #indicators = [];
92
593
  #isDestroyed = false;
@@ -101,12 +602,10 @@ var Tabs = class _Tabs {
101
602
  this.#rootElement = root;
102
603
  this.#defaults = this.#mergeOptions(this.#defaults, _Tabs.defaults);
103
604
  this.#settings = this.#mergeOptions(this.#defaults, options);
104
- if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
105
- Object.assign(this.#settings.animation, {
106
- content: { duration: 0 },
107
- indicator: { duration: 0 }
108
- });
109
- }
605
+ matchMedia("(prefers-reduced-motion: reduce)").matches && Object.assign(this.#settings.animation, {
606
+ content: { duration: 0 },
607
+ indicator: { duration: 0 }
608
+ });
110
609
  const NOT_NESTED = `:not(:scope ${this.#settings.selector.panel} *)`;
111
610
  this.#listElements = [
112
611
  ...this.#rootElement.querySelectorAll(
@@ -160,9 +659,7 @@ var Tabs = class _Tabs {
160
659
  }
161
660
  const binding = createBinding(tabsByIndex, panel);
162
661
  this.#bindings.set(tab, binding);
163
- if (i < length) {
164
- this.#bindings.set(panel, binding);
165
- }
662
+ i < length && this.#bindings.set(panel, binding);
166
663
  });
167
664
  this.#initialize();
168
665
  }
@@ -207,11 +704,7 @@ var Tabs = class _Tabs {
207
704
  }
208
705
  style2.setProperty("inline-size", "100%");
209
706
  style2.setProperty("position", "absolute");
210
- if (p === panel && !hasFocusable(p)) {
211
- p.setAttribute("tabindex", "0");
212
- } else {
213
- p.removeAttribute("tabindex");
214
- }
707
+ p === panel && !hasFocusable(p) ? p.setAttribute("tabindex", "0") : p.removeAttribute("tabindex");
215
708
  });
216
709
  this.#panelElements.forEach((p, i) => {
217
710
  if (p === panel) {
@@ -221,7 +714,7 @@ var Tabs = class _Tabs {
221
714
  if (!tab2) {
222
715
  return;
223
716
  }
224
- p.setAttribute("hidden", isFocusable(tab2) ? "until-found" : "");
717
+ p.setAttribute("hidden", isFocusable2(tab2) ? "until-found" : "");
225
718
  }
226
719
  });
227
720
  this.#animation?.cancel();
@@ -294,6 +787,10 @@ var Tabs = class _Tabs {
294
787
  this.#isDestroyed = true;
295
788
  this.#eventController?.abort();
296
789
  this.#eventController = null;
790
+ this.#cleanupsRovingTabIndex.forEach((cleanup) => {
791
+ cleanup();
792
+ });
793
+ this.#cleanupsRovingTabIndex.length = 0;
297
794
  this.#indicators.forEach((indicator) => {
298
795
  indicator.destroy(force);
299
796
  });
@@ -346,12 +843,8 @@ var Tabs = class _Tabs {
346
843
  "style"
347
844
  ]);
348
845
  this.#listElements.forEach((list, i) => {
349
- if (this.#settings.avoidDuplicates && i) {
350
- list.setAttribute("aria-hidden", "true");
351
- }
352
- if (this.#settings.vertical) {
353
- list.setAttribute("aria-orientation", "vertical");
354
- }
846
+ this.#settings.avoidDuplicates && i && list.setAttribute("aria-hidden", "true");
847
+ this.#settings.vertical && list.setAttribute("aria-orientation", "vertical");
355
848
  list.setAttribute("role", "tablist");
356
849
  });
357
850
  saveAttributes(this.#tabElements, [
@@ -367,29 +860,19 @@ var Tabs = class _Tabs {
367
860
  if (!panel) {
368
861
  return;
369
862
  }
370
- if (i < this.#panelElements.length) {
371
- saveAttributes(
372
- [panel],
373
- ["aria-controls", "aria-labelledby", "id", "role", "tabindex"]
374
- );
375
- }
863
+ i < this.#panelElements.length && saveAttributes(
864
+ [panel],
865
+ ["aria-controls", "aria-labelledby", "id", "role", "tabindex"]
866
+ );
376
867
  panel.id ||= `tabs-panel-${id}`;
377
868
  addTokenToAttribute(tab, "aria-controls", panel.id);
378
- if (!tab.hasAttribute("aria-selected")) {
379
- tab.setAttribute("aria-selected", "false");
380
- }
869
+ !tab.hasAttribute("aria-selected") && tab.setAttribute("aria-selected", "false");
381
870
  const isAvoided = this.#isAvoidedTab(tab);
382
871
  if (!isAvoided) {
383
872
  tab.id ||= `tabs-tab-${id}`;
384
873
  }
385
874
  tab.setAttribute("role", "tab");
386
- tab.setAttribute(
387
- "tabindex",
388
- tab.ariaSelected === "true" && !isAvoided ? "0" : "-1"
389
- );
390
- if (!isFocusable(tab)) {
391
- tab.style.setProperty("pointer-events", "none");
392
- }
875
+ !isFocusable2(tab) && tab.style.setProperty("pointer-events", "none");
393
876
  addTokenToAttribute(panel, "aria-labelledby", tab.id);
394
877
  tab.addEventListener("click", this.#onTabClick, { signal });
395
878
  tab.addEventListener("focus", this.#onTabFocus, { signal });
@@ -405,13 +888,26 @@ var Tabs = class _Tabs {
405
888
  });
406
889
  this.#panelElements.forEach((panel) => {
407
890
  panel.setAttribute("role", "tabpanel");
408
- if (!panel.hasAttribute("hidden") && !hasFocusable(panel)) {
409
- panel.setAttribute("tabindex", "0");
410
- }
891
+ !panel.hasAttribute("hidden") && !hasFocusable(panel) && panel.setAttribute("tabindex", "0");
411
892
  panel.addEventListener("beforematch", this.#onPanelBeforeMatch, {
412
893
  signal
413
894
  });
414
895
  });
896
+ this.#listElements.forEach((list) => {
897
+ this.#cleanupsRovingTabIndex.push(
898
+ createRovingTabIndex(list, {
899
+ direction: list.ariaOrientation === "undefined" ? void 0 : this.#settings.vertical ? "vertical" : "horizontal",
900
+ selector: this.#settings.selector.tab,
901
+ wrap: true
902
+ })
903
+ );
904
+ list.querySelectorAll(this.#settings.selector.tab).forEach((tab) => {
905
+ tab.setAttribute(
906
+ "tabindex",
907
+ tab.ariaSelected === "true" && !this.#isAvoidedTab(tab) ? "0" : "-1"
908
+ );
909
+ });
910
+ });
415
911
  this.#rootElement.setAttribute("data-tabs-initialized", "");
416
912
  }
417
913
  #onTabClick = (event) => {
@@ -427,6 +923,7 @@ var Tabs = class _Tabs {
427
923
  if (!(tab instanceof HTMLElement)) {
428
924
  return;
429
925
  }
926
+ !this.#settings.manual && tab.click();
430
927
  this.#isAvoidedTab(tab) && tab.blur();
431
928
  };
432
929
  #onTabKeyDown = (event) => {
@@ -434,63 +931,19 @@ var Tabs = class _Tabs {
434
931
  if (altKey || ctrlKey || metaKey || shiftKey) {
435
932
  return;
436
933
  }
437
- const currentTab = event.currentTarget;
438
- if (!(currentTab instanceof HTMLElement)) {
934
+ if (!["Enter", " "].includes(key)) {
439
935
  return;
440
936
  }
441
- const list = currentTab.closest(this.#settings.selector.list);
442
- if (!list) {
443
- return;
444
- }
445
- const isBoth = list.ariaOrientation === "undefined";
446
- const isHorizontal = list.ariaOrientation !== "vertical";
447
- if (![
448
- "Enter",
449
- " ",
450
- "End",
451
- "Home",
452
- ...isBoth ? ["ArrowLeft", "ArrowUp"] : [`Arrow${isHorizontal ? "Left" : "Up"}`],
453
- ...isBoth ? ["ArrowRight", "ArrowDown"] : [`Arrow${isHorizontal ? "Right" : "Down"}`]
454
- ].includes(key)) {
455
- return;
456
- }
457
- const focusables = [
458
- ...list.querySelectorAll(this.#settings.selector.tab)
459
- ].filter(isFocusable);
460
- const active = getActiveElement();
937
+ const active = getActiveElement2();
461
938
  if (!(active instanceof HTMLElement)) {
462
939
  return;
463
940
  }
464
941
  event.preventDefault();
465
- const currentIndex = focusables.indexOf(active);
466
- let newIndex = currentIndex;
467
942
  switch (key) {
468
943
  case "Enter":
469
944
  case " ":
470
945
  active.click();
471
946
  return;
472
- case "End":
473
- newIndex = -1;
474
- break;
475
- case "Home":
476
- newIndex = 0;
477
- break;
478
- case "ArrowLeft":
479
- case "ArrowUp":
480
- newIndex = currentIndex - 1;
481
- break;
482
- case "ArrowRight":
483
- case "ArrowDown":
484
- newIndex = (currentIndex + 1) % focusables.length;
485
- break;
486
- }
487
- const newTab = focusables.at(newIndex);
488
- if (!newTab) {
489
- return;
490
- }
491
- newTab.focus();
492
- if (!this.#settings.manual) {
493
- newTab.click();
494
947
  }
495
948
  };
496
949
  #onPanelBeforeMatch = (event) => {
@@ -621,7 +1074,7 @@ var TabsIndicator = class {
621
1074
  function createBinding(tabs, panel) {
622
1075
  return { tabs, panel, animation: null };
623
1076
  }
624
- function getActiveElement() {
1077
+ function getActiveElement2() {
625
1078
  let current = document.activeElement;
626
1079
  while (current?.shadowRoot?.activeElement) {
627
1080
  current = current.shadowRoot.activeElement;
@@ -635,14 +1088,14 @@ function hasFocusable(container) {
635
1088
  )
636
1089
  ].filter((element) => element.checkVisibility()).length;
637
1090
  }
638
- function isFocusable(element) {
1091
+ function isFocusable2(element) {
639
1092
  return !element.hasAttribute("disabled");
640
1093
  }
641
1094
  /**
642
1095
  * Tabs
643
1096
  * WAI-ARIA compliant tabs pattern implementation in TypeScript.
644
1097
  *
645
- * @version 1.3.11
1098
+ * @version 1.4.0
646
1099
  * @author Yusuke Kamiyamane
647
1100
  * @license MIT
648
1101
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -660,6 +1113,45 @@ function isFocusable(element) {
660
1113
  * @copyright Copyright (c) Yusuke Kamiyamane
661
1114
  * @see {@link https://github.com/y14e/attributes-utils}
662
1115
  *)
1116
+
1117
+ @y14e/roving-tabindex/dist/index.js:
1118
+ (**
1119
+ * Roving Tabindex
1120
+ * Lightweight roving tabindex utility with fully focus management.
1121
+ * Designed for accessible menus, tabs, toolbars, and composite widgets.
1122
+ *
1123
+ * @version 1.3.1
1124
+ * @author Yusuke Kamiyamane
1125
+ * @license MIT
1126
+ * @copyright Copyright (c) Yusuke Kamiyamane
1127
+ * @see {@link https://github.com/y14e/roving-tabindex}
1128
+ *)
1129
+ (*! Bundled license information:
1130
+
1131
+ @y14e/attributes-utils/dist/index.js:
1132
+ (**
1133
+ * Attributes Utils
1134
+ *
1135
+ * @version 1.0.5
1136
+ * @author Yusuke Kamiyamane
1137
+ * @license MIT
1138
+ * @copyright Copyright (c) Yusuke Kamiyamane
1139
+ * @see {@link https://github.com/y14e/attributes-utils}
1140
+ *)
1141
+
1142
+ power-focusable/dist/index.js:
1143
+ (**
1144
+ * Power Focusable
1145
+ * High-precision focus management utility with full composed tree support.
1146
+ * Handles complex focus rules including tabindex ordering, radio groups, inert.
1147
+ *
1148
+ * @version 4.2.0
1149
+ * @author Yusuke Kamiyamane
1150
+ * @license MIT
1151
+ * @copyright Copyright (c) Yusuke Kamiyamane
1152
+ * @see {@link https://github.com/y14e/power-focusable}
1153
+ *)
1154
+ *)
663
1155
  */
664
1156
 
665
1157
  module.exports = Tabs;