@sapudex/web-components 2.36.0 → 2.37.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 (143) hide show
  1. package/cypress/specs/Pagination.cy.tsx +52 -0
  2. package/cypress/specs/SelectBox.cy.tsx +1 -1
  3. package/cypress/specs/TabNavigation.cy.tsx +314 -0
  4. package/cypress/specs/Table.cy.tsx +170 -0
  5. package/cypress/support/component-imports.instrumented.ts +2 -0
  6. package/cypress/support/component-imports.src.ts +2 -0
  7. package/dist/CountrySelector.d.ts +2 -0
  8. package/dist/CountrySelector.js +12 -3
  9. package/dist/CountrySelector.js.map +1 -1
  10. package/dist/CountrySelectorTemplate.d.ts +2 -0
  11. package/dist/CountrySelectorTemplate.js +8 -0
  12. package/dist/CountrySelectorTemplate.js.map +1 -0
  13. package/dist/FilterMenu.d.ts +3 -3
  14. package/dist/FilterMenu.js +8 -9
  15. package/dist/FilterMenu.js.map +1 -1
  16. package/dist/FilterMenuTemplate.d.ts +2 -0
  17. package/dist/FilterMenuTemplate.js +27 -0
  18. package/dist/FilterMenuTemplate.js.map +1 -0
  19. package/dist/Pagination.js +1 -0
  20. package/dist/Pagination.js.map +1 -1
  21. package/dist/SearchableListBaseTemplate.d.ts +2 -0
  22. package/dist/SearchableListBaseTemplate.js +32 -0
  23. package/dist/SearchableListBaseTemplate.js.map +1 -0
  24. package/dist/SecondaryNavigation.d.ts +2 -0
  25. package/dist/SecondaryNavigation.js +20 -0
  26. package/dist/SecondaryNavigation.js.map +1 -1
  27. package/dist/SelectBase.d.ts +7 -6
  28. package/dist/SelectBase.js +2 -2
  29. package/dist/SelectBase.js.map +1 -1
  30. package/dist/SelectBox.d.ts +2 -0
  31. package/dist/SelectBox.js +11 -3
  32. package/dist/SelectBox.js.map +1 -1
  33. package/dist/SelectBoxTemplate.d.ts +2 -0
  34. package/dist/SelectBoxTemplate.js +27 -0
  35. package/dist/SelectBoxTemplate.js.map +1 -0
  36. package/dist/Tab.d.ts +9 -0
  37. package/dist/Tab.js +6 -0
  38. package/dist/Tab.js.map +1 -1
  39. package/dist/TabNavigation.js +32 -12
  40. package/dist/TabNavigation.js.map +1 -1
  41. package/dist/TableGroupRowTemplate.js +1 -1
  42. package/dist/TableGroupRowTemplate.js.map +1 -1
  43. package/dist/TableTemplate.js +1 -1
  44. package/dist/TableTemplate.js.map +1 -1
  45. package/dist/TagTemplate.js +2 -4
  46. package/dist/TagTemplate.js.map +1 -1
  47. package/dist/assets/{InputSuggestions-BIR_sdOv.js → InputSuggestions-CAJNg_KD.js} +1 -1
  48. package/dist/assets/{ListItemStandardExpandableTextTemplate-5xOuY0en.js → ListItemStandardExpandableTextTemplate-CTxLxjip.js} +1 -1
  49. package/dist/assets/{bundle.esm-C8GwJvJr.js → bundle.esm-AREsL-HM.js} +240 -240
  50. package/dist/css/themes/MastheadMenu.css +1 -1
  51. package/dist/css/themes/MastheadMobileMenu.css +1 -1
  52. package/dist/css/themes/MediaPlayer.css +1 -1
  53. package/dist/css/themes/RatingBar.css +1 -1
  54. package/dist/css/themes/RatingIndicator.css +1 -1
  55. package/dist/css/themes/Slider.css +1 -1
  56. package/dist/css/themes/TabNavigation.css +1 -1
  57. package/dist/css/themes/TableGroupRow.css +1 -1
  58. package/dist/custom-elements-internal.json +19 -0
  59. package/dist/custom-elements.json +19 -0
  60. package/dist/generated/templates/TabHeaderTemplate.lit.js +4 -4
  61. package/dist/generated/templates/TabHeaderTemplate.lit.js.map +1 -1
  62. package/dist/generated/templates/TabInOverflowTemplate.lit.js +2 -2
  63. package/dist/generated/templates/TabInOverflowTemplate.lit.js.map +1 -1
  64. package/dist/generated/themes/MastheadMenu.css.d.ts +1 -1
  65. package/dist/generated/themes/MastheadMenu.css.js +1 -1
  66. package/dist/generated/themes/MastheadMenu.css.js.map +1 -1
  67. package/dist/generated/themes/MastheadMobileMenu.css.d.ts +1 -1
  68. package/dist/generated/themes/MastheadMobileMenu.css.js +1 -1
  69. package/dist/generated/themes/MastheadMobileMenu.css.js.map +1 -1
  70. package/dist/generated/themes/MediaPlayer.css.d.ts +1 -1
  71. package/dist/generated/themes/MediaPlayer.css.js +1 -1
  72. package/dist/generated/themes/MediaPlayer.css.js.map +1 -1
  73. package/dist/generated/themes/RatingBar.css.d.ts +1 -1
  74. package/dist/generated/themes/RatingBar.css.js +1 -1
  75. package/dist/generated/themes/RatingBar.css.js.map +1 -1
  76. package/dist/generated/themes/RatingIndicator.css.d.ts +1 -1
  77. package/dist/generated/themes/RatingIndicator.css.js +1 -1
  78. package/dist/generated/themes/RatingIndicator.css.js.map +1 -1
  79. package/dist/generated/themes/Slider.css.d.ts +1 -1
  80. package/dist/generated/themes/Slider.css.js +1 -1
  81. package/dist/generated/themes/Slider.css.js.map +1 -1
  82. package/dist/generated/themes/TabNavigation.css.d.ts +1 -1
  83. package/dist/generated/themes/TabNavigation.css.js +1 -1
  84. package/dist/generated/themes/TabNavigation.css.js.map +1 -1
  85. package/dist/generated/themes/TableGroupRow.css.d.ts +1 -1
  86. package/dist/generated/themes/TableGroupRow.css.js +1 -1
  87. package/dist/generated/themes/TableGroupRow.css.js.map +1 -1
  88. package/dist/vscode.html-custom-data.json +5 -0
  89. package/dist/web-types.json +11 -1
  90. package/package.json +1 -1
  91. package/src/CountrySelector.ts +14 -3
  92. package/src/CountrySelectorTemplate.tsx +12 -0
  93. package/src/FilterMenu.ts +9 -10
  94. package/src/FilterMenuTemplate.tsx +104 -0
  95. package/src/Pagination.ts +1 -0
  96. package/src/SearchableListBaseTemplate.tsx +157 -0
  97. package/src/SecondaryNavigation.ts +20 -0
  98. package/src/SelectBase.ts +7 -6
  99. package/src/SelectBox.ts +13 -3
  100. package/src/SelectBoxTemplate.tsx +165 -0
  101. package/src/Tab.ts +14 -0
  102. package/src/TabHeader.hbs +6 -2
  103. package/src/TabInOverflow.hbs +2 -0
  104. package/src/TabNavigation.ts +34 -12
  105. package/src/TableGroupRowTemplate.tsx +26 -17
  106. package/src/TableTemplate.tsx +1 -1
  107. package/src/TagTemplate.tsx +2 -4
  108. package/src/generated/templates/TabHeaderTemplate.lit.ts +4 -4
  109. package/src/generated/templates/TabInOverflowTemplate.lit.ts +2 -2
  110. package/src/generated/themes/MastheadMenu.css.ts +1 -1
  111. package/src/generated/themes/MastheadMobileMenu.css.ts +1 -1
  112. package/src/generated/themes/MediaPlayer.css.ts +1 -1
  113. package/src/generated/themes/RatingBar.css.ts +1 -1
  114. package/src/generated/themes/RatingIndicator.css.ts +1 -1
  115. package/src/generated/themes/Slider.css.ts +1 -1
  116. package/src/generated/themes/TabNavigation.css.ts +1 -1
  117. package/src/generated/themes/TableGroupRow.css.ts +1 -1
  118. package/src/themes/MastheadMenu.css +4 -2
  119. package/src/themes/MastheadMobileMenu.css +4 -2
  120. package/src/themes/MediaPlayer.css +12 -0
  121. package/src/themes/RatingBar.css +14 -5
  122. package/src/themes/TabNavigation.css +6 -0
  123. package/src/themes/TableGroupRow.css +5 -0
  124. package/dist/generated/templates/CountrySelectorTemplate.lit.d.ts +0 -3
  125. package/dist/generated/templates/CountrySelectorTemplate.lit.js +0 -8
  126. package/dist/generated/templates/CountrySelectorTemplate.lit.js.map +0 -1
  127. package/dist/generated/templates/FilterMenuTemplate.lit.d.ts +0 -3
  128. package/dist/generated/templates/FilterMenuTemplate.lit.js +0 -13
  129. package/dist/generated/templates/FilterMenuTemplate.lit.js.map +0 -1
  130. package/dist/generated/templates/SearchableListBaseTemplate.lit.d.ts +0 -3
  131. package/dist/generated/templates/SearchableListBaseTemplate.lit.js +0 -19
  132. package/dist/generated/templates/SearchableListBaseTemplate.lit.js.map +0 -1
  133. package/dist/generated/templates/SelectBoxTemplate.lit.d.ts +0 -3
  134. package/dist/generated/templates/SelectBoxTemplate.lit.js +0 -20
  135. package/dist/generated/templates/SelectBoxTemplate.lit.js.map +0 -1
  136. package/src/CountrySelector.hbs +0 -6
  137. package/src/FilterMenu.hbs +0 -88
  138. package/src/SearchableListBase.hbs +0 -148
  139. package/src/SelectBox.hbs +0 -138
  140. package/src/generated/templates/CountrySelectorTemplate.lit.ts +0 -12
  141. package/src/generated/templates/FilterMenuTemplate.lit.ts +0 -17
  142. package/src/generated/templates/SearchableListBaseTemplate.lit.ts +0 -23
  143. package/src/generated/templates/SelectBoxTemplate.lit.ts +0 -24
@@ -0,0 +1,52 @@
1
+ import { UDExPagination } from "../support/component-imports.js";
2
+
3
+ describe("UDExPagination", () => {
4
+ describe("Focus management — _pageFocusedManually flag reset", () => {
5
+ it("does not call focusActivePage on programmatic currentPageIndex change after a user click", () => {
6
+ // Mount pagination with enough pages to navigate
7
+ cy.mount(
8
+ <div>
9
+ <button id="external-focus-target">External</button>
10
+ <UDExPagination id="pagination" pages={5} currentPageIndex={0} />
11
+ </div>,
12
+ );
13
+
14
+ // Wait for the component to render fully
15
+ cy.get("[udex-pagination]")
16
+ .shadow()
17
+ .find(".udex-pagination__page[data-index='0']")
18
+ .should("exist");
19
+
20
+ // Click on page 3 (index 2) — this sets _pageFocusedManually = true,
21
+ // triggers re-render, focusActivePage() runs and resets the flag
22
+ cy.get("[udex-pagination]")
23
+ .shadow()
24
+ .find(".udex-pagination__page[data-index='2']")
25
+ .click();
26
+
27
+ // Verify the click was registered (page 3 is now active)
28
+ cy.get("[udex-pagination]")
29
+ .shadow()
30
+ .find(".udex-pagination__page[data-index='2']")
31
+ .should("have.attr", "active");
32
+
33
+ // Move focus to an external element so we can detect if focus moves back
34
+ cy.get("#external-focus-target").focus();
35
+ cy.get("#external-focus-target").should("have.focus");
36
+
37
+ // Programmatically change currentPageIndex — this should NOT trigger focusActivePage
38
+ // because _pageFocusedManually was already consumed (reset to false) on the prior render
39
+ cy.get("[udex-pagination]").then(($el) => {
40
+ ($el[0] as any).currentPageIndex = 4;
41
+ });
42
+
43
+ // Allow time for re-render to complete
44
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
45
+ cy.wait(100);
46
+
47
+ // Assert that focus did NOT move to the pagination page button.
48
+ // Focus should remain on the external button.
49
+ cy.get("#external-focus-target").should("have.focus");
50
+ });
51
+ });
52
+ });
@@ -1155,7 +1155,7 @@ describe("SelectBox", () => {
1155
1155
  .find("udex-tag-group").shadow()
1156
1156
  .find("udex-tag")
1157
1157
  .first()
1158
- .should("have.attr", "text", "5 stars");
1158
+ .should("have.attr", "text", "5 stars");
1159
1159
  });
1160
1160
 
1161
1161
  it("MultiSelect mode - no single-label shown after opening dropdown and clicking outside", () => {
@@ -1613,6 +1613,320 @@ describe("Tab Navigation component", () => {
1613
1613
  });
1614
1614
  });
1615
1615
 
1616
+ // ── Disabled tab behaviour ──────────────────────────────────────────────────
1617
+
1618
+ describe("Disabled tab", () => {
1619
+ describe("Rendering", () => {
1620
+ it("renders aria-disabled='true' on a disabled tab button", () => {
1621
+ cy.mount(
1622
+ <UDExTabNavigation>
1623
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1624
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1625
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1626
+ </UDExTabNavigation>
1627
+ );
1628
+
1629
+ cy.get("[udex-tab-navigation]")
1630
+ .shadow()
1631
+ .find("button[role='tab']")
1632
+ .eq(1)
1633
+ .should("have.attr", "aria-disabled", "true");
1634
+ });
1635
+
1636
+ it("does not set native disabled attribute — tab remains in DOM for assistive technology", () => {
1637
+ cy.mount(
1638
+ <UDExTabNavigation>
1639
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1640
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1641
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1642
+ </UDExTabNavigation>
1643
+ );
1644
+
1645
+ cy.get("[udex-tab-navigation]")
1646
+ .shadow()
1647
+ .find("button[role='tab']")
1648
+ .eq(1)
1649
+ .should("not.have.attr", "disabled");
1650
+ });
1651
+
1652
+ it("applies disabled opacity style to the disabled tab button", () => {
1653
+ cy.mount(
1654
+ <UDExTabNavigation>
1655
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1656
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1657
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1658
+ </UDExTabNavigation>
1659
+ );
1660
+
1661
+ cy.get("[udex-tab-navigation]")
1662
+ .shadow()
1663
+ .find("button[role='tab'][aria-disabled='true']")
1664
+ .should("have.css", "opacity", "0.4");
1665
+ });
1666
+
1667
+ it("does not apply aria-disabled on enabled tabs", () => {
1668
+ cy.mount(
1669
+ <UDExTabNavigation>
1670
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1671
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1672
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1673
+ </UDExTabNavigation>
1674
+ );
1675
+
1676
+ cy.get("[udex-tab-navigation]")
1677
+ .shadow()
1678
+ .find("button[role='tab']")
1679
+ .eq(0)
1680
+ .should("not.have.attr", "aria-disabled");
1681
+
1682
+ cy.get("[udex-tab-navigation]")
1683
+ .shadow()
1684
+ .find("button[role='tab']")
1685
+ .eq(2)
1686
+ .should("not.have.attr", "aria-disabled");
1687
+ });
1688
+ });
1689
+
1690
+ describe("Interaction", () => {
1691
+ it("does not change the selected tab when a disabled tab is clicked", () => {
1692
+ cy.mount(
1693
+ <UDExTabNavigation>
1694
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1695
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1696
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1697
+ </UDExTabNavigation>
1698
+ );
1699
+
1700
+ // Force click because pointer-events: none prevents normal click
1701
+ cy.get("[udex-tab-navigation]")
1702
+ .shadow()
1703
+ .find("button[role='tab']")
1704
+ .eq(1)
1705
+ .click({ force: true });
1706
+
1707
+ // Tab 1 should remain selected
1708
+ cy.get("[udex-tab-navigation]")
1709
+ .shadow()
1710
+ .find("button[role='tab'][aria-selected='true']")
1711
+ .should("contain.text", "Tab 1");
1712
+
1713
+ // Tab 2 should NOT be selected
1714
+ cy.get("[udex-tab-navigation]")
1715
+ .shadow()
1716
+ .find("button[role='tab']")
1717
+ .eq(1)
1718
+ .should("have.attr", "aria-selected", "false");
1719
+ });
1720
+
1721
+ it("does not fire tab-select event when a disabled tab is clicked", () => {
1722
+ const tabSelectHandler = cy.stub().as("tabSelect");
1723
+
1724
+ cy.mount(
1725
+ <UDExTabNavigation onTabSelect={tabSelectHandler}>
1726
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1727
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1728
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1729
+ </UDExTabNavigation>
1730
+ );
1731
+
1732
+ cy.get("[udex-tab-navigation]")
1733
+ .shadow()
1734
+ .find("button[role='tab']")
1735
+ .eq(1)
1736
+ .click({ force: true });
1737
+
1738
+ cy.get("@tabSelect").should("not.have.been.called");
1739
+ });
1740
+ });
1741
+
1742
+ describe("Keyboard navigation", () => {
1743
+ it("disabled tab has tabindex='-1' even when it would otherwise be the roving-tabindex target", () => {
1744
+ cy.mount(
1745
+ <UDExTabNavigation>
1746
+ <UDExTab text="Tab 1" disabled selected><div>Content 1</div></UDExTab>
1747
+ <UDExTab text="Tab 2"><div>Content 2</div></UDExTab>
1748
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1749
+ </UDExTabNavigation>
1750
+ );
1751
+
1752
+ // The disabled tab should always have tabindex -1
1753
+ cy.get("[udex-tab-navigation]")
1754
+ .shadow()
1755
+ .find("button[role='tab'][aria-disabled='true']")
1756
+ .should("have.attr", "tabindex", "-1");
1757
+ });
1758
+
1759
+ it("arrow-key navigation skips disabled tab (ArrowRight)", () => {
1760
+ cy.mount(
1761
+ <UDExTabNavigation>
1762
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1763
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1764
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1765
+ </UDExTabNavigation>
1766
+ );
1767
+
1768
+ // Focus Tab 1 and press ArrowRight — should skip Tab 2 (disabled) and land on Tab 3
1769
+ cy.get("[udex-tab-navigation]")
1770
+ .shadow()
1771
+ .find("button[role='tab']")
1772
+ .eq(0)
1773
+ .focus()
1774
+ .realPress("ArrowRight");
1775
+
1776
+ cy.get("[udex-tab-navigation]")
1777
+ .shadow()
1778
+ .find("button[role='tab']")
1779
+ .eq(2)
1780
+ .should("be.focused");
1781
+ });
1782
+
1783
+ it("arrow-key navigation skips disabled tab (ArrowLeft)", () => {
1784
+ cy.mount(
1785
+ <UDExTabNavigation>
1786
+ <UDExTab text="Tab 1"><div>Content 1</div></UDExTab>
1787
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1788
+ <UDExTab text="Tab 3" selected><div>Content 3</div></UDExTab>
1789
+ </UDExTabNavigation>
1790
+ );
1791
+
1792
+ // Focus Tab 3 and press ArrowLeft — should skip Tab 2 (disabled) and land on Tab 1
1793
+ cy.get("[udex-tab-navigation]")
1794
+ .shadow()
1795
+ .find("button[role='tab']")
1796
+ .eq(2)
1797
+ .focus()
1798
+ .realPress("ArrowLeft");
1799
+
1800
+ cy.get("[udex-tab-navigation]")
1801
+ .shadow()
1802
+ .find("button[role='tab']")
1803
+ .eq(0)
1804
+ .should("be.focused");
1805
+ });
1806
+
1807
+ it("arrow-key navigation skips multiple consecutive disabled tabs", () => {
1808
+ cy.mount(
1809
+ <UDExTabNavigation>
1810
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1811
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1812
+ <UDExTab text="Tab 3" disabled><div>Content 3</div></UDExTab>
1813
+ <UDExTab text="Tab 4"><div>Content 4</div></UDExTab>
1814
+ </UDExTabNavigation>
1815
+ );
1816
+
1817
+ cy.get("[udex-tab-navigation]")
1818
+ .shadow()
1819
+ .find("button[role='tab']")
1820
+ .eq(0)
1821
+ .focus()
1822
+ .realPress("ArrowRight");
1823
+
1824
+ cy.get("[udex-tab-navigation]")
1825
+ .shadow()
1826
+ .find("button[role='tab']")
1827
+ .eq(3)
1828
+ .should("be.focused");
1829
+ });
1830
+
1831
+ it("arrow-key navigation skips disabled tabs in vertical mode (ArrowDown)", () => {
1832
+ cy.mount(
1833
+ <UDExTabNavigation orientation="Vertical">
1834
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1835
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1836
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1837
+ </UDExTabNavigation>
1838
+ );
1839
+
1840
+ cy.get("[udex-tab-navigation]")
1841
+ .shadow()
1842
+ .find("button[role='tab']")
1843
+ .eq(0)
1844
+ .focus()
1845
+ .realPress("ArrowDown");
1846
+
1847
+ cy.get("[udex-tab-navigation]")
1848
+ .shadow()
1849
+ .find("button[role='tab']")
1850
+ .eq(2)
1851
+ .should("be.focused");
1852
+ });
1853
+ });
1854
+
1855
+ describe("Default selection", () => {
1856
+ it("selects the first non-disabled tab when the first tab is disabled and none are explicitly selected", () => {
1857
+ cy.mount(
1858
+ <UDExTabNavigation>
1859
+ <UDExTab text="Tab 1" disabled><div>Content 1</div></UDExTab>
1860
+ <UDExTab text="Tab 2"><div>Content 2</div></UDExTab>
1861
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1862
+ </UDExTabNavigation>
1863
+ );
1864
+
1865
+ cy.get("[udex-tab-navigation]")
1866
+ .shadow()
1867
+ .find("button[role='tab'][aria-selected='true']")
1868
+ .should("contain.text", "Tab 2");
1869
+ });
1870
+
1871
+ it("selects the first non-disabled tab when multiple leading tabs are disabled", () => {
1872
+ cy.mount(
1873
+ <UDExTabNavigation>
1874
+ <UDExTab text="Tab 1" disabled><div>Content 1</div></UDExTab>
1875
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1876
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1877
+ <UDExTab text="Tab 4"><div>Content 4</div></UDExTab>
1878
+ </UDExTabNavigation>
1879
+ );
1880
+
1881
+ cy.get("[udex-tab-navigation]")
1882
+ .shadow()
1883
+ .find("button[role='tab'][aria-selected='true']")
1884
+ .should("contain.text", "Tab 3");
1885
+ });
1886
+
1887
+ it("ignores selected attribute on a disabled tab and falls back to first enabled tab", () => {
1888
+ cy.mount(
1889
+ <UDExTabNavigation>
1890
+ <UDExTab text="Tab 1" disabled selected><div>Content 1</div></UDExTab>
1891
+ <UDExTab text="Tab 2"><div>Content 2</div></UDExTab>
1892
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1893
+ </UDExTabNavigation>
1894
+ );
1895
+
1896
+ // The disabled tab cannot be selected, so the first enabled tab is chosen
1897
+ cy.get("[udex-tab-navigation]")
1898
+ .shadow()
1899
+ .find("button[role='tab'][aria-selected='true']")
1900
+ .should("contain.text", "Tab 2");
1901
+ });
1902
+ });
1903
+
1904
+ describe("Auto-play skips disabled tabs", () => {
1905
+ it("auto-play advances past disabled tabs", () => {
1906
+ cy.clock(0, ["setInterval", "clearInterval"]);
1907
+ cy.mount(
1908
+ <UDExTabNavigation
1909
+ orientation="Vertical"
1910
+ autoPlay
1911
+ autoPlayInterval={1000}
1912
+ >
1913
+ <UDExTab text="Tab 1" selected><div>Content 1</div></UDExTab>
1914
+ <UDExTab text="Tab 2" disabled><div>Content 2</div></UDExTab>
1915
+ <UDExTab text="Tab 3"><div>Content 3</div></UDExTab>
1916
+ </UDExTabNavigation>
1917
+ );
1918
+
1919
+ // After 1 tick, auto-play should skip disabled Tab 2 and select Tab 3
1920
+ cy.tick(1000);
1921
+
1922
+ cy.get("[udex-tab-navigation]")
1923
+ .shadow()
1924
+ .find("button[role='tab'][aria-selected='true']")
1925
+ .should("contain.text", "Tab 3");
1926
+ });
1927
+ });
1928
+ });
1929
+
1616
1930
  // ── Process Tab variant ──────────────────────────────────────────────────────
1617
1931
 
1618
1932
  describe("TabNavigation - Process Tab variant", () => {
@@ -269,6 +269,10 @@ describe("Table", () => {
269
269
  cy.get("#table").shadow().find(".udex-table").should("have.attr", "aria-label", "table devices")
270
270
  });
271
271
 
272
+ it("header holder has role presentation to be transparent in the ARIA tree", () => {
273
+ cy.get("#table").shadow().find(".udex-table__header-holder").should("have.attr", "role", "presentation");
274
+ });
275
+
272
276
  it("renders the right amount of header rows", () => {
273
277
  cy.get("#table").find("udex-table-header-row").should("have.length", 1)
274
278
  });
@@ -357,3 +361,169 @@ describe("Table", () => {
357
361
  })
358
362
  });
359
363
  });
364
+
365
+ const multipleGroupRowsTable = <UDExTable id="table-multi-group">
366
+ <UDExTableHeaderRow slot="headerRow">
367
+ <UDExTableHeaderCell>Product</UDExTableHeaderCell>
368
+ <UDExTableHeaderCell>Price</UDExTableHeaderCell>
369
+ </UDExTableHeaderRow>
370
+ <UDExTableGroupRow id="group-row-1" group-title="Group A" collapsible>
371
+ <UDExTableRow>
372
+ <UDExTableCell>Item A1</UDExTableCell>
373
+ <UDExTableCell>$100</UDExTableCell>
374
+ </UDExTableRow>
375
+ </UDExTableGroupRow>
376
+ <UDExTableGroupRow id="group-row-2" group-title="Group B" collapsible>
377
+ <UDExTableRow>
378
+ <UDExTableCell>Item B1</UDExTableCell>
379
+ <UDExTableCell>$200</UDExTableCell>
380
+ </UDExTableRow>
381
+ </UDExTableGroupRow>
382
+ <UDExTableGroupRow id="group-row-3" group-title="Group C" collapsible>
383
+ <UDExTableRow>
384
+ <UDExTableCell>Item C1</UDExTableCell>
385
+ <UDExTableCell>$300</UDExTableCell>
386
+ </UDExTableRow>
387
+ </UDExTableGroupRow>
388
+ </UDExTable>;
389
+
390
+ describe("TableGroupRow Accessibility", () => {
391
+ beforeEach(() => {
392
+ cy.mount(multipleGroupRowsTable);
393
+ });
394
+
395
+ it("generates unique button IDs for each collapsible group row", () => {
396
+ const ids: string[] = [];
397
+
398
+ cy.get("[udex-table-group-row]").each($groupRow => {
399
+ cy.wrap($groupRow).shadow().find(".udex-table-grow-btn").invoke("attr", "id").then(id => {
400
+ expect(id).to.match(/^udex-table-group-opener-/);
401
+ ids.push(id as string);
402
+ });
403
+ }).then(() => {
404
+ const uniqueIds = new Set(ids);
405
+ expect(uniqueIds.size).to.equal(ids.length, "All group row button IDs must be unique");
406
+ });
407
+ });
408
+
409
+ it("button aria-controls references an existing element in the shadow DOM", () => {
410
+ cy.get("#group-row-1").shadow().find(".udex-table-grow-btn").then($btn => {
411
+ const controlsId = $btn.attr("aria-controls");
412
+ expect(controlsId).to.match(/^udex-table-group-rows-/);
413
+ cy.get("#group-row-1").shadow().find(`#${controlsId}`).should("exist").and("have.class", "udex-table-grow-rows");
414
+ });
415
+ });
416
+
417
+ it("button aria-controls value matches the rows container ID on the same instance", () => {
418
+ cy.get("[udex-table-group-row]").each($groupRow => {
419
+ cy.wrap($groupRow).shadow().find(".udex-table-grow-btn").invoke("attr", "aria-controls").then(controlsValue => {
420
+ cy.wrap($groupRow).shadow().find(".udex-table-grow-rows").invoke("attr", "id").should("equal", controlsValue);
421
+ });
422
+ });
423
+ });
424
+
425
+ it("each rows container has a unique ID matching its group row instance", () => {
426
+ const containerIds: string[] = [];
427
+
428
+ cy.get("[udex-table-group-row]").each($groupRow => {
429
+ cy.wrap($groupRow).shadow().find(".udex-table-grow-rows").invoke("attr", "id").then(id => {
430
+ expect(id).to.match(/^udex-table-group-rows-/);
431
+ containerIds.push(id as string);
432
+ });
433
+ }).then(() => {
434
+ const uniqueIds = new Set(containerIds);
435
+ expect(uniqueIds.size).to.equal(containerIds.length, "All rows container IDs must be unique");
436
+ });
437
+ });
438
+ });
439
+
440
+ const nonCollapsibleGroupRowTable = <UDExTable id="table-non-collapsible-group">
441
+ <UDExTableHeaderRow slot="headerRow">
442
+ <UDExTableHeaderCell>Product</UDExTableHeaderCell>
443
+ <UDExTableHeaderCell>Price</UDExTableHeaderCell>
444
+ </UDExTableHeaderRow>
445
+ <UDExTableGroupRow id="non-collapsible-group" group-title="Static Group">
446
+ <UDExTableRow>
447
+ <UDExTableCell>Item 1</UDExTableCell>
448
+ <UDExTableCell>$50</UDExTableCell>
449
+ </UDExTableRow>
450
+ </UDExTableGroupRow>
451
+ </UDExTable>;
452
+
453
+ describe("TableGroupRow ARIA Structure", () => {
454
+ describe("Collapsible group row", () => {
455
+ beforeEach(() => {
456
+ cy.mount(multipleGroupRowsTable);
457
+ });
458
+
459
+ it("contains a div with role='row' and class 'udex-table-group-header-row'", () => {
460
+ cy.get("#group-row-1").shadow().find("div[role='row']")
461
+ .should("exist")
462
+ .and("have.class", "udex-table-group-header-row");
463
+ });
464
+
465
+ it("the row wrapper contains a div with role='rowheader'", () => {
466
+ cy.get("#group-row-1").shadow().find("div[role='row'] > div[role='rowheader']")
467
+ .should("exist")
468
+ .and("have.class", "udex-table-group-header-cell");
469
+ });
470
+
471
+ it("the rowheader contains the button with class 'udex-table-grow-btn'", () => {
472
+ cy.get("#group-row-1").shadow().find("div[role='rowheader'] > button.udex-table-grow-btn")
473
+ .should("exist");
474
+ });
475
+
476
+ it("the row and rowheader wrappers have display:contents", () => {
477
+ cy.get("#group-row-1").shadow().find(".udex-table-group-header-row").then($row => {
478
+ expect(window.getComputedStyle($row[0]).display).to.equal("contents");
479
+ });
480
+ cy.get("#group-row-1").shadow().find(".udex-table-group-header-cell").then($cell => {
481
+ expect(window.getComputedStyle($cell[0]).display).to.equal("contents");
482
+ });
483
+ });
484
+
485
+ it("all collapsible group rows have the row > rowheader > button structure", () => {
486
+ cy.get("[udex-table-group-row]").each($groupRow => {
487
+ cy.wrap($groupRow).shadow().find("div[role='row'] > div[role='rowheader'] > button.udex-table-grow-btn")
488
+ .should("exist");
489
+ });
490
+ });
491
+ });
492
+
493
+ describe("Non-collapsible group row", () => {
494
+ beforeEach(() => {
495
+ cy.mount(nonCollapsibleGroupRowTable);
496
+ });
497
+
498
+ it("contains a div with role='row' and class 'udex-table-group-header-row'", () => {
499
+ cy.get("#non-collapsible-group").shadow().find("div[role='row']")
500
+ .should("exist")
501
+ .and("have.class", "udex-table-group-header-row");
502
+ });
503
+
504
+ it("the row wrapper contains a div with role='rowheader'", () => {
505
+ cy.get("#non-collapsible-group").shadow().find("div[role='row'] > div[role='rowheader']")
506
+ .should("exist")
507
+ .and("have.class", "udex-table-group-header-cell");
508
+ });
509
+
510
+ it("the rowheader contains a div (not button) with class 'udex-table-grow-btn'", () => {
511
+ cy.get("#non-collapsible-group").shadow().find("div[role='rowheader'] > div.udex-table-grow-btn")
512
+ .should("exist");
513
+ });
514
+
515
+ it("the non-collapsible title div displays the group title", () => {
516
+ cy.get("#non-collapsible-group").shadow().find("div[role='rowheader'] .udex-table-grow-btn")
517
+ .should("have.text", "Static Group");
518
+ });
519
+
520
+ it("the row and rowheader wrappers have display:contents", () => {
521
+ cy.get("#non-collapsible-group").shadow().find(".udex-table-group-header-row").then($row => {
522
+ expect(window.getComputedStyle($row[0]).display).to.equal("contents");
523
+ });
524
+ cy.get("#non-collapsible-group").shadow().find(".udex-table-group-header-cell").then($cell => {
525
+ expect(window.getComputedStyle($cell[0]).display).to.equal("contents");
526
+ });
527
+ });
528
+ });
529
+ });
@@ -43,6 +43,7 @@ import UDExMessageStrip from "../../instrumented/MessageStrip.js";
43
43
  import UDExModal from "../../instrumented/Modal.js";
44
44
  import UDExSearch from "../../instrumented/Search.js";
45
45
  import UDExMediaPlayer from "../../instrumented/MediaPlayer.js";
46
+ import UDExPagination from "../../instrumented/Pagination.js";
46
47
  import UDExPasswordField from "../../instrumented/PasswordField.js";
47
48
  import UDExPhoneNumberField from "../../instrumented/PhoneNumberField.js";
48
49
  import UDExPromoTile from "../../instrumented/PromoTile.js";
@@ -128,6 +129,7 @@ export {
128
129
  UDExModal,
129
130
  UDExMediaPlayer,
130
131
  UDExSearch,
132
+ UDExPagination,
131
133
  UDExPasswordField,
132
134
  UDExPhoneNumberField,
133
135
  UDExPromoTile,
@@ -43,6 +43,7 @@ import UDExMessageStrip from "../../src/MessageStrip.js";
43
43
  import UDExModal from "../../src/Modal.js";
44
44
  import UDExSearch from "../../src/Search.js";
45
45
  import UDExMediaPlayer from "../../src/MediaPlayer.js";
46
+ import UDExPagination from "../../src/Pagination.js";
46
47
  import UDExPasswordField from "../../src/PasswordField.js";
47
48
  import UDExPhoneNumberField from "../../src/PhoneNumberField.js";
48
49
  import UDExPromoTile from "../../src/PromoTile.js";
@@ -128,6 +129,7 @@ export {
128
129
  UDExModal,
129
130
  UDExMediaPlayer,
130
131
  UDExSearch,
132
+ UDExPagination,
131
133
  UDExPasswordField,
132
134
  UDExPhoneNumberField,
133
135
  UDExPromoTile,
@@ -71,6 +71,8 @@ declare class UDExCountrySelector extends UDExSelectBase {
71
71
  prepareCountrySelectorItems(): void;
72
72
  getItemTemplate(country: Country): string | undefined;
73
73
  getCountryLabel(countryName?: string, localName?: string): string | null;
74
+ get countryFlag(): object;
75
+ get searchableList(): import("@ui5/webcomponents-base").JSX.Element;
74
76
  get selectedCountryName(): string | null;
75
77
  get flagAriaLabel(): string;
76
78
  get selectedCountryFlagX(): string | undefined;
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
8
- import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
8
+ import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
9
9
  import property from "@ui5/webcomponents-base/dist/decorators/property.js";
10
10
  import UI5Button from "@ui5/webcomponents/dist/Button.js";
11
11
  import "@ui5/webcomponents-icons/dist/navigation-down-arrow.js";
@@ -20,7 +20,9 @@ import UDExSelectBase from "./SelectBase.js";
20
20
  import UDExListItem from "./ListItem.js";
21
21
  import UDExTextField from "./TextField.js";
22
22
  import UDExStatusMessage from "./StatusMessage.js";
23
- import SelectBoxTemplate from "./generated/templates/SelectBoxTemplate.lit.js";
23
+ import SelectBoxTemplate from "./SelectBoxTemplate.js";
24
+ import CountrySelectorTemplate from "./CountrySelectorTemplate.js";
25
+ import SearchableListBaseTemplate from "./SearchableListBaseTemplate.js";
24
26
  // Styles
25
27
  import CountrySelectorCss from "./generated/themes/CountrySelector.css.js";
26
28
  import TextFieldCss from "./generated/themes/TextField.css.js";
@@ -143,6 +145,13 @@ let UDExCountrySelector = class UDExCountrySelector extends UDExSelectBase {
143
145
  getCountryLabel(countryName, localName) {
144
146
  return localName ? `${countryName} (${localName})` : countryName || null;
145
147
  }
148
+ get countryFlag() {
149
+ return CountrySelectorTemplate.call(this);
150
+ }
151
+ get searchableList() {
152
+ this.defaultAccessibleRole = this._listBoxRole;
153
+ return SearchableListBaseTemplate.call(this);
154
+ }
146
155
  get selectedCountryName() {
147
156
  return this._selectedCountry ? this.getCountryLabel(this._selectedCountry.countryName, this._selectedCountry.localName) : "";
148
157
  }
@@ -174,7 +183,7 @@ __decorate([
174
183
  UDExCountrySelector = __decorate([
175
184
  customElement({
176
185
  tag: "udex-country-selector",
177
- renderer: litRender,
186
+ renderer: jsxRenderer,
178
187
  styles: [
179
188
  TextFieldCss,
180
189
  SelectBaseCss,