@sapudex/web-components 2.51.0 → 2.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/cypress/specs/Avatar.cy.tsx +103 -0
  2. package/cypress/specs/CountrySelector.cy.tsx +21 -0
  3. package/cypress/specs/List.cy.tsx +119 -10
  4. package/cypress/specs/SelectBox.cy.tsx +426 -70
  5. package/dist/Avatar.d.ts +10 -1
  6. package/dist/Avatar.js +43 -3
  7. package/dist/Avatar.js.map +1 -1
  8. package/dist/AvatarTemplate.js +1 -1
  9. package/dist/AvatarTemplate.js.map +1 -1
  10. package/dist/CountrySelector.js +1 -0
  11. package/dist/CountrySelector.js.map +1 -1
  12. package/dist/List.d.ts +10 -0
  13. package/dist/List.js +35 -0
  14. package/dist/List.js.map +1 -1
  15. package/dist/ListGroupHeaderBase.js +3 -0
  16. package/dist/ListGroupHeaderBase.js.map +1 -1
  17. package/dist/ListItem.d.ts +1 -0
  18. package/dist/ListItem.js +11 -3
  19. package/dist/ListItem.js.map +1 -1
  20. package/dist/ListItemBase.d.ts +9 -0
  21. package/dist/ListItemBase.js +12 -0
  22. package/dist/ListItemBase.js.map +1 -1
  23. package/dist/ListSelectAll.js +1 -1
  24. package/dist/ListSelectAll.js.map +1 -1
  25. package/dist/MediaPlayer.js +8 -0
  26. package/dist/MediaPlayer.js.map +1 -1
  27. package/dist/SearchableListBase.d.ts +10 -0
  28. package/dist/SearchableListBase.js +29 -6
  29. package/dist/SearchableListBase.js.map +1 -1
  30. package/dist/SearchableListBaseTemplate.js +32 -5
  31. package/dist/SearchableListBaseTemplate.js.map +1 -1
  32. package/dist/SelectBase.d.ts +65 -1
  33. package/dist/SelectBase.js +446 -59
  34. package/dist/SelectBase.js.map +1 -1
  35. package/dist/SelectBox.d.ts +5 -0
  36. package/dist/SelectBox.js +33 -2
  37. package/dist/SelectBox.js.map +1 -1
  38. package/dist/SelectBoxTemplate.js +2 -2
  39. package/dist/SelectBoxTemplate.js.map +1 -1
  40. package/dist/assets/{InputSuggestions-7eP5S95I.js → InputSuggestions-0yCPXAmq.js} +1 -1
  41. package/dist/assets/{ListItemStandardExpandableTextTemplate-CLcWBFcg.js → ListItemStandardExpandableTextTemplate-DjZEnCXN.js} +1 -1
  42. package/dist/assets/{bundle-CkxyEA8c.css → bundle-C7Yft7IG.css} +1 -1
  43. package/dist/assets/{bundle.esm-D4--IDh7.js → bundle.esm-DJuMR5ko.js} +241 -241
  44. package/dist/assets/messagebundle_lv-D06tlATH.js +1 -0
  45. package/dist/assets/messagebundle_nl-BKUpVV-D.js +1 -0
  46. package/dist/css/themes/AccordionItem.css +1 -1
  47. package/dist/css/themes/BreadcrumbsPopover.css +1 -1
  48. package/dist/css/themes/Contact.css +1 -1
  49. package/dist/css/themes/Divider.css +1 -1
  50. package/dist/css/themes/FilterMenu.css +1 -1
  51. package/dist/css/themes/Link.css +1 -1
  52. package/dist/css/themes/ListGroupHeader.css +1 -1
  53. package/dist/css/themes/ListItem.css +1 -1
  54. package/dist/css/themes/MessageStrip.css +1 -1
  55. package/dist/css/themes/Search.css +1 -1
  56. package/dist/css/themes/SelectBase.css +1 -1
  57. package/dist/css/themes/SelectBox.css +1 -1
  58. package/dist/css/themes/Slider.css +1 -1
  59. package/dist/css/themes/Tile.css +1 -1
  60. package/dist/css/themes/Toast.css +1 -1
  61. package/dist/css/themes/Tooltip.css +1 -1
  62. package/dist/custom-elements-internal.json +167 -156
  63. package/dist/custom-elements.json +152 -141
  64. package/dist/generated/assets/i18n/messagebundle_lv.json +1 -1
  65. package/dist/generated/assets/i18n/messagebundle_nl.json +1 -1
  66. package/dist/generated/templates/ListTemplate.lit.js +1 -1
  67. package/dist/generated/templates/ListTemplate.lit.js.map +1 -1
  68. package/dist/generated/themes/AccordionItem.css.d.ts +1 -1
  69. package/dist/generated/themes/AccordionItem.css.js +1 -1
  70. package/dist/generated/themes/AccordionItem.css.js.map +1 -1
  71. package/dist/generated/themes/BreadcrumbsPopover.css.d.ts +1 -1
  72. package/dist/generated/themes/BreadcrumbsPopover.css.js +1 -1
  73. package/dist/generated/themes/BreadcrumbsPopover.css.js.map +1 -1
  74. package/dist/generated/themes/Contact.css.d.ts +1 -1
  75. package/dist/generated/themes/Contact.css.js +1 -1
  76. package/dist/generated/themes/Contact.css.js.map +1 -1
  77. package/dist/generated/themes/Divider.css.d.ts +1 -1
  78. package/dist/generated/themes/Divider.css.js +1 -1
  79. package/dist/generated/themes/Divider.css.js.map +1 -1
  80. package/dist/generated/themes/FilterMenu.css.d.ts +1 -1
  81. package/dist/generated/themes/FilterMenu.css.js +1 -1
  82. package/dist/generated/themes/FilterMenu.css.js.map +1 -1
  83. package/dist/generated/themes/Link.css.d.ts +1 -1
  84. package/dist/generated/themes/Link.css.js +1 -1
  85. package/dist/generated/themes/Link.css.js.map +1 -1
  86. package/dist/generated/themes/ListGroupHeader.css.d.ts +1 -1
  87. package/dist/generated/themes/ListGroupHeader.css.js +1 -1
  88. package/dist/generated/themes/ListGroupHeader.css.js.map +1 -1
  89. package/dist/generated/themes/ListItem.css.d.ts +1 -1
  90. package/dist/generated/themes/ListItem.css.js +1 -1
  91. package/dist/generated/themes/ListItem.css.js.map +1 -1
  92. package/dist/generated/themes/MessageStrip.css.d.ts +1 -1
  93. package/dist/generated/themes/MessageStrip.css.js +1 -1
  94. package/dist/generated/themes/MessageStrip.css.js.map +1 -1
  95. package/dist/generated/themes/Search.css.d.ts +1 -1
  96. package/dist/generated/themes/Search.css.js +1 -1
  97. package/dist/generated/themes/Search.css.js.map +1 -1
  98. package/dist/generated/themes/SelectBase.css.d.ts +1 -1
  99. package/dist/generated/themes/SelectBase.css.js +1 -1
  100. package/dist/generated/themes/SelectBase.css.js.map +1 -1
  101. package/dist/generated/themes/SelectBox.css.d.ts +1 -1
  102. package/dist/generated/themes/SelectBox.css.js +1 -1
  103. package/dist/generated/themes/SelectBox.css.js.map +1 -1
  104. package/dist/generated/themes/Slider.css.d.ts +1 -1
  105. package/dist/generated/themes/Slider.css.js +1 -1
  106. package/dist/generated/themes/Slider.css.js.map +1 -1
  107. package/dist/generated/themes/Tile.css.d.ts +1 -1
  108. package/dist/generated/themes/Tile.css.js +1 -1
  109. package/dist/generated/themes/Tile.css.js.map +1 -1
  110. package/dist/generated/themes/Toast.css.d.ts +1 -1
  111. package/dist/generated/themes/Toast.css.js +1 -1
  112. package/dist/generated/themes/Toast.css.js.map +1 -1
  113. package/dist/generated/themes/Tooltip.css.d.ts +1 -1
  114. package/dist/generated/themes/Tooltip.css.js +1 -1
  115. package/dist/generated/themes/Tooltip.css.js.map +1 -1
  116. package/dist/messagebundle_lv.properties +126 -0
  117. package/dist/messagebundle_nl.properties +126 -0
  118. package/dist/vscode.html-custom-data.json +1 -1
  119. package/dist/web-types.json +3 -3
  120. package/package.json +1 -1
  121. package/src/Avatar.ts +52 -3
  122. package/src/AvatarTemplate.tsx +2 -2
  123. package/src/CountrySelector.ts +1 -0
  124. package/src/List.hbs +2 -1
  125. package/src/List.ts +36 -0
  126. package/src/ListGroupHeaderBase.ts +3 -0
  127. package/src/ListItem.ts +12 -3
  128. package/src/ListItemBase.ts +11 -0
  129. package/src/ListSelectAll.ts +1 -1
  130. package/src/MediaPlayer.ts +8 -0
  131. package/src/SearchableListBase.ts +31 -8
  132. package/src/SearchableListBaseTemplate.tsx +38 -6
  133. package/src/SelectBase.ts +466 -69
  134. package/src/SelectBox.ts +37 -2
  135. package/src/SelectBoxTemplate.tsx +4 -3
  136. package/src/generated/templates/ListTemplate.lit.ts +1 -1
  137. package/src/generated/themes/AccordionItem.css.ts +1 -1
  138. package/src/generated/themes/BreadcrumbsPopover.css.ts +1 -1
  139. package/src/generated/themes/Contact.css.ts +1 -1
  140. package/src/generated/themes/Divider.css.ts +1 -1
  141. package/src/generated/themes/FilterMenu.css.ts +1 -1
  142. package/src/generated/themes/Link.css.ts +1 -1
  143. package/src/generated/themes/ListGroupHeader.css.ts +1 -1
  144. package/src/generated/themes/ListItem.css.ts +1 -1
  145. package/src/generated/themes/MessageStrip.css.ts +1 -1
  146. package/src/generated/themes/Search.css.ts +1 -1
  147. package/src/generated/themes/SelectBase.css.ts +1 -1
  148. package/src/generated/themes/SelectBox.css.ts +1 -1
  149. package/src/generated/themes/Slider.css.ts +1 -1
  150. package/src/generated/themes/Tile.css.ts +1 -1
  151. package/src/generated/themes/Toast.css.ts +1 -1
  152. package/src/generated/themes/Tooltip.css.ts +1 -1
  153. package/src/i18n/messagebundle_lv.properties +126 -0
  154. package/src/i18n/messagebundle_nl.properties +126 -0
  155. package/src/themes/AccordionItem.css +8 -3
  156. package/src/themes/BreadcrumbsPopover.css +1 -1
  157. package/src/themes/Contact.css +3 -3
  158. package/src/themes/Divider.css +2 -0
  159. package/src/themes/FilterMenu.css +1 -1
  160. package/src/themes/Link.css +20 -2
  161. package/src/themes/ListGroupHeader.css +18 -1
  162. package/src/themes/ListItem.css +2 -1
  163. package/src/themes/MessageStrip.css +8 -5
  164. package/src/themes/Search.css +3 -3
  165. package/src/themes/SelectBase.css +2 -1
  166. package/src/themes/SelectBox.css +10 -0
  167. package/src/themes/Slider.css +1 -1
  168. package/src/themes/Tile.css +2 -2
  169. package/src/themes/Toast.css +1 -1
  170. package/src/themes/Tooltip.css +1 -1
  171. package/dist/assets/messagebundle_lv-BHId5zho.js +0 -1
  172. package/dist/assets/messagebundle_nl-etaldyfo.js +0 -1
package/src/Avatar.ts CHANGED
@@ -10,6 +10,7 @@ import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
10
10
  import { ATTENTION_BADGE, AVATAR_TEXT, DISABLED } from "./generated/i18n/i18n-defaults.js";
11
11
  import type AvatarColorScheme from "@ui5/webcomponents/dist/types/AvatarColorScheme.js";
12
12
  import translate from "./utils/translate.js";
13
+ import "@ui5/webcomponents-icons/dist/employee.js";
13
14
 
14
15
  // Template
15
16
  import AvatarTemplate from "./AvatarTemplate.js";
@@ -148,11 +149,11 @@ class UDExAvatar extends UI5Element {
148
149
  /**
149
150
  * Defines the name of the fallback icon.
150
151
  *
151
- * @default ""
152
+ * @default "employee"
152
153
  * @public
153
154
  */
154
155
  @property({ type: String })
155
- fallbackIcon: string = "";
156
+ fallbackIcon: string = "employee";
156
157
 
157
158
  /**
158
159
  * Defines the name of the UI5 Icon, that will be displayed.
@@ -208,11 +209,59 @@ class UDExAvatar extends UI5Element {
208
209
  @slot({ type: HTMLElement })
209
210
  badge!: Array<HTMLElement>;
210
211
 
212
+ /**
213
+ * @private
214
+ */
215
+ @property({ noAttribute: true })
216
+ _imageErrored: boolean = false;
217
+
211
218
  @i18n("@sapudex/web-components")
212
219
  static i18nBundle: I18nBundle;
213
220
 
221
+ private _trackedImageElement: HTMLImageElement | null = null;
222
+
223
+ private _handleImageError = (): void => {
224
+ this._imageErrored = true;
225
+ };
226
+
227
+ private _handleImageLoad = (): void => {
228
+ if (this._imageErrored) {
229
+ this._imageErrored = false;
230
+ }
231
+ };
232
+
233
+ private _syncImageListener(): void {
234
+ const newImage = this.image.find((el): el is HTMLImageElement => el instanceof HTMLImageElement) ?? null;
235
+
236
+ if (newImage === this._trackedImageElement) {
237
+ return;
238
+ }
239
+
240
+ if (this._trackedImageElement) {
241
+ this._trackedImageElement.removeEventListener("error", this._handleImageError);
242
+ this._trackedImageElement.removeEventListener("load", this._handleImageLoad);
243
+ }
244
+
245
+ this._trackedImageElement = newImage;
246
+
247
+ if (newImage) {
248
+ newImage.addEventListener("error", this._handleImageError);
249
+ newImage.addEventListener("load", this._handleImageLoad);
250
+
251
+ if (newImage.complete && newImage.src) {
252
+ this._imageErrored = !newImage.naturalWidth;
253
+ return;
254
+ }
255
+ }
256
+
257
+ if (this._imageErrored) {
258
+ this._imageErrored = false;
259
+ }
260
+ }
261
+
214
262
  onAfterRendering(): void {
215
263
  this.title = this.initials;
264
+ this._syncImageListener();
216
265
  }
217
266
  handleClick(e: Event) {
218
267
  e.stopPropagation();
@@ -258,7 +307,7 @@ class UDExAvatar extends UI5Element {
258
307
  get udexAvatarClass(): string {
259
308
  let avatarClass = "udex-avatar";
260
309
 
261
- if (this.imageSlot) {
310
+ if (this.imageSlot && !this._imageErrored) {
262
311
  avatarClass += " udex-avatar--with-image";
263
312
  }
264
313
 
@@ -12,10 +12,10 @@ export default function AvatarTemplate(this: Avatar) {
12
12
  initials={this.initials}
13
13
  interactive={this.interactive}
14
14
  fallbackIcon={this.fallbackIcon}
15
- icon={this.icon}
15
+ icon={this._imageErrored ? this.fallbackIcon : this.icon}
16
16
  shape={this.shape}
17
17
  size={this.size}>
18
- {this.imageSlot && <slot></slot>}
18
+ {this.imageSlot && !this._imageErrored && <slot></slot>}
19
19
  {this.badgeSlot && <slot name="badge" slot="badge"></slot>}
20
20
  </UI5Avatar>
21
21
  );
@@ -154,6 +154,7 @@ class UDExCountrySelector extends UDExSelectBase {
154
154
  }
155
155
 
156
156
  handleDropdownClose(): void {
157
+ this._setActiveDescendant(null);
157
158
  this._open = false;
158
159
  this.fireEventDropdownClose();
159
160
  }
package/src/List.hbs CHANGED
@@ -11,8 +11,9 @@
11
11
  <div
12
12
  id="udex-list"
13
13
  class="udex-list"
14
- role="{{accessibleRole}}"
14
+ role="{{effectiveAccessibleRole}}"
15
15
  aria-labelledby="{{ariaLabelledBy}}"
16
+ aria-multiselectable="{{_listAriaMultiSelectable}}"
16
17
  aria-busy="{{ariaBusy}}">
17
18
  <slot></slot>
18
19
  </div>
package/src/List.ts CHANGED
@@ -478,12 +478,29 @@ class UDExList extends UI5Element {
478
478
  }
479
479
 
480
480
  prepareListItemsMode(): void {
481
+ const treeMode = this._hasCollapsibleGroups;
481
482
  this.getItemsFromList().forEach((item, index) => {
482
483
  item.mode = this.mode;
483
484
  item.controlled = this.controlled;
484
485
  item.type = this.type;
485
486
  item._icon = this.itemIcon;
486
487
  item._tabIndex = this.getTabIndex(index, item);
488
+ if (item.isGroup) {
489
+ // Only upgrade _treeMode to true; never force it to false, since
490
+ // SearchableListBaseTemplate may have set _tree-mode=true externally.
491
+ if (treeMode) {
492
+ item._treeMode = true;
493
+ }
494
+ } else if (treeMode) {
495
+ // Set accessibleRole for standalone List tree mode.
496
+ // Never overwrite a value already set by an external template (e.g. FilterMenu
497
+ // sets accessible-role="treeitem" on children via SearchableListBaseTemplate).
498
+ item.accessibleRole = "treeitem";
499
+ item.setAttribute("_list-managed-role", "");
500
+ } else if (item.getAttribute("_list-managed-role") !== null) {
501
+ item.accessibleRole = "";
502
+ item.removeAttribute("_list-managed-role");
503
+ }
487
504
  });
488
505
  }
489
506
 
@@ -566,6 +583,25 @@ class UDExList extends UI5Element {
566
583
  get holderHeight(): string {
567
584
  return this.height ? this.height : "auto";
568
585
  }
586
+
587
+ /** True when at least one slotted group header has the collapsible attribute. */
588
+ get _hasCollapsibleGroups(): boolean {
589
+ return this.getItems().some(item => item.isGroup && item.hasAttribute("collapsible"));
590
+ }
591
+
592
+ /**
593
+ * The effective ARIA role for the inner list div.
594
+ * When the list has collapsible group headers it uses the tree pattern so that
595
+ * JAWS enters application mode and delivers arrow-key events to the component.
596
+ */
597
+ get effectiveAccessibleRole(): string {
598
+ return this._hasCollapsibleGroups ? "tree" : this.accessibleRole;
599
+ }
600
+
601
+ /** aria-multiselectable exposed only for MultiSelect tree (collapsible groups). */
602
+ get _listAriaMultiSelectable(): string | undefined {
603
+ return (this._hasCollapsibleGroups && this.modeMultiSelect) ? "true" : undefined;
604
+ }
569
605
  }
570
606
 
571
607
  UDExList.define();
@@ -219,6 +219,9 @@ class UDExListGroupHeaderBase extends UDExListItemBase {
219
219
  }
220
220
 
221
221
  get groupWrapperRole(): string {
222
+ if (this._useHostAriaRole) {
223
+ return "presentation";
224
+ }
222
225
  return this._treeMode ? "none" : "group";
223
226
  }
224
227
 
package/src/ListItem.ts CHANGED
@@ -366,11 +366,20 @@ class UDExListItem extends UDExListItemBase {
366
366
  }
367
367
 
368
368
  get roleItem(): string {
369
+ // When the host element carries the role (aria-activedescendant mode), the
370
+ // inner [part=item] div must be presentational to avoid a duplicate ARIA node.
371
+ if (this._useHostAriaRole) {
372
+ return "presentation";
373
+ }
374
+ return this.accessibleRole.length > 0 ? this.accessibleRole : listItemRole;
375
+ }
376
+
377
+ get effectiveRole(): string {
369
378
  return this.accessibleRole.length > 0 ? this.accessibleRole : listItemRole;
370
379
  }
371
380
 
372
381
  get ariaCheckedValue(): "true" | "false" | "mixed" | undefined {
373
- if (this.roleItem !== "treeitem") {
382
+ if (this.effectiveRole !== "treeitem") {
374
383
  return undefined;
375
384
  }
376
385
  // Only MultiSelect treeitems expose aria-checked; SingleSelect uses aria-selected.
@@ -384,7 +393,7 @@ class UDExListItem extends UDExListItemBase {
384
393
  }
385
394
 
386
395
  get ariaSelectedValue(): "true" | "false" | undefined {
387
- if (this.roleItem !== "treeitem") {
396
+ if (this.effectiveRole !== "treeitem") {
388
397
  return undefined;
389
398
  }
390
399
  // Only SingleSelect treeitems expose aria-selected; MultiSelect uses aria-checked.
@@ -402,7 +411,7 @@ class UDExListItem extends UDExListItemBase {
402
411
  }
403
412
 
404
413
  get checkboxAriaHidden(): boolean {
405
- return this.roleItem === "treeitem";
414
+ return this.effectiveRole === "treeitem";
406
415
  }
407
416
 
408
417
  get _ariaDisabled(): boolean | undefined {
@@ -229,6 +229,17 @@ class UDExListItemBase extends UI5Element {
229
229
  @property({ type: Boolean })
230
230
  _filtered!: boolean;
231
231
 
232
+ /**
233
+ * When true, the host element carries role + ARIA state attributes directly
234
+ * (set by the outer template) so that aria-activedescendant works across shadow DOM.
235
+ * The inner [part=item] div is then rendered as role="presentation".
236
+ *
237
+ * @default false
238
+ * @private
239
+ */
240
+ @property({ type: Boolean })
241
+ _useHostAriaRole: boolean = false;
242
+
232
243
  /**
233
244
  * Defines the tab index of the component.
234
245
  *
@@ -104,7 +104,7 @@ class UDExListSelectAll extends UDExListGroupHeaderBase {
104
104
  }
105
105
 
106
106
  get groupListClassName(): string {
107
- return `${groupListClass} ${this._additionalClassName}`;
107
+ return groupListClass;
108
108
  }
109
109
  }
110
110
 
@@ -338,7 +338,15 @@ class UDExMediaPlayer extends UI5Element {
338
338
 
339
339
  applyScrollIntoTheView() {
340
340
  const bigPlayButton = this.player.bigPlayButton?.el();
341
+ let mouseActivated = false;
342
+ bigPlayButton.addEventListener("mousedown", () => {
343
+ mouseActivated = true;
344
+ });
341
345
  bigPlayButton.addEventListener("focus", () => {
346
+ if (mouseActivated) {
347
+ mouseActivated = false;
348
+ return;
349
+ }
342
350
  this.player.el().scrollIntoView({
343
351
  behavior: "smooth",
344
352
  block: "center",
@@ -268,6 +268,7 @@ class UDExSearchableListBase extends UI5Element {
268
268
  _shouldFilterItems?: boolean;
269
269
  _accSearchResultInfo?: string;
270
270
  _groupCollapseStateBeforeSearch: Map<string, boolean>;
271
+ _pendingHeightReset: boolean = false;
271
272
 
272
273
  constructor() {
273
274
  super();
@@ -282,6 +283,19 @@ class UDExSearchableListBase extends UI5Element {
282
283
  }
283
284
  }
284
285
 
286
+ onAfterRendering() {
287
+ if (this._pendingHeightReset) {
288
+ this._pendingHeightReset = false;
289
+ this.shadowRoot?.querySelectorAll<HTMLElement & { setContentHeight(): void }>("[udex-list-group-header]")
290
+ .forEach(header => {
291
+ if (header.hasAttribute("collapsible")) {
292
+ header.style.removeProperty("--udex-list-content-max-height");
293
+ header.setContentHeight();
294
+ }
295
+ });
296
+ }
297
+ }
298
+
285
299
  onItemPress(e:CustomEvent) {
286
300
  const dropdownItemId: string = this.getDataTokenId(e.detail.item as UDExListItemBase);
287
301
  this.handleDropdownItemPress(dropdownItemId);
@@ -484,21 +498,19 @@ class UDExSearchableListBase extends UI5Element {
484
498
  }
485
499
 
486
500
  restoreGroupCollapseState(): void {
487
- if (!this._groupCollapseStateBeforeSearch.size) {
488
- return;
489
- }
490
-
491
501
  this.items.forEach(item => {
492
502
  if (item.isGroup) {
493
- const collapsed = this._groupCollapseStateBeforeSearch.get(item._id);
494
-
495
- if (collapsed !== undefined) {
496
- item.collapsed = collapsed;
503
+ if (this._groupCollapseStateBeforeSearch.size) {
504
+ const collapsed = this._groupCollapseStateBeforeSearch.get(item._id);
505
+ if (collapsed !== undefined) {
506
+ item.collapsed = collapsed;
507
+ }
497
508
  }
498
509
  }
499
510
  });
500
511
 
501
512
  this._groupCollapseStateBeforeSearch.clear();
513
+ this._pendingHeightReset = true;
502
514
  }
503
515
 
504
516
  getSlottedItems(items: Array<UDExListItemBase | UDExListGroupHeaderBase>): Array<UDExListItemBase | UDExListGroupHeaderBase> {
@@ -732,6 +744,17 @@ class UDExSearchableListBase extends UI5Element {
732
744
  return false;
733
745
  }
734
746
 
747
+ /**
748
+ * Whether items are navigated via aria-activedescendant (focus stays on the
749
+ * combobox input) rather than via DOM focus on each item. When true,
750
+ * SearchableListBaseTemplate places role + ARIA state attrs on the host
751
+ * element of each list-item so that screen readers can read them through the
752
+ * aria-activedescendant pointer.
753
+ */
754
+ get _activeDescendantMode(): boolean {
755
+ return false;
756
+ }
757
+
735
758
  get additionalClassName(): string {
736
759
  return `udex-searchable-list__header`;
737
760
  }
@@ -9,6 +9,7 @@ import UDExListItem from "./ListItem.js";
9
9
  import UDExListGroupHeader from "./ListGroupHeader.js";
10
10
  import UDExListSelectAll from "./ListSelectAll.js";
11
11
  import type { AriaRole } from "@ui5/webcomponents-base/dist/types.js";
12
+ import { cleanLabel } from "./utils/listItemLabelUtils.js";
12
13
 
13
14
  export default function SearchableListBaseTemplate(this: UDExSearchableListBase) {
14
15
  return (
@@ -58,7 +59,7 @@ export default function SearchableListBaseTemplate(this: UDExSearchableListBase)
58
59
  {this._filteredItems.map((processedItem, index) => (
59
60
  processedItem.isGroup
60
61
  ? renderGroup.call(this, processedItem, index)
61
- : renderListItem(processedItem.item as UDExListItem, index, "none", this.listItemRole, this.fireFirstIconClickEvent, this.fireSecondIconClickEvent)
62
+ : renderListItem(processedItem.item as UDExListItem, index, "none", this.listItemRole, this.fireFirstIconClickEvent, this.fireSecondIconClickEvent, this._activeDescendantMode, this.isTreeStructure, this.modeMultiSelect)
62
63
  ))}
63
64
  </UDExList>
64
65
  )}
@@ -89,7 +90,7 @@ function renderSelectAll(this: UDExSearchableListBase, processedItem: UDExProces
89
90
  onFirstIconClick={this.fireFirstIconClickEvent}
90
91
  onSecondIconClick={this.fireSecondIconClickEvent}>
91
92
  {groupItems.map((child, i) =>
92
- renderListItem(child as UDExListItem, i, this.listItemRole, undefined, this.fireFirstIconClickEvent, this.fireSecondIconClickEvent)
93
+ renderListItem(child as UDExListItem, i, this.listItemRole, undefined, this.fireFirstIconClickEvent, this.fireSecondIconClickEvent, this._activeDescendantMode, this.isTreeStructure, this.modeMultiSelect)
93
94
  )}
94
95
  </UDExListSelectAll>
95
96
  );
@@ -99,10 +100,22 @@ function renderGroupHeader(this: UDExSearchableListBase, processedItem: UDExProc
99
100
  const { item, groupItems } = processedItem;
100
101
  const header = item as UDExListGroupHeaderBase;
101
102
  const childAccessibleRole = this.isTreeStructure ? "treeitem" : undefined;
103
+ // In aria-activedescendant mode (SelectBox), each group header host element
104
+ // must carry role + ARIA state so screen readers can reach them via the pointer.
105
+ const hostAriaAttrs = this._activeDescendantMode ? {
106
+ role: (this.isTreeStructure ? "treeitem" : "option") as AriaRole,
107
+ "aria-label": cleanLabel(item.label ?? ""),
108
+ "aria-selected": (this.modeMultiSelect ? undefined : (item.selected ? "true" : "false")) as string | undefined,
109
+ "aria-checked": (this.modeMultiSelect ? (header.indeterminate ? "mixed" : (item.selected ? "true" : "false")) : undefined) as string | undefined,
110
+ "aria-disabled": item.disabled ? "true" as string : undefined,
111
+ "aria-expanded": header.collapsible ? (header.expanded ? "true" as string : "false" as string) : undefined,
112
+ "_use-host-aria-role": true,
113
+ } : { role: (this.isTreeStructure ? "none" : "group") as AriaRole };
102
114
  return (
103
115
  <UDExListGroupHeader
104
116
  key={index}
105
- role={this.isTreeStructure ? "none" : "group"}
117
+ id={item._id}
118
+ {...hostAriaAttrs}
106
119
  _tree-mode={this.isTreeStructure || undefined}
107
120
  {...(index === 0 ? { "_no-border": "" } : {})}
108
121
  _additional-class-name="udex-searchable-list__header"
@@ -121,7 +134,7 @@ function renderGroupHeader(this: UDExSearchableListBase, processedItem: UDExProc
121
134
  onSecondIconClick={this.fireSecondIconClickEvent}
122
135
  indeterminate={header.indeterminate}>
123
136
  {groupItems.map((child, i) =>
124
- renderListItem(child as UDExListItem, i, "none", childAccessibleRole, this.fireFirstIconClickEvent, this.fireSecondIconClickEvent)
137
+ renderListItem(child as UDExListItem, i, "none", childAccessibleRole, this.fireFirstIconClickEvent, this.fireSecondIconClickEvent, this._activeDescendantMode, this.isTreeStructure, this.modeMultiSelect)
125
138
  )}
126
139
  </UDExListGroupHeader>
127
140
  );
@@ -134,11 +147,30 @@ function renderListItem(
134
147
  accessibleRole: string | undefined,
135
148
  onFirstIconClick: (e: CustomEvent) => void,
136
149
  onSecondIconClick: (e: CustomEvent) => void,
150
+ activeDescendantMode?: boolean,
151
+ isTreeStructure?: boolean,
152
+ modeMultiSelect?: boolean,
137
153
  ) {
154
+ // In aria-activedescendant mode (SelectBox), role + ARIA state must be on the
155
+ // host element so screen readers can reach them via the aria-activedescendant pointer.
156
+ const effectiveRole = accessibleRole ?? (role !== "none" ? role : undefined) ?? "option";
157
+ const hostRole = activeDescendantMode ? (effectiveRole as AriaRole) : (role as AriaRole);
158
+ const hostAriaAttrs = activeDescendantMode ? {
159
+ "aria-label": "test",
160
+ // option role (listbox): always aria-selected; treeitem + MultiSelect: aria-checked only
161
+ "aria-selected": (effectiveRole !== "treeitem" || !modeMultiSelect) ? (item.selected ? "true" : "false") as string : undefined,
162
+ "aria-checked": (effectiveRole === "treeitem" && !!modeMultiSelect) ? (item.indeterminate ? "mixed" : (item.selected ? "true" : "false")) as string : undefined,
163
+ "aria-disabled": item.disabled ? "true" as string : undefined,
164
+ "_use-host-aria-role": true,
165
+ ...(accessibleRole ? { "accessible-role": accessibleRole } : {}),
166
+ } : {
167
+ ...(accessibleRole ? { "accessible-role": accessibleRole } : {}),
168
+ };
138
169
  return (
139
170
  <UDExListItem
140
171
  key={index}
141
- role={role as AriaRole}
172
+ id={item._id}
173
+ role={hostRole}
142
174
  data-token-id={item._id}
143
175
  controlled={item.controlled}
144
176
  mode={item.mode}
@@ -164,7 +196,7 @@ function renderListItem(
164
196
  flag-position={item.flagPosition}
165
197
  size={item.size}
166
198
  type={item.type}
167
- {...(accessibleRole ? { "accessible-role": accessibleRole } : {})}
199
+ {...hostAriaAttrs}
168
200
  {...((item as any).innerHTML ? { dangerouslySetInnerHTML: { __html: (item as any).innerHTML } } : {})}>
169
201
  </UDExListItem>
170
202
  );