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