@sapudex/web-components 2.51.0 → 2.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/cypress/specs/CountrySelector.cy.tsx +21 -0
  2. package/cypress/specs/List.cy.tsx +119 -10
  3. package/cypress/specs/SelectBox.cy.tsx +414 -70
  4. package/dist/CountrySelector.js +1 -0
  5. package/dist/CountrySelector.js.map +1 -1
  6. package/dist/List.d.ts +10 -0
  7. package/dist/List.js +35 -0
  8. package/dist/List.js.map +1 -1
  9. package/dist/ListGroupHeaderBase.js +3 -0
  10. package/dist/ListGroupHeaderBase.js.map +1 -1
  11. package/dist/ListItem.d.ts +1 -0
  12. package/dist/ListItem.js +11 -3
  13. package/dist/ListItem.js.map +1 -1
  14. package/dist/ListItemBase.d.ts +9 -0
  15. package/dist/ListItemBase.js +12 -0
  16. package/dist/ListItemBase.js.map +1 -1
  17. package/dist/ListSelectAll.js +1 -1
  18. package/dist/ListSelectAll.js.map +1 -1
  19. package/dist/SearchableListBase.d.ts +10 -0
  20. package/dist/SearchableListBase.js +29 -6
  21. package/dist/SearchableListBase.js.map +1 -1
  22. package/dist/SearchableListBaseTemplate.js +32 -5
  23. package/dist/SearchableListBaseTemplate.js.map +1 -1
  24. package/dist/SelectBase.d.ts +65 -1
  25. package/dist/SelectBase.js +446 -59
  26. package/dist/SelectBase.js.map +1 -1
  27. package/dist/SelectBox.js +24 -2
  28. package/dist/SelectBox.js.map +1 -1
  29. package/dist/SelectBoxTemplate.js +2 -2
  30. package/dist/SelectBoxTemplate.js.map +1 -1
  31. package/dist/assets/{InputSuggestions-7eP5S95I.js → InputSuggestions-0wafO-g3.js} +1 -1
  32. package/dist/assets/{ListItemStandardExpandableTextTemplate-CLcWBFcg.js → ListItemStandardExpandableTextTemplate-DoOYtqI8.js} +1 -1
  33. package/dist/assets/{bundle.esm-D4--IDh7.js → bundle.esm-DwHZ3nCW.js} +241 -241
  34. package/dist/assets/messagebundle_nl-BKUpVV-D.js +1 -0
  35. package/dist/css/themes/AccordionItem.css +1 -1
  36. package/dist/css/themes/Divider.css +1 -1
  37. package/dist/css/themes/Link.css +1 -1
  38. package/dist/css/themes/ListGroupHeader.css +1 -1
  39. package/dist/css/themes/ListItem.css +1 -1
  40. package/dist/css/themes/SelectBase.css +1 -1
  41. package/dist/css/themes/SelectBox.css +1 -1
  42. package/dist/generated/assets/i18n/messagebundle_nl.json +1 -1
  43. package/dist/generated/templates/ListTemplate.lit.js +1 -1
  44. package/dist/generated/templates/ListTemplate.lit.js.map +1 -1
  45. package/dist/generated/themes/AccordionItem.css.d.ts +1 -1
  46. package/dist/generated/themes/AccordionItem.css.js +1 -1
  47. package/dist/generated/themes/AccordionItem.css.js.map +1 -1
  48. package/dist/generated/themes/Divider.css.d.ts +1 -1
  49. package/dist/generated/themes/Divider.css.js +1 -1
  50. package/dist/generated/themes/Divider.css.js.map +1 -1
  51. package/dist/generated/themes/Link.css.d.ts +1 -1
  52. package/dist/generated/themes/Link.css.js +1 -1
  53. package/dist/generated/themes/Link.css.js.map +1 -1
  54. package/dist/generated/themes/ListGroupHeader.css.d.ts +1 -1
  55. package/dist/generated/themes/ListGroupHeader.css.js +1 -1
  56. package/dist/generated/themes/ListGroupHeader.css.js.map +1 -1
  57. package/dist/generated/themes/ListItem.css.d.ts +1 -1
  58. package/dist/generated/themes/ListItem.css.js +1 -1
  59. package/dist/generated/themes/ListItem.css.js.map +1 -1
  60. package/dist/generated/themes/SelectBase.css.d.ts +1 -1
  61. package/dist/generated/themes/SelectBase.css.js +1 -1
  62. package/dist/generated/themes/SelectBase.css.js.map +1 -1
  63. package/dist/generated/themes/SelectBox.css.d.ts +1 -1
  64. package/dist/generated/themes/SelectBox.css.js +1 -1
  65. package/dist/generated/themes/SelectBox.css.js.map +1 -1
  66. package/dist/messagebundle_nl.properties +126 -0
  67. package/dist/web-types.json +1 -1
  68. package/package.json +1 -1
  69. package/src/CountrySelector.ts +1 -0
  70. package/src/List.hbs +2 -1
  71. package/src/List.ts +36 -0
  72. package/src/ListGroupHeaderBase.ts +3 -0
  73. package/src/ListItem.ts +12 -3
  74. package/src/ListItemBase.ts +11 -0
  75. package/src/ListSelectAll.ts +1 -1
  76. package/src/SearchableListBase.ts +31 -8
  77. package/src/SearchableListBaseTemplate.tsx +38 -6
  78. package/src/SelectBase.ts +466 -69
  79. package/src/SelectBox.ts +27 -2
  80. package/src/SelectBoxTemplate.tsx +4 -3
  81. package/src/generated/templates/ListTemplate.lit.ts +1 -1
  82. package/src/generated/themes/AccordionItem.css.ts +1 -1
  83. package/src/generated/themes/Divider.css.ts +1 -1
  84. package/src/generated/themes/Link.css.ts +1 -1
  85. package/src/generated/themes/ListGroupHeader.css.ts +1 -1
  86. package/src/generated/themes/ListItem.css.ts +1 -1
  87. package/src/generated/themes/SelectBase.css.ts +1 -1
  88. package/src/generated/themes/SelectBox.css.ts +1 -1
  89. package/src/i18n/messagebundle_nl.properties +126 -0
  90. package/src/themes/AccordionItem.css +8 -3
  91. package/src/themes/Divider.css +2 -0
  92. package/src/themes/Link.css +20 -2
  93. package/src/themes/ListGroupHeader.css +18 -1
  94. package/src/themes/ListItem.css +2 -1
  95. package/src/themes/SelectBase.css +2 -1
  96. package/src/themes/SelectBox.css +10 -0
  97. package/dist/assets/messagebundle_nl-etaldyfo.js +0 -1
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
 
@@ -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
  );