@sapudex/web-components 2.61.0 → 2.62.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 (156) hide show
  1. package/cypress/specs/AccordionSpotlight.cy.tsx +43 -2
  2. package/cypress/specs/Carousel.cy.tsx +88 -0
  3. package/cypress/specs/CountrySelector.cy.tsx +233 -6
  4. package/cypress/specs/FilterMenu.cy.tsx +39 -45
  5. package/cypress/specs/List.cy.tsx +624 -10
  6. package/cypress/specs/ListItem.cy.tsx +50 -0
  7. package/cypress/specs/SelectBox.cy.tsx +239 -131
  8. package/cypress/specs/SelectBoxAccessibility.cy.tsx +3109 -0
  9. package/cypress/specs/TagGroup.cy.tsx +168 -0
  10. package/dist/AccordionSpotlight.d.ts +7 -2
  11. package/dist/AccordionSpotlight.js +31 -6
  12. package/dist/AccordionSpotlight.js.map +1 -1
  13. package/dist/AccordionSpotlightItem.d.ts +3 -1
  14. package/dist/AccordionSpotlightItem.js +3 -1
  15. package/dist/AccordionSpotlightItem.js.map +1 -1
  16. package/dist/Carousel.d.ts +1 -0
  17. package/dist/Carousel.js +19 -3
  18. package/dist/Carousel.js.map +1 -1
  19. package/dist/CountrySelector.js +1 -3
  20. package/dist/CountrySelector.js.map +1 -1
  21. package/dist/List.d.ts +2 -0
  22. package/dist/List.js +22 -4
  23. package/dist/List.js.map +1 -1
  24. package/dist/ListGroupHeader.js +5 -1
  25. package/dist/ListGroupHeader.js.map +1 -1
  26. package/dist/ListGroupHeaderBase.d.ts +2 -0
  27. package/dist/ListGroupHeaderBase.js +15 -2
  28. package/dist/ListGroupHeaderBase.js.map +1 -1
  29. package/dist/ListGroupHeaderTemplate.js +14 -22
  30. package/dist/ListGroupHeaderTemplate.js.map +1 -1
  31. package/dist/ListItem.d.ts +1 -2
  32. package/dist/ListItem.js +14 -12
  33. package/dist/ListItem.js.map +1 -1
  34. package/dist/ListItemBase.d.ts +5 -0
  35. package/dist/ListItemBase.js +11 -1
  36. package/dist/ListItemBase.js.map +1 -1
  37. package/dist/ListItemTemplate.js +1 -1
  38. package/dist/ListItemTemplate.js.map +1 -1
  39. package/dist/ListSelectAllTemplate.js +1 -1
  40. package/dist/ListSelectAllTemplate.js.map +1 -1
  41. package/dist/ListTemplate.js +2 -1
  42. package/dist/ListTemplate.js.map +1 -1
  43. package/dist/Search.d.ts +13 -3
  44. package/dist/Search.js +36 -11
  45. package/dist/Search.js.map +1 -1
  46. package/dist/SearchableListBase.d.ts +2 -0
  47. package/dist/SearchableListBase.js +13 -3
  48. package/dist/SearchableListBase.js.map +1 -1
  49. package/dist/SearchableListBaseTemplate.js +42 -33
  50. package/dist/SearchableListBaseTemplate.js.map +1 -1
  51. package/dist/SelectBase.d.ts +18 -33
  52. package/dist/SelectBase.js +124 -139
  53. package/dist/SelectBase.js.map +1 -1
  54. package/dist/SelectBox.d.ts +3 -0
  55. package/dist/SelectBox.js +19 -7
  56. package/dist/SelectBox.js.map +1 -1
  57. package/dist/SelectBoxTemplate.js +3 -3
  58. package/dist/SelectBoxTemplate.js.map +1 -1
  59. package/dist/SideNavigationItem.js +2 -0
  60. package/dist/SideNavigationItem.js.map +1 -1
  61. package/dist/SideNavigationItemTemplate.d.ts +2 -0
  62. package/dist/SideNavigationItemTemplate.js +15 -0
  63. package/dist/SideNavigationItemTemplate.js.map +1 -0
  64. package/dist/SideNavigationMobile.js +1 -2
  65. package/dist/SideNavigationMobile.js.map +1 -1
  66. package/dist/SideNavigationSubItem.js +2 -1
  67. package/dist/SideNavigationSubItem.js.map +1 -1
  68. package/dist/SideNavigationSubItemTemplate.js +1 -2
  69. package/dist/SideNavigationSubItemTemplate.js.map +1 -1
  70. package/dist/TagGroup.d.ts +3 -0
  71. package/dist/TagGroup.js +31 -0
  72. package/dist/TagGroup.js.map +1 -1
  73. package/dist/TagGroupTemplate.js +1 -1
  74. package/dist/TagGroupTemplate.js.map +1 -1
  75. package/dist/assets/{InputSuggestions-1OnUn6pM.js → InputSuggestions-BPKrPqPb.js} +1 -1
  76. package/dist/assets/{ListItemStandardExpandableTextTemplate-CoBAMp_v.js → ListItemStandardExpandableTextTemplate-DkdOcCxC.js} +1 -1
  77. package/dist/assets/{bundle.esm-BqaVWFKf.js → bundle.esm-17Yt5gDr.js} +232 -233
  78. package/dist/css/themes/AccordionSpotlight.css +1 -1
  79. package/dist/css/themes/ListGroupHeader.css +1 -1
  80. package/dist/css/themes/SideNavigationItem.css +1 -1
  81. package/dist/css/themes/SideNavigationMobile.css +1 -1
  82. package/dist/css/themes/SideNavigationSubItem.css +1 -1
  83. package/dist/css/themes/Table.css +1 -1
  84. package/dist/custom-elements-internal.json +7 -7
  85. package/dist/custom-elements.json +7 -7
  86. package/dist/generated/i18n/i18n-defaults.d.ts +2 -1
  87. package/dist/generated/i18n/i18n-defaults.js +2 -1
  88. package/dist/generated/i18n/i18n-defaults.js.map +1 -1
  89. package/dist/generated/themes/AccordionSpotlight.css.d.ts +1 -1
  90. package/dist/generated/themes/AccordionSpotlight.css.js +1 -1
  91. package/dist/generated/themes/AccordionSpotlight.css.js.map +1 -1
  92. package/dist/generated/themes/ListGroupHeader.css.d.ts +1 -1
  93. package/dist/generated/themes/ListGroupHeader.css.js +1 -1
  94. package/dist/generated/themes/ListGroupHeader.css.js.map +1 -1
  95. package/dist/generated/themes/SideNavigationItem.css.d.ts +1 -1
  96. package/dist/generated/themes/SideNavigationItem.css.js +1 -1
  97. package/dist/generated/themes/SideNavigationItem.css.js.map +1 -1
  98. package/dist/generated/themes/SideNavigationMobile.css.d.ts +1 -1
  99. package/dist/generated/themes/SideNavigationMobile.css.js +1 -1
  100. package/dist/generated/themes/SideNavigationMobile.css.js.map +1 -1
  101. package/dist/generated/themes/SideNavigationSubItem.css.d.ts +1 -1
  102. package/dist/generated/themes/SideNavigationSubItem.css.js +1 -1
  103. package/dist/generated/themes/SideNavigationSubItem.css.js.map +1 -1
  104. package/dist/generated/themes/Table.css.d.ts +1 -1
  105. package/dist/generated/themes/Table.css.js +1 -1
  106. package/dist/generated/themes/Table.css.js.map +1 -1
  107. package/dist/messagebundle.properties +3 -0
  108. package/dist/vscode.html-custom-data.json +4 -4
  109. package/dist/web-types.json +9 -9
  110. package/package.json +1 -1
  111. package/src/AccordionSpotlight.ts +35 -6
  112. package/src/AccordionSpotlightItem.ts +3 -1
  113. package/src/Carousel.ts +22 -3
  114. package/src/CountrySelector.ts +1 -3
  115. package/src/List.ts +21 -4
  116. package/src/ListGroupHeader.ts +5 -1
  117. package/src/ListGroupHeaderBase.ts +16 -2
  118. package/src/ListGroupHeaderTemplate.tsx +29 -47
  119. package/src/ListItem.ts +14 -13
  120. package/src/ListItemBase.ts +14 -1
  121. package/src/ListItemTemplate.tsx +4 -4
  122. package/src/ListSelectAllTemplate.tsx +10 -8
  123. package/src/ListTemplate.tsx +14 -10
  124. package/src/Search.ts +48 -13
  125. package/src/SearchableListBase.ts +15 -3
  126. package/src/SearchableListBaseTemplate.tsx +51 -36
  127. package/src/SelectBase.ts +130 -144
  128. package/src/SelectBox.ts +21 -7
  129. package/src/SelectBoxTemplate.tsx +10 -9
  130. package/src/SideNavigationItem.ts +2 -0
  131. package/src/SideNavigationItemTemplate.tsx +97 -0
  132. package/src/SideNavigationMobile.ts +1 -2
  133. package/src/SideNavigationSubItem.ts +2 -1
  134. package/src/SideNavigationSubItemTemplate.tsx +7 -6
  135. package/src/TagGroup.ts +32 -0
  136. package/src/TagGroupTemplate.tsx +1 -1
  137. package/src/generated/i18n/i18n-defaults.ts +2 -2
  138. package/src/generated/themes/AccordionSpotlight.css.ts +1 -1
  139. package/src/generated/themes/ListGroupHeader.css.ts +1 -1
  140. package/src/generated/themes/SideNavigationItem.css.ts +1 -1
  141. package/src/generated/themes/SideNavigationMobile.css.ts +1 -1
  142. package/src/generated/themes/SideNavigationSubItem.css.ts +1 -1
  143. package/src/generated/themes/Table.css.ts +1 -1
  144. package/src/i18n/messagebundle.properties +3 -0
  145. package/src/themes/AccordionSpotlight.css +9 -9
  146. package/src/themes/ListGroupHeader.css +2 -43
  147. package/src/themes/SideNavigationItem.css +56 -6
  148. package/src/themes/SideNavigationMobile.css +61 -15
  149. package/src/themes/SideNavigationSubItem.css +14 -109
  150. package/src/themes/Table.css +6 -1
  151. package/dist/css/themes/SideNavigationMobileDropdown.css +0 -1
  152. package/dist/generated/themes/SideNavigationMobileDropdown.css.d.ts +0 -2
  153. package/dist/generated/themes/SideNavigationMobileDropdown.css.js +0 -8
  154. package/dist/generated/themes/SideNavigationMobileDropdown.css.js.map +0 -1
  155. package/src/generated/themes/SideNavigationMobileDropdown.css.ts +0 -9
  156. package/src/themes/SideNavigationMobileDropdown.css +0 -36
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sapudex/web-components",
4
- "version": "2.61.0",
4
+ "version": "2.62.0",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
7
7
  },
@@ -75,13 +75,14 @@ class UDExAccordionSpotlight extends UI5Element {
75
75
  };
76
76
 
77
77
  /**
78
- * The index of the currently expanded item (0-based).
79
- * Derived from items' <code>selected</code> attribute; kept in sync by the parent.
78
+ * The index of the currently expanded item (0-based), or <code>-1</code> when
79
+ * no item is expanded. Derived from items' <code>selected</code> attribute;
80
+ * kept in sync by the parent.
80
81
  *
81
82
  * @private
82
83
  */
83
84
  @property({ type: Number, noAttribute: true })
84
- _selectedIndex: number = 0;
85
+ _selectedIndex: number = -1;
85
86
 
86
87
  /**
87
88
  * Vertical alignment of the navigation sidebar relative to the content panel.
@@ -155,9 +156,18 @@ class UDExAccordionSpotlight extends UI5Element {
155
156
  /**
156
157
  * The slotted AccordionSpotlightItem elements.
157
158
  *
159
+ * <code>invalidateOnChildChange</code> lets the component react when an item's
160
+ * <code>selected</code> property is set programmatically, so
161
+ * <code>item.selected = true</code> switches the expanded item at runtime.
162
+ *
158
163
  * @public
159
164
  */
160
- @slot({ type: HTMLElement, "default": true, individualSlots: true })
165
+ @slot({
166
+ type: HTMLElement,
167
+ "default": true,
168
+ individualSlots: true,
169
+ invalidateOnChildChange: true,
170
+ })
161
171
  items!: DefaultSlot<AccordionSpotlightItem>;
162
172
 
163
173
  /**
@@ -235,8 +245,27 @@ class UDExAccordionSpotlight extends UI5Element {
235
245
  return;
236
246
  }
237
247
 
238
- const foundIndex = this.items.findIndex(item => item.selected);
239
- const activeIndex = foundIndex === -1 ? 0 : foundIndex;
248
+ const selectedIndexes = this.items.reduce<number[]>((acc, item, index) => {
249
+ if (item.selected) {
250
+ acc.push(index);
251
+ }
252
+ return acc;
253
+ }, []);
254
+
255
+ // Prefer a selected item that differs from the currently tracked index — it
256
+ // signals a selection requested via `item.selected = true`. Otherwise keep
257
+ // the current index if it is still selected. When no item is selected, leave
258
+ // the component with no active item (-1) rather than forcing the first one.
259
+ let activeIndex: number;
260
+ const newlySelected = selectedIndexes.find(index => index !== this._selectedIndex);
261
+ if (newlySelected !== undefined) {
262
+ activeIndex = newlySelected;
263
+ } else if (selectedIndexes.includes(this._selectedIndex)) {
264
+ activeIndex = this._selectedIndex;
265
+ } else {
266
+ activeIndex = selectedIndexes[0] ?? -1;
267
+ }
268
+
240
269
  this._selectedIndex = activeIndex;
241
270
 
242
271
  this.items.forEach((item, index) => {
@@ -57,7 +57,9 @@ class UDExAccordionSpotlightItem extends UI5Element {
57
57
 
58
58
  /**
59
59
  * Whether this item is currently expanded.
60
- * Set on the element directly to control the initial selection.
60
+ * Set on the element directly to control the initial selection, or at runtime
61
+ * (<code>item.selected = true</code>) to expand this item programmatically —
62
+ * the parent deselects the other items automatically.
61
63
  *
62
64
  * @default false
63
65
  * @public
package/src/Carousel.ts CHANGED
@@ -369,8 +369,7 @@ class UDExCarousel extends UI5Element {
369
369
  return;
370
370
  }
371
371
 
372
- this._carousel.navigateArrowLeft();
373
- this._calculateDisabledButtonsState();
372
+ this._navigateToPage(-1);
374
373
  }
375
374
 
376
375
  _nextButtonClick() {
@@ -378,7 +377,27 @@ class UDExCarousel extends UI5Element {
378
377
  return;
379
378
  }
380
379
 
381
- this._carousel.navigateArrowRight();
380
+ this._navigateToPage(1);
381
+ }
382
+
383
+ _navigateToPage(direction: 1 | -1) {
384
+ const carousel = this._carousel;
385
+ if (!carousel) {
386
+ return;
387
+ }
388
+
389
+ let newPageIndex = carousel._currentPageIndex + direction;
390
+
391
+ if (this.cyclic) {
392
+ const lastPageIndex = carousel.items.length - carousel.effectiveItemsPerPage;
393
+ if (newPageIndex > lastPageIndex) {
394
+ newPageIndex = 0;
395
+ } else if (newPageIndex < 0) {
396
+ newPageIndex = carousel.items.length - 1;
397
+ }
398
+ }
399
+
400
+ carousel._changePageIndex(newPageIndex);
382
401
  this._calculateDisabledButtonsState();
383
402
  }
384
403
 
@@ -154,9 +154,7 @@ class UDExCountrySelector extends UDExSelectBase {
154
154
  }
155
155
 
156
156
  handleDropdownClose(): void {
157
- this._setActiveDescendant(null);
158
- this._open = false;
159
- this.fireEventDropdownClose();
157
+ this._finishDropdownClose();
160
158
  }
161
159
 
162
160
  handleOutsideClick(): void {
package/src/List.ts CHANGED
@@ -174,6 +174,14 @@ class UDExList extends UI5Element {
174
174
  static i18nBundle: I18nBundle;
175
175
 
176
176
  onBeforeRendering(): void {
177
+ if (this._hasHostAriaRole) {
178
+ const label = this.modeSelected ? this.ariaLabelModeText : this.singleContentAriaLabel;
179
+ if (label) {
180
+ this.setAttribute("aria-label", label);
181
+ } else {
182
+ this.removeAttribute("aria-label");
183
+ }
184
+ }
177
185
  this.prepareListItemsMode();
178
186
  this._focusHandler.resetMatchingItems();
179
187
  this._focusHandler._focusableListItems = this.getKeyboardNavigationItems();
@@ -495,7 +503,7 @@ class UDExList extends UI5Element {
495
503
  }
496
504
 
497
505
  prepareListItemsMode(): void {
498
- const treeMode = this._hasCollapsibleGroups;
506
+ const treeMode = this.modeSelected && this._hasCollapsibleGroups;
499
507
  this.getItemsFromList().forEach((item, index) => {
500
508
  item.mode = this.mode;
501
509
  item.controlled = this.controlled;
@@ -503,10 +511,14 @@ class UDExList extends UI5Element {
503
511
  item._icon = this.itemIcon;
504
512
  item._tabIndex = this.getTabIndex(index, item);
505
513
  if (item.isGroup) {
506
- // Only upgrade _treeMode to true; never force it to false, since
507
- // SearchableListBaseTemplate may have set _tree-mode=true externally.
508
- if (treeMode) {
514
+ // Track only List-owned upgrades so mode changes can reset them without
515
+ // removing tree semantics supplied by SelectBox/SearchableListBase.
516
+ if (treeMode && !item._treeMode) {
509
517
  item._treeMode = true;
518
+ item.setAttribute("_list-managed-tree-mode", "");
519
+ } else if (!treeMode && item.hasAttribute("_list-managed-tree-mode")) {
520
+ item._treeMode = false;
521
+ item.removeAttribute("_list-managed-tree-mode");
510
522
  }
511
523
  } else if (treeMode) {
512
524
  // Set accessibleRole for standalone List tree mode.
@@ -615,6 +627,11 @@ class UDExList extends UI5Element {
615
627
  return this._hasCollapsibleGroups ? "tree" : this.accessibleRole;
616
628
  }
617
629
 
630
+ /** @private Whether the list's owning role is already exposed on its host. */
631
+ get _hasHostAriaRole(): boolean {
632
+ return ["tree", "listbox", "list"].includes(this.getAttribute("role") || "");
633
+ }
634
+
618
635
  /**
619
636
  * Role for SingleContent mode. listbox/option is for selection widgets only;
620
637
  * SingleContent is informational so "list"/"listitem" is the correct semantic.
@@ -7,7 +7,7 @@ import {
7
7
  import "@ui5/webcomponents-icons/dist/slim-arrow-right.js";
8
8
  import UDExListGroupHeaderBase from "./ListGroupHeaderBase.js";
9
9
  import ListGroupHeaderTemplate from "./ListGroupHeaderTemplate.js";
10
- import { UDExListItemSize } from "./types/List.js";
10
+ import { UDExListItemSize, UDExListMode } from "./types/List.js";
11
11
  import type { UDExListItemBaseEvents } from "./ListItemBase.js";
12
12
  import type UDExListItemBase from "./ListItemBase.js";
13
13
 
@@ -85,6 +85,10 @@ class UDExListGroupHeader extends UDExListGroupHeaderBase {
85
85
  }
86
86
 
87
87
  handleKeyDown(e: KeyboardEvent) {
88
+ if (this.mode === UDExListMode.SingleContent && (isEnter(e) || isSpace(e))) {
89
+ this.handleCollapseIconClick(e);
90
+ return;
91
+ }
88
92
  if (isLeft(e) && !this.collapsed) {
89
93
  this.handleCollapseIconClick(e);
90
94
  return;
@@ -4,6 +4,7 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
4
4
  import property from "@ui5/webcomponents-base/dist/decorators/property.js";
5
5
  import jsxRenderer from "@ui5/webcomponents-base/renderer/JsxRenderer.js";
6
6
  import UDExListItemBase from "./ListItemBase.js";
7
+ import { UDExListMode } from "./types/List.js";
7
8
 
8
9
  const listGroupHeaderClass: string = "udex-list__group-header";
9
10
 
@@ -220,7 +221,14 @@ class UDExListGroupHeaderBase extends UDExListItemBase {
220
221
  }
221
222
 
222
223
  get isTreeAccessibleRole(): boolean {
223
- return this._treeMode;
224
+ // Host-owned tree semantics must not depend on the child's rendering order
225
+ // or a subsequent List mode-preparation pass.
226
+ return this._treeMode || (this._useHostAriaRole && this.getAttribute("role") === "treeitem");
227
+ }
228
+
229
+ /** @private Informational groups outside a host-owned or externally supplied tree. */
230
+ get isSingleContentList(): boolean {
231
+ return this.mode === UDExListMode.SingleContent && !this._treeMode && !this._useHostAriaRole;
224
232
  }
225
233
 
226
234
  // FilterMenu real-focus SingleSelect non-collapsible group: its options are
@@ -263,6 +271,9 @@ class UDExListGroupHeaderBase extends UDExListItemBase {
263
271
  if (this._useHostAriaRole) {
264
272
  return "presentation";
265
273
  }
274
+ if (this.isSingleContentList) {
275
+ return "listitem";
276
+ }
266
277
  // Tree mode keeps the treeitem/group nesting; per-group-listbox mode moves the
267
278
  // single labelled role="listbox" onto the inner options wrapper — the outer div
268
279
  // is transparent so screen readers don't announce a redundant "group" around it.
@@ -273,7 +284,10 @@ class UDExListGroupHeaderBase extends UDExListItemBase {
273
284
  }
274
285
 
275
286
  get contentRegionRole(): string {
276
- return this._treeMode ? "group" : "region";
287
+ if (this.isSingleContentList) {
288
+ return "list";
289
+ }
290
+ return this.isTreeAccessibleRole ? "group" : "region";
277
291
  }
278
292
 
279
293
  get nonCollapsibleContainerRole(): string {
@@ -57,6 +57,7 @@ function SingleCollapsibleHeaderRow(this: UDExListGroupHeader) {
57
57
  class="udex-list__header-title"
58
58
  {...({ innerHTML: this.label } as object)}>
59
59
  </span>
60
+ {this.supportingText && " "}
60
61
  {this.supportingText && (
61
62
  <span id="group-supporting-text" class={`udex-list__supporting-text ${this.sizeClass}`}>{this.supportingText}</span>
62
63
  )}
@@ -79,48 +80,32 @@ function SingleCollapsibleHeaderRow(this: UDExListGroupHeader) {
79
80
  }
80
81
 
81
82
  function SingleGroupHeaderCollapsible(this: UDExListGroupHeader) {
82
- // Conformant WAI-ARIA tree (FilterMenu real focus): nest the child role="group"
83
- // INSIDE this treeitem so screen readers count children per group natively — no
84
- // explicit aria-posinset/aria-setsize (which JAWS distrusts). Excludes SelectBox
85
- // (aria-activedescendant, _useHostAriaRole) and non-tree modes, which keep the flat
86
- // structure with the content region emitted as a sibling by the template body.
87
- const nested = this.isTreeAccessibleRole && !this._useHostAriaRole;
88
83
  // In aria-activedescendant mode (SelectBox) the HOST element is the treeitem and
89
84
  // already carries aria-expanded + the accessible name. This inner div must then be
90
85
  // presentational (singleCollapsibleHeaderRole) and must NOT duplicate those states,
91
86
  // otherwise it is a second treeitem nested inside the host treeitem with no owning
92
- // tree/group (axe/SiteImprove aria-required-parent). It stays focusable, though:
93
- // clicking the collapsible-header body relies on it receiving DOM focus to redirect
94
- // focus back to the combobox input — the same focusable, role="presentation" shape
95
- // the list-item [part=item] divs already use. The real-focus (FilterMenu) path keeps
87
+ // tree/group (axe/SiteImprove aria-required-parent). It has no tab stop in that
88
+ // mode: DOM focus stays on the combobox input. Real-focus (FilterMenu) keeps
96
89
  // the treeitem semantics plus controls/labelling on this div.
97
90
  const headerAriaAttrs = this._useHostAriaRole ? {} : {
98
91
  "aria-expanded": this.expanded,
99
92
  "aria-controls": this.ariaControlsId,
100
93
  "aria-labelledby": this.collapsibleHeaderLabelledBy,
94
+ // Own the sibling group without including its visible options in this row's label.
95
+ "aria-owns": this.isTreeAccessibleRole ? this.ariaControlsId : undefined,
101
96
  };
102
97
  return (
103
98
  <div
104
- class={`udex-list__group-content ${nested ? "udex-list__group-content--nested " : ""}udex-list__group-header-wrap ${this.sizeClass}`}
99
+ class={`udex-list__group-content udex-list__group-header-wrap ${this.sizeClass}`}
105
100
  part="group-item"
106
101
  id={`${this._id}_group-content__focusable`}
107
102
  role={this.singleCollapsibleHeaderRole as AriaRole}
108
- tabIndex={0}
103
+ tabIndex={this._useHostAriaRole ? undefined : 0}
104
+ onMouseDown={this.handleCollapseIconMouseDown}
109
105
  onKeyDown={this.handleKeyDown}
110
106
  {...headerAriaAttrs}
111
107
  _tab-index={Number(this._effectiveTabIndex)}>
112
- {nested ? (
113
- <>
114
- <div class="udex-list__group-header-row">
115
- {SingleCollapsibleHeaderRow.call(this)}
116
- </div>
117
- <div id={this.ariaControlsId} role={this.contentRegionRole as AriaRole} class="udex-list__group-list__content">
118
- <slot></slot>
119
- </div>
120
- </>
121
- ) : (
122
- SingleCollapsibleHeaderRow.call(this)
123
- )}
108
+ {SingleCollapsibleHeaderRow.call(this)}
124
109
  </div>
125
110
  );
126
111
  }
@@ -155,6 +140,7 @@ function MultiOrRadioGroupHeader(this: UDExListGroupHeader) {
155
140
  size={this.size}
156
141
  supportingText={this.supportingText}
157
142
  accessible-role={this.nonCollapsibleHeaderAccessibleRole}
143
+ _useHostAriaRole={this._useHostAriaRole}
158
144
  _additional-class-name={this._additionalClassName}
159
145
  _tab-index={Number(this._effectiveTabIndex)}
160
146
  _aria-pos-in-set={this._ariaPosInSet}
@@ -169,12 +155,13 @@ function MultiOrRadioGroupHeaderCollapsible(this: UDExListGroupHeader) {
169
155
  if (this.isNonActiveHeader) {
170
156
  return (
171
157
  <header
172
- role="button"
158
+ role={this._useHostAriaRole ? "presentation" : "button"}
173
159
  id="group-heading"
174
160
  class="udex-list__inactive-header udex-list__group-header-wrap"
175
- tabIndex={Number(this._effectiveTabIndex)}
176
- aria-expanded={this.expanded}
177
- aria-controls={this.ariaControlsId}>
161
+ tabIndex={this._useHostAriaRole ? undefined : Number(this._effectiveTabIndex)}
162
+ aria-expanded={this._useHostAriaRole ? undefined : this.expanded}
163
+ aria-controls={this._useHostAriaRole ? undefined : this.ariaControlsId}
164
+ onMouseDown={this.handleCollapseIconMouseDown}>
178
165
  <div class="udex-list__inactive-header-content">
179
166
  <span class="udex-list__header-title" {...({ innerHTML: this.label } as object)}></span>
180
167
  {this.supportingText && (
@@ -196,13 +183,13 @@ function MultiOrRadioGroupHeaderCollapsible(this: UDExListGroupHeader) {
196
183
  </header>
197
184
  );
198
185
  }
186
+ // Keep this host transparent: aria-expanded/aria-controls would override role="none"
187
+ // and interrupt the inner treeitem's required context. The inner row exposes expanded.
199
188
  return (
200
189
  <UDExListItem
201
190
  id="group-heading"
202
191
  part="group-item"
203
192
  role="none"
204
- aria-expanded={this.expanded}
205
- aria-controls={this.ariaControlsId}
206
193
  selected={this.selected}
207
194
  indeterminate={this.indeterminate}
208
195
  collapsible={this.collapsible}
@@ -217,6 +204,7 @@ function MultiOrRadioGroupHeaderCollapsible(this: UDExListGroupHeader) {
217
204
  size={this.size}
218
205
  supportingText={this.supportingText}
219
206
  accessible-role={this.collapsibleHeaderAccessibleRole}
207
+ _useHostAriaRole={this._useHostAriaRole}
220
208
  onAccordionIconClick={this.handleCollapseIconClick as EventListener}
221
209
  _additional-class-name={this._additionalClassName}
222
210
  _tab-index={Number(this._effectiveTabIndex)}
@@ -229,16 +217,12 @@ function MultiOrRadioGroupHeaderCollapsible(this: UDExListGroupHeader) {
229
217
  }
230
218
 
231
219
  export default function ListGroupHeaderTemplate(this: UDExListGroupHeader) {
232
- // Single-select real-focus tree: SingleGroupHeaderCollapsible now emits the
233
- // role="group" content region as a CHILD of the treeitem (conformant nesting),
234
- // so the template body must not also emit it as a sibling.
235
- const singleSelectNested = this.isTreeAccessibleRole && !this.useListItemHeader && !this._useHostAriaRole;
236
220
  return (
237
221
  <div
238
222
  part="item"
239
223
  class="udex-list__group"
240
224
  role={this.groupWrapperRole as AriaRole}
241
- aria-labelledby={(!this.isTreeAccessibleRole && !this.isPerGroupListbox) ? "group-heading" : undefined}>
225
+ aria-labelledby={(!this._useHostAriaRole && !this.isTreeAccessibleRole && !this.isPerGroupListbox) ? "group-heading" : undefined}>
242
226
  {this.collapsible ? (
243
227
  <div
244
228
  class="udex-list__group-list"
@@ -248,17 +232,15 @@ export default function ListGroupHeaderTemplate(this: UDExListGroupHeader) {
248
232
  ? MultiOrRadioGroupHeaderCollapsible.call(this)
249
233
  : SingleGroupHeaderCollapsible.call(this)
250
234
  }
251
- {!singleSelectNested && (
252
- <div id={this.ariaControlsId} role={this.contentRegionRole as AriaRole} class="udex-list__group-list__content">
253
- {this.isTreeAccessibleRole ? (
235
+ <div id={this.ariaControlsId} role={this.contentRegionRole as AriaRole} class="udex-list__group-list__content">
236
+ {this.isTreeAccessibleRole || this.isSingleContentList ? (
237
+ <slot></slot>
238
+ ) : (
239
+ <div role="listbox" aria-label="group">
254
240
  <slot></slot>
255
- ) : (
256
- <div role="listbox" aria-label="group">
257
- <slot></slot>
258
- </div>
259
- )}
260
- </div>
261
- )}
241
+ </div>
242
+ )}
243
+ </div>
262
244
  </div>
263
245
  ) : (
264
246
  <div
@@ -270,8 +252,8 @@ export default function ListGroupHeaderTemplate(this: UDExListGroupHeader) {
270
252
  ? MultiOrRadioGroupHeader.call(this)
271
253
  : SingleGroupHeader.call(this)
272
254
  }
273
- {this.isTreeAccessibleRole ? (
274
- <div role="group">
255
+ {this.isTreeAccessibleRole || this.isSingleContentList ? (
256
+ <div role={this.contentRegionRole as AriaRole}>
275
257
  <slot></slot>
276
258
  </div>
277
259
  ) : this.isPerGroupListbox ? (
package/src/ListItem.ts CHANGED
@@ -21,7 +21,7 @@ import UDExRadioButton from "./RadioButton.js";
21
21
  import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
22
22
  import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
23
23
  import {
24
- INDETERMINATE, SELECTED, UNSELECTED, RATING_INDICATOR_TEXT, RATING_COUNT_TITLE, TOTAL,
24
+ INDETERMINATE, SELECTED, UNSELECTED,
25
25
  } from "./generated/i18n/i18n-defaults.js";
26
26
  import translate from "./utils/translate.js";
27
27
 
@@ -267,7 +267,10 @@ class UDExListItem extends UDExListItemBase {
267
267
  document.removeEventListener("mouseup", this.deactivate);
268
268
  }
269
269
 
270
- handleMouseDown() {
270
+ handleMouseDown(e: MouseEvent) {
271
+ if (this._useHostAriaRole) {
272
+ e.preventDefault();
273
+ }
271
274
  this.activate();
272
275
  }
273
276
 
@@ -276,6 +279,10 @@ class UDExListItem extends UDExListItemBase {
276
279
  }
277
280
 
278
281
  handleKeyUp(e: KeyboardEvent) {
282
+ // Space activates a native button on keyup; the row must not cancel it.
283
+ if (this.firstIconElement === e.target || this.secondIconElement === e.target || (e.target as HTMLElement).classList.contains("udex-list__group-heading__icon")) {
284
+ return;
285
+ }
279
286
  e.preventDefault();
280
287
  if (isSpace(e) || isEnter(e)) {
281
288
  this.deactivate();
@@ -372,7 +379,8 @@ class UDExListItem extends UDExListItemBase {
372
379
  if (this._useHostAriaRole) {
373
380
  return "presentation";
374
381
  }
375
- if (this.accessibleRole.length > 0) {
382
+ // Removing the reflected attribute can reset the property to null.
383
+ if (this.accessibleRole) {
376
384
  return this.accessibleRole;
377
385
  }
378
386
  // SingleContent is informational (not a selection widget) — listitem is semantically correct
@@ -380,11 +388,11 @@ class UDExListItem extends UDExListItemBase {
380
388
  }
381
389
 
382
390
  get effectiveRole(): string {
383
- return this.accessibleRole.length > 0 ? this.accessibleRole : listItemRole;
391
+ return this.accessibleRole || listItemRole;
384
392
  }
385
393
 
386
394
  get ariaCheckedValue(): "true" | "false" | "mixed" | undefined {
387
- if (this.effectiveRole !== "treeitem") {
395
+ if (this.roleItem !== "treeitem") {
388
396
  return undefined;
389
397
  }
390
398
  // Only MultiSelect treeitems expose aria-checked; SingleSelect uses aria-selected.
@@ -424,7 +432,7 @@ class UDExListItem extends UDExListItemBase {
424
432
  // actually expand/collapse. Non-collapsible group headers must NOT report an
425
433
  // expanded/collapsed state (nothing to toggle); the tree hierarchy is conveyed
426
434
  // structurally by the nested role="group", not by aria-expanded.
427
- if (!this.collapsible) {
435
+ if (this._useHostAriaRole || !this.collapsible) {
428
436
  return undefined;
429
437
  }
430
438
  return this.expanded ? "true" : "false";
@@ -460,13 +468,6 @@ class UDExListItem extends UDExListItemBase {
460
468
  }
461
469
  return `${this.selected ? translate(UDExListItem, SELECTED) : translate(UDExListItem, UNSELECTED)} `;
462
470
  }
463
-
464
- get ratingAriaLabel(): string {
465
- const ratingText = this.ratingCount ? `${translate(UDExListItem, RATING_COUNT_TITLE)} ${this.ratingCount}` : "";
466
- const totalText = this.totalCount ? `${translate(UDExListItem, TOTAL)} ${this.totalCount}` : "";
467
- const ratingTitleText = translate(UDExListItem, RATING_INDICATOR_TEXT);
468
- return [...[ratingText, totalText].filter(Boolean), `${ratingTitleText} ${this.rating} of ${this.ratingMax}`].join(", ");
469
- }
470
471
  }
471
472
 
472
473
  UDExListItem.define();
@@ -8,7 +8,9 @@ import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.j
8
8
  import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
9
9
  import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
10
10
  import translate from "./utils/translate.js";
11
- import { COLLAPSE, EXPAND } from "./generated/i18n/i18n-defaults.js";
11
+ import {
12
+ COLLAPSE, EXPAND, RATING_COUNT_TITLE, RATING_INDICATOR_TEXT, TOTAL,
13
+ } from "./generated/i18n/i18n-defaults.js";
12
14
 
13
15
  export type UDExListItemBaseEvents = {
14
16
  "first-icon-click": UDExListItemBaseEventDetail,
@@ -407,6 +409,17 @@ class UDExListItemBase extends UI5Element {
407
409
  get hasRating(): boolean {
408
410
  return !!this.rating;
409
411
  }
412
+
413
+ /**
414
+ * Shared rating description for rendered options and their source items.
415
+ * @private
416
+ */
417
+ get ratingAriaLabel(): string {
418
+ const ratingText = this.ratingCount ? `${translate(UDExListItemBase, RATING_COUNT_TITLE)} ${this.ratingCount}` : "";
419
+ const totalText = this.totalCount ? `${translate(UDExListItemBase, TOTAL)} ${this.totalCount}` : "";
420
+ const ratingTitleText = translate(UDExListItemBase, RATING_INDICATOR_TEXT);
421
+ return [...[ratingText, totalText].filter(Boolean), `${ratingTitleText} ${this.rating} of ${this.ratingMax}`].join(", ");
422
+ }
410
423
  }
411
424
 
412
425
  export default UDExListItemBase;
@@ -136,8 +136,8 @@ export default function ListItemTemplate(this: UDExListItem) {
136
136
  {this.modeSelected && (
137
137
  <div
138
138
  part="item"
139
- tabIndex={Number(this._effectiveTabIndex)}
140
- aria-disabled={this._ariaDisabled}
139
+ tabIndex={this._useHostAriaRole ? undefined : Number(this._effectiveTabIndex)}
140
+ aria-disabled={this._useHostAriaRole ? undefined : this._ariaDisabled}
141
141
  class={this.className}
142
142
  role={this.roleItem as AriaRole}
143
143
  aria-checked={this.ariaCheckedValue}
@@ -160,7 +160,7 @@ export default function ListItemTemplate(this: UDExListItem) {
160
160
  part="checkbox"
161
161
  tabindex={-1}
162
162
  indeterminate={this.indeterminate}
163
- accessible-name={`checkbox ${this.stateAccessibleName}`}
163
+ accessible-name={this._useHostAriaRole ? undefined : `checkbox ${this.stateAccessibleName}`}
164
164
  checked={this.selected}
165
165
  disabled={this.disabled}
166
166
  aria-hidden="true"
@@ -183,7 +183,7 @@ export default function ListItemTemplate(this: UDExListItem) {
183
183
  <div
184
184
  part="item"
185
185
  role={this.roleItem as AriaRole}
186
- aria-selected={this.selected}
186
+ aria-selected={this.ariaSelectedValue}
187
187
  class={this.className}>
188
188
  {ContentList.call(this)}
189
189
  </div>
@@ -6,22 +6,24 @@ export default function ListSelectAllTemplate(this: UDExListSelectAll) {
6
6
  <div
7
7
  part="item"
8
8
  class="udex-list__group"
9
- role="group"
10
- aria-labelledby="group-select-all">
9
+ role={this._useHostAriaRole ? "presentation" : "group"}
10
+ aria-labelledby={this._useHostAriaRole ? undefined : "group-select-all"}>
11
11
  <div
12
- role="listbox"
13
- aria-label="select-all-group"
12
+ role={this._useHostAriaRole ? "presentation" : "listbox"}
13
+ aria-label={this._useHostAriaRole ? undefined : "select-all-group"}
14
14
  class={this.groupListClassName}
15
15
  onui5-item-press={this.handleSelectAllGroupItemsPress}>
16
16
  <UDExListItem
17
- role="group"
17
+ role={this._useHostAriaRole ? "none" : "group"}
18
18
  part="group-item"
19
19
  accessible-role="option"
20
+ _useHostAriaRole={this._useHostAriaRole}
21
+ label={this._useHostAriaRole ? this.selectAllLabel : undefined}
20
22
  mode={this.multiselectMode}
21
23
  onClick={this.handleSelectAllPress}
22
24
  class={this.className}
23
25
  selected={this.selected}
24
- aria-current={this.selected}
26
+ aria-current={this._useHostAriaRole ? undefined : this.selected}
25
27
  indeterminate={this.indeterminate}
26
28
  controlled={this.controlled}
27
29
  onKeyDown={this.handleKeyDownPress}
@@ -29,9 +31,9 @@ export default function ListSelectAllTemplate(this: UDExListSelectAll) {
29
31
  _additional-class-name={this.additionalClassName}
30
32
  supportingText={this.supportingText}
31
33
  divider>
32
- {this.selectAllLabel}
34
+ {!this._useHostAriaRole && this.selectAllLabel}
33
35
  </UDExListItem>
34
- <slot></slot>
36
+ {this._useHostAriaRole ? <div role="group"><slot></slot></div> : <slot></slot>}
35
37
  </div>
36
38
  </div>
37
39
  );
@@ -2,6 +2,8 @@ import type UDExList from "./List.js";
2
2
  import type { AriaRole } from "@ui5/webcomponents-base/types.js";
3
3
 
4
4
  export default function ListTemplate(this: UDExList) {
5
+ const hasHostAriaRole = this._hasHostAriaRole;
6
+
5
7
  return (
6
8
  <>
7
9
  {this.modeSelected && (
@@ -13,16 +15,18 @@ export default function ListTemplate(this: UDExList) {
13
15
  onui5-header-press={this.handleItemPress}>
14
16
  <div class="udex-list__scroll-container">
15
17
  {HeaderText.call(this)}
16
- <span id={`${this._id}-mode-label`} class="udex-list__hidden-text">
17
- {this.ariaLabelModeText}
18
- </span>
18
+ {!hasHostAriaRole && (
19
+ <span id={this.ariaLabelledBy} class="udex-list__hidden-text">
20
+ {this.ariaLabelModeText}
21
+ </span>
22
+ )}
19
23
  <div
20
24
  id="udex-list"
21
25
  class="udex-list"
22
- role={this.effectiveAccessibleRole as AriaRole}
23
- aria-labelledby={this.ariaLabelledBy}
24
- aria-multiselectable={this._listAriaMultiSelectable as "true" | undefined}
25
- aria-busy={(this.ariaBusy ?? undefined) as "true" | "false" | undefined}>
26
+ role={(hasHostAriaRole ? "none" : this.effectiveAccessibleRole) as AriaRole}
27
+ aria-labelledby={hasHostAriaRole ? undefined : this.ariaLabelledBy}
28
+ aria-multiselectable={(hasHostAriaRole ? undefined : this._listAriaMultiSelectable) as "true" | undefined}
29
+ aria-busy={(hasHostAriaRole ? undefined : this.ariaBusy ?? undefined) as "true" | "false" | undefined}>
26
30
  <slot></slot>
27
31
  </div>
28
32
  </div>
@@ -34,9 +38,9 @@ export default function ListTemplate(this: UDExList) {
34
38
  <div
35
39
  id="udex-list"
36
40
  class="udex-list"
37
- role={this.singleContentRole as AriaRole}
38
- aria-label={this.singleContentAriaLabel}
39
- aria-busy={(this.ariaBusy ?? undefined) as "true" | "false" | undefined}>
41
+ role={(hasHostAriaRole ? "none" : this.singleContentRole) as AriaRole}
42
+ aria-label={hasHostAriaRole ? undefined : this.singleContentAriaLabel}
43
+ aria-busy={(hasHostAriaRole ? undefined : this.ariaBusy ?? undefined) as "true" | "false" | undefined}>
40
44
  <slot></slot>
41
45
  </div>
42
46
  </>