@aurodesignsystem-dev/auro-formkit 0.0.0-pr1608.0 → 0.0.0-pr1608.1

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.
Files changed (51) hide show
  1. package/components/checkbox/demo/accessibility.md +15 -4
  2. package/components/checkbox/demo/customize.min.js +25 -10
  3. package/components/checkbox/demo/getting-started.min.js +25 -10
  4. package/components/checkbox/demo/index.min.js +25 -10
  5. package/components/checkbox/dist/auro-checkbox-group.d.ts +9 -0
  6. package/components/checkbox/dist/index.js +25 -10
  7. package/components/checkbox/dist/registered.js +25 -10
  8. package/components/combobox/demo/customize.min.js +46 -4
  9. package/components/combobox/demo/getting-started.min.js +46 -4
  10. package/components/combobox/demo/index.min.js +46 -4
  11. package/components/combobox/dist/index.js +46 -4
  12. package/components/combobox/dist/registered.js +46 -4
  13. package/components/counter/demo/customize.min.js +339 -23
  14. package/components/counter/demo/getting-started.min.js +339 -23
  15. package/components/counter/demo/index.min.js +339 -23
  16. package/components/counter/dist/counterGroupKeyboardStrategy.d.ts +1 -0
  17. package/components/counter/dist/index.js +339 -23
  18. package/components/counter/dist/registered.js +339 -23
  19. package/components/datepicker/demo/customize.min.js +46 -4
  20. package/components/datepicker/demo/getting-started.min.js +46 -4
  21. package/components/datepicker/demo/index.min.js +46 -4
  22. package/components/datepicker/dist/index.js +46 -4
  23. package/components/datepicker/dist/registered.js +46 -4
  24. package/components/dropdown/demo/customize.min.js +44 -2
  25. package/components/dropdown/demo/getting-started.min.js +45 -3
  26. package/components/dropdown/demo/index.min.js +44 -2
  27. package/components/dropdown/dist/index.js +44 -2
  28. package/components/dropdown/dist/registered.js +44 -2
  29. package/components/form/demo/customize.min.js +527 -57
  30. package/components/form/demo/getting-started.min.js +527 -57
  31. package/components/form/demo/index.min.js +527 -57
  32. package/components/form/demo/registerDemoDeps.min.js +527 -57
  33. package/components/input/demo/customize.min.js +1 -1
  34. package/components/input/demo/getting-started.min.js +1 -1
  35. package/components/input/demo/index.min.js +1 -1
  36. package/components/input/dist/index.js +1 -1
  37. package/components/input/dist/registered.js +1 -1
  38. package/components/radio/demo/accessibility.md +13 -4
  39. package/components/radio/demo/customize.min.js +25 -12
  40. package/components/radio/demo/getting-started.min.js +25 -12
  41. package/components/radio/demo/index.min.js +25 -12
  42. package/components/radio/dist/auro-radio-group.d.ts +9 -0
  43. package/components/radio/dist/index.js +25 -12
  44. package/components/radio/dist/registered.js +25 -12
  45. package/components/select/demo/customize.min.js +45 -3
  46. package/components/select/demo/getting-started.min.js +45 -3
  47. package/components/select/demo/index.min.js +45 -3
  48. package/components/select/dist/index.js +45 -3
  49. package/components/select/dist/registered.js +45 -3
  50. package/custom-elements.json +1507 -1485
  51. package/package.json +3 -3
@@ -1127,7 +1127,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$2 {
1127
1127
  }
1128
1128
  };
1129
1129
 
1130
- var formkitVersion$1 = '202609021913';
1130
+ var formkitVersion$1 = '202609022217';
1131
1131
 
1132
1132
  // Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
1133
1133
  // See LICENSE in the project root for license information.
@@ -3382,6 +3382,48 @@ const computePosition = (reference, floating, options) => {
3382
3382
  });
3383
3383
  };
3384
3384
 
3385
+ /**
3386
+ * Generates a v4 UUID with a three-tier fallback for insecure contexts.
3387
+ *
3388
+ * Tier 1 (secure contexts): crypto.randomUUID() — native, fully random.
3389
+ * Tier 2 (insecure contexts): crypto.getRandomValues() — available on all
3390
+ * modern browsers regardless of secure-context status.
3391
+ * Tier 3 (last resort, no crypto API): new Date().getTime() — lower entropy,
3392
+ * but sufficient for element IDs where collision risk is negligible.
3393
+ *
3394
+ * See: https://w3c.github.io/webcrypto/#dom-crypto-randomuuid (secure-context restriction)
3395
+ */
3396
+ function generateUUID() {
3397
+ let uuid;
3398
+
3399
+ if (
3400
+ typeof crypto !== "undefined" &&
3401
+ typeof crypto.randomUUID === "function"
3402
+ ) {
3403
+ uuid = crypto.randomUUID();
3404
+ } else if (
3405
+ typeof crypto !== "undefined" &&
3406
+ typeof crypto.getRandomValues === "function"
3407
+ ) {
3408
+ const bytes = new Uint8Array(16);
3409
+ crypto.getRandomValues(bytes);
3410
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
3411
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
3412
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0"));
3413
+ uuid = `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
3414
+ } else {
3415
+ let d = new Date().getTime();
3416
+ uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
3417
+ const r = (d % 16) | 0;
3418
+ d = (d - r) / 16 || new Date().getTime();
3419
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
3420
+ });
3421
+ }
3422
+
3423
+ // CSS selectors require IDs starting with a letter; replace a leading digit with 'a'.
3424
+ return /^[0-9]/.test(uuid) ? `a${uuid.slice(1)}` : uuid;
3425
+ }
3426
+
3385
3427
  /* eslint-disable line-comment-position, no-inline-comments */
3386
3428
 
3387
3429
 
@@ -4400,7 +4442,7 @@ class AuroFloatingUI {
4400
4442
 
4401
4443
  this.id = element.getAttribute("id");
4402
4444
  if (!this.id) {
4403
- this.id = window.crypto.randomUUID();
4445
+ this.id = generateUUID();
4404
4446
  element.setAttribute("id", this.id);
4405
4447
  }
4406
4448
 
@@ -4484,7 +4526,7 @@ class AuroFloatingUI {
4484
4526
  }
4485
4527
 
4486
4528
  // Selectors for focusable elements
4487
- const FOCUSABLE_SELECTORS = [
4529
+ const FOCUSABLE_SELECTORS$1 = [
4488
4530
  "a[href]",
4489
4531
  "button:not([disabled])",
4490
4532
  "textarea:not([disabled])",
@@ -4498,7 +4540,7 @@ const FOCUSABLE_SELECTORS = [
4498
4540
  ];
4499
4541
 
4500
4542
  // List of custom components that are known to be focusable
4501
- const FOCUSABLE_COMPONENTS = [
4543
+ const FOCUSABLE_COMPONENTS$1 = [
4502
4544
  "auro-checkbox",
4503
4545
  "auro-radio",
4504
4546
  "auro-dropdown",
@@ -4520,12 +4562,12 @@ const FOCUSABLE_COMPONENTS = [
4520
4562
  * @param {HTMLElement} element The element to check for focusability.
4521
4563
  * @returns {boolean} True if the element is a focusable custom component, false otherwise.
4522
4564
  */
4523
- function isFocusableComponent(element) {
4565
+ function isFocusableComponent$1(element) {
4524
4566
  const componentName = element.tagName.toLowerCase();
4525
4567
 
4526
4568
  // Guard Clause: Element is a focusable component
4527
4569
  if (
4528
- !FOCUSABLE_COMPONENTS.some(
4570
+ !FOCUSABLE_COMPONENTS$1.some(
4529
4571
  (name) => element.hasAttribute(name) || componentName === name,
4530
4572
  )
4531
4573
  )
@@ -4549,7 +4591,7 @@ function isFocusableComponent(element) {
4549
4591
  * @param {HTMLElement} element The element whose tabindex to read.
4550
4592
  * @returns {?number} The numeric tabindex or null if missing/invalid.
4551
4593
  */
4552
- function getNumericTabIndex(element) {
4594
+ function getNumericTabIndex$1(element) {
4553
4595
  const raw = element.getAttribute("tabindex");
4554
4596
  if (raw == null) return null;
4555
4597
 
@@ -4565,7 +4607,7 @@ function getNumericTabIndex(element) {
4565
4607
  * @param {HTMLElement} container The container to search within
4566
4608
  * @returns {Array<HTMLElement>} An array of focusable elements within the container.
4567
4609
  */
4568
- function getFocusableElements(container) {
4610
+ function getFocusableElements$1(container) {
4569
4611
  // Get elements in DOM order by walking the tree
4570
4612
  const orderedFocusableElements = [];
4571
4613
 
@@ -4574,7 +4616,7 @@ function getFocusableElements(container) {
4574
4616
  // Check if current element is focusable
4575
4617
  if (root.nodeType === Node.ELEMENT_NODE) {
4576
4618
  // Check if this is a custom component that is focusable
4577
- const isComponentFocusable = isFocusableComponent(root);
4619
+ const isComponentFocusable = isFocusableComponent$1(root);
4578
4620
 
4579
4621
  if (isComponentFocusable) {
4580
4622
  // Add the component itself as a focusable element and don't traverse its shadow DOM
@@ -4583,7 +4625,7 @@ function getFocusableElements(container) {
4583
4625
  }
4584
4626
 
4585
4627
  // Check if the element itself matches any selector
4586
- for (const selector of FOCUSABLE_SELECTORS) {
4628
+ for (const selector of FOCUSABLE_SELECTORS$1) {
4587
4629
  if (root.matches?.(selector)) {
4588
4630
  orderedFocusableElements.push(root);
4589
4631
  break; // Once we know it's focusable, no need to check other selectors
@@ -4637,20 +4679,20 @@ function getFocusableElements(container) {
4637
4679
 
4638
4680
  // First extract elements with valid positive tabindex
4639
4681
  const elementsWithTabindex = uniqueElements.filter((el) => {
4640
- const tabindex = getNumericTabIndex(el);
4682
+ const tabindex = getNumericTabIndex$1(el);
4641
4683
  return tabindex !== null && tabindex > 0;
4642
4684
  });
4643
4685
 
4644
4686
  // Sort these elements by their tabindex value
4645
4687
  elementsWithTabindex.sort((a, b) => {
4646
- const aIndex = getNumericTabIndex(a) ?? 0;
4647
- const bIndex = getNumericTabIndex(b) ?? 0;
4688
+ const aIndex = getNumericTabIndex$1(a) ?? 0;
4689
+ const bIndex = getNumericTabIndex$1(b) ?? 0;
4648
4690
  return aIndex - bIndex;
4649
4691
  });
4650
4692
 
4651
4693
  // Elements without tabindex (preserving their original order)
4652
4694
  const elementsWithoutTabindex = uniqueElements.filter((el) => {
4653
- const tabindex = getNumericTabIndex(el);
4695
+ const tabindex = getNumericTabIndex$1(el);
4654
4696
 
4655
4697
  // Elements without tabindex or with tabindex of 0 stay in DOM order
4656
4698
  return tabindex === null || tabindex === 0;
@@ -5445,7 +5487,7 @@ class AuroHelpText extends i$2 {
5445
5487
  }
5446
5488
  }
5447
5489
 
5448
- var formkitVersion = '202609021913';
5490
+ var formkitVersion = '202609022217';
5449
5491
 
5450
5492
  /**
5451
5493
  * AuroElement is the shared base class for layout-aware Auro form components.
@@ -5745,7 +5787,7 @@ class AuroDropdown extends AuroElement$1 {
5745
5787
  */
5746
5788
  focus() {
5747
5789
  if (this.isPopoverVisible && this.bibContent) {
5748
- const focusables = getFocusableElements(this.bibContent);
5790
+ const focusables = getFocusableElements$1(this.bibContent);
5749
5791
  if (focusables.length > 0) {
5750
5792
  focusables[0].focus();
5751
5793
  }
@@ -5777,7 +5819,7 @@ class AuroDropdown extends AuroElement$1 {
5777
5819
  // wrappers like <div> containing a <button>). If none found, try
5778
5820
  // focusing the element directly (works for custom elements like
5779
5821
  // auro-input that have delegatesFocus or a custom focus() method).
5780
- const descendants = getFocusableElements(el);
5822
+ const descendants = getFocusableElements$1(el);
5781
5823
  if (descendants.length > 0) {
5782
5824
  descendants[0].focus();
5783
5825
  return;
@@ -5789,7 +5831,7 @@ class AuroDropdown extends AuroElement$1 {
5789
5831
  }
5790
5832
  }
5791
5833
  // Fallback: try DOM children (non-slotted content)
5792
- const focusables = getFocusableElements(this.trigger);
5834
+ const focusables = getFocusableElements$1(this.trigger);
5793
5835
  if (focusables.length > 0) {
5794
5836
  focusables[0].focus();
5795
5837
  }
@@ -6468,7 +6510,7 @@ class AuroDropdown extends AuroElement$1 {
6468
6510
  }
6469
6511
 
6470
6512
  // Collect focusable elements from the bib content.
6471
- const focusables = getFocusableElements(this.bibContent);
6513
+ const focusables = getFocusableElements$1(this.bibContent);
6472
6514
 
6473
6515
  // Fallback: try from slotted content directly
6474
6516
  if (!focusables.length) {
@@ -6477,7 +6519,7 @@ class AuroDropdown extends AuroElement$1 {
6477
6519
 
6478
6520
  for (const node of assignedNodes) {
6479
6521
  if (node.nodeType === Node.ELEMENT_NODE) {
6480
- focusables.push(...getFocusableElements(node));
6522
+ focusables.push(...getFocusableElements$1(node));
6481
6523
  }
6482
6524
  }
6483
6525
  }
@@ -6537,7 +6579,7 @@ class AuroDropdown extends AuroElement$1 {
6537
6579
 
6538
6580
  // Move initial focus into the bib content, matching FocusTrap behavior
6539
6581
  requestAnimationFrame(() => {
6540
- const focusables = getFocusableElements(this.bibContent);
6582
+ const focusables = getFocusableElements$1(this.bibContent);
6541
6583
  if (focusables.length) {
6542
6584
  focusables[0].focus();
6543
6585
  }
@@ -6550,7 +6592,7 @@ class AuroDropdown extends AuroElement$1 {
6550
6592
  // popover asynchronously — a synchronous focus() would target
6551
6593
  // zero-dimension elements and be silently ignored.
6552
6594
  requestAnimationFrame(() => {
6553
- const focusables = getFocusableElements(this.bibContent);
6595
+ const focusables = getFocusableElements$1(this.bibContent);
6554
6596
  if (focusables.length) {
6555
6597
  focusables[0].focus();
6556
6598
  }
@@ -6768,7 +6810,7 @@ class AuroDropdown extends AuroElement$1 {
6768
6810
  if (triggerContentNodes) {
6769
6811
 
6770
6812
  // See if any of them are focusable elements
6771
- this.triggerContentFocusable = triggerContentNodes.some((node) => getFocusableElements(node).length > 0);
6813
+ this.triggerContentFocusable = triggerContentNodes.some((node) => getFocusableElements$1(node).length > 0);
6772
6814
 
6773
6815
  // If any of them are focusable elements
6774
6816
  if (this.triggerContentFocusable) {
@@ -7530,9 +7572,241 @@ class AuroElement extends i$2 {
7530
7572
  }
7531
7573
  }
7532
7574
 
7575
+ // Selectors for focusable elements
7576
+ const FOCUSABLE_SELECTORS = [
7577
+ "a[href]",
7578
+ "button:not([disabled])",
7579
+ "textarea:not([disabled])",
7580
+ "input:not([disabled])",
7581
+ "select:not([disabled])",
7582
+ '[role="tab"]:not([disabled])',
7583
+ '[role="link"]:not([disabled])',
7584
+ '[role="button"]:not([disabled])',
7585
+ '[tabindex]:not([tabindex="-1"])',
7586
+ '[contenteditable]:not([contenteditable="false"])',
7587
+ ];
7588
+
7589
+ // List of custom components that are known to be focusable
7590
+ const FOCUSABLE_COMPONENTS = [
7591
+ "auro-checkbox",
7592
+ "auro-radio",
7593
+ "auro-dropdown",
7594
+ "auro-button",
7595
+ "auro-combobox",
7596
+ "auro-input",
7597
+ "auro-counter",
7598
+ // 'auro-menu', // Auro menu is not focusable by default, it uses a different interaction model
7599
+ "auro-select",
7600
+ "auro-datepicker",
7601
+ "auro-hyperlink",
7602
+ "auro-accordion",
7603
+ ];
7604
+
7605
+ /**
7606
+ * Determines if a given element is a custom focusable component.
7607
+ * Returns true if the element matches a known focusable component and is not disabled.
7608
+ *
7609
+ * @param {HTMLElement} element The element to check for focusability.
7610
+ * @returns {boolean} True if the element is a focusable custom component, false otherwise.
7611
+ */
7612
+ function isFocusableComponent(element) {
7613
+ const componentName = element.tagName.toLowerCase();
7614
+
7615
+ // Guard Clause: Element is a focusable component
7616
+ if (
7617
+ !FOCUSABLE_COMPONENTS.some(
7618
+ (name) => element.hasAttribute(name) || componentName === name,
7619
+ )
7620
+ )
7621
+ return false;
7622
+
7623
+ // Guard Clause: Element is not disabled
7624
+ if (element.hasAttribute("disabled")) return false;
7625
+
7626
+ // Guard Clause: The element is a hyperlink and has no href attribute
7627
+ if (componentName.match("hyperlink") && !element.hasAttribute("href"))
7628
+ return false;
7629
+
7630
+ // If all guard clauses pass, the element is a focusable component
7631
+ return true;
7632
+ }
7633
+
7634
+ /**
7635
+ * Safely get a numeric tabindex for an element.
7636
+ * Returns a number if the tabindex is a valid integer, otherwise null.
7637
+ *
7638
+ * @param {HTMLElement} element The element whose tabindex to read.
7639
+ * @returns {?number} The numeric tabindex or null if missing/invalid.
7640
+ */
7641
+ function getNumericTabIndex(element) {
7642
+ const raw = element.getAttribute("tabindex");
7643
+ if (raw == null) return null;
7644
+
7645
+ const value = Number.parseInt(raw, 10);
7646
+ return Number.isNaN(value) ? null : value;
7647
+ }
7648
+
7649
+ /**
7650
+ * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
7651
+ * Returns a unique, ordered array of elements that can receive focus.
7652
+ * Also sorts elements with tabindex first, preserving their order.
7653
+ *
7654
+ * @param {HTMLElement} container The container to search within
7655
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
7656
+ */
7657
+ function getFocusableElements(container) {
7658
+ // Get elements in DOM order by walking the tree
7659
+ const orderedFocusableElements = [];
7660
+
7661
+ // Define a recursive function to collect focusable elements in DOM order
7662
+ const collectFocusableElements = (root) => {
7663
+ // Check if current element is focusable
7664
+ if (root.nodeType === Node.ELEMENT_NODE) {
7665
+ // Check if this is a custom component that is focusable
7666
+ const isComponentFocusable = isFocusableComponent(root);
7667
+
7668
+ if (isComponentFocusable) {
7669
+ // Add the component itself as a focusable element and don't traverse its shadow DOM
7670
+ orderedFocusableElements.push(root);
7671
+ return; // Skip traversing inside this component
7672
+ }
7673
+
7674
+ // Check if the element itself matches any selector
7675
+ for (const selector of FOCUSABLE_SELECTORS) {
7676
+ if (root.matches?.(selector)) {
7677
+ orderedFocusableElements.push(root);
7678
+ break; // Once we know it's focusable, no need to check other selectors
7679
+ }
7680
+ }
7681
+
7682
+ // Process shadow DOM only for non-Auro components
7683
+ if (root.shadowRoot) {
7684
+ // Process shadow DOM children in order
7685
+ if (root.shadowRoot.children) {
7686
+ Array.from(root.shadowRoot.children).forEach((child) => {
7687
+ collectFocusableElements(child);
7688
+ });
7689
+ }
7690
+ }
7691
+
7692
+ // Process slots and their assigned nodes in order
7693
+ if (root.tagName === "SLOT") {
7694
+ const assignedNodes = root.assignedNodes({ flatten: true });
7695
+ for (const node of assignedNodes) {
7696
+ collectFocusableElements(node);
7697
+ }
7698
+ } else {
7699
+ // Process light DOM children in order
7700
+ if (root.children) {
7701
+ Array.from(root.children).forEach((child) => {
7702
+ collectFocusableElements(child);
7703
+ });
7704
+ }
7705
+ }
7706
+ }
7707
+ };
7708
+
7709
+ // Start the traversal from the container
7710
+ collectFocusableElements(container);
7711
+
7712
+ // Remove duplicates that might have been collected through different paths
7713
+ // while preserving order
7714
+ const uniqueElements = [];
7715
+ const seen = new Set();
7716
+
7717
+ for (const element of orderedFocusableElements) {
7718
+ if (!seen.has(element)) {
7719
+ seen.add(element);
7720
+ uniqueElements.push(element);
7721
+ }
7722
+ }
7723
+
7724
+ // Move tab-indexed elements to the front while preserving their order
7725
+ // This ensures that elements with tabindex are prioritized in the focus order
7726
+
7727
+ // First extract elements with valid positive tabindex
7728
+ const elementsWithTabindex = uniqueElements.filter((el) => {
7729
+ const tabindex = getNumericTabIndex(el);
7730
+ return tabindex !== null && tabindex > 0;
7731
+ });
7732
+
7733
+ // Sort these elements by their tabindex value
7734
+ elementsWithTabindex.sort((a, b) => {
7735
+ const aIndex = getNumericTabIndex(a) ?? 0;
7736
+ const bIndex = getNumericTabIndex(b) ?? 0;
7737
+ return aIndex - bIndex;
7738
+ });
7739
+
7740
+ // Elements without tabindex (preserving their original order)
7741
+ const elementsWithoutTabindex = uniqueElements.filter((el) => {
7742
+ const tabindex = getNumericTabIndex(el);
7743
+
7744
+ // Elements without tabindex or with tabindex of 0 stay in DOM order
7745
+ return tabindex === null || tabindex === 0;
7746
+ });
7747
+
7748
+ // Combine both arrays with tabindex elements first
7749
+ const tabIndexedUniqueElements = [
7750
+ ...elementsWithTabindex,
7751
+ ...elementsWithoutTabindex,
7752
+ ];
7753
+
7754
+ return tabIndexedUniqueElements;
7755
+ }
7756
+
7533
7757
  // Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
7534
7758
  // See LICENSE in the project root for license information.
7535
7759
 
7760
+
7761
+ /**
7762
+ * Whether `el` is currently visible enough to receive focus. Prefers the
7763
+ * browser's `checkVisibility()`; otherwise combines a display/visibility check
7764
+ * with an ancestor-hidden probe that keeps position:fixed elements visible.
7765
+ * @param {Element} el - The element to test.
7766
+ * @returns {boolean}
7767
+ */
7768
+ function isVisibleForFocus(el) {
7769
+ if (typeof el.checkVisibility === 'function') {
7770
+ return el.checkVisibility();
7771
+ }
7772
+ const style = el.ownerDocument.defaultView.getComputedStyle(el);
7773
+ if (style.display === 'none' || style.visibility === 'hidden') {
7774
+ return false;
7775
+ }
7776
+ return el.offsetParent !== null || style.position === 'fixed';
7777
+ }
7778
+
7779
+ /**
7780
+ * Return the tab stop that comes before the counter-group in page tab order,
7781
+ * skipping any hidden entries the walker doesn't filter itself. The group host
7782
+ * itself is not a tab stop; its focusables (the internal dropdown and the
7783
+ * counters) are, so anchor the search on the group's earliest focusable.
7784
+ * @param {Element} component - The auro-counter-group host element.
7785
+ * @returns {Element|null}
7786
+ */
7787
+ function getPreviousTabStop(component) {
7788
+ const tabStops = getFocusableElements(component.ownerDocument.body);
7789
+ const counters = component.counters || [];
7790
+ const owned = [
7791
+ component,
7792
+ component.dropdown,
7793
+ ...counters
7794
+ ].filter(Boolean);
7795
+ let groupStart = tabStops.length;
7796
+ owned.forEach((node) => {
7797
+ const index = tabStops.indexOf(node);
7798
+ if (index >= 0 && index < groupStart) {
7799
+ groupStart = index;
7800
+ }
7801
+ });
7802
+ for (let index = groupStart - 1; index >= 0; index -= 1) {
7803
+ if (isVisibleForFocus(tabStops[index])) {
7804
+ return tabStops[index];
7805
+ }
7806
+ }
7807
+ return null;
7808
+ }
7809
+
7536
7810
  const counterGroupKeyboardStrategy = {
7537
7811
  Escape(component, evt) {
7538
7812
  if (!component.dropdown || !component.dropdown.isPopoverVisible) {
@@ -7546,6 +7820,48 @@ const counterGroupKeyboardStrategy = {
7546
7820
 
7547
7821
  component.dropdown.hide();
7548
7822
  },
7823
+
7824
+ Tab(component, evt, ctx) {
7825
+ // Forward Tab already exits past the bib content and the focusout handler
7826
+ // closes the bib. Fullscreen relies on the dialog's native focus trapping.
7827
+ if (!ctx.isExpanded || ctx.isModal || !evt.shiftKey) {
7828
+ return;
7829
+ }
7830
+
7831
+ // The trigger precedes the bib content in the flattened tab order, so a
7832
+ // native Shift+Tab off the first counter lands back on the trigger and
7833
+ // leaves the bib open. Only intercept when focus is on the first focusable
7834
+ // in the bib; otherwise let the browser move to the previous counter.
7835
+ // `composedPath()` reports the real event origin through every shadow
7836
+ // boundary, unlike `document.activeElement`, which retargets to the first
7837
+ // slotted counter and cannot tell the counters apart.
7838
+ const focusables = getFocusableElements(component.dropdown.bibContent);
7839
+ if (!focusables.length) {
7840
+ return;
7841
+ }
7842
+
7843
+ const path = evt.composedPath();
7844
+ const activeIndex = focusables.findIndex((el) => path.includes(el));
7845
+ if (activeIndex !== 0) {
7846
+ return;
7847
+ }
7848
+
7849
+ const previousTabStop = getPreviousTabStop(component);
7850
+
7851
+ // Always suppress the native Shift+Tab. Otherwise it fires against the bib
7852
+ // as it is torn down and lands focus on the trigger (which precedes the
7853
+ // first counter), re-opening the exact loop this handler prevents. When
7854
+ // nothing precedes the group, focus falls to the body instead of the
7855
+ // trigger, which reads as exiting the group backward.
7856
+ evt.preventDefault();
7857
+ component.hideBib();
7858
+
7859
+ if (!previousTabStop) {
7860
+ return;
7861
+ }
7862
+
7863
+ doubleRaf(() => previousTabStop.focus());
7864
+ },
7549
7865
  };
7550
7866
 
7551
7867
  /* eslint-disable lit/no-invalid-html, lit/binding-positions, max-lines, no-underscore-dangle, arrow-parens, no-confusing-arrow, curly, dot-location, no-inline-comments, line-comment-position,
@@ -1,3 +1,4 @@
1
1
  export namespace counterGroupKeyboardStrategy {
2
2
  function Escape(component: any, evt: any): void;
3
+ function Tab(component: any, evt: any, ctx: any): void;
3
4
  }