@sapudex/web-components 2.51.0 → 2.52.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 (97) hide show
  1. package/cypress/specs/CountrySelector.cy.tsx +21 -0
  2. package/cypress/specs/List.cy.tsx +119 -10
  3. package/cypress/specs/SelectBox.cy.tsx +414 -70
  4. package/dist/CountrySelector.js +1 -0
  5. package/dist/CountrySelector.js.map +1 -1
  6. package/dist/List.d.ts +10 -0
  7. package/dist/List.js +35 -0
  8. package/dist/List.js.map +1 -1
  9. package/dist/ListGroupHeaderBase.js +3 -0
  10. package/dist/ListGroupHeaderBase.js.map +1 -1
  11. package/dist/ListItem.d.ts +1 -0
  12. package/dist/ListItem.js +11 -3
  13. package/dist/ListItem.js.map +1 -1
  14. package/dist/ListItemBase.d.ts +9 -0
  15. package/dist/ListItemBase.js +12 -0
  16. package/dist/ListItemBase.js.map +1 -1
  17. package/dist/ListSelectAll.js +1 -1
  18. package/dist/ListSelectAll.js.map +1 -1
  19. package/dist/SearchableListBase.d.ts +10 -0
  20. package/dist/SearchableListBase.js +29 -6
  21. package/dist/SearchableListBase.js.map +1 -1
  22. package/dist/SearchableListBaseTemplate.js +32 -5
  23. package/dist/SearchableListBaseTemplate.js.map +1 -1
  24. package/dist/SelectBase.d.ts +65 -1
  25. package/dist/SelectBase.js +446 -59
  26. package/dist/SelectBase.js.map +1 -1
  27. package/dist/SelectBox.js +24 -2
  28. package/dist/SelectBox.js.map +1 -1
  29. package/dist/SelectBoxTemplate.js +2 -2
  30. package/dist/SelectBoxTemplate.js.map +1 -1
  31. package/dist/assets/{InputSuggestions-7eP5S95I.js → InputSuggestions-0wafO-g3.js} +1 -1
  32. package/dist/assets/{ListItemStandardExpandableTextTemplate-CLcWBFcg.js → ListItemStandardExpandableTextTemplate-DoOYtqI8.js} +1 -1
  33. package/dist/assets/{bundle.esm-D4--IDh7.js → bundle.esm-DwHZ3nCW.js} +241 -241
  34. package/dist/assets/messagebundle_nl-BKUpVV-D.js +1 -0
  35. package/dist/css/themes/AccordionItem.css +1 -1
  36. package/dist/css/themes/Divider.css +1 -1
  37. package/dist/css/themes/Link.css +1 -1
  38. package/dist/css/themes/ListGroupHeader.css +1 -1
  39. package/dist/css/themes/ListItem.css +1 -1
  40. package/dist/css/themes/SelectBase.css +1 -1
  41. package/dist/css/themes/SelectBox.css +1 -1
  42. package/dist/generated/assets/i18n/messagebundle_nl.json +1 -1
  43. package/dist/generated/templates/ListTemplate.lit.js +1 -1
  44. package/dist/generated/templates/ListTemplate.lit.js.map +1 -1
  45. package/dist/generated/themes/AccordionItem.css.d.ts +1 -1
  46. package/dist/generated/themes/AccordionItem.css.js +1 -1
  47. package/dist/generated/themes/AccordionItem.css.js.map +1 -1
  48. package/dist/generated/themes/Divider.css.d.ts +1 -1
  49. package/dist/generated/themes/Divider.css.js +1 -1
  50. package/dist/generated/themes/Divider.css.js.map +1 -1
  51. package/dist/generated/themes/Link.css.d.ts +1 -1
  52. package/dist/generated/themes/Link.css.js +1 -1
  53. package/dist/generated/themes/Link.css.js.map +1 -1
  54. package/dist/generated/themes/ListGroupHeader.css.d.ts +1 -1
  55. package/dist/generated/themes/ListGroupHeader.css.js +1 -1
  56. package/dist/generated/themes/ListGroupHeader.css.js.map +1 -1
  57. package/dist/generated/themes/ListItem.css.d.ts +1 -1
  58. package/dist/generated/themes/ListItem.css.js +1 -1
  59. package/dist/generated/themes/ListItem.css.js.map +1 -1
  60. package/dist/generated/themes/SelectBase.css.d.ts +1 -1
  61. package/dist/generated/themes/SelectBase.css.js +1 -1
  62. package/dist/generated/themes/SelectBase.css.js.map +1 -1
  63. package/dist/generated/themes/SelectBox.css.d.ts +1 -1
  64. package/dist/generated/themes/SelectBox.css.js +1 -1
  65. package/dist/generated/themes/SelectBox.css.js.map +1 -1
  66. package/dist/messagebundle_nl.properties +126 -0
  67. package/dist/web-types.json +1 -1
  68. package/package.json +1 -1
  69. package/src/CountrySelector.ts +1 -0
  70. package/src/List.hbs +2 -1
  71. package/src/List.ts +36 -0
  72. package/src/ListGroupHeaderBase.ts +3 -0
  73. package/src/ListItem.ts +12 -3
  74. package/src/ListItemBase.ts +11 -0
  75. package/src/ListSelectAll.ts +1 -1
  76. package/src/SearchableListBase.ts +31 -8
  77. package/src/SearchableListBaseTemplate.tsx +38 -6
  78. package/src/SelectBase.ts +466 -69
  79. package/src/SelectBox.ts +27 -2
  80. package/src/SelectBoxTemplate.tsx +4 -3
  81. package/src/generated/templates/ListTemplate.lit.ts +1 -1
  82. package/src/generated/themes/AccordionItem.css.ts +1 -1
  83. package/src/generated/themes/Divider.css.ts +1 -1
  84. package/src/generated/themes/Link.css.ts +1 -1
  85. package/src/generated/themes/ListGroupHeader.css.ts +1 -1
  86. package/src/generated/themes/ListItem.css.ts +1 -1
  87. package/src/generated/themes/SelectBase.css.ts +1 -1
  88. package/src/generated/themes/SelectBox.css.ts +1 -1
  89. package/src/i18n/messagebundle_nl.properties +126 -0
  90. package/src/themes/AccordionItem.css +8 -3
  91. package/src/themes/Divider.css +2 -0
  92. package/src/themes/Link.css +20 -2
  93. package/src/themes/ListGroupHeader.css +18 -1
  94. package/src/themes/ListItem.css +2 -1
  95. package/src/themes/SelectBase.css +2 -1
  96. package/src/themes/SelectBox.css +10 -0
  97. 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", () => {
@@ -1002,6 +995,22 @@ describe("SelectBox", () => {
1002
995
  cy.get("@selectall")
1003
996
  .should("have.been.called");
1004
997
  });
998
+
999
+ // Bug fix: groupListClassName no longer appends _additionalClassName, so the
1000
+ // sticky marker forwarded from SelectBase must not land on the inner list container
1001
+ // (previously caused an unwanted padding-top gap at the top of the dropdown).
1002
+ it("does not apply the udex-list__sticky class to the inner list container", () => {
1003
+ cy.mount(selectAll);
1004
+ cy.get("#select-box").shadow().as("select");
1005
+
1006
+ cy.get("@select").find(".udex-text-field__trigger").click();
1007
+
1008
+ cy.get("@select")
1009
+ .find("udex-list")
1010
+ .find("udex-list-select-all").shadow()
1011
+ .find(".udex-list__group-list")
1012
+ .should("not.have.class", "udex-list__sticky");
1013
+ });
1005
1014
  });
1006
1015
 
1007
1016
  describe("Select Box events tests",async () => {
@@ -1542,11 +1551,12 @@ describe("SelectBox", () => {
1542
1551
  .find(".udex-select-box__dropdown")
1543
1552
  .should("have.attr", "open");
1544
1553
 
1554
+ // Updated: focus stays on the input; the last list item becomes the active
1555
+ // descendant (marked via _active-descendant) rather than receiving DOM focus.
1545
1556
  cy.get("#select-box").shadow()
1546
1557
  .find("udex-list-item")
1547
1558
  .last()
1548
- .shadow().find("[part=item]")
1549
- .should("be.focused");
1559
+ .should("have.attr", "_active-descendant");
1550
1560
  });
1551
1561
 
1552
1562
  // when hasFooter is true, pressing Tab on a focused list item moves focus to the
@@ -1564,11 +1574,10 @@ describe("SelectBox", () => {
1564
1574
  .find(".udex-select-box__dropdown")
1565
1575
  .should("have.attr", "open");
1566
1576
 
1567
- // A list item should now be focused (not the search input).
1577
+ // A list item should now be the active descendant (DOM focus stays on the input).
1568
1578
  cy.get("#select-box").shadow()
1569
- .find("udex-list-item").eq(0).shadow()
1570
- .find("[part=item]")
1571
- .should("be.focused");
1579
+ .find("udex-list-item").eq(0)
1580
+ .should("have.attr", "_active-descendant");
1572
1581
 
1573
1582
  // Tab should be intercepted and focus redirected into the footer.
1574
1583
  cy.realPress("Tab");
@@ -1866,15 +1875,17 @@ describe("SelectBox", () => {
1866
1875
  cy.get("#next-focus").should("be.focused");
1867
1876
  });
1868
1877
 
1869
- it("Shift+Tab from list item closes dropdown and moves focus to search input", () => {
1878
+ it("Shift+Tab from list item closes dropdown and moves focus to previous focusable element", () => {
1870
1879
  cy.mount(shortComponentWithFocusTargets);
1871
1880
  openDropdownOnFirstItem();
1872
1881
 
1873
1882
  cy.realPress(["Shift", "Tab"]);
1874
1883
 
1875
1884
  cy.get("#select-box").shadow()
1876
- .find(".udex-text-field__search")
1877
- .should("be.focused");
1885
+ .find(".udex-select-box__dropdown")
1886
+ .should("not.have.attr", "open");
1887
+
1888
+ cy.get("#previous-focus").should("be.focused");
1878
1889
  });
1879
1890
 
1880
1891
  it("Escape from list item closes dropdown and returns focus to trigger", () => {
@@ -1901,6 +1912,59 @@ describe("SelectBox", () => {
1901
1912
  cy.get("#select-box").shadow().find("#firstIcon").should("be.focused");
1902
1913
  });
1903
1914
 
1915
+ // Added: covers the _handleArrowRightInRow fix — before moving DOM focus into
1916
+ // the first icon button we call _setActiveDescendant(null), which strips the
1917
+ // _udex-active-descendant JS state marker from the list item so only the icon's
1918
+ // :focus ring is visible (not both rings simultaneously).
1919
+ it("ArrowRight into the first icon removes _udex-active-descendant from the list item", () => {
1920
+ cy.mount(shortComponentWithFocusTargets);
1921
+ openDropdownOnFirstItem();
1922
+
1923
+ // Precondition: the first list item is the active descendant before ArrowRight.
1924
+ cy.get("#select-box").shadow()
1925
+ .find("udex-list-item").eq(0)
1926
+ .should("have.class", "_udex-active-descendant");
1927
+
1928
+ cy.realPress("ArrowRight");
1929
+
1930
+ // The list item's active-descendant marker is cleared so it no longer shows a ring.
1931
+ cy.get("#select-box").shadow()
1932
+ .find("udex-list-item").eq(0)
1933
+ .should("not.have.class", "_udex-active-descendant");
1934
+
1935
+ // DOM focus moves onto the icon button (its :focus ring is now the only one).
1936
+ cy.get("#select-box").shadow().find("#firstIcon").should("be.focused");
1937
+ });
1938
+
1939
+ // Added: covers the _handleArrowLeftInRow fix — when ArrowLeft returns from the
1940
+ // first icon button back to the list item we call _setActiveDescendant(renderedItem)
1941
+ // then move DOM focus back to the search input. This restores the list item's
1942
+ // _udex-active-descendant ring while clearing the icon's :focus ring, so only the
1943
+ // list item's ring is visible (not both rings simultaneously).
1944
+ it("ArrowLeft from the first icon restores the list item ring and removes icon focus", () => {
1945
+ cy.mount(shortComponentWithFocusTargets);
1946
+ openDropdownOnFirstItem();
1947
+
1948
+ // Enter the first icon button.
1949
+ cy.realPress("ArrowRight");
1950
+
1951
+ // Precondition: DOM focus is on the icon button.
1952
+ cy.get("#select-box").shadow().find("#firstIcon").should("be.focused");
1953
+
1954
+ // Navigate back to the list item.
1955
+ cy.realPress("ArrowLeft");
1956
+
1957
+ // The list item's active-descendant marker is restored so its ring shows again.
1958
+ cy.get("#select-box").shadow()
1959
+ .find("udex-list-item").eq(0)
1960
+ .should("have.class", "_udex-active-descendant");
1961
+
1962
+ // DOM focus returns to the search input, clearing the icon's :focus ring.
1963
+ cy.get("#select-box").shadow()
1964
+ .find(".udex-text-field__search")
1965
+ .should("be.focused");
1966
+ });
1967
+
1904
1968
  it("ArrowRight from firstIcon button focuses secondIcon button", () => {
1905
1969
  cy.mount(shortComponentWithFocusTargets);
1906
1970
  openDropdownOnFirstItem();
@@ -1940,10 +2004,11 @@ describe("SelectBox", () => {
1940
2004
  cy.realPress("ArrowRight");
1941
2005
  cy.realPress("ArrowLeft");
1942
2006
 
2007
+ // Updated: navigating back from the icon button makes the list item the
2008
+ // active descendant (DOM focus returns to the input, not the item).
1943
2009
  cy.get("#select-box").shadow()
1944
2010
  .find("udex-list-item").eq(0)
1945
- .shadow().find("[part=item]")
1946
- .should("be.focused");
2011
+ .should("have.attr", "_active-descendant");
1947
2012
  });
1948
2013
 
1949
2014
  it("ArrowDown from firstIcon button navigates to next list item", () => {
@@ -1955,8 +2020,7 @@ describe("SelectBox", () => {
1955
2020
 
1956
2021
  cy.get("#select-box").shadow()
1957
2022
  .find("udex-list-item").eq(1)
1958
- .shadow().find("[part=item]")
1959
- .should("be.focused");
2023
+ .should("have.attr", "_active-descendant");
1960
2024
  });
1961
2025
 
1962
2026
  it("ArrowUp from firstIcon button navigates to previous list item (wraps to last)", () => {
@@ -1968,8 +2032,7 @@ describe("SelectBox", () => {
1968
2032
 
1969
2033
  cy.get("#select-box").shadow()
1970
2034
  .find("udex-list-item").last()
1971
- .shadow().find("[part=item]")
1972
- .should("be.focused");
2035
+ .should("have.attr", "_active-descendant");
1973
2036
  });
1974
2037
 
1975
2038
  it("ArrowDown from secondIcon button navigates to next list item", () => {
@@ -1982,8 +2045,7 @@ describe("SelectBox", () => {
1982
2045
 
1983
2046
  cy.get("#select-box").shadow()
1984
2047
  .find("udex-list-item").eq(1)
1985
- .shadow().find("[part=item]")
1986
- .should("be.focused");
2048
+ .should("have.attr", "_active-descendant");
1987
2049
  });
1988
2050
 
1989
2051
  it("Escape from icon button closes dropdown and returns focus to input", () => {
@@ -2368,10 +2430,8 @@ describe("SelectBox", () => {
2368
2430
  });
2369
2431
  });
2370
2432
 
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", () => {
2433
+ // Tree accessibility pattern: SelectBox uses tree pattern when group headers are present.
2434
+ describe("Tree accessibility pattern with group headers", () => {
2375
2435
  const selectBoxMultiSelectGroup = <div style="width: 350px; position:relative;">
2376
2436
  <UDExSelectBox
2377
2437
  id="select-box-tree-regression"
@@ -2390,49 +2450,333 @@ describe("SelectBox", () => {
2390
2450
  .find(".udex-text-field__trigger").click();
2391
2451
  });
2392
2452
 
2393
- it("host [udex-list] has NO aria-multiselectable attribute", () => {
2453
+ it("host [udex-list] has aria-multiselectable=\"true\" (tree + MultiSelect)", () => {
2394
2454
  cy.get("#select-box-tree-regression").shadow()
2395
- .find("[udex-list]").should("not.have.attr", "aria-multiselectable");
2455
+ .find("[udex-list]").should("have.attr", "aria-multiselectable", "true");
2396
2456
  });
2397
2457
 
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\")", () => {
2458
+ it("inner list div has role=\"tree\"", () => {
2403
2459
  cy.get("#select-box-tree-regression").shadow()
2404
2460
  .find("[udex-list]").shadow()
2405
- .find("#udex-list").should("have.attr", "role", "listbox");
2461
+ .find("#udex-list").should("have.attr", "role", "tree");
2406
2462
  });
2407
2463
 
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", () => {
2464
+ it("group header host has _tree-mode attribute", () => {
2412
2465
  cy.get("#select-box-tree-regression").shadow()
2413
2466
  .find("udex-list-group-header")
2414
- .should("not.have.attr", "_tree-mode");
2467
+ .should("have.attr", "_tree-mode");
2415
2468
  });
2416
2469
 
2417
- it("child items have role=\"option\", not \"treeitem\"", () => {
2470
+ it("child items have role=\"treeitem\" on the host (aria-activedescendant pattern)", () => {
2418
2471
  cy.get("#select-box-tree-regression").shadow()
2419
2472
  .find("udex-list-group-header")
2420
- .find("udex-list-item").eq(0).shadow()
2421
- .find("[part=item]").should("have.attr", "role", "option");
2473
+ .find("udex-list-item").eq(0)
2474
+ .should("have.attr", "role", "treeitem");
2422
2475
  });
2423
2476
 
2424
- it("child items have NO aria-checked (roleItem !== treeitem)", () => {
2477
+ it("child items have aria-checked on the host (MultiSelect tree)", () => {
2478
+ cy.get("#select-box-tree-regression").shadow()
2479
+ .find("udex-list-group-header")
2480
+ .find("udex-list-item").eq(0)
2481
+ .should("have.attr", "aria-checked", "false");
2482
+ });
2483
+
2484
+ it("child items' inner [part=item] div is role=\"presentation\" (host carries the role)", () => {
2425
2485
  cy.get("#select-box-tree-regression").shadow()
2426
2486
  .find("udex-list-group-header")
2427
2487
  .find("udex-list-item").eq(0).shadow()
2428
- .find("[part=item]").should("not.have.attr", "aria-checked");
2488
+ .find("[part=item]").should("have.attr", "role", "presentation");
2429
2489
  });
2430
2490
 
2431
- it("child items' checkbox is NOT aria-hidden (JAWS double-announce guard only applies to tree)", () => {
2491
+ it("child items' checkbox is aria-hidden (tree mode, JAWS uses host aria-checked)", () => {
2432
2492
  cy.get("#select-box-tree-regression").shadow()
2433
2493
  .find("udex-list-group-header")
2434
2494
  .find("udex-list-item").eq(0).shadow()
2435
- .find("udex-checkbox").should("not.have.attr", "aria-hidden");
2495
+ .find("udex-checkbox").should("have.attr", "aria-hidden");
2496
+ });
2497
+ });
2498
+
2499
+ // Covers the aria-live="polite" region that announces the active item
2500
+ // during keyboard navigation. _setActiveDescendant(item) sets _liveRegionText to
2501
+ // "Label, X of Y"; _setActiveDescendant(null) (on close/Escape) resets it to "".
2502
+ describe("Select Box live region announcements", () => {
2503
+ it("populates the live region with 'Label, X of Y' when ArrowDown activates an item", () => {
2504
+ cy.mount(shortComponent);
2505
+ cy.get("#select-box").shadow()
2506
+ .find(".udex-text-field__search")
2507
+ .click()
2508
+ .realPress("ArrowDown");
2509
+
2510
+ // shortComponent has 6 items; the first item "JavaScript" becomes active.
2511
+ cy.get("#select-box").shadow()
2512
+ .find('[aria-live="polite"]')
2513
+ .invoke("text")
2514
+ .should("match", /^JavaScript, 1 of 6$/);
2515
+ });
2516
+
2517
+ // Added: covers _treePositionText — for a grouped SelectBox the announcement
2518
+ // carries the tree position. ArrowDown activates the group header ("level 0");
2519
+ // a further ArrowDown reaches its first child ("level 1").
2520
+ it("uses tree positional text ('level 0' for the group header, 'level 1' for its child)", () => {
2521
+ cy.mount(groupHeaderCollapsible);
2522
+ cy.get("#select-box").shadow()
2523
+ .find(".udex-text-field__search")
2524
+ .click()
2525
+ .realPress("ArrowDown");
2526
+
2527
+ // ArrowDown activates the group header -> tree text reports "level 0".
2528
+ cy.get("#select-box").shadow()
2529
+ .find('[aria-live="polite"]')
2530
+ .invoke("text")
2531
+ .should("contain", "level 0");
2532
+
2533
+ // A further ArrowDown reaches the first child -> tree text reports "level 1".
2534
+ cy.get("#select-box").shadow()
2535
+ .find(".udex-text-field__search")
2536
+ .realPress("ArrowDown");
2537
+
2538
+ cy.get("#select-box").shadow()
2539
+ .find('[aria-live="polite"]')
2540
+ .invoke("text")
2541
+ .should("contain", "level 1");
2542
+ });
2543
+
2544
+ it("clears the live region text when the dropdown closes on Escape", () => {
2545
+ cy.mount(shortComponent);
2546
+ cy.get("#select-box").shadow()
2547
+ .find(".udex-text-field__search")
2548
+ .click()
2549
+ .realPress("ArrowDown");
2550
+
2551
+ // Precondition: the live region is populated by the navigation above.
2552
+ cy.get("#select-box").shadow()
2553
+ .find('[aria-live="polite"]')
2554
+ .invoke("text")
2555
+ .should("not.be.empty");
2556
+
2557
+ cy.get("#select-box").shadow()
2558
+ .find(".udex-text-field__search")
2559
+ .realPress("Escape");
2560
+
2561
+ cy.get("#select-box").shadow()
2562
+ .find(".udex-select-box__dropdown")
2563
+ .should("not.have.attr", "open");
2564
+
2565
+ // _setActiveDescendant(null) resets _liveRegionText to "".
2566
+ cy.get("#select-box").shadow()
2567
+ .find('[aria-live="polite"]')
2568
+ .invoke("text")
2569
+ .should("equal", "");
2570
+ });
2571
+
2572
+ it("appends 'Unselected' to the live region for an unchecked MultiSelect item", () => {
2573
+ cy.mount(multiSelect);
2574
+ cy.get("#select-box").shadow()
2575
+ .find(".udex-text-field__search")
2576
+ .click()
2577
+ .realPress("ArrowDown");
2578
+
2579
+ // First MultiSelect item is not selected -> state suffix is "Unselected".
2580
+ cy.get("#select-box").shadow()
2581
+ .find('[aria-live="polite"]')
2582
+ .should("contain.text", "Unselected");
2583
+ });
2584
+
2585
+ it("appends 'Selected' to the live region for a checked MultiSelect item", () => {
2586
+ const checkedMultiSelect = <div style="width: 350px; position:relative;">
2587
+ <UDExSelectBox
2588
+ id="select-box"
2589
+ height="400px"
2590
+ label="Programming languages"
2591
+ mode={UDExListMode.MultiSelect}>
2592
+ <UDExSelectBoxItem selected label="JavaScript" supportingText="Frontend web development"></UDExSelectBoxItem>
2593
+ <UDExSelectBoxItem label="Python" supportingText="Used in data analysis"></UDExSelectBoxItem>
2594
+ <UDExSelectBoxItem label="Java" supportingText="Building large-scale applications"></UDExSelectBoxItem>
2595
+ </UDExSelectBox>
2596
+ </div>;
2597
+
2598
+ cy.mount(checkedMultiSelect);
2599
+ cy.get("#select-box").shadow()
2600
+ .find(".udex-text-field__search")
2601
+ .click()
2602
+ .realPress("ArrowDown");
2603
+
2604
+ // First MultiSelect item is pre-selected -> state suffix is "Selected".
2605
+ cy.get("#select-box").shadow()
2606
+ .find('[aria-live="polite"]')
2607
+ .should("contain.text", "Selected");
2608
+ });
2609
+
2610
+ it("appends 'collapsed' to the live region for a collapsed collapsible group header", () => {
2611
+ const collapsedGroupHeader = <div style="width: 350px; position:relative;">
2612
+ <UDExSelectBox
2613
+ id="select-box"
2614
+ mode={UDExListMode.MultiSelect}
2615
+ label="Programming languages">
2616
+ <UDExSelectBoxGroupHeader label="Programming Languages" collapsible collapsed>
2617
+ <UDExSelectBoxItem label="JavaScript" supportingText="Frontend web development"></UDExSelectBoxItem>
2618
+ <UDExSelectBoxItem label="Python" supportingText="Used in data analysis"></UDExSelectBoxItem>
2619
+ </UDExSelectBoxGroupHeader>
2620
+ </UDExSelectBox>
2621
+ </div>;
2622
+
2623
+ cy.mount(collapsedGroupHeader);
2624
+ cy.get("#select-box").shadow()
2625
+ .find(".udex-text-field__search")
2626
+ .click()
2627
+ .realPress("ArrowDown");
2628
+
2629
+ // ArrowDown activates the collapsed group header -> tree text reports
2630
+ // "level 0" and the state suffix is "collapsed".
2631
+ cy.get("#select-box").shadow()
2632
+ .find('[aria-live="polite"]')
2633
+ .invoke("text")
2634
+ .should("contain", "level 0")
2635
+ .and("contain", "collapsed");
2636
+ });
2637
+
2638
+ it("appends 'expanded' to the live region for an expanded collapsible group header", () => {
2639
+ // groupHeaderCollapsible starts expanded (no `collapsed` attribute).
2640
+ cy.mount(groupHeaderCollapsible);
2641
+ cy.get("#select-box").shadow()
2642
+ .find(".udex-text-field__search")
2643
+ .click()
2644
+ .realPress("ArrowDown");
2645
+
2646
+ // ArrowDown activates the expanded group header -> tree text reports
2647
+ // "level 0" and the state suffix is "expanded".
2648
+ cy.get("#select-box").shadow()
2649
+ .find('[aria-live="polite"]')
2650
+ .invoke("text")
2651
+ .should("contain", "level 0")
2652
+ .and("contain", "expanded");
2653
+ });
2654
+ });
2655
+
2656
+ // Added: covers the double-outline / flicker fix. After a click on a list item
2657
+ // (SelectBase.handleItemSelection, MultiSelect path) or a group header
2658
+ // (SelectBox.handleHeaderPress), DOM focus is returned to the search input via
2659
+ // (this._search as HTMLElement).focus(). This prevents the clicked element from
2660
+ // retaining its CSS :focus ring while _udex-active-descendant later tracks a
2661
+ // different item during arrow navigation (which would show two rings at once).
2662
+ describe("Select Box click focus retention (double-outline fix)", () => {
2663
+ it("MultiSelect mode - clicking a list item returns DOM focus to the search input", () => {
2664
+ cy.mount(multiSelect);
2665
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
2666
+
2667
+ cy.get("#select-box").shadow()
2668
+ .find(".udex-select-box__dropdown")
2669
+ .should("have.attr", "open");
2670
+
2671
+ // Click the second list item — the dropdown stays open in MultiSelect mode.
2672
+ cy.get("#select-box").shadow()
2673
+ .find("udex-list-item").eq(1)
2674
+ .click();
2675
+
2676
+ // The fix returns DOM focus to the search input rather than leaving it on
2677
+ // the clicked item.
2678
+ cy.get("#select-box").shadow()
2679
+ .find(".udex-text-field__search")
2680
+ .should("be.focused");
2681
+ });
2682
+
2683
+ it("MultiSelect mode - the clicked list item does not retain DOM focus", () => {
2684
+ cy.mount(multiSelect);
2685
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
2686
+
2687
+ cy.get("#select-box").shadow()
2688
+ .find("udex-list-item").eq(1)
2689
+ .click();
2690
+
2691
+ // The clicked item must NOT be the active element — otherwise its :focus
2692
+ // ring would linger alongside the active-descendant ring.
2693
+ cy.get("#select-box").shadow()
2694
+ .find("udex-list-item").eq(1)
2695
+ .should("not.be.focused");
2696
+ });
2697
+
2698
+ it("MultiSelect mode - clicking a group header returns DOM focus to the search input", () => {
2699
+ cy.mount(groupHeader);
2700
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
2701
+
2702
+ cy.get("#select-box").shadow()
2703
+ .find(".udex-select-box__dropdown")
2704
+ .should("have.attr", "open");
2705
+
2706
+ // Click the group header's own focusable row (the inner udex-list-item in
2707
+ // the group header shadow) — this fires the header-press event which routes
2708
+ // to SelectBox.handleHeaderPress.
2709
+ cy.get("#select-box").shadow()
2710
+ .find("udex-list-group-header")
2711
+ .eq(0)
2712
+ .shadow()
2713
+ .find("udex-list-item")
2714
+ .click();
2715
+
2716
+ // SelectBox.handleHeaderPress calls (this._search).focus() after onHeaderPress,
2717
+ // so DOM focus must land on the search input.
2718
+ cy.get("#select-box").shadow()
2719
+ .find(".udex-text-field__search")
2720
+ .should("be.focused");
2721
+ });
2722
+
2723
+ it("MultiSelect mode - after clicking an item then ArrowDown, only the newly navigated item carries the active-descendant ring", () => {
2724
+ cy.mount(multiSelect);
2725
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
2726
+
2727
+ cy.get("#select-box").shadow()
2728
+ .find(".udex-select-box__dropdown")
2729
+ .should("have.attr", "open");
2730
+
2731
+ // Click the second item (index 1). handleItemSelection sets it as the active
2732
+ // descendant and returns focus to the search input.
2733
+ cy.get("#select-box").shadow()
2734
+ .find("udex-list-item").eq(1)
2735
+ .click();
2736
+
2737
+ cy.get("#select-box").shadow()
2738
+ .find("udex-list-item").eq(1)
2739
+ .should("have.class", "_udex-active-descendant");
2740
+
2741
+ // ArrowDown (dispatched on the focused search input via onKeyDownSearch)
2742
+ // advances the active descendant to the third item (index 2).
2743
+ cy.realPress("ArrowDown");
2744
+
2745
+ // Only the newly navigated item (index 2) carries the ring class.
2746
+ cy.get("#select-box").shadow()
2747
+ .find("udex-list-item").eq(2)
2748
+ .should("have.class", "_udex-active-descendant");
2749
+
2750
+ // The previously clicked item (index 1) must have lost the ring class — no
2751
+ // double outline.
2752
+ cy.get("#select-box").shadow()
2753
+ .find("udex-list-item").eq(1)
2754
+ .should("not.have.class", "_udex-active-descendant");
2755
+
2756
+ // Exactly one element bears the active-descendant class at any time.
2757
+ cy.get("#select-box").shadow()
2758
+ .find("._udex-active-descendant")
2759
+ .should("have.length", 1);
2760
+ });
2761
+ });
2762
+
2763
+ // Added: covers aria-required wiring on the combobox input in SelectBoxTemplate.
2764
+ // The `required` boolean (inherited from SelectBase) drives aria-required={this.required}.
2765
+ describe("aria-required accessibility", () => {
2766
+ it("does not set aria-required=\"true\" on the combobox input by default (required not set)", () => {
2767
+ cy.mount(<UDExSelectBox id="select-box" label="Programming languages"></UDExSelectBox>);
2768
+
2769
+ cy.get("#select-box").shadow()
2770
+ .find('input[role="combobox"]')
2771
+ .should("not.have.attr", "aria-required", "true");
2772
+ });
2773
+
2774
+ it("sets aria-required=\"true\" on the combobox input when required is set", () => {
2775
+ cy.mount(<UDExSelectBox id="select-box" label="Programming languages" required></UDExSelectBox>);
2776
+
2777
+ cy.get("#select-box").shadow()
2778
+ .find('input[role="combobox"]')
2779
+ .should("have.attr", "aria-required", "true");
2436
2780
  });
2437
2781
  });
2438
2782
  });
@@ -93,6 +93,7 @@ let UDExCountrySelector = class UDExCountrySelector extends UDExSelectBase {
93
93
  this.dropdownBeforeOpen();
94
94
  }
95
95
  handleDropdownClose() {
96
+ this._setActiveDescendant(null);
96
97
  this._open = false;
97
98
  this.fireEventDropdownClose();
98
99
  }