@sapudex/web-components 2.51.0 → 2.53.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.
Files changed (172) hide show
  1. package/cypress/specs/Avatar.cy.tsx +103 -0
  2. package/cypress/specs/CountrySelector.cy.tsx +21 -0
  3. package/cypress/specs/List.cy.tsx +119 -10
  4. package/cypress/specs/SelectBox.cy.tsx +426 -70
  5. package/dist/Avatar.d.ts +10 -1
  6. package/dist/Avatar.js +43 -3
  7. package/dist/Avatar.js.map +1 -1
  8. package/dist/AvatarTemplate.js +1 -1
  9. package/dist/AvatarTemplate.js.map +1 -1
  10. package/dist/CountrySelector.js +1 -0
  11. package/dist/CountrySelector.js.map +1 -1
  12. package/dist/List.d.ts +10 -0
  13. package/dist/List.js +35 -0
  14. package/dist/List.js.map +1 -1
  15. package/dist/ListGroupHeaderBase.js +3 -0
  16. package/dist/ListGroupHeaderBase.js.map +1 -1
  17. package/dist/ListItem.d.ts +1 -0
  18. package/dist/ListItem.js +11 -3
  19. package/dist/ListItem.js.map +1 -1
  20. package/dist/ListItemBase.d.ts +9 -0
  21. package/dist/ListItemBase.js +12 -0
  22. package/dist/ListItemBase.js.map +1 -1
  23. package/dist/ListSelectAll.js +1 -1
  24. package/dist/ListSelectAll.js.map +1 -1
  25. package/dist/MediaPlayer.js +8 -0
  26. package/dist/MediaPlayer.js.map +1 -1
  27. package/dist/SearchableListBase.d.ts +10 -0
  28. package/dist/SearchableListBase.js +29 -6
  29. package/dist/SearchableListBase.js.map +1 -1
  30. package/dist/SearchableListBaseTemplate.js +32 -5
  31. package/dist/SearchableListBaseTemplate.js.map +1 -1
  32. package/dist/SelectBase.d.ts +65 -1
  33. package/dist/SelectBase.js +446 -59
  34. package/dist/SelectBase.js.map +1 -1
  35. package/dist/SelectBox.d.ts +5 -0
  36. package/dist/SelectBox.js +33 -2
  37. package/dist/SelectBox.js.map +1 -1
  38. package/dist/SelectBoxTemplate.js +2 -2
  39. package/dist/SelectBoxTemplate.js.map +1 -1
  40. package/dist/assets/{InputSuggestions-7eP5S95I.js → InputSuggestions-0yCPXAmq.js} +1 -1
  41. package/dist/assets/{ListItemStandardExpandableTextTemplate-CLcWBFcg.js → ListItemStandardExpandableTextTemplate-DjZEnCXN.js} +1 -1
  42. package/dist/assets/{bundle-CkxyEA8c.css → bundle-C7Yft7IG.css} +1 -1
  43. package/dist/assets/{bundle.esm-D4--IDh7.js → bundle.esm-DJuMR5ko.js} +241 -241
  44. package/dist/assets/messagebundle_lv-D06tlATH.js +1 -0
  45. package/dist/assets/messagebundle_nl-BKUpVV-D.js +1 -0
  46. package/dist/css/themes/AccordionItem.css +1 -1
  47. package/dist/css/themes/BreadcrumbsPopover.css +1 -1
  48. package/dist/css/themes/Contact.css +1 -1
  49. package/dist/css/themes/Divider.css +1 -1
  50. package/dist/css/themes/FilterMenu.css +1 -1
  51. package/dist/css/themes/Link.css +1 -1
  52. package/dist/css/themes/ListGroupHeader.css +1 -1
  53. package/dist/css/themes/ListItem.css +1 -1
  54. package/dist/css/themes/MessageStrip.css +1 -1
  55. package/dist/css/themes/Search.css +1 -1
  56. package/dist/css/themes/SelectBase.css +1 -1
  57. package/dist/css/themes/SelectBox.css +1 -1
  58. package/dist/css/themes/Slider.css +1 -1
  59. package/dist/css/themes/Tile.css +1 -1
  60. package/dist/css/themes/Toast.css +1 -1
  61. package/dist/css/themes/Tooltip.css +1 -1
  62. package/dist/custom-elements-internal.json +167 -156
  63. package/dist/custom-elements.json +152 -141
  64. package/dist/generated/assets/i18n/messagebundle_lv.json +1 -1
  65. package/dist/generated/assets/i18n/messagebundle_nl.json +1 -1
  66. package/dist/generated/templates/ListTemplate.lit.js +1 -1
  67. package/dist/generated/templates/ListTemplate.lit.js.map +1 -1
  68. package/dist/generated/themes/AccordionItem.css.d.ts +1 -1
  69. package/dist/generated/themes/AccordionItem.css.js +1 -1
  70. package/dist/generated/themes/AccordionItem.css.js.map +1 -1
  71. package/dist/generated/themes/BreadcrumbsPopover.css.d.ts +1 -1
  72. package/dist/generated/themes/BreadcrumbsPopover.css.js +1 -1
  73. package/dist/generated/themes/BreadcrumbsPopover.css.js.map +1 -1
  74. package/dist/generated/themes/Contact.css.d.ts +1 -1
  75. package/dist/generated/themes/Contact.css.js +1 -1
  76. package/dist/generated/themes/Contact.css.js.map +1 -1
  77. package/dist/generated/themes/Divider.css.d.ts +1 -1
  78. package/dist/generated/themes/Divider.css.js +1 -1
  79. package/dist/generated/themes/Divider.css.js.map +1 -1
  80. package/dist/generated/themes/FilterMenu.css.d.ts +1 -1
  81. package/dist/generated/themes/FilterMenu.css.js +1 -1
  82. package/dist/generated/themes/FilterMenu.css.js.map +1 -1
  83. package/dist/generated/themes/Link.css.d.ts +1 -1
  84. package/dist/generated/themes/Link.css.js +1 -1
  85. package/dist/generated/themes/Link.css.js.map +1 -1
  86. package/dist/generated/themes/ListGroupHeader.css.d.ts +1 -1
  87. package/dist/generated/themes/ListGroupHeader.css.js +1 -1
  88. package/dist/generated/themes/ListGroupHeader.css.js.map +1 -1
  89. package/dist/generated/themes/ListItem.css.d.ts +1 -1
  90. package/dist/generated/themes/ListItem.css.js +1 -1
  91. package/dist/generated/themes/ListItem.css.js.map +1 -1
  92. package/dist/generated/themes/MessageStrip.css.d.ts +1 -1
  93. package/dist/generated/themes/MessageStrip.css.js +1 -1
  94. package/dist/generated/themes/MessageStrip.css.js.map +1 -1
  95. package/dist/generated/themes/Search.css.d.ts +1 -1
  96. package/dist/generated/themes/Search.css.js +1 -1
  97. package/dist/generated/themes/Search.css.js.map +1 -1
  98. package/dist/generated/themes/SelectBase.css.d.ts +1 -1
  99. package/dist/generated/themes/SelectBase.css.js +1 -1
  100. package/dist/generated/themes/SelectBase.css.js.map +1 -1
  101. package/dist/generated/themes/SelectBox.css.d.ts +1 -1
  102. package/dist/generated/themes/SelectBox.css.js +1 -1
  103. package/dist/generated/themes/SelectBox.css.js.map +1 -1
  104. package/dist/generated/themes/Slider.css.d.ts +1 -1
  105. package/dist/generated/themes/Slider.css.js +1 -1
  106. package/dist/generated/themes/Slider.css.js.map +1 -1
  107. package/dist/generated/themes/Tile.css.d.ts +1 -1
  108. package/dist/generated/themes/Tile.css.js +1 -1
  109. package/dist/generated/themes/Tile.css.js.map +1 -1
  110. package/dist/generated/themes/Toast.css.d.ts +1 -1
  111. package/dist/generated/themes/Toast.css.js +1 -1
  112. package/dist/generated/themes/Toast.css.js.map +1 -1
  113. package/dist/generated/themes/Tooltip.css.d.ts +1 -1
  114. package/dist/generated/themes/Tooltip.css.js +1 -1
  115. package/dist/generated/themes/Tooltip.css.js.map +1 -1
  116. package/dist/messagebundle_lv.properties +126 -0
  117. package/dist/messagebundle_nl.properties +126 -0
  118. package/dist/vscode.html-custom-data.json +1 -1
  119. package/dist/web-types.json +3 -3
  120. package/package.json +1 -1
  121. package/src/Avatar.ts +52 -3
  122. package/src/AvatarTemplate.tsx +2 -2
  123. package/src/CountrySelector.ts +1 -0
  124. package/src/List.hbs +2 -1
  125. package/src/List.ts +36 -0
  126. package/src/ListGroupHeaderBase.ts +3 -0
  127. package/src/ListItem.ts +12 -3
  128. package/src/ListItemBase.ts +11 -0
  129. package/src/ListSelectAll.ts +1 -1
  130. package/src/MediaPlayer.ts +8 -0
  131. package/src/SearchableListBase.ts +31 -8
  132. package/src/SearchableListBaseTemplate.tsx +38 -6
  133. package/src/SelectBase.ts +466 -69
  134. package/src/SelectBox.ts +37 -2
  135. package/src/SelectBoxTemplate.tsx +4 -3
  136. package/src/generated/templates/ListTemplate.lit.ts +1 -1
  137. package/src/generated/themes/AccordionItem.css.ts +1 -1
  138. package/src/generated/themes/BreadcrumbsPopover.css.ts +1 -1
  139. package/src/generated/themes/Contact.css.ts +1 -1
  140. package/src/generated/themes/Divider.css.ts +1 -1
  141. package/src/generated/themes/FilterMenu.css.ts +1 -1
  142. package/src/generated/themes/Link.css.ts +1 -1
  143. package/src/generated/themes/ListGroupHeader.css.ts +1 -1
  144. package/src/generated/themes/ListItem.css.ts +1 -1
  145. package/src/generated/themes/MessageStrip.css.ts +1 -1
  146. package/src/generated/themes/Search.css.ts +1 -1
  147. package/src/generated/themes/SelectBase.css.ts +1 -1
  148. package/src/generated/themes/SelectBox.css.ts +1 -1
  149. package/src/generated/themes/Slider.css.ts +1 -1
  150. package/src/generated/themes/Tile.css.ts +1 -1
  151. package/src/generated/themes/Toast.css.ts +1 -1
  152. package/src/generated/themes/Tooltip.css.ts +1 -1
  153. package/src/i18n/messagebundle_lv.properties +126 -0
  154. package/src/i18n/messagebundle_nl.properties +126 -0
  155. package/src/themes/AccordionItem.css +8 -3
  156. package/src/themes/BreadcrumbsPopover.css +1 -1
  157. package/src/themes/Contact.css +3 -3
  158. package/src/themes/Divider.css +2 -0
  159. package/src/themes/FilterMenu.css +1 -1
  160. package/src/themes/Link.css +20 -2
  161. package/src/themes/ListGroupHeader.css +18 -1
  162. package/src/themes/ListItem.css +2 -1
  163. package/src/themes/MessageStrip.css +8 -5
  164. package/src/themes/Search.css +3 -3
  165. package/src/themes/SelectBase.css +2 -1
  166. package/src/themes/SelectBox.css +10 -0
  167. package/src/themes/Slider.css +1 -1
  168. package/src/themes/Tile.css +2 -2
  169. package/src/themes/Toast.css +1 -1
  170. package/src/themes/Tooltip.css +1 -1
  171. package/dist/assets/messagebundle_lv-BHId5zho.js +0 -1
  172. package/dist/assets/messagebundle_nl-etaldyfo.js +0 -1
@@ -702,24 +702,22 @@ describe("SelectBox", () => {
702
702
  cy.get("#select-box").shadow().find(".udex-text-field__search").click();
703
703
  cy.realPress("ArrowDown");
704
704
 
705
+ // Updated: DOM focus stays on the input; the active item is marked via _active-descendant.
705
706
  cy.get("#select-box").shadow()
706
707
  .find("udex-list-item").eq(0)
707
- .shadow().find("[part=item]")
708
- .should("be.focused");
708
+ .should("have.attr", "_active-descendant");
709
709
 
710
710
  cy.realPress("ArrowDown");
711
711
 
712
712
  cy.get("#select-box").shadow()
713
713
  .find("udex-list-item").eq(1)
714
- .shadow().find("[part=item]")
715
- .should("be.focused");
714
+ .should("have.attr", "_active-descendant");
716
715
 
717
716
  cy.realPress("ArrowUp");
718
717
 
719
718
  cy.get("#select-box").shadow()
720
719
  .find("udex-list-item").eq(0)
721
- .shadow().find("[part=item]")
722
- .should("be.focused");
720
+ .should("have.attr", "_active-descendant");
723
721
  });
724
722
 
725
723
  it("MultiSelect mode - arrow keys navigate through collapsible group header and its items", () => {
@@ -728,40 +726,35 @@ describe("SelectBox", () => {
728
726
  cy.get("#select-box").shadow().find(".udex-text-field__search").click();
729
727
  cy.realPress("ArrowDown");
730
728
 
729
+ // Updated: DOM focus stays on the input; the active group header is marked
730
+ // via the _active-descendant attribute rather than receiving DOM focus.
731
731
  cy.get("#select-box").shadow()
732
- .find("udex-list-group-header").shadow()
733
- .find(".udex-list__group-header-wrap").shadow()
734
- .find("[part=item]")
735
- .should("be.focused");
732
+ .find("udex-list-group-header")
733
+ .should("have.attr", "_active-descendant");
736
734
 
737
735
  cy.realPress("ArrowDown");
738
736
 
739
737
  cy.get("#select-box").shadow()
740
738
  .find("udex-list-item").eq(0)
741
- .shadow().find("[part=item]")
742
- .should("be.focused");
739
+ .should("have.attr", "_active-descendant");
743
740
 
744
741
  cy.realPress("ArrowDown");
745
742
 
746
743
  cy.get("#select-box").shadow()
747
744
  .find("udex-list-item").eq(1)
748
- .shadow().find("[part=item]")
749
- .should("be.focused");
745
+ .should("have.attr", "_active-descendant");
750
746
 
751
747
  cy.realPress("ArrowUp");
752
748
 
753
749
  cy.get("#select-box").shadow()
754
750
  .find("udex-list-item").eq(0)
755
- .shadow().find("[part=item]")
756
- .should("be.focused");
751
+ .should("have.attr", "_active-descendant");
757
752
 
758
753
  cy.realPress("ArrowUp");
759
754
 
760
755
  cy.get("#select-box").shadow()
761
- .find("udex-list-group-header").shadow()
762
- .find(".udex-list__group-header-wrap").shadow()
763
- .find("[part=item]")
764
- .should("be.focused");
756
+ .find("udex-list-group-header")
757
+ .should("have.attr", "_active-descendant");
765
758
  });
766
759
 
767
760
  it("MultiSelect mode - Left arrow collapses a collapsible group header, Right arrow expands it", () => {
@@ -770,12 +763,11 @@ describe("SelectBox", () => {
770
763
  cy.get("#select-box").shadow().find(".udex-text-field__search").click();
771
764
  cy.realPress("ArrowDown");
772
765
 
773
- // Focus the collapsible group header wrapper.
766
+ // Updated: the collapsible group header is the active descendant (DOM focus
767
+ // stays on the input), marked via the _active-descendant attribute.
774
768
  cy.get("#select-box").shadow()
775
- .find("udex-list-group-header").shadow()
776
- .find(".udex-list__group-header-wrap").shadow()
777
- .find("[part=item]")
778
- .should("be.focused");
769
+ .find("udex-list-group-header")
770
+ .should("have.attr", "_active-descendant");
779
771
 
780
772
  // Fixture starts expanded: neither the header host nor its children are collapsed.
781
773
  cy.get("#select-box").shadow()
@@ -936,13 +928,14 @@ describe("SelectBox", () => {
936
928
 
937
929
  cy.get("@trigger").click();
938
930
 
939
- cy.get("#select-box").shadow()
940
- .find("udex-list-item").eq(1)
941
- .click();
931
+ // Navigate into the list with ArrowDown (aria-activedescendant model keeps
932
+ // DOM focus on the input throughout). Shift+Tab then closes the dropdown
933
+ // and moves focus to the previous focusable element on the page.
934
+ cy.get("@select").find(".udex-text-field__search").realPress("ArrowDown");
942
935
 
943
936
  cy.realPress(["Shift", "Tab"]);
944
937
 
945
- cy.get("@select").find(".udex-text-field__search").should("be.focused");
938
+ cy.get("#previous-focus").should("be.focused");
946
939
  });
947
940
 
948
941
  it("focus moves to input and dropdown closes when press Esc on the list items", () => {
@@ -964,6 +957,18 @@ describe("SelectBox", () => {
964
957
  .find(".udex-select-box__dropdown")
965
958
  .should("not.have.attr","open");
966
959
  });
960
+
961
+ it("focus() method moves focus to the inner search input", () => {
962
+ cy.mount(<UDExSelectBox id="select-box" label="Languages">
963
+ <UDExSelectBoxItem label="JavaScript"></UDExSelectBoxItem>
964
+ </UDExSelectBox>);
965
+
966
+ cy.get("#select-box").then($el => {
967
+ ($el[0] as any).focus();
968
+ });
969
+
970
+ cy.get("#select-box").shadow().find(".udex-text-field__search").should("be.focused");
971
+ });
967
972
  });
968
973
 
969
974
  describe("Select Box Select All header tests",async () => {
@@ -1002,6 +1007,22 @@ describe("SelectBox", () => {
1002
1007
  cy.get("@selectall")
1003
1008
  .should("have.been.called");
1004
1009
  });
1010
+
1011
+ // Bug fix: groupListClassName no longer appends _additionalClassName, so the
1012
+ // sticky marker forwarded from SelectBase must not land on the inner list container
1013
+ // (previously caused an unwanted padding-top gap at the top of the dropdown).
1014
+ it("does not apply the udex-list__sticky class to the inner list container", () => {
1015
+ cy.mount(selectAll);
1016
+ cy.get("#select-box").shadow().as("select");
1017
+
1018
+ cy.get("@select").find(".udex-text-field__trigger").click();
1019
+
1020
+ cy.get("@select")
1021
+ .find("udex-list")
1022
+ .find("udex-list-select-all").shadow()
1023
+ .find(".udex-list__group-list")
1024
+ .should("not.have.class", "udex-list__sticky");
1025
+ });
1005
1026
  });
1006
1027
 
1007
1028
  describe("Select Box events tests",async () => {
@@ -1542,11 +1563,12 @@ describe("SelectBox", () => {
1542
1563
  .find(".udex-select-box__dropdown")
1543
1564
  .should("have.attr", "open");
1544
1565
 
1566
+ // Updated: focus stays on the input; the last list item becomes the active
1567
+ // descendant (marked via _active-descendant) rather than receiving DOM focus.
1545
1568
  cy.get("#select-box").shadow()
1546
1569
  .find("udex-list-item")
1547
1570
  .last()
1548
- .shadow().find("[part=item]")
1549
- .should("be.focused");
1571
+ .should("have.attr", "_active-descendant");
1550
1572
  });
1551
1573
 
1552
1574
  // when hasFooter is true, pressing Tab on a focused list item moves focus to the
@@ -1564,11 +1586,10 @@ describe("SelectBox", () => {
1564
1586
  .find(".udex-select-box__dropdown")
1565
1587
  .should("have.attr", "open");
1566
1588
 
1567
- // A list item should now be focused (not the search input).
1589
+ // A list item should now be the active descendant (DOM focus stays on the input).
1568
1590
  cy.get("#select-box").shadow()
1569
- .find("udex-list-item").eq(0).shadow()
1570
- .find("[part=item]")
1571
- .should("be.focused");
1591
+ .find("udex-list-item").eq(0)
1592
+ .should("have.attr", "_active-descendant");
1572
1593
 
1573
1594
  // Tab should be intercepted and focus redirected into the footer.
1574
1595
  cy.realPress("Tab");
@@ -1866,15 +1887,17 @@ describe("SelectBox", () => {
1866
1887
  cy.get("#next-focus").should("be.focused");
1867
1888
  });
1868
1889
 
1869
- it("Shift+Tab from list item closes dropdown and moves focus to search input", () => {
1890
+ it("Shift+Tab from list item closes dropdown and moves focus to previous focusable element", () => {
1870
1891
  cy.mount(shortComponentWithFocusTargets);
1871
1892
  openDropdownOnFirstItem();
1872
1893
 
1873
1894
  cy.realPress(["Shift", "Tab"]);
1874
1895
 
1875
1896
  cy.get("#select-box").shadow()
1876
- .find(".udex-text-field__search")
1877
- .should("be.focused");
1897
+ .find(".udex-select-box__dropdown")
1898
+ .should("not.have.attr", "open");
1899
+
1900
+ cy.get("#previous-focus").should("be.focused");
1878
1901
  });
1879
1902
 
1880
1903
  it("Escape from list item closes dropdown and returns focus to trigger", () => {
@@ -1901,6 +1924,59 @@ describe("SelectBox", () => {
1901
1924
  cy.get("#select-box").shadow().find("#firstIcon").should("be.focused");
1902
1925
  });
1903
1926
 
1927
+ // Added: covers the _handleArrowRightInRow fix — before moving DOM focus into
1928
+ // the first icon button we call _setActiveDescendant(null), which strips the
1929
+ // _udex-active-descendant JS state marker from the list item so only the icon's
1930
+ // :focus ring is visible (not both rings simultaneously).
1931
+ it("ArrowRight into the first icon removes _udex-active-descendant from the list item", () => {
1932
+ cy.mount(shortComponentWithFocusTargets);
1933
+ openDropdownOnFirstItem();
1934
+
1935
+ // Precondition: the first list item is the active descendant before ArrowRight.
1936
+ cy.get("#select-box").shadow()
1937
+ .find("udex-list-item").eq(0)
1938
+ .should("have.class", "_udex-active-descendant");
1939
+
1940
+ cy.realPress("ArrowRight");
1941
+
1942
+ // The list item's active-descendant marker is cleared so it no longer shows a ring.
1943
+ cy.get("#select-box").shadow()
1944
+ .find("udex-list-item").eq(0)
1945
+ .should("not.have.class", "_udex-active-descendant");
1946
+
1947
+ // DOM focus moves onto the icon button (its :focus ring is now the only one).
1948
+ cy.get("#select-box").shadow().find("#firstIcon").should("be.focused");
1949
+ });
1950
+
1951
+ // Added: covers the _handleArrowLeftInRow fix — when ArrowLeft returns from the
1952
+ // first icon button back to the list item we call _setActiveDescendant(renderedItem)
1953
+ // then move DOM focus back to the search input. This restores the list item's
1954
+ // _udex-active-descendant ring while clearing the icon's :focus ring, so only the
1955
+ // list item's ring is visible (not both rings simultaneously).
1956
+ it("ArrowLeft from the first icon restores the list item ring and removes icon focus", () => {
1957
+ cy.mount(shortComponentWithFocusTargets);
1958
+ openDropdownOnFirstItem();
1959
+
1960
+ // Enter the first icon button.
1961
+ cy.realPress("ArrowRight");
1962
+
1963
+ // Precondition: DOM focus is on the icon button.
1964
+ cy.get("#select-box").shadow().find("#firstIcon").should("be.focused");
1965
+
1966
+ // Navigate back to the list item.
1967
+ cy.realPress("ArrowLeft");
1968
+
1969
+ // The list item's active-descendant marker is restored so its ring shows again.
1970
+ cy.get("#select-box").shadow()
1971
+ .find("udex-list-item").eq(0)
1972
+ .should("have.class", "_udex-active-descendant");
1973
+
1974
+ // DOM focus returns to the search input, clearing the icon's :focus ring.
1975
+ cy.get("#select-box").shadow()
1976
+ .find(".udex-text-field__search")
1977
+ .should("be.focused");
1978
+ });
1979
+
1904
1980
  it("ArrowRight from firstIcon button focuses secondIcon button", () => {
1905
1981
  cy.mount(shortComponentWithFocusTargets);
1906
1982
  openDropdownOnFirstItem();
@@ -1940,10 +2016,11 @@ describe("SelectBox", () => {
1940
2016
  cy.realPress("ArrowRight");
1941
2017
  cy.realPress("ArrowLeft");
1942
2018
 
2019
+ // Updated: navigating back from the icon button makes the list item the
2020
+ // active descendant (DOM focus returns to the input, not the item).
1943
2021
  cy.get("#select-box").shadow()
1944
2022
  .find("udex-list-item").eq(0)
1945
- .shadow().find("[part=item]")
1946
- .should("be.focused");
2023
+ .should("have.attr", "_active-descendant");
1947
2024
  });
1948
2025
 
1949
2026
  it("ArrowDown from firstIcon button navigates to next list item", () => {
@@ -1955,8 +2032,7 @@ describe("SelectBox", () => {
1955
2032
 
1956
2033
  cy.get("#select-box").shadow()
1957
2034
  .find("udex-list-item").eq(1)
1958
- .shadow().find("[part=item]")
1959
- .should("be.focused");
2035
+ .should("have.attr", "_active-descendant");
1960
2036
  });
1961
2037
 
1962
2038
  it("ArrowUp from firstIcon button navigates to previous list item (wraps to last)", () => {
@@ -1968,8 +2044,7 @@ describe("SelectBox", () => {
1968
2044
 
1969
2045
  cy.get("#select-box").shadow()
1970
2046
  .find("udex-list-item").last()
1971
- .shadow().find("[part=item]")
1972
- .should("be.focused");
2047
+ .should("have.attr", "_active-descendant");
1973
2048
  });
1974
2049
 
1975
2050
  it("ArrowDown from secondIcon button navigates to next list item", () => {
@@ -1982,8 +2057,7 @@ describe("SelectBox", () => {
1982
2057
 
1983
2058
  cy.get("#select-box").shadow()
1984
2059
  .find("udex-list-item").eq(1)
1985
- .shadow().find("[part=item]")
1986
- .should("be.focused");
2060
+ .should("have.attr", "_active-descendant");
1987
2061
  });
1988
2062
 
1989
2063
  it("Escape from icon button closes dropdown and returns focus to input", () => {
@@ -2368,10 +2442,8 @@ describe("SelectBox", () => {
2368
2442
  });
2369
2443
  });
2370
2444
 
2371
- // Added: regression coverage for the WCAG 4.1.2 Treeview Checkbox pattern
2372
- // in SearchableListBase. SelectBox sets defaultAccessibleRole = "listbox",
2373
- // so it must NOT switch to the tree pattern even in MultiSelect + groups.
2374
- describe("Tree accessibility pattern regression", () => {
2445
+ // Tree accessibility pattern: SelectBox uses tree pattern when group headers are present.
2446
+ describe("Tree accessibility pattern with group headers", () => {
2375
2447
  const selectBoxMultiSelectGroup = <div style="width: 350px; position:relative;">
2376
2448
  <UDExSelectBox
2377
2449
  id="select-box-tree-regression"
@@ -2390,49 +2462,333 @@ describe("SelectBox", () => {
2390
2462
  .find(".udex-text-field__trigger").click();
2391
2463
  });
2392
2464
 
2393
- it("host [udex-list] has NO aria-multiselectable attribute", () => {
2465
+ it("host [udex-list] has aria-multiselectable=\"true\" (tree + MultiSelect)", () => {
2394
2466
  cy.get("#select-box-tree-regression").shadow()
2395
- .find("[udex-list]").should("not.have.attr", "aria-multiselectable");
2467
+ .find("[udex-list]").should("have.attr", "aria-multiselectable", "true");
2396
2468
  });
2397
2469
 
2398
- // Updated: `accessibleRoleForList` no longer collapses to `"group"` for grouped
2399
- // non-tree mode — it now returns `defaultAccessibleRole` (`"listbox"` for SelectBox)
2400
- // in every non-tree case. This aligns with the test's original intent (title says
2401
- // "keeps role=\"listbox\""); the prior `role="group"` assertion was already stale.
2402
- it("inner list div keeps role=\"listbox\" (not \"tree\")", () => {
2470
+ it("inner list div has role=\"tree\"", () => {
2403
2471
  cy.get("#select-box-tree-regression").shadow()
2404
2472
  .find("[udex-list]").shadow()
2405
- .find("#udex-list").should("have.attr", "role", "listbox");
2473
+ .find("#udex-list").should("have.attr", "role", "tree");
2406
2474
  });
2407
2475
 
2408
- // Updated: `accessibleRole` string property on udex-list-group-header was replaced
2409
- // by a `_treeMode` boolean (reflected as the `_tree-mode` attribute). SelectBox is
2410
- // not tree-capable so the boolean must stay false and the attribute must be absent.
2411
- it("group header host has NO _tree-mode attribute", () => {
2476
+ it("group header host has _tree-mode attribute", () => {
2412
2477
  cy.get("#select-box-tree-regression").shadow()
2413
2478
  .find("udex-list-group-header")
2414
- .should("not.have.attr", "_tree-mode");
2479
+ .should("have.attr", "_tree-mode");
2415
2480
  });
2416
2481
 
2417
- it("child items have role=\"option\", not \"treeitem\"", () => {
2482
+ it("child items have role=\"treeitem\" on the host (aria-activedescendant pattern)", () => {
2418
2483
  cy.get("#select-box-tree-regression").shadow()
2419
2484
  .find("udex-list-group-header")
2420
- .find("udex-list-item").eq(0).shadow()
2421
- .find("[part=item]").should("have.attr", "role", "option");
2485
+ .find("udex-list-item").eq(0)
2486
+ .should("have.attr", "role", "treeitem");
2487
+ });
2488
+
2489
+ it("child items have aria-checked on the host (MultiSelect tree)", () => {
2490
+ cy.get("#select-box-tree-regression").shadow()
2491
+ .find("udex-list-group-header")
2492
+ .find("udex-list-item").eq(0)
2493
+ .should("have.attr", "aria-checked", "false");
2422
2494
  });
2423
2495
 
2424
- it("child items have NO aria-checked (roleItem !== treeitem)", () => {
2496
+ it("child items' inner [part=item] div is role=\"presentation\" (host carries the role)", () => {
2425
2497
  cy.get("#select-box-tree-regression").shadow()
2426
2498
  .find("udex-list-group-header")
2427
2499
  .find("udex-list-item").eq(0).shadow()
2428
- .find("[part=item]").should("not.have.attr", "aria-checked");
2500
+ .find("[part=item]").should("have.attr", "role", "presentation");
2429
2501
  });
2430
2502
 
2431
- it("child items' checkbox is NOT aria-hidden (JAWS double-announce guard only applies to tree)", () => {
2503
+ it("child items' checkbox is aria-hidden (tree mode, JAWS uses host aria-checked)", () => {
2432
2504
  cy.get("#select-box-tree-regression").shadow()
2433
2505
  .find("udex-list-group-header")
2434
2506
  .find("udex-list-item").eq(0).shadow()
2435
- .find("udex-checkbox").should("not.have.attr", "aria-hidden");
2507
+ .find("udex-checkbox").should("have.attr", "aria-hidden");
2508
+ });
2509
+ });
2510
+
2511
+ // Covers the aria-live="polite" region that announces the active item
2512
+ // during keyboard navigation. _setActiveDescendant(item) sets _liveRegionText to
2513
+ // "Label, X of Y"; _setActiveDescendant(null) (on close/Escape) resets it to "".
2514
+ describe("Select Box live region announcements", () => {
2515
+ it("populates the live region with 'Label, X of Y' when ArrowDown activates an item", () => {
2516
+ cy.mount(shortComponent);
2517
+ cy.get("#select-box").shadow()
2518
+ .find(".udex-text-field__search")
2519
+ .click()
2520
+ .realPress("ArrowDown");
2521
+
2522
+ // shortComponent has 6 items; the first item "JavaScript" becomes active.
2523
+ cy.get("#select-box").shadow()
2524
+ .find('[aria-live="polite"]')
2525
+ .invoke("text")
2526
+ .should("match", /^JavaScript, 1 of 6$/);
2527
+ });
2528
+
2529
+ // Added: covers _treePositionText — for a grouped SelectBox the announcement
2530
+ // carries the tree position. ArrowDown activates the group header ("level 0");
2531
+ // a further ArrowDown reaches its first child ("level 1").
2532
+ it("uses tree positional text ('level 0' for the group header, 'level 1' for its child)", () => {
2533
+ cy.mount(groupHeaderCollapsible);
2534
+ cy.get("#select-box").shadow()
2535
+ .find(".udex-text-field__search")
2536
+ .click()
2537
+ .realPress("ArrowDown");
2538
+
2539
+ // ArrowDown activates the group header -> tree text reports "level 0".
2540
+ cy.get("#select-box").shadow()
2541
+ .find('[aria-live="polite"]')
2542
+ .invoke("text")
2543
+ .should("contain", "level 0");
2544
+
2545
+ // A further ArrowDown reaches the first child -> tree text reports "level 1".
2546
+ cy.get("#select-box").shadow()
2547
+ .find(".udex-text-field__search")
2548
+ .realPress("ArrowDown");
2549
+
2550
+ cy.get("#select-box").shadow()
2551
+ .find('[aria-live="polite"]')
2552
+ .invoke("text")
2553
+ .should("contain", "level 1");
2554
+ });
2555
+
2556
+ it("clears the live region text when the dropdown closes on Escape", () => {
2557
+ cy.mount(shortComponent);
2558
+ cy.get("#select-box").shadow()
2559
+ .find(".udex-text-field__search")
2560
+ .click()
2561
+ .realPress("ArrowDown");
2562
+
2563
+ // Precondition: the live region is populated by the navigation above.
2564
+ cy.get("#select-box").shadow()
2565
+ .find('[aria-live="polite"]')
2566
+ .invoke("text")
2567
+ .should("not.be.empty");
2568
+
2569
+ cy.get("#select-box").shadow()
2570
+ .find(".udex-text-field__search")
2571
+ .realPress("Escape");
2572
+
2573
+ cy.get("#select-box").shadow()
2574
+ .find(".udex-select-box__dropdown")
2575
+ .should("not.have.attr", "open");
2576
+
2577
+ // _setActiveDescendant(null) resets _liveRegionText to "".
2578
+ cy.get("#select-box").shadow()
2579
+ .find('[aria-live="polite"]')
2580
+ .invoke("text")
2581
+ .should("equal", "");
2582
+ });
2583
+
2584
+ it("appends 'Unselected' to the live region for an unchecked MultiSelect item", () => {
2585
+ cy.mount(multiSelect);
2586
+ cy.get("#select-box").shadow()
2587
+ .find(".udex-text-field__search")
2588
+ .click()
2589
+ .realPress("ArrowDown");
2590
+
2591
+ // First MultiSelect item is not selected -> state suffix is "Unselected".
2592
+ cy.get("#select-box").shadow()
2593
+ .find('[aria-live="polite"]')
2594
+ .should("contain.text", "Unselected");
2595
+ });
2596
+
2597
+ it("appends 'Selected' to the live region for a checked MultiSelect item", () => {
2598
+ const checkedMultiSelect = <div style="width: 350px; position:relative;">
2599
+ <UDExSelectBox
2600
+ id="select-box"
2601
+ height="400px"
2602
+ label="Programming languages"
2603
+ mode={UDExListMode.MultiSelect}>
2604
+ <UDExSelectBoxItem selected label="JavaScript" supportingText="Frontend web development"></UDExSelectBoxItem>
2605
+ <UDExSelectBoxItem label="Python" supportingText="Used in data analysis"></UDExSelectBoxItem>
2606
+ <UDExSelectBoxItem label="Java" supportingText="Building large-scale applications"></UDExSelectBoxItem>
2607
+ </UDExSelectBox>
2608
+ </div>;
2609
+
2610
+ cy.mount(checkedMultiSelect);
2611
+ cy.get("#select-box").shadow()
2612
+ .find(".udex-text-field__search")
2613
+ .click()
2614
+ .realPress("ArrowDown");
2615
+
2616
+ // First MultiSelect item is pre-selected -> state suffix is "Selected".
2617
+ cy.get("#select-box").shadow()
2618
+ .find('[aria-live="polite"]')
2619
+ .should("contain.text", "Selected");
2620
+ });
2621
+
2622
+ it("appends 'collapsed' to the live region for a collapsed collapsible group header", () => {
2623
+ const collapsedGroupHeader = <div style="width: 350px; position:relative;">
2624
+ <UDExSelectBox
2625
+ id="select-box"
2626
+ mode={UDExListMode.MultiSelect}
2627
+ label="Programming languages">
2628
+ <UDExSelectBoxGroupHeader label="Programming Languages" collapsible collapsed>
2629
+ <UDExSelectBoxItem label="JavaScript" supportingText="Frontend web development"></UDExSelectBoxItem>
2630
+ <UDExSelectBoxItem label="Python" supportingText="Used in data analysis"></UDExSelectBoxItem>
2631
+ </UDExSelectBoxGroupHeader>
2632
+ </UDExSelectBox>
2633
+ </div>;
2634
+
2635
+ cy.mount(collapsedGroupHeader);
2636
+ cy.get("#select-box").shadow()
2637
+ .find(".udex-text-field__search")
2638
+ .click()
2639
+ .realPress("ArrowDown");
2640
+
2641
+ // ArrowDown activates the collapsed group header -> tree text reports
2642
+ // "level 0" and the state suffix is "collapsed".
2643
+ cy.get("#select-box").shadow()
2644
+ .find('[aria-live="polite"]')
2645
+ .invoke("text")
2646
+ .should("contain", "level 0")
2647
+ .and("contain", "collapsed");
2648
+ });
2649
+
2650
+ it("appends 'expanded' to the live region for an expanded collapsible group header", () => {
2651
+ // groupHeaderCollapsible starts expanded (no `collapsed` attribute).
2652
+ cy.mount(groupHeaderCollapsible);
2653
+ cy.get("#select-box").shadow()
2654
+ .find(".udex-text-field__search")
2655
+ .click()
2656
+ .realPress("ArrowDown");
2657
+
2658
+ // ArrowDown activates the expanded group header -> tree text reports
2659
+ // "level 0" and the state suffix is "expanded".
2660
+ cy.get("#select-box").shadow()
2661
+ .find('[aria-live="polite"]')
2662
+ .invoke("text")
2663
+ .should("contain", "level 0")
2664
+ .and("contain", "expanded");
2665
+ });
2666
+ });
2667
+
2668
+ // Added: covers the double-outline / flicker fix. After a click on a list item
2669
+ // (SelectBase.handleItemSelection, MultiSelect path) or a group header
2670
+ // (SelectBox.handleHeaderPress), DOM focus is returned to the search input via
2671
+ // (this._search as HTMLElement).focus(). This prevents the clicked element from
2672
+ // retaining its CSS :focus ring while _udex-active-descendant later tracks a
2673
+ // different item during arrow navigation (which would show two rings at once).
2674
+ describe("Select Box click focus retention (double-outline fix)", () => {
2675
+ it("MultiSelect mode - clicking a list item returns DOM focus to the search input", () => {
2676
+ cy.mount(multiSelect);
2677
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
2678
+
2679
+ cy.get("#select-box").shadow()
2680
+ .find(".udex-select-box__dropdown")
2681
+ .should("have.attr", "open");
2682
+
2683
+ // Click the second list item — the dropdown stays open in MultiSelect mode.
2684
+ cy.get("#select-box").shadow()
2685
+ .find("udex-list-item").eq(1)
2686
+ .click();
2687
+
2688
+ // The fix returns DOM focus to the search input rather than leaving it on
2689
+ // the clicked item.
2690
+ cy.get("#select-box").shadow()
2691
+ .find(".udex-text-field__search")
2692
+ .should("be.focused");
2693
+ });
2694
+
2695
+ it("MultiSelect mode - the clicked list item does not retain DOM focus", () => {
2696
+ cy.mount(multiSelect);
2697
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
2698
+
2699
+ cy.get("#select-box").shadow()
2700
+ .find("udex-list-item").eq(1)
2701
+ .click();
2702
+
2703
+ // The clicked item must NOT be the active element — otherwise its :focus
2704
+ // ring would linger alongside the active-descendant ring.
2705
+ cy.get("#select-box").shadow()
2706
+ .find("udex-list-item").eq(1)
2707
+ .should("not.be.focused");
2708
+ });
2709
+
2710
+ it("MultiSelect mode - clicking a group header returns DOM focus to the search input", () => {
2711
+ cy.mount(groupHeader);
2712
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
2713
+
2714
+ cy.get("#select-box").shadow()
2715
+ .find(".udex-select-box__dropdown")
2716
+ .should("have.attr", "open");
2717
+
2718
+ // Click the group header's own focusable row (the inner udex-list-item in
2719
+ // the group header shadow) — this fires the header-press event which routes
2720
+ // to SelectBox.handleHeaderPress.
2721
+ cy.get("#select-box").shadow()
2722
+ .find("udex-list-group-header")
2723
+ .eq(0)
2724
+ .shadow()
2725
+ .find("udex-list-item")
2726
+ .click();
2727
+
2728
+ // SelectBox.handleHeaderPress calls (this._search).focus() after onHeaderPress,
2729
+ // so DOM focus must land on the search input.
2730
+ cy.get("#select-box").shadow()
2731
+ .find(".udex-text-field__search")
2732
+ .should("be.focused");
2733
+ });
2734
+
2735
+ it("MultiSelect mode - after clicking an item then ArrowDown, only the newly navigated item carries the active-descendant ring", () => {
2736
+ cy.mount(multiSelect);
2737
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
2738
+
2739
+ cy.get("#select-box").shadow()
2740
+ .find(".udex-select-box__dropdown")
2741
+ .should("have.attr", "open");
2742
+
2743
+ // Click the second item (index 1). handleItemSelection sets it as the active
2744
+ // descendant and returns focus to the search input.
2745
+ cy.get("#select-box").shadow()
2746
+ .find("udex-list-item").eq(1)
2747
+ .click();
2748
+
2749
+ cy.get("#select-box").shadow()
2750
+ .find("udex-list-item").eq(1)
2751
+ .should("have.class", "_udex-active-descendant");
2752
+
2753
+ // ArrowDown (dispatched on the focused search input via onKeyDownSearch)
2754
+ // advances the active descendant to the third item (index 2).
2755
+ cy.realPress("ArrowDown");
2756
+
2757
+ // Only the newly navigated item (index 2) carries the ring class.
2758
+ cy.get("#select-box").shadow()
2759
+ .find("udex-list-item").eq(2)
2760
+ .should("have.class", "_udex-active-descendant");
2761
+
2762
+ // The previously clicked item (index 1) must have lost the ring class — no
2763
+ // double outline.
2764
+ cy.get("#select-box").shadow()
2765
+ .find("udex-list-item").eq(1)
2766
+ .should("not.have.class", "_udex-active-descendant");
2767
+
2768
+ // Exactly one element bears the active-descendant class at any time.
2769
+ cy.get("#select-box").shadow()
2770
+ .find("._udex-active-descendant")
2771
+ .should("have.length", 1);
2772
+ });
2773
+ });
2774
+
2775
+ // Added: covers aria-required wiring on the combobox input in SelectBoxTemplate.
2776
+ // The `required` boolean (inherited from SelectBase) drives aria-required={this.required}.
2777
+ describe("aria-required accessibility", () => {
2778
+ it("does not set aria-required=\"true\" on the combobox input by default (required not set)", () => {
2779
+ cy.mount(<UDExSelectBox id="select-box" label="Programming languages"></UDExSelectBox>);
2780
+
2781
+ cy.get("#select-box").shadow()
2782
+ .find('input[role="combobox"]')
2783
+ .should("not.have.attr", "aria-required", "true");
2784
+ });
2785
+
2786
+ it("sets aria-required=\"true\" on the combobox input when required is set", () => {
2787
+ cy.mount(<UDExSelectBox id="select-box" label="Programming languages" required></UDExSelectBox>);
2788
+
2789
+ cy.get("#select-box").shadow()
2790
+ .find('input[role="combobox"]')
2791
+ .should("have.attr", "aria-required", "true");
2436
2792
  });
2437
2793
  });
2438
2794
  });