@sapudex/web-components 2.48.0 → 2.49.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 (307) hide show
  1. package/bundle.esm.js +2 -0
  2. package/cypress/specs/AccordionSpotlight.cy.tsx +402 -0
  3. package/cypress/specs/BusyIndicator.cy.tsx +15 -0
  4. package/cypress/specs/Carousel.cy.tsx +142 -0
  5. package/cypress/specs/FilterMenu.cy.tsx +968 -10
  6. package/cypress/specs/List.cy.tsx +106 -0
  7. package/cypress/specs/ListItem.cy.tsx +291 -1
  8. package/cypress/specs/MediaPlayer.cy.tsx +32 -0
  9. package/cypress/specs/SelectBox.cy.tsx +134 -3
  10. package/cypress/support/component-imports.instrumented.ts +6 -0
  11. package/cypress/support/component-imports.src.ts +6 -0
  12. package/dist/AccordionSpotlight.d.ts +149 -0
  13. package/dist/AccordionSpotlight.js +284 -0
  14. package/dist/AccordionSpotlight.js.map +1 -0
  15. package/dist/AccordionSpotlightItem.d.ts +76 -0
  16. package/dist/AccordionSpotlightItem.js +115 -0
  17. package/dist/AccordionSpotlightItem.js.map +1 -0
  18. package/dist/AccordionSpotlightItemTemplate.d.ts +2 -0
  19. package/dist/AccordionSpotlightItemTemplate.js +5 -0
  20. package/dist/AccordionSpotlightItemTemplate.js.map +1 -0
  21. package/dist/AccordionSpotlightTemplate.d.ts +2 -0
  22. package/dist/AccordionSpotlightTemplate.js +10 -0
  23. package/dist/AccordionSpotlightTemplate.js.map +1 -0
  24. package/dist/BusyIndicator.d.ts +9 -0
  25. package/dist/BusyIndicator.js +7 -1
  26. package/dist/BusyIndicator.js.map +1 -1
  27. package/dist/Carousel.d.ts +26 -15
  28. package/dist/Carousel.js +71 -55
  29. package/dist/Carousel.js.map +1 -1
  30. package/dist/FilterMenu.d.ts +12 -2
  31. package/dist/FilterMenu.js +24 -4
  32. package/dist/FilterMenu.js.map +1 -1
  33. package/dist/FilterMenuTemplate.js +1 -1
  34. package/dist/FilterMenuTemplate.js.map +1 -1
  35. package/dist/List.d.ts +1 -0
  36. package/dist/List.js +24 -11
  37. package/dist/List.js.map +1 -1
  38. package/dist/ListGroupHeader.js +5 -1
  39. package/dist/ListGroupHeader.js.map +1 -1
  40. package/dist/ListGroupHeaderBase.d.ts +22 -0
  41. package/dist/ListGroupHeaderBase.js +44 -0
  42. package/dist/ListGroupHeaderBase.js.map +1 -1
  43. package/dist/ListItem.d.ts +4 -0
  44. package/dist/ListItem.js +32 -0
  45. package/dist/ListItem.js.map +1 -1
  46. package/dist/MediaPlayer.d.ts +2 -0
  47. package/dist/MediaPlayer.js +12 -0
  48. package/dist/MediaPlayer.js.map +1 -1
  49. package/dist/PromoTile.js +1 -1
  50. package/dist/PromoTile.js.map +1 -1
  51. package/dist/SearchableListBase.d.ts +10 -1
  52. package/dist/SearchableListBase.js +23 -7
  53. package/dist/SearchableListBase.js.map +1 -1
  54. package/dist/SearchableListBaseTemplate.js +3 -2
  55. package/dist/SearchableListBaseTemplate.js.map +1 -1
  56. package/dist/SelectBase.d.ts +14 -0
  57. package/dist/SelectBase.js +56 -3
  58. package/dist/SelectBase.js.map +1 -1
  59. package/dist/SelectBoxTemplate.js +1 -1
  60. package/dist/SelectBoxTemplate.js.map +1 -1
  61. package/dist/TabNavigation.d.ts +6 -0
  62. package/dist/TabNavigation.js +5 -0
  63. package/dist/TabNavigation.js.map +1 -1
  64. package/dist/assets/{InputSuggestions-BcK6O4Bk.js → InputSuggestions-DaB1A6jW.js} +1 -1
  65. package/dist/assets/{ListItemStandardExpandableTextTemplate-u600dmHC.js → ListItemStandardExpandableTextTemplate-Co4KQuVw.js} +1 -1
  66. package/dist/assets/{bundle-JYo3bV8v.css → bundle-CkxyEA8c.css} +1 -1
  67. package/dist/assets/{bundle.esm-C6Ie2WeI.js → bundle.esm-al3NYhoB.js} +241 -238
  68. package/dist/assets/messagebundle_ar-BUZ_gTvQ.js +1 -0
  69. package/dist/assets/messagebundle_no-BZQfeWxT.js +1 -0
  70. package/dist/css/themes/Accordion.css +1 -1
  71. package/dist/css/themes/AccordionItem.css +1 -1
  72. package/dist/css/themes/AccordionSpotlight.css +1 -0
  73. package/dist/css/themes/AccordionSpotlightItem.css +1 -0
  74. package/dist/css/themes/Badge.css +1 -1
  75. package/dist/css/themes/BusyIndicator.css +1 -0
  76. package/dist/css/themes/Button.css +1 -1
  77. package/dist/css/themes/Divider.css +1 -1
  78. package/dist/css/themes/Dropdown.css +1 -1
  79. package/dist/css/themes/FilterMenu.css +1 -1
  80. package/dist/css/themes/Footer.css +1 -1
  81. package/dist/css/themes/ImageGridTableItem.css +1 -1
  82. package/dist/css/themes/MastheadShell.css +1 -1
  83. package/dist/css/themes/MediaPlayer.css +1 -1
  84. package/dist/css/themes/PhoneNumberField.css +1 -1
  85. package/dist/css/themes/ProgressBar.css +1 -1
  86. package/dist/css/themes/PromoTile.css +1 -1
  87. package/dist/css/themes/Review.css +1 -1
  88. package/dist/css/themes/ReviewItem.css +1 -1
  89. package/dist/css/themes/Search.css +1 -1
  90. package/dist/css/themes/SelectBase.css +1 -1
  91. package/dist/css/themes/SelectBox.css +1 -1
  92. package/dist/css/themes/SideNavigation.css +1 -1
  93. package/dist/css/themes/SideNavigationGroup.css +1 -1
  94. package/dist/css/themes/SocialMediaButton.css +1 -1
  95. package/dist/css/themes/StepInput.css +1 -1
  96. package/dist/css/themes/Switch.css +1 -1
  97. package/dist/css/themes/TabNavigation.css +1 -1
  98. package/dist/css/themes/Tag.css +1 -1
  99. package/dist/css/themes/TagGroup.css +1 -1
  100. package/dist/css/themes/TextArea.css +1 -1
  101. package/dist/css/themes/TextField.css +1 -1
  102. package/dist/css/themes/Toast.css +1 -1
  103. package/dist/custom-elements-internal.json +426 -35
  104. package/dist/custom-elements.json +384 -49
  105. package/dist/generated/assets/i18n/messagebundle_ar.json +1 -1
  106. package/dist/generated/assets/i18n/messagebundle_no.json +1 -1
  107. package/dist/generated/templates/ListGroupHeaderTemplate.lit.js +24 -21
  108. package/dist/generated/templates/ListGroupHeaderTemplate.lit.js.map +1 -1
  109. package/dist/generated/templates/ListItemTemplate.lit.js +2 -2
  110. package/dist/generated/templates/ListItemTemplate.lit.js.map +1 -1
  111. package/dist/generated/themes/Accordion.css.d.ts +1 -1
  112. package/dist/generated/themes/Accordion.css.js +1 -1
  113. package/dist/generated/themes/Accordion.css.js.map +1 -1
  114. package/dist/generated/themes/AccordionItem.css.d.ts +1 -1
  115. package/dist/generated/themes/AccordionItem.css.js +1 -1
  116. package/dist/generated/themes/AccordionItem.css.js.map +1 -1
  117. package/dist/generated/themes/AccordionSpotlight.css.d.ts +2 -0
  118. package/dist/generated/themes/AccordionSpotlight.css.js +8 -0
  119. package/dist/generated/themes/AccordionSpotlight.css.js.map +1 -0
  120. package/dist/generated/themes/AccordionSpotlightItem.css.d.ts +2 -0
  121. package/dist/generated/themes/AccordionSpotlightItem.css.js +8 -0
  122. package/dist/generated/themes/AccordionSpotlightItem.css.js.map +1 -0
  123. package/dist/generated/themes/Badge.css.d.ts +1 -1
  124. package/dist/generated/themes/Badge.css.js +1 -1
  125. package/dist/generated/themes/Badge.css.js.map +1 -1
  126. package/dist/generated/themes/BusyIndicator.css.d.ts +2 -0
  127. package/dist/generated/themes/BusyIndicator.css.js +8 -0
  128. package/dist/generated/themes/BusyIndicator.css.js.map +1 -0
  129. package/dist/generated/themes/Button.css.d.ts +1 -1
  130. package/dist/generated/themes/Button.css.js +1 -1
  131. package/dist/generated/themes/Button.css.js.map +1 -1
  132. package/dist/generated/themes/Divider.css.d.ts +1 -1
  133. package/dist/generated/themes/Divider.css.js +1 -1
  134. package/dist/generated/themes/Divider.css.js.map +1 -1
  135. package/dist/generated/themes/Dropdown.css.d.ts +1 -1
  136. package/dist/generated/themes/Dropdown.css.js +1 -1
  137. package/dist/generated/themes/Dropdown.css.js.map +1 -1
  138. package/dist/generated/themes/FilterMenu.css.d.ts +1 -1
  139. package/dist/generated/themes/FilterMenu.css.js +1 -1
  140. package/dist/generated/themes/FilterMenu.css.js.map +1 -1
  141. package/dist/generated/themes/Footer.css.d.ts +1 -1
  142. package/dist/generated/themes/Footer.css.js +1 -1
  143. package/dist/generated/themes/Footer.css.js.map +1 -1
  144. package/dist/generated/themes/ImageGridTableItem.css.d.ts +1 -1
  145. package/dist/generated/themes/ImageGridTableItem.css.js +1 -1
  146. package/dist/generated/themes/ImageGridTableItem.css.js.map +1 -1
  147. package/dist/generated/themes/MastheadShell.css.d.ts +1 -1
  148. package/dist/generated/themes/MastheadShell.css.js +1 -1
  149. package/dist/generated/themes/MastheadShell.css.js.map +1 -1
  150. package/dist/generated/themes/MediaPlayer.css.d.ts +1 -1
  151. package/dist/generated/themes/MediaPlayer.css.js +1 -1
  152. package/dist/generated/themes/MediaPlayer.css.js.map +1 -1
  153. package/dist/generated/themes/PhoneNumberField.css.d.ts +1 -1
  154. package/dist/generated/themes/PhoneNumberField.css.js +1 -1
  155. package/dist/generated/themes/PhoneNumberField.css.js.map +1 -1
  156. package/dist/generated/themes/ProgressBar.css.d.ts +1 -1
  157. package/dist/generated/themes/ProgressBar.css.js +1 -1
  158. package/dist/generated/themes/ProgressBar.css.js.map +1 -1
  159. package/dist/generated/themes/PromoTile.css.d.ts +1 -1
  160. package/dist/generated/themes/PromoTile.css.js +1 -1
  161. package/dist/generated/themes/PromoTile.css.js.map +1 -1
  162. package/dist/generated/themes/Review.css.d.ts +1 -1
  163. package/dist/generated/themes/Review.css.js +1 -1
  164. package/dist/generated/themes/Review.css.js.map +1 -1
  165. package/dist/generated/themes/ReviewItem.css.d.ts +1 -1
  166. package/dist/generated/themes/ReviewItem.css.js +1 -1
  167. package/dist/generated/themes/ReviewItem.css.js.map +1 -1
  168. package/dist/generated/themes/Search.css.d.ts +1 -1
  169. package/dist/generated/themes/Search.css.js +1 -1
  170. package/dist/generated/themes/Search.css.js.map +1 -1
  171. package/dist/generated/themes/SelectBase.css.d.ts +1 -1
  172. package/dist/generated/themes/SelectBase.css.js +1 -1
  173. package/dist/generated/themes/SelectBase.css.js.map +1 -1
  174. package/dist/generated/themes/SelectBox.css.d.ts +1 -1
  175. package/dist/generated/themes/SelectBox.css.js +1 -1
  176. package/dist/generated/themes/SelectBox.css.js.map +1 -1
  177. package/dist/generated/themes/SideNavigation.css.d.ts +1 -1
  178. package/dist/generated/themes/SideNavigation.css.js +1 -1
  179. package/dist/generated/themes/SideNavigation.css.js.map +1 -1
  180. package/dist/generated/themes/SideNavigationGroup.css.d.ts +1 -1
  181. package/dist/generated/themes/SideNavigationGroup.css.js +1 -1
  182. package/dist/generated/themes/SideNavigationGroup.css.js.map +1 -1
  183. package/dist/generated/themes/SocialMediaButton.css.d.ts +1 -1
  184. package/dist/generated/themes/SocialMediaButton.css.js +1 -1
  185. package/dist/generated/themes/SocialMediaButton.css.js.map +1 -1
  186. package/dist/generated/themes/StepInput.css.d.ts +1 -1
  187. package/dist/generated/themes/StepInput.css.js +1 -1
  188. package/dist/generated/themes/StepInput.css.js.map +1 -1
  189. package/dist/generated/themes/Switch.css.d.ts +1 -1
  190. package/dist/generated/themes/Switch.css.js +1 -1
  191. package/dist/generated/themes/Switch.css.js.map +1 -1
  192. package/dist/generated/themes/TabNavigation.css.d.ts +1 -1
  193. package/dist/generated/themes/TabNavigation.css.js +1 -1
  194. package/dist/generated/themes/TabNavigation.css.js.map +1 -1
  195. package/dist/generated/themes/Tag.css.d.ts +1 -1
  196. package/dist/generated/themes/Tag.css.js +1 -1
  197. package/dist/generated/themes/Tag.css.js.map +1 -1
  198. package/dist/generated/themes/TagGroup.css.d.ts +1 -1
  199. package/dist/generated/themes/TagGroup.css.js +1 -1
  200. package/dist/generated/themes/TagGroup.css.js.map +1 -1
  201. package/dist/generated/themes/TextArea.css.d.ts +1 -1
  202. package/dist/generated/themes/TextArea.css.js +1 -1
  203. package/dist/generated/themes/TextArea.css.js.map +1 -1
  204. package/dist/generated/themes/TextField.css.d.ts +1 -1
  205. package/dist/generated/themes/TextField.css.js +1 -1
  206. package/dist/generated/themes/TextField.css.js.map +1 -1
  207. package/dist/generated/themes/Toast.css.d.ts +1 -1
  208. package/dist/generated/themes/Toast.css.js +1 -1
  209. package/dist/generated/themes/Toast.css.js.map +1 -1
  210. package/dist/messagebundle_ar.properties +99 -0
  211. package/dist/messagebundle_no.properties +126 -1
  212. package/dist/vscode.html-custom-data.json +76 -11
  213. package/dist/web-types.json +194 -26
  214. package/package.json +1 -1
  215. package/src/AccordionSpotlight.ts +321 -0
  216. package/src/AccordionSpotlightItem.ts +117 -0
  217. package/src/AccordionSpotlightItemTemplate.tsx +7 -0
  218. package/src/AccordionSpotlightTemplate.tsx +107 -0
  219. package/src/BusyIndicator.ts +17 -2
  220. package/src/Carousel.ts +118 -80
  221. package/src/FilterMenu.ts +25 -4
  222. package/src/FilterMenuTemplate.tsx +1 -1
  223. package/src/List.ts +23 -11
  224. package/src/ListGroupHeader.hbs +11 -10
  225. package/src/ListGroupHeader.ts +5 -1
  226. package/src/ListGroupHeaderBase.ts +48 -0
  227. package/src/ListItem.hbs +5 -1
  228. package/src/ListItem.ts +36 -0
  229. package/src/MediaPlayer.ts +11 -0
  230. package/src/PromoTile.ts +1 -1
  231. package/src/SearchableListBase.ts +24 -7
  232. package/src/SearchableListBaseTemplate.tsx +5 -2
  233. package/src/SelectBase.ts +58 -3
  234. package/src/SelectBoxTemplate.tsx +1 -0
  235. package/src/TabNavigation.ts +6 -0
  236. package/src/generated/templates/ListGroupHeaderTemplate.lit.ts +24 -21
  237. package/src/generated/templates/ListItemTemplate.lit.ts +2 -2
  238. package/src/generated/themes/Accordion.css.ts +1 -1
  239. package/src/generated/themes/AccordionItem.css.ts +1 -1
  240. package/src/generated/themes/AccordionSpotlight.css.ts +9 -0
  241. package/src/generated/themes/AccordionSpotlightItem.css.ts +9 -0
  242. package/src/generated/themes/Badge.css.ts +1 -1
  243. package/src/generated/themes/BusyIndicator.css.ts +9 -0
  244. package/src/generated/themes/Button.css.ts +1 -1
  245. package/src/generated/themes/Divider.css.ts +1 -1
  246. package/src/generated/themes/Dropdown.css.ts +1 -1
  247. package/src/generated/themes/FilterMenu.css.ts +1 -1
  248. package/src/generated/themes/Footer.css.ts +1 -1
  249. package/src/generated/themes/ImageGridTableItem.css.ts +1 -1
  250. package/src/generated/themes/MastheadShell.css.ts +1 -1
  251. package/src/generated/themes/MediaPlayer.css.ts +1 -1
  252. package/src/generated/themes/PhoneNumberField.css.ts +1 -1
  253. package/src/generated/themes/ProgressBar.css.ts +1 -1
  254. package/src/generated/themes/PromoTile.css.ts +1 -1
  255. package/src/generated/themes/Review.css.ts +1 -1
  256. package/src/generated/themes/ReviewItem.css.ts +1 -1
  257. package/src/generated/themes/Search.css.ts +1 -1
  258. package/src/generated/themes/SelectBase.css.ts +1 -1
  259. package/src/generated/themes/SelectBox.css.ts +1 -1
  260. package/src/generated/themes/SideNavigation.css.ts +1 -1
  261. package/src/generated/themes/SideNavigationGroup.css.ts +1 -1
  262. package/src/generated/themes/SocialMediaButton.css.ts +1 -1
  263. package/src/generated/themes/StepInput.css.ts +1 -1
  264. package/src/generated/themes/Switch.css.ts +1 -1
  265. package/src/generated/themes/TabNavigation.css.ts +1 -1
  266. package/src/generated/themes/Tag.css.ts +1 -1
  267. package/src/generated/themes/TagGroup.css.ts +1 -1
  268. package/src/generated/themes/TextArea.css.ts +1 -1
  269. package/src/generated/themes/TextField.css.ts +1 -1
  270. package/src/generated/themes/Toast.css.ts +1 -1
  271. package/src/i18n/messagebundle_ar.properties +99 -0
  272. package/src/i18n/messagebundle_no.properties +126 -1
  273. package/src/themes/Accordion.css +1 -1
  274. package/src/themes/AccordionItem.css +4 -4
  275. package/src/themes/AccordionSpotlight.css +264 -0
  276. package/src/themes/AccordionSpotlightItem.css +3 -0
  277. package/src/themes/Badge.css +7 -7
  278. package/src/themes/BusyIndicator.css +24 -0
  279. package/src/themes/Button.css +57 -57
  280. package/src/themes/Divider.css +1 -1
  281. package/src/themes/Dropdown.css +1 -1
  282. package/src/themes/FilterMenu.css +2 -2
  283. package/src/themes/Footer.css +1 -1
  284. package/src/themes/ImageGridTableItem.css +1 -1
  285. package/src/themes/MastheadShell.css +2 -2
  286. package/src/themes/MediaPlayer.css +5 -34
  287. package/src/themes/PhoneNumberField.css +6 -6
  288. package/src/themes/ProgressBar.css +4 -0
  289. package/src/themes/PromoTile.css +4 -4
  290. package/src/themes/Review.css +2 -2
  291. package/src/themes/ReviewItem.css +2 -2
  292. package/src/themes/Search.css +5 -5
  293. package/src/themes/SelectBase.css +14 -14
  294. package/src/themes/SelectBox.css +1 -1
  295. package/src/themes/SideNavigation.css +1 -1
  296. package/src/themes/SideNavigationGroup.css +1 -1
  297. package/src/themes/SocialMediaButton.css +2 -2
  298. package/src/themes/StepInput.css +8 -8
  299. package/src/themes/Switch.css +9 -9
  300. package/src/themes/TabNavigation.css +1 -1
  301. package/src/themes/Tag.css +4 -4
  302. package/src/themes/TagGroup.css +1 -0
  303. package/src/themes/TextArea.css +4 -4
  304. package/src/themes/TextField.css +13 -13
  305. package/src/themes/Toast.css +1 -1
  306. package/dist/assets/messagebundle_ar-BNhVFt1-.js +0 -1
  307. package/dist/assets/messagebundle_no-BIUfmTWJ.js +0 -1
@@ -215,6 +215,89 @@ describe("Filter Menu component", () => {
215
215
  cy.get("@triggerBtn").should("have.attr", "aria-expanded", "false");
216
216
  });
217
217
 
218
+ // Added: regression for the JAWS arrow-key announcement fix.
219
+ // The trigger used to expose role="combobox" + aria-haspopup="true", which made
220
+ // JAWS announce arrow-key navigation (comboboxes imply arrow-driven popups). But
221
+ // FilterMenu opens ONLY on click / Enter / Space (handleTriggerKeydown handles just
222
+ // Space/Enter, never arrows). It is really a button that opens a dialog-style filter
223
+ // panel, so the trigger now exposes role="button" + aria-haspopup="dialog", and the
224
+ // popover exposes accessible-role="Dialog" to match the haspopup promise.
225
+ describe("Trigger ARIA — button role with dialog popup", () => {
226
+ const trigger = () =>
227
+ cy.get("[udex-filter-menu]").shadow().find("#filterMenuTrigger");
228
+
229
+ it('trigger has role="button" (not "combobox")', () => {
230
+ trigger().should("have.attr", "role", "button");
231
+ });
232
+
233
+ it('trigger does NOT have role="combobox"', () => {
234
+ trigger().should("not.have.attr", "role", "combobox");
235
+ });
236
+
237
+ it('trigger has aria-haspopup="dialog" (not "true")', () => {
238
+ trigger().should("have.attr", "aria-haspopup", "dialog");
239
+ });
240
+
241
+ it('trigger aria-expanded reflects state: "false" closed, "true" after click', () => {
242
+ trigger().should("have.attr", "aria-expanded", "false");
243
+ trigger().click();
244
+ trigger().should("have.attr", "aria-expanded", "true");
245
+ });
246
+
247
+ it("ArrowDown on the focused trigger does NOT open the dropdown", () => {
248
+ trigger().should("have.attr", "aria-expanded", "false");
249
+
250
+ trigger().focus().should("be.focused");
251
+ cy.realPress("ArrowDown");
252
+
253
+ // Dropdown must stay closed — arrow keys are not an activation path.
254
+ trigger().should("have.attr", "aria-expanded", "false");
255
+ cy.get("[udex-filter-menu]").shadow()
256
+ .find("ui5-popover").should("not.have.attr", "open");
257
+ });
258
+
259
+ it("ArrowUp on the focused trigger does NOT open the dropdown", () => {
260
+ trigger().should("have.attr", "aria-expanded", "false");
261
+
262
+ trigger().focus().should("be.focused");
263
+ cy.realPress("ArrowUp");
264
+
265
+ trigger().should("have.attr", "aria-expanded", "false");
266
+ cy.get("[udex-filter-menu]").shadow()
267
+ .find("ui5-popover").should("not.have.attr", "open");
268
+ });
269
+
270
+ // Enter and Space share the same handleTriggerKeydown branch
271
+ // (isSpace(e) || isEnter(e)); Enter exercises that activation path and proves
272
+ // the contrast with the ignored arrow keys above.
273
+ it("Enter DOES open the dropdown (contrast with arrow keys)", () => {
274
+ trigger().should("have.attr", "aria-expanded", "false");
275
+
276
+ trigger().focus().should("be.focused");
277
+ cy.realPress("Enter");
278
+
279
+ trigger().should("have.attr", "aria-expanded", "true");
280
+ });
281
+
282
+ it('popover exposes accessible-role="Dialog" when opened', () => {
283
+ trigger().click();
284
+ trigger().should("have.attr", "aria-expanded", "true");
285
+
286
+ cy.get("[udex-filter-menu]").shadow()
287
+ .find("ui5-popover").should("have.attr", "accessible-role", "Dialog");
288
+ });
289
+
290
+ it("trigger aria-controls points at the popover id", () => {
291
+ cy.get("[udex-filter-menu]").then(($el) => {
292
+ const id = ($el[0] as UDExFilterMenu)._id;
293
+ const popoverId = `${id}_udex-filter-menu-dropdown`;
294
+ trigger().should("have.attr", "aria-controls", popoverId);
295
+ cy.get("[udex-filter-menu]").shadow()
296
+ .find("ui5-popover").should("have.attr", "id", popoverId);
297
+ });
298
+ });
299
+ });
300
+
218
301
  it("checks label and supporting text for the item", () => {
219
302
  cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__trigger").click();
220
303
  cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__list")
@@ -641,30 +724,55 @@ describe("Filter Menu component", () => {
641
724
  cy.get("@triggerBtn").should("be.focused");
642
725
  });
643
726
 
727
+ // Updated: removed the click-to-select + re-query + cy.focus() sequence. This test's
728
+ // intent is footer tab-order (list -> Result -> Reset), and the footer buttons render
729
+ // for MultiSelect regardless of selection. On open, the component focuses the first
730
+ // navigable element itself (focusFirstItem), so we Tab straight from there. We assert
731
+ // focus only at each footer stop — those have Cypress retry and verify the real intent;
732
+ // the exact opened-focus element (two shadow levels deep in the Select All header) is
733
+ // not asserted to avoid a brittle, ambiguous oracle.
644
734
  it("focus goes to Reset button when pressing Tab on a list item and Result button", () => {
645
735
  cy.mount(filterMenuSelectAll);
646
736
  cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__trigger").as("triggerBtn");
647
737
  cy.get("@triggerBtn").click();
648
738
 
649
- cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__list").as("list");
650
- cy.get("@list").find("udex-list-item").eq(0).shadow().find(".udex-list__item").as("listItem");
651
- cy.get("@listItem").click();
652
-
653
- cy.realPress("Tab");
739
+ // Dropdown open establishes real focus on the first navigable element (component-driven).
740
+ // Assert focus at each footer stop so every Tab fires from a settled source.
654
741
  cy.realPress("Tab");
742
+ cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__result-btn").shadow().find("button")
743
+ .should("have.focus");
655
744
 
745
+ cy.realPress("Tab");
656
746
  cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__reset-btn").shadow().find("button")
657
747
  .should("have.focus");
658
748
  });
659
749
 
750
+ // Updated: this test verifies footer tab-order WITH a selected item, so the selection
751
+ // is now established via property BEFORE opening the dropdown instead of clicking an
752
+ // item inside the open dropdown. Setting item.selected pre-open means the list renders
753
+ // already-selected (captured by _getInitialState on before-open), so there is no
754
+ // in-dropdown re-render to race the Tab. On open, focus lands on the Select All header
755
+ // wrapper (component-driven via focusHeader()), and we Tab from there to the Result btn.
660
756
  it("focus goes to Results button when pressing Tab on a list item and at least one item is selected", () => {
661
757
  cy.mount(filterMenuSelectAll);
758
+
759
+ // Select the first item via its public property before the dropdown opens.
760
+ cy.get("[udex-filter-menu]").find("udex-filter-menu-item").eq(0).then(($item) => {
761
+ ($item[0] as unknown as UDExFilterMenuItem).selected = true;
762
+ });
763
+
662
764
  cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__trigger").as("triggerBtn");
663
765
  cy.get("@triggerBtn").click();
664
766
 
665
- cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__list").find("udex-list-item").eq(0).shadow().find(".udex-list__item").as("listItem");
666
- cy.get("@listItem").click();
767
+ // Confirm the item rendered selected (separate query — chaining a second have.attr
768
+ // asserts against the first attribute's string value, not the element).
769
+ cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__list")
770
+ .find("udex-list-item").eq(0).should("have.attr", "selected");
667
771
 
772
+ // Dropdown open establishes real focus on the first navigable element (component-driven).
773
+ // Tab to the Result button; the footer-stop assertion (with Cypress retry) verifies the
774
+ // tab-order intent. The exact opened-focus element is not asserted (ambiguous, two
775
+ // shadow levels deep in the Select All header).
668
776
  cy.realPress("Tab");
669
777
 
670
778
  cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__result-btn").shadow().find("button")
@@ -687,16 +795,29 @@ describe("Filter Menu component", () => {
687
795
  cy.get("#next-focus").should("have.focus");
688
796
  });
689
797
 
798
+ // Updated: removed the click-to-select + re-query + cy.focus() sequence. This test's
799
+ // intent is the full footer tab chain (first interactive element -> Result -> Reset ->
800
+ // #next-focus), and the footer buttons render for MultiSelect regardless of selection.
801
+ // On open, the component focuses the first navigable element itself (focusFirstItem), so
802
+ // we Tab straight from there. We assert focus only at each footer stop — those have
803
+ // Cypress retry and verify the real intent; the exact opened-focus element (the group
804
+ // header wrapper, in shadow DOM) is not asserted to avoid a brittle, ambiguous oracle.
690
805
  it("focus goes to next interactive element on the page when pressing Tab on Reset button", () => {
691
806
  cy.mount(filterMenuGroup);
692
807
  cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__trigger").as("triggerBtn");
693
808
  cy.get("@triggerBtn").click();
694
809
 
695
- cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__list").find("udex-list-item").eq(0).shadow().find(".udex-list__item").as("listItem");
696
- cy.get("@listItem").click();
697
-
810
+ // Dropdown open establishes real focus on the first navigable element (component-driven).
811
+ // Assert focus at each footer stop so every Tab fires from a settled source; the final
812
+ // Tab from the Reset button closes the dropdown and moves focus to #next-focus.
698
813
  cy.realPress("Tab");
814
+ cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__result-btn").shadow().find("button")
815
+ .should("have.focus");
816
+
699
817
  cy.realPress("Tab");
818
+ cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__reset-btn").shadow().find("button")
819
+ .should("have.focus");
820
+
700
821
  cy.realPress("Tab");
701
822
 
702
823
  cy.get("#next-focus").should("have.focus");
@@ -1241,4 +1362,841 @@ describe("Filter Menu component", () => {
1241
1362
  .should("equal", 0);
1242
1363
  });
1243
1364
  });
1365
+
1366
+ // Added: WCAG 4.1.2 Treeview Checkbox pattern for MultiSelect + grouped items.
1367
+ // Reference: WAI-ARIA Authoring Practices "Treeview Checkbox".
1368
+ describe("Tree accessibility pattern (MultiSelect + groups)", () => {
1369
+ const filterMenuMultiSelectGroupCollapsible = <UDExFilterMenu
1370
+ id="filter-menu-tree-collapsible"
1371
+ mode={UDExListMode.MultiSelect}
1372
+ filter-name="Filter Name">
1373
+ <UDExFilterMenuGroupHeader collapsible label="Programming Languages">
1374
+ <UDExFilterMenuItem label="JavaScript"></UDExFilterMenuItem>
1375
+ <UDExFilterMenuItem label="Python"></UDExFilterMenuItem>
1376
+ <UDExFilterMenuItem label="Java"></UDExFilterMenuItem>
1377
+ </UDExFilterMenuGroupHeader>
1378
+ </UDExFilterMenu>;
1379
+
1380
+ const filterMenuMultiSelectGroupNonCollapsible = <UDExFilterMenu
1381
+ id="filter-menu-tree-non-collapsible"
1382
+ mode={UDExListMode.MultiSelect}
1383
+ filter-name="Filter Name">
1384
+ <UDExFilterMenuGroupHeader label="Programming Languages">
1385
+ <UDExFilterMenuItem label="JavaScript"></UDExFilterMenuItem>
1386
+ <UDExFilterMenuItem label="Python"></UDExFilterMenuItem>
1387
+ <UDExFilterMenuItem label="Java"></UDExFilterMenuItem>
1388
+ </UDExFilterMenuGroupHeader>
1389
+ </UDExFilterMenu>;
1390
+
1391
+ const filterMenuMultiSelectGroupNonInteractive = <UDExFilterMenu
1392
+ id="filter-menu-tree-non-interactive"
1393
+ mode={UDExListMode.MultiSelect}
1394
+ filter-name="Filter Name">
1395
+ <UDExFilterMenuGroupHeader non-interactive label="Programming Languages">
1396
+ <UDExFilterMenuItem label="JavaScript"></UDExFilterMenuItem>
1397
+ <UDExFilterMenuItem label="Python"></UDExFilterMenuItem>
1398
+ </UDExFilterMenuGroupHeader>
1399
+ </UDExFilterMenu>;
1400
+
1401
+ // Updated: this regression fixture used to have `collapsible` on the group header.
1402
+ // Since SingleSelect + at least one collapsible group now activates the tree pattern
1403
+ // too, the non-tree regression must be exercised via a NON-collapsible group.
1404
+ const filterMenuSingleSelectGroup = <UDExFilterMenu
1405
+ id="filter-menu-single-group"
1406
+ mode={UDExListMode.SingleSelect}
1407
+ filter-name="Filter Name">
1408
+ <UDExFilterMenuGroupHeader label="Programming Languages">
1409
+ <UDExFilterMenuItem label="JavaScript"></UDExFilterMenuItem>
1410
+ <UDExFilterMenuItem label="Python"></UDExFilterMenuItem>
1411
+ </UDExFilterMenuGroupHeader>
1412
+ </UDExFilterMenu>;
1413
+
1414
+ // Added: SingleSelect + at least one collapsible group DOES activate the tree pattern
1415
+ // (headers become treeitems with aria-expanded, items become treeitems with aria-selected).
1416
+ const filterMenuSingleSelectGroupCollapsible = <UDExFilterMenu
1417
+ id="filter-menu-single-tree-collapsible"
1418
+ mode={UDExListMode.SingleSelect}
1419
+ filter-name="Filter Name">
1420
+ <UDExFilterMenuGroupHeader collapsible label="Programming Languages">
1421
+ <UDExFilterMenuItem label="JavaScript"></UDExFilterMenuItem>
1422
+ <UDExFilterMenuItem label="Python"></UDExFilterMenuItem>
1423
+ <UDExFilterMenuItem label="Java"></UDExFilterMenuItem>
1424
+ </UDExFilterMenuGroupHeader>
1425
+ </UDExFilterMenu>;
1426
+
1427
+ const filterMenuMultiSelectFlat = <UDExFilterMenu
1428
+ id="filter-menu-multi-flat"
1429
+ mode={UDExListMode.MultiSelect}
1430
+ filter-name="Filter Name">
1431
+ <UDExFilterMenuItem label="JavaScript"></UDExFilterMenuItem>
1432
+ <UDExFilterMenuItem label="Python"></UDExFilterMenuItem>
1433
+ </UDExFilterMenu>;
1434
+
1435
+ const openMenu = (id: string) => {
1436
+ cy.get(`#${id}`).shadow().find(".udex-filter-menu__trigger").click();
1437
+ };
1438
+
1439
+ describe("MultiSelect + collapsible groups", () => {
1440
+ beforeEach(() => {
1441
+ cy.mount(filterMenuMultiSelectGroupCollapsible);
1442
+ openMenu("filter-menu-tree-collapsible");
1443
+ });
1444
+
1445
+ it("host [udex-list] has aria-multiselectable=\"true\"", () => {
1446
+ cy.get("#filter-menu-tree-collapsible").shadow()
1447
+ .find("[udex-list]").should("have.attr", "aria-multiselectable", "true");
1448
+ });
1449
+
1450
+ it("inner list div has role=\"tree\"", () => {
1451
+ cy.get("#filter-menu-tree-collapsible").shadow()
1452
+ .find("[udex-list]").shadow()
1453
+ .find("#udex-list").should("have.attr", "role", "tree");
1454
+ });
1455
+
1456
+ it("group header host [udex-list-group-header] has role=\"none\"", () => {
1457
+ cy.get("#filter-menu-tree-collapsible").shadow()
1458
+ .find("udex-list-group-header").should("have.attr", "role", "none");
1459
+ });
1460
+
1461
+ // Updated: `accessibleRole` string property on udex-list-group-header was replaced
1462
+ // by a `_treeMode` boolean, reflected as the `_tree-mode` attribute (present when
1463
+ // true). role="tree" now lives on the udex-list host + its shadow #udex-list, not
1464
+ // on the group header host. The host role stays "none" (tested above) so that its
1465
+ // treeitem child passes through to the tree parent.
1466
+ it("group header host [udex-list-group-header] has _tree-mode attribute in tree mode", () => {
1467
+ cy.get("#filter-menu-tree-collapsible").shadow()
1468
+ .find("udex-list-group-header").should("have.attr", "_tree-mode");
1469
+ });
1470
+
1471
+ it("udex-list host has role=\"none\" (transparent host; tree role lives on shadow #udex-list)", () => {
1472
+ cy.get("#filter-menu-tree-collapsible").shadow()
1473
+ .find("[udex-list]").should("have.attr", "role", "none");
1474
+ });
1475
+
1476
+ it("group header inner wrapper has role=\"none\" and no aria-labelledby", () => {
1477
+ cy.get("#filter-menu-tree-collapsible").shadow()
1478
+ .find("udex-list-group-header").shadow()
1479
+ .find("[part=item]")
1480
+ .should("have.attr", "role", "none")
1481
+ .and("not.have.attr", "aria-labelledby");
1482
+ });
1483
+
1484
+ // Updated: aria-expanded now lives on the shadow [part=item] div (same element as
1485
+ // role/aria-checked), not on the host. The host still exposes accessible-role which
1486
+ // feeds the shadow role.
1487
+ it("group header's inner udex-list-item has accessible-role=\"treeitem\"", () => {
1488
+ cy.get("#filter-menu-tree-collapsible").shadow()
1489
+ .find("udex-list-group-header").shadow()
1490
+ .find("udex-list-item")
1491
+ .should("have.attr", "accessible-role", "treeitem");
1492
+ });
1493
+
1494
+ it("group header's inner rendered role is \"treeitem\" on [part=item]", () => {
1495
+ cy.get("#filter-menu-tree-collapsible").shadow()
1496
+ .find("udex-list-group-header").shadow()
1497
+ .find("udex-list-item").shadow()
1498
+ .find("[part=item]").should("have.attr", "role", "treeitem");
1499
+ });
1500
+
1501
+ // Added: aria-expanded is on the shadow [part=item] div (was previously only on the
1502
+ // host, which has role="none" and is invisible to AT).
1503
+ it("group header's [part=item] has aria-expanded=\"true\" when expanded (default)", () => {
1504
+ cy.get("#filter-menu-tree-collapsible").shadow()
1505
+ .find("udex-list-group-header").shadow()
1506
+ .find("udex-list-item").shadow()
1507
+ .find("[part=item]").should("have.attr", "aria-expanded", "true");
1508
+ });
1509
+
1510
+ it("group header's [part=item] has aria-expanded=\"false\" after collapsing", () => {
1511
+ cy.get("#filter-menu-tree-collapsible").shadow()
1512
+ .find("udex-list-group-header").then($gh => {
1513
+ ($gh[0] as HTMLElement & { collapsed: boolean }).collapsed = true;
1514
+ });
1515
+
1516
+ cy.get("#filter-menu-tree-collapsible").shadow()
1517
+ .find("udex-list-group-header").shadow()
1518
+ .find("udex-list-item").shadow()
1519
+ .find("[part=item]").should("have.attr", "aria-expanded", "false");
1520
+ });
1521
+
1522
+ it("content region has role=\"group\" (not \"region\") in tree mode", () => {
1523
+ cy.get("#filter-menu-tree-collapsible").shadow()
1524
+ .find("udex-list-group-header").shadow()
1525
+ .find(".udex-list__group-list__content").should("have.attr", "role", "group");
1526
+ });
1527
+
1528
+ it("content region does NOT wrap slot in an inner role=\"listbox\" div", () => {
1529
+ cy.get("#filter-menu-tree-collapsible").shadow()
1530
+ .find("udex-list-group-header").shadow()
1531
+ .find(".udex-list__group-list__content [role=listbox]").should("not.exist");
1532
+ });
1533
+
1534
+ // Updated: host udex-list-item has role="none"; the treeitem role is set via
1535
+ // accessible-role on the host (which the template renders onto [part=item]).
1536
+ it("child items get accessible-role=\"treeitem\" on their host element", () => {
1537
+ cy.get("#filter-menu-tree-collapsible").shadow()
1538
+ .find("udex-list-group-header")
1539
+ .find("udex-list-item").each($item => {
1540
+ cy.wrap($item).should("have.attr", "accessible-role", "treeitem");
1541
+ });
1542
+ });
1543
+
1544
+ it("child items get role=\"treeitem\" on their inner [part=item]", () => {
1545
+ cy.get("#filter-menu-tree-collapsible").shadow()
1546
+ .find("udex-list-group-header")
1547
+ .find("udex-list-item").eq(0).shadow()
1548
+ .find("[part=item]").should("have.attr", "role", "treeitem");
1549
+ });
1550
+
1551
+ it("child items' inner checkbox is aria-hidden to avoid double-announce", () => {
1552
+ cy.get("#filter-menu-tree-collapsible").shadow()
1553
+ .find("udex-list-group-header")
1554
+ .find("udex-list-item").eq(0).shadow()
1555
+ .find("udex-checkbox").should("have.attr", "aria-hidden");
1556
+ });
1557
+
1558
+ it("child item aria-checked=\"false\" when unselected", () => {
1559
+ cy.get("#filter-menu-tree-collapsible").shadow()
1560
+ .find("udex-list-group-header")
1561
+ .find("udex-list-item").eq(0).shadow()
1562
+ .find("[part=item]").should("have.attr", "aria-checked", "false");
1563
+ });
1564
+
1565
+ it("child item aria-checked=\"true\" after selection", () => {
1566
+ cy.get("#filter-menu-tree-collapsible").shadow()
1567
+ .find("udex-list-group-header")
1568
+ .find("udex-list-item").eq(0).shadow()
1569
+ .find(".udex-list__item").click();
1570
+
1571
+ cy.get("#filter-menu-tree-collapsible").shadow()
1572
+ .find("udex-list-group-header")
1573
+ .find("udex-list-item").eq(0).shadow()
1574
+ .find("[part=item]").should("have.attr", "aria-checked", "true");
1575
+ });
1576
+
1577
+ it("group header aria-checked=\"mixed\" when some (not all) children selected", () => {
1578
+ cy.get("#filter-menu-tree-collapsible").shadow()
1579
+ .find("udex-list-group-header")
1580
+ .find("udex-list-item").eq(0).shadow()
1581
+ .find(".udex-list__item").click();
1582
+
1583
+ cy.get("#filter-menu-tree-collapsible").shadow()
1584
+ .find("udex-list-group-header").shadow()
1585
+ .find("udex-list-item").shadow()
1586
+ .find("[part=item]").should("have.attr", "aria-checked", "mixed");
1587
+ });
1588
+
1589
+ it("group header aria-checked=\"true\" when all children selected", () => {
1590
+ cy.get("#filter-menu-tree-collapsible").shadow()
1591
+ .find("udex-list-group-header").as("group");
1592
+
1593
+ cy.get("@group").find("udex-list-item").each($item => {
1594
+ cy.wrap($item).shadow().find(".udex-list__item").click();
1595
+ });
1596
+
1597
+ cy.get("@group").shadow()
1598
+ .find("udex-list-item").shadow()
1599
+ .find("[part=item]").should("have.attr", "aria-checked", "true");
1600
+ });
1601
+
1602
+ it("group header aria-checked=\"false\" when no children selected (initial)", () => {
1603
+ cy.get("#filter-menu-tree-collapsible").shadow()
1604
+ .find("udex-list-group-header").shadow()
1605
+ .find("udex-list-item").shadow()
1606
+ .find("[part=item]").should("have.attr", "aria-checked", "false");
1607
+ });
1608
+ });
1609
+
1610
+ describe("MultiSelect + non-collapsible groups", () => {
1611
+ beforeEach(() => {
1612
+ cy.mount(filterMenuMultiSelectGroupNonCollapsible);
1613
+ openMenu("filter-menu-tree-non-collapsible");
1614
+ });
1615
+
1616
+ it("still activates the tree pattern on the list", () => {
1617
+ cy.get("#filter-menu-tree-non-collapsible").shadow()
1618
+ .find("[udex-list]").should("have.attr", "aria-multiselectable", "true");
1619
+ cy.get("#filter-menu-tree-non-collapsible").shadow()
1620
+ .find("[udex-list]").shadow()
1621
+ .find("#udex-list").should("have.attr", "role", "tree");
1622
+ });
1623
+
1624
+ it("non-collapsible group's list container has role=\"group\" (not \"listbox\") in tree mode", () => {
1625
+ cy.get("#filter-menu-tree-non-collapsible").shadow()
1626
+ .find("udex-list-group-header").shadow()
1627
+ .find(".udex-list__group-list").should("have.attr", "role", "group");
1628
+ });
1629
+
1630
+ it("non-collapsible active header's inner udex-list-item has accessible-role=\"treeitem\"", () => {
1631
+ cy.get("#filter-menu-tree-non-collapsible").shadow()
1632
+ .find("udex-list-group-header").shadow()
1633
+ .find("udex-list-item").should("have.attr", "accessible-role", "treeitem");
1634
+ });
1635
+
1636
+ it("children have role=\"treeitem\" and aria-checked wired up", () => {
1637
+ cy.get("#filter-menu-tree-non-collapsible").shadow()
1638
+ .find("udex-list-group-header")
1639
+ .find("udex-list-item").eq(0).shadow()
1640
+ .find("[part=item]")
1641
+ .should("have.attr", "role", "treeitem")
1642
+ .and("have.attr", "aria-checked", "false");
1643
+ });
1644
+ });
1645
+
1646
+ describe("Non-interactive group header in tree mode", () => {
1647
+ beforeEach(() => {
1648
+ cy.mount(filterMenuMultiSelectGroupNonInteractive);
1649
+ openMenu("filter-menu-tree-non-interactive");
1650
+ });
1651
+
1652
+ it("non-interactive header still renders as <header> (no treeitem role on the header itself)", () => {
1653
+ cy.get("#filter-menu-tree-non-interactive").shadow()
1654
+ .find("udex-list-group-header").shadow()
1655
+ .find(".udex-list__inactive-header").should("exist");
1656
+ });
1657
+
1658
+ // Updated: check accessible-role on the host and role on the shadow [part=item]
1659
+ // (host role is "none"; the treeitem role is applied inside the shadow template).
1660
+ it("but its children still become treeitems", () => {
1661
+ cy.get("#filter-menu-tree-non-interactive").shadow()
1662
+ .find("udex-list-group-header")
1663
+ .find("udex-list-item").eq(0)
1664
+ .should("have.attr", "accessible-role", "treeitem");
1665
+
1666
+ cy.get("#filter-menu-tree-non-interactive").shadow()
1667
+ .find("udex-list-group-header")
1668
+ .find("udex-list-item").eq(0).shadow()
1669
+ .find("[part=item]").should("have.attr", "role", "treeitem");
1670
+ });
1671
+ });
1672
+
1673
+ // Updated: SingleSelect + grouped is only guaranteed non-tree when NO group is
1674
+ // collapsible. SingleSelect + collapsible groups now activates the tree pattern
1675
+ // (covered by the "SingleSelect + collapsible groups" describe below).
1676
+ // Added: covers the broadened isTreeStructure branch (FilterMenu.isTreeStructure):
1677
+ // grouped && modeSingleSelect && hasCollapsibleGroup
1678
+ // In this mode:
1679
+ // - the list host still activates tree/treeitem semantics
1680
+ // - aria-multiselectable is NOT set (SingleSelect distinction)
1681
+ // - leaf items expose aria-selected (not aria-checked)
1682
+ // - header renders via the udex-list-item partial (useListItemHeader === true)
1683
+ // and exposes aria-expanded on [part=item]
1684
+ describe("SingleSelect + collapsible groups", () => {
1685
+ beforeEach(() => {
1686
+ cy.mount(filterMenuSingleSelectGroupCollapsible);
1687
+ openMenu("filter-menu-single-tree-collapsible");
1688
+ });
1689
+
1690
+ it("inner list div has role=\"tree\" (SingleSelect + collapsible activates tree)", () => {
1691
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1692
+ .find("[udex-list]").shadow()
1693
+ .find("#udex-list").should("have.attr", "role", "tree");
1694
+ });
1695
+
1696
+ it("host [udex-list] has NO aria-multiselectable attribute (SingleSelect distinction)", () => {
1697
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1698
+ .find("[udex-list]").should("not.have.attr", "aria-multiselectable");
1699
+ });
1700
+
1701
+ it("group header host has role=\"none\" and _tree-mode attribute present", () => {
1702
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1703
+ .find("udex-list-group-header")
1704
+ .should("have.attr", "role", "none")
1705
+ .and("have.attr", "_tree-mode");
1706
+ });
1707
+
1708
+ it("group header shadow .udex-list__group has role=\"none\"", () => {
1709
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1710
+ .find("udex-list-group-header").shadow()
1711
+ .find(".udex-list__group").should("have.attr", "role", "none");
1712
+ });
1713
+
1714
+ // Updated: useListItemHeader is now `modeMultiSelectOrRadioGroup` only, so a
1715
+ // collapsible SingleSelect header (even in tree mode) renders the plain
1716
+ // `single-group-header-collapsible` <div> partial — NOT a <udex-list-item>. The
1717
+ // div carries the treeitem semantics directly: role, aria-expanded, aria-controls.
1718
+ // This is the fix for the bug where the header was incorrectly selectable via
1719
+ // click/Enter (the udex-list-item partial fired its own selection handlers).
1720
+ it("group header renders as a plain div (.udex-list__group-content), not a udex-list-item", () => {
1721
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1722
+ .find("udex-list-group-header").shadow()
1723
+ .find(".udex-list__group-content").should("exist");
1724
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1725
+ .find("udex-list-group-header").shadow()
1726
+ .find("udex-list-item").should("not.exist");
1727
+ });
1728
+
1729
+ it("group header div has role=\"treeitem\" (tree mode via singleCollapsibleHeaderRole)", () => {
1730
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1731
+ .find("udex-list-group-header").shadow()
1732
+ .find(".udex-list__group-content")
1733
+ .should("have.attr", "role", "treeitem");
1734
+ });
1735
+
1736
+ it("group header div has aria-expanded=\"true\" when expanded (default)", () => {
1737
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1738
+ .find("udex-list-group-header").shadow()
1739
+ .find(".udex-list__group-content").should("have.attr", "aria-expanded", "true");
1740
+ });
1741
+
1742
+ it("group header div has aria-expanded=\"false\" after collapsing", () => {
1743
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1744
+ .find("udex-list-group-header").then($gh => {
1745
+ ($gh[0] as HTMLElement & { collapsed: boolean }).collapsed = true;
1746
+ });
1747
+
1748
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1749
+ .find("udex-list-group-header").shadow()
1750
+ .find(".udex-list__group-content").should("have.attr", "aria-expanded", "false");
1751
+ });
1752
+
1753
+ // The header div is not a selectable widget in this mode — it must NOT expose any
1754
+ // selection-state attribute (neither aria-checked nor aria-selected). Selection is
1755
+ // reserved for the leaf treeitems.
1756
+ // Updated: split the chained `.should(...).and(...)` into two separately-queried
1757
+ // assertions. The chained `.and("not.have.attr", ...)` was re-yielding the subject
1758
+ // as undefined (chai-jQuery "attr on undefined"); a fresh `.find()` per assertion is
1759
+ // reliable.
1760
+ it("group header div does NOT have aria-checked or aria-selected (not selectable)", () => {
1761
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1762
+ .find("udex-list-group-header").shadow()
1763
+ .find(".udex-list__group-content")
1764
+ .should("not.have.attr", "aria-checked");
1765
+
1766
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1767
+ .find("udex-list-group-header").shadow()
1768
+ .find(".udex-list__group-content")
1769
+ .should("not.have.attr", "aria-selected");
1770
+ });
1771
+
1772
+ it("child items get accessible-role=\"treeitem\" on their host element", () => {
1773
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1774
+ .find("udex-list-group-header")
1775
+ .find("udex-list-item").each($item => {
1776
+ cy.wrap($item).should("have.attr", "accessible-role", "treeitem");
1777
+ });
1778
+ });
1779
+
1780
+ it("child items get role=\"treeitem\" on their inner [part=item]", () => {
1781
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1782
+ .find("udex-list-group-header")
1783
+ .find("udex-list-item").eq(0).shadow()
1784
+ .find("[part=item]").should("have.attr", "role", "treeitem");
1785
+ });
1786
+
1787
+ it("child items have aria-selected=\"false\" when unselected (initial)", () => {
1788
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1789
+ .find("udex-list-group-header")
1790
+ .find("udex-list-item").eq(0).shadow()
1791
+ .find("[part=item]").should("have.attr", "aria-selected", "false");
1792
+ });
1793
+
1794
+ it("child items do NOT have aria-checked in SingleSelect tree mode", () => {
1795
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1796
+ .find("udex-list-group-header")
1797
+ .find("udex-list-item").eq(0).shadow()
1798
+ .find("[part=item]").should("not.have.attr", "aria-checked");
1799
+ });
1800
+
1801
+ it("clicking a child sets its aria-selected=\"true\"", () => {
1802
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1803
+ .find("udex-list-group-header")
1804
+ .find("udex-list-item").eq(0).shadow()
1805
+ .find(".udex-list__item").click();
1806
+
1807
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1808
+ .find("udex-list-group-header")
1809
+ .find("udex-list-item").eq(0).shadow()
1810
+ .find("[part=item]").should("have.attr", "aria-selected", "true");
1811
+ });
1812
+
1813
+ // SingleSelect semantics: only one item can be selected at a time. Selecting a
1814
+ // second item must set the first item's aria-selected back to "false".
1815
+ // Updated: FilterMenu in SingleSelect mode auto-closes the dropdown when an item is
1816
+ // selected (the popover gets display:none), so the two clicks cannot happen in one
1817
+ // open session — the original test's second click failed on a hidden popover. Re-open
1818
+ // the menu between selections. This also verifies the first selection persists across
1819
+ // reopen and is correctly cleared when the second item is chosen.
1820
+ it("selecting a second child reverts the first child's aria-selected to \"false\"", () => {
1821
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1822
+ .find("udex-list-group-header")
1823
+ .find("udex-list-item").eq(0).shadow()
1824
+ .find(".udex-list__item").click();
1825
+
1826
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1827
+ .find("udex-list-group-header")
1828
+ .find("udex-list-item").eq(0).shadow()
1829
+ .find("[part=item]").should("have.attr", "aria-selected", "true");
1830
+
1831
+ // Dropdown auto-closed after the first selection — reopen it to select the second.
1832
+ openMenu("filter-menu-single-tree-collapsible");
1833
+
1834
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1835
+ .find("udex-list-group-header")
1836
+ .find("udex-list-item").eq(1).shadow()
1837
+ .find(".udex-list__item").click();
1838
+
1839
+ // Reopen once more to inspect the resulting selection state.
1840
+ openMenu("filter-menu-single-tree-collapsible");
1841
+
1842
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1843
+ .find("udex-list-group-header")
1844
+ .find("udex-list-item").eq(0).shadow()
1845
+ .find("[part=item]").should("have.attr", "aria-selected", "false");
1846
+
1847
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1848
+ .find("udex-list-group-header")
1849
+ .find("udex-list-item").eq(1).shadow()
1850
+ .find("[part=item]").should("have.attr", "aria-selected", "true");
1851
+ });
1852
+
1853
+ // Updated: the header is now a plain <div>. Clicking its body does nothing at all
1854
+ // (no @click handler is wired on the div). This asserts no child gets selected.
1855
+ // Full click/Enter/Space + collapse coverage is in the dedicated
1856
+ // "activation collapses only, never selects" describe below.
1857
+ it("clicking the header body does NOT select any child", () => {
1858
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1859
+ .find("udex-list-group-header").shadow()
1860
+ .find(".udex-list__group-content").click();
1861
+
1862
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1863
+ .find("udex-list-group-header")
1864
+ .find("udex-list-item").each($item => {
1865
+ cy.wrap($item).shadow()
1866
+ .find("[part=item]").should("have.attr", "aria-selected", "false");
1867
+ });
1868
+ });
1869
+
1870
+ it("ArrowRight on a collapsed header expands it (same as MultiSelect tree)", () => {
1871
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1872
+ .find("udex-list-group-header").then($gh => {
1873
+ ($gh[0] as HTMLElement & { collapsed: boolean }).collapsed = true;
1874
+ });
1875
+
1876
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1877
+ .find("udex-list-group-header").shadow()
1878
+ .find(".udex-list__group-content").should("have.attr", "aria-expanded", "false");
1879
+
1880
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1881
+ .find("udex-list-group-header").shadow()
1882
+ .find(".udex-list__group-content").focus();
1883
+ cy.realPress("ArrowRight");
1884
+
1885
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1886
+ .find("udex-list-group-header").shadow()
1887
+ .find(".udex-list__group-content").should("have.attr", "aria-expanded", "true");
1888
+ });
1889
+
1890
+ it("ArrowLeft on an expanded header collapses it (same as MultiSelect tree)", () => {
1891
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1892
+ .find("udex-list-group-header").shadow()
1893
+ .find(".udex-list__group-content").should("have.attr", "aria-expanded", "true");
1894
+
1895
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1896
+ .find("udex-list-group-header").shadow()
1897
+ .find(".udex-list__group-content").focus();
1898
+ cy.realPress("ArrowLeft");
1899
+
1900
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1901
+ .find("udex-list-group-header").shadow()
1902
+ .find(".udex-list__group-content").should("have.attr", "aria-expanded", "false");
1903
+ });
1904
+ });
1905
+
1906
+ // Updated: covers the bug fix for a collapsible SingleSelect group header being
1907
+ // incorrectly selectable. The fix renders the header as a plain <div>
1908
+ // (useListItemHeader === modeMultiSelectOrRadioGroup, so SingleSelect no longer
1909
+ // uses the <udex-list-item> partial). Consequences:
1910
+ // - The header <div> has NO @click handler, so clicking its body does NOTHING
1911
+ // (no select, and — unlike the old fix — no collapse either).
1912
+ // - The <div> wires @keydown=handleKeyDown, which ONLY handles Left/Right arrows.
1913
+ // There is no Enter/Space selection path.
1914
+ // - Collapse/expand happens via ArrowLeft/ArrowRight or the arrow <udex-button>.
1915
+ //
1916
+ // Oracle notes:
1917
+ // - "is the header selectable?" → the <div> exposes neither aria-selected nor
1918
+ // aria-checked, and clicking never sets `selected` on the host. It must stay
1919
+ // unselected.
1920
+ // - "did the toggle work?" → aria-expanded on the same <div> (driven by collapse
1921
+ // state) and the `collapsed` attribute on the host + slotted children.
1922
+ describe("SingleSelect collapsible header — never selectable, collapses via arrows/arrow-button only", () => {
1923
+ // Updated: this describe block was missing the mount + open setup, so every test
1924
+ // failed with "#filter-menu-single-tree-collapsible never found". Mirror the sibling
1925
+ // "SingleSelect + collapsible groups" block's beforeEach. The fixture and openMenu
1926
+ // helper are defined in the parent describe scope.
1927
+ beforeEach(() => {
1928
+ cy.mount(filterMenuSingleSelectGroupCollapsible);
1929
+ openMenu("filter-menu-single-tree-collapsible");
1930
+ });
1931
+
1932
+ const headerDiv = () =>
1933
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1934
+ .find("udex-list-group-header").shadow()
1935
+ .find(".udex-list__group-content");
1936
+
1937
+ const arrowButton = () =>
1938
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1939
+ .find("udex-list-group-header").shadow()
1940
+ .find(".udex-list__group-heading__icon");
1941
+
1942
+ const childItems = () =>
1943
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1944
+ .find("udex-list-group-header")
1945
+ .find("udex-list-item");
1946
+
1947
+ // 1) Clicking the header body does NOT select it (no aria-selected/aria-checked,
1948
+ // no `selected` on the host) and does NOT select any child.
1949
+ // Updated: split chained `.should(...).and(...)` into separate assertions on freshly
1950
+ // queried elements — the chained `.and("not.have.attr", ...)` re-yields the subject
1951
+ // as undefined through the double-shadow `.find()` chain (chai-jQuery "attr on
1952
+ // undefined").
1953
+ it("clicking the header body does NOT expose a selected state on the header", () => {
1954
+ headerDiv().should("not.have.attr", "aria-selected");
1955
+ headerDiv().should("not.have.attr", "aria-checked");
1956
+
1957
+ headerDiv().click();
1958
+
1959
+ headerDiv().should("not.have.attr", "aria-selected");
1960
+ headerDiv().should("not.have.attr", "aria-checked");
1961
+ });
1962
+
1963
+ it("clicking the header body does NOT set the selected property on the host group header", () => {
1964
+ headerDiv().click();
1965
+
1966
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1967
+ .find("udex-list-group-header").then($gh => {
1968
+ expect(($gh[0] as HTMLElement & { selected: boolean }).selected).to.not.equal(true);
1969
+ });
1970
+ });
1971
+
1972
+ it("clicking the header body does NOT select any child item", () => {
1973
+ headerDiv().click();
1974
+
1975
+ childItems().each($item => {
1976
+ cy.wrap($item).shadow()
1977
+ .find("[part=item]").should("have.attr", "aria-selected", "false");
1978
+ });
1979
+ });
1980
+
1981
+ // 2) Clicking the header body must NOT collapse the group either — there is no
1982
+ // @click handler on the div in the new implementation.
1983
+ it("clicking the header body does NOT collapse the group", () => {
1984
+ headerDiv().should("have.attr", "aria-expanded", "true");
1985
+
1986
+ headerDiv().click();
1987
+
1988
+ headerDiv().should("have.attr", "aria-expanded", "true");
1989
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
1990
+ .find("udex-list-group-header").should("not.have.attr", "collapsed");
1991
+ });
1992
+
1993
+ // 3) Pressing Enter on the focused header does NOT select it (no Enter selection
1994
+ // path on the div) and does NOT select any child.
1995
+ it("pressing Enter on the focused header does NOT expose a selected state", () => {
1996
+ headerDiv().focus();
1997
+ cy.realPress("Enter");
1998
+
1999
+ headerDiv().should("not.have.attr", "aria-selected");
2000
+ headerDiv().should("not.have.attr", "aria-checked");
2001
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
2002
+ .find("udex-list-group-header").then($gh => {
2003
+ expect(($gh[0] as HTMLElement & { selected: boolean }).selected).to.not.equal(true);
2004
+ });
2005
+ });
2006
+
2007
+ it("pressing Enter on the focused header does NOT select any child item", () => {
2008
+ headerDiv().focus();
2009
+ cy.realPress("Enter");
2010
+
2011
+ childItems().each($item => {
2012
+ cy.wrap($item).shadow()
2013
+ .find("[part=item]").should("have.attr", "aria-selected", "false");
2014
+ });
2015
+ });
2016
+
2017
+ // 4) Left/Right arrows still collapse/expand. handleKeyDown collapses on ArrowLeft
2018
+ // when expanded and expands on ArrowRight when collapsed.
2019
+ it("ArrowLeft collapses the expanded group and propagates `collapsed` to children", () => {
2020
+ headerDiv().should("have.attr", "aria-expanded", "true");
2021
+ childItems().each($item => {
2022
+ cy.wrap($item).should("not.have.attr", "collapsed");
2023
+ });
2024
+
2025
+ headerDiv().focus();
2026
+ cy.realPress("ArrowLeft");
2027
+
2028
+ headerDiv().should("have.attr", "aria-expanded", "false");
2029
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
2030
+ .find("udex-list-group-header").should("have.attr", "collapsed");
2031
+ childItems().each($item => {
2032
+ cy.wrap($item).should("have.attr", "collapsed");
2033
+ });
2034
+ });
2035
+
2036
+ it("ArrowRight expands the collapsed group again", () => {
2037
+ headerDiv().focus();
2038
+ cy.realPress("ArrowLeft");
2039
+ headerDiv().should("have.attr", "aria-expanded", "false");
2040
+
2041
+ headerDiv().focus();
2042
+ cy.realPress("ArrowRight");
2043
+ headerDiv().should("have.attr", "aria-expanded", "true");
2044
+ });
2045
+
2046
+ // 5) Clicking the arrow <udex-button> still collapses/expands and never selects.
2047
+ it("clicking the arrow button collapses the group without selecting it", () => {
2048
+ headerDiv().should("have.attr", "aria-expanded", "true");
2049
+
2050
+ arrowButton().click();
2051
+
2052
+ headerDiv().should("have.attr", "aria-expanded", "false");
2053
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
2054
+ .find("udex-list-group-header").should("have.attr", "collapsed");
2055
+ headerDiv().should("not.have.attr", "aria-selected");
2056
+ });
2057
+
2058
+ it("clicking the arrow button a second time expands the group again", () => {
2059
+ arrowButton().click();
2060
+ headerDiv().should("have.attr", "aria-expanded", "false");
2061
+
2062
+ arrowButton().click();
2063
+ headerDiv().should("have.attr", "aria-expanded", "true");
2064
+ cy.get("#filter-menu-single-tree-collapsible").shadow()
2065
+ .find("udex-list-group-header").should("not.have.attr", "collapsed");
2066
+ });
2067
+ });
2068
+
2069
+ describe("Regression: SingleSelect + non-collapsible groups does NOT get tree pattern", () => {
2070
+ beforeEach(() => {
2071
+ cy.mount(filterMenuSingleSelectGroup);
2072
+ openMenu("filter-menu-single-group");
2073
+ });
2074
+
2075
+ it("host [udex-list] has NO aria-multiselectable attribute", () => {
2076
+ cy.get("#filter-menu-single-group").shadow()
2077
+ .find("[udex-list]").should("not.have.attr", "aria-multiselectable");
2078
+ });
2079
+
2080
+ // Updated: `accessibleRoleForList` no longer collapses to `"group"` for grouped
2081
+ // non-tree mode — it now returns `defaultAccessibleRole` (`"listbox"`) in every
2082
+ // non-tree case. JAWS enters forms mode on `role="listbox"` but stays in virtual
2083
+ // buffer mode on `role="group"`, so the fix is to keep the widget role stable.
2084
+ it("inner list div has role=\"listbox\", not \"tree\"", () => {
2085
+ cy.get("#filter-menu-single-group").shadow()
2086
+ .find("[udex-list]").shadow()
2087
+ .find("#udex-list").should("have.attr", "role", "listbox");
2088
+ });
2089
+
2090
+ // Updated: `accessible-role="tree"` on the group header host was replaced by the
2091
+ // `_tree-mode` boolean attribute. SingleSelect must not activate tree mode, so the
2092
+ // attribute must be absent (and the host role stays "group" for non-tree grouped
2093
+ // mode).
2094
+ it("group header host has role=\"group\" and no _tree-mode attribute", () => {
2095
+ cy.get("#filter-menu-single-group").shadow()
2096
+ .find("udex-list-group-header")
2097
+ .should("have.attr", "role", "group")
2098
+ .and("not.have.attr", "_tree-mode");
2099
+ });
2100
+
2101
+ // Updated: since the host is always role="none" and accessible-role drives the
2102
+ // shadow role, the meaningful regression check is against both the host's
2103
+ // accessible-role AND the shadow [part=item]'s rendered role.
2104
+ it("child items do NOT have accessible-role=\"treeitem\" nor shadow role=\"treeitem\"", () => {
2105
+ cy.get("#filter-menu-single-group").shadow()
2106
+ .find("udex-list-group-header")
2107
+ .find("udex-list-item").each($item => {
2108
+ cy.wrap($item).should("not.have.attr", "accessible-role", "treeitem");
2109
+ cy.wrap($item).shadow()
2110
+ .find("[part=item]").should("not.have.attr", "role", "treeitem");
2111
+ });
2112
+ });
2113
+
2114
+ it("child items have NO aria-checked (property returns undefined)", () => {
2115
+ cy.get("#filter-menu-single-group").shadow()
2116
+ .find("udex-list-group-header")
2117
+ .find("udex-list-item").eq(0).shadow()
2118
+ .find("[part=item]").should("not.have.attr", "aria-checked");
2119
+ });
2120
+ });
2121
+
2122
+ describe("Regression: MultiSelect WITHOUT groups does NOT get tree pattern", () => {
2123
+ beforeEach(() => {
2124
+ cy.mount(filterMenuMultiSelectFlat);
2125
+ openMenu("filter-menu-multi-flat");
2126
+ });
2127
+
2128
+ it("host [udex-list] has NO aria-multiselectable attribute", () => {
2129
+ cy.get("#filter-menu-multi-flat").shadow()
2130
+ .find("[udex-list]").should("not.have.attr", "aria-multiselectable");
2131
+ });
2132
+
2133
+ it("inner list div does NOT have role=\"tree\"", () => {
2134
+ cy.get("#filter-menu-multi-flat").shadow()
2135
+ .find("[udex-list]").shadow()
2136
+ .find("#udex-list").should("not.have.attr", "role", "tree");
2137
+ });
2138
+
2139
+ // Updated: check the meaningful shadow role in addition to the host attribute,
2140
+ // since the host role is always "none".
2141
+ it("flat items do NOT have accessible-role=\"treeitem\" nor shadow role=\"treeitem\"", () => {
2142
+ cy.get("#filter-menu-multi-flat").shadow()
2143
+ .find("udex-list-item").each($item => {
2144
+ cy.wrap($item).should("not.have.attr", "accessible-role", "treeitem");
2145
+ cy.wrap($item).shadow()
2146
+ .find("[part=item]").should("not.have.attr", "role", "treeitem");
2147
+ });
2148
+ });
2149
+
2150
+ it("flat items have no aria-checked (roleItem !== treeitem)", () => {
2151
+ cy.get("#filter-menu-multi-flat").shadow()
2152
+ .find("udex-list-item").eq(0).shadow()
2153
+ .find("[part=item]").should("not.have.attr", "aria-checked");
2154
+ });
2155
+ });
2156
+
2157
+ // Added: regression for FilterMenu.isTreeStructure Select-All exclusion.
2158
+ // A "Select All" header (UDExFilterMenuSelectAll) extends UDExListGroupHeaderBase
2159
+ // whose `isGroup` getter is true, so a MultiSelect FilterMenu with ONLY a Select All
2160
+ // header (and no real UDExFilterMenuGroupHeader) previously entered tree mode by
2161
+ // mistake — rendering role="tree"/role="treeitem"/aria-multiselectable. That is
2162
+ // semantically wrong (a Select All affordance is a bulk-select control over a flat
2163
+ // list, not a hierarchical group) and it broke the footer Tab-order focus tests.
2164
+ // isTreeStructure now excludes Select All headers (isSelectAllHeader === true) when
2165
+ // detecting groups, so this configuration stays a flat listbox.
2166
+ describe("Regression: MultiSelect + Select All header (no real group) does NOT get tree pattern", () => {
2167
+ beforeEach(() => {
2168
+ cy.mount(filterMenuSelectAll);
2169
+ cy.get("#filter-menu-select-all").shadow()
2170
+ .find(".udex-filter-menu__trigger").click();
2171
+ });
2172
+
2173
+ it("inner list div has role=\"listbox\", not \"tree\"", () => {
2174
+ cy.get("#filter-menu-select-all").shadow()
2175
+ .find("[udex-list]").shadow()
2176
+ .find("#udex-list").should("have.attr", "role", "listbox");
2177
+ });
2178
+
2179
+ it("host [udex-list] has NO aria-multiselectable attribute", () => {
2180
+ cy.get("#filter-menu-select-all").shadow()
2181
+ .find("[udex-list]").should("not.have.attr", "aria-multiselectable");
2182
+ });
2183
+
2184
+ it("a leaf item's inner [part=item] has role=\"option\", not \"treeitem\"", () => {
2185
+ // The leaf items are slotted light-DOM children of udex-list-select-all;
2186
+ // eq(0) is the first leaf (the Select All header's own inner item lives inside
2187
+ // the udex-list-select-all shadow, so it is not matched by this light-DOM find).
2188
+ cy.get("#filter-menu-select-all").shadow()
2189
+ .find(".udex-filter-menu__list")
2190
+ .find("udex-list-item").eq(0).shadow()
2191
+ .find("[part=item]").should("have.attr", "role", "option");
2192
+ });
2193
+
2194
+ it("a leaf item's inner [part=item] has NO aria-checked (not a treeitem)", () => {
2195
+ cy.get("#filter-menu-select-all").shadow()
2196
+ .find(".udex-filter-menu__list")
2197
+ .find("udex-list-item").eq(0).shadow()
2198
+ .find("[part=item]").should("not.have.attr", "aria-checked");
2199
+ });
2200
+ });
2201
+ });
1244
2202
  });