@sapudex/web-components 2.43.0 → 2.45.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 (175) hide show
  1. package/cypress/specs/DatePicker.cy.tsx +259 -0
  2. package/cypress/specs/FilterMenu.cy.tsx +96 -0
  3. package/cypress/specs/MastheadItem.cy.tsx +88 -0
  4. package/cypress/specs/MastheadShell.cy.tsx +3 -1
  5. package/cypress/specs/SelectBox.cy.tsx +82 -0
  6. package/cypress/specs/TagGroup.cy.tsx +115 -0
  7. package/cypress/specs/TextField.cy.tsx +64 -1
  8. package/cypress/support/component-imports.instrumented.ts +2 -0
  9. package/cypress/support/component-imports.src.ts +2 -0
  10. package/dist/Carousel.d.ts +5 -1
  11. package/dist/Carousel.js +10 -1
  12. package/dist/Carousel.js.map +1 -1
  13. package/dist/DatePicker.d.ts +4 -0
  14. package/dist/DatePicker.js +19 -13
  15. package/dist/DatePicker.js.map +1 -1
  16. package/dist/DatePickerPopoverTemplate.js +1 -1
  17. package/dist/DatePickerPopoverTemplate.js.map +1 -1
  18. package/dist/DatePickerTemplate.js +1 -1
  19. package/dist/DatePickerTemplate.js.map +1 -1
  20. package/dist/FilterMenu.d.ts +4 -0
  21. package/dist/FilterMenu.js +19 -0
  22. package/dist/FilterMenu.js.map +1 -1
  23. package/dist/ListItemBase.js +2 -2
  24. package/dist/ListItemBase.js.map +1 -1
  25. package/dist/MastheadItem.d.ts +6 -3
  26. package/dist/MastheadItem.js +18 -0
  27. package/dist/MastheadItem.js.map +1 -1
  28. package/dist/MastheadMobileMenu.d.ts +1 -0
  29. package/dist/MastheadMobileMenu.js +5 -0
  30. package/dist/MastheadMobileMenu.js.map +1 -1
  31. package/dist/MastheadProfileMenu.d.ts +5 -4
  32. package/dist/MastheadProfileMenu.js +15 -12
  33. package/dist/MastheadProfileMenu.js.map +1 -1
  34. package/dist/MastheadShell.d.ts +2 -0
  35. package/dist/MastheadShell.js +9 -2
  36. package/dist/MastheadShell.js.map +1 -1
  37. package/dist/SearchableListBase.d.ts +6 -1
  38. package/dist/SearchableListBase.js +43 -3
  39. package/dist/SearchableListBase.js.map +1 -1
  40. package/dist/SelectBase.d.ts +3 -1
  41. package/dist/SelectBase.js +20 -15
  42. package/dist/SelectBase.js.map +1 -1
  43. package/dist/SelectBoxTemplate.js +2 -2
  44. package/dist/SelectBoxTemplate.js.map +1 -1
  45. package/dist/TagGroup.d.ts +13 -1
  46. package/dist/TagGroup.js +54 -7
  47. package/dist/TagGroup.js.map +1 -1
  48. package/dist/TagGroupTemplate.js +2 -2
  49. package/dist/TagGroupTemplate.js.map +1 -1
  50. package/dist/TextField.d.ts +7 -0
  51. package/dist/TextField.js +10 -0
  52. package/dist/TextField.js.map +1 -1
  53. package/dist/TextFieldTemplate.js +1 -1
  54. package/dist/TextFieldTemplate.js.map +1 -1
  55. package/dist/Tooltip.js +2 -1
  56. package/dist/Tooltip.js.map +1 -1
  57. package/dist/assets/{InputSuggestions-DorMeMJC.js → InputSuggestions-_syMxKYw.js} +1 -1
  58. package/dist/assets/{ListItemStandardExpandableTextTemplate-BoavHZKO.js → ListItemStandardExpandableTextTemplate-CKRbK6UW.js} +1 -1
  59. package/dist/assets/{bundle.esm-Bji3bkxT.js → bundle.esm-DMmyJHkn.js} +197 -197
  60. package/dist/assets/messagebundle_pt-D9rap3lj.js +1 -0
  61. package/dist/assets/messagebundle_ro-BTNJ9YdP.js +1 -0
  62. package/dist/assets/messagebundle_ru-BTDmH8CB.js +1 -0
  63. package/dist/assets/messagebundle_sk-Lb4jvtpc.js +1 -0
  64. package/dist/assets/messagebundle_sl-BZi2bKSy.js +1 -0
  65. package/dist/css/themes/CountrySelector.css +1 -1
  66. package/dist/css/themes/ListItem.css +1 -1
  67. package/dist/css/themes/MastheadCountrySelector.css +1 -1
  68. package/dist/css/themes/MastheadItem.css +1 -1
  69. package/dist/css/themes/MastheadMenu.css +1 -1
  70. package/dist/css/themes/MastheadMobileMenu.css +1 -1
  71. package/dist/css/themes/MastheadProfileMenu.css +1 -1
  72. package/dist/css/themes/RadioButton.css +1 -1
  73. package/dist/css/themes/TagGroup.css +1 -1
  74. package/dist/css/themes/TextField.css +1 -1
  75. package/dist/custom-elements-internal.json +87 -17
  76. package/dist/custom-elements.json +94 -17
  77. package/dist/generated/assets/i18n/messagebundle_pt.json +1 -1
  78. package/dist/generated/assets/i18n/messagebundle_ro.json +1 -1
  79. package/dist/generated/assets/i18n/messagebundle_ru.json +1 -1
  80. package/dist/generated/assets/i18n/messagebundle_sk.json +1 -1
  81. package/dist/generated/assets/i18n/messagebundle_sl.json +1 -1
  82. package/dist/generated/templates/MastheadProfileMenuTemplate.lit.js +2 -2
  83. package/dist/generated/templates/MastheadProfileMenuTemplate.lit.js.map +1 -1
  84. package/dist/generated/templates/MastheadShellTemplate.lit.js +2 -2
  85. package/dist/generated/templates/MastheadShellTemplate.lit.js.map +1 -1
  86. package/dist/generated/themes/CountrySelector.css.d.ts +1 -1
  87. package/dist/generated/themes/CountrySelector.css.js +1 -1
  88. package/dist/generated/themes/CountrySelector.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/MastheadCountrySelector.css.d.ts +1 -1
  93. package/dist/generated/themes/MastheadCountrySelector.css.js +1 -1
  94. package/dist/generated/themes/MastheadCountrySelector.css.js.map +1 -1
  95. package/dist/generated/themes/MastheadItem.css.d.ts +1 -1
  96. package/dist/generated/themes/MastheadItem.css.js +1 -1
  97. package/dist/generated/themes/MastheadItem.css.js.map +1 -1
  98. package/dist/generated/themes/MastheadMenu.css.d.ts +1 -1
  99. package/dist/generated/themes/MastheadMenu.css.js +1 -1
  100. package/dist/generated/themes/MastheadMenu.css.js.map +1 -1
  101. package/dist/generated/themes/MastheadMobileMenu.css.d.ts +1 -1
  102. package/dist/generated/themes/MastheadMobileMenu.css.js +1 -1
  103. package/dist/generated/themes/MastheadMobileMenu.css.js.map +1 -1
  104. package/dist/generated/themes/MastheadProfileMenu.css.d.ts +1 -1
  105. package/dist/generated/themes/MastheadProfileMenu.css.js +1 -1
  106. package/dist/generated/themes/MastheadProfileMenu.css.js.map +1 -1
  107. package/dist/generated/themes/RadioButton.css.d.ts +1 -1
  108. package/dist/generated/themes/RadioButton.css.js +1 -1
  109. package/dist/generated/themes/RadioButton.css.js.map +1 -1
  110. package/dist/generated/themes/TagGroup.css.d.ts +1 -1
  111. package/dist/generated/themes/TagGroup.css.js +1 -1
  112. package/dist/generated/themes/TagGroup.css.js.map +1 -1
  113. package/dist/generated/themes/TextField.css.d.ts +1 -1
  114. package/dist/generated/themes/TextField.css.js +1 -1
  115. package/dist/generated/themes/TextField.css.js.map +1 -1
  116. package/dist/messagebundle_pt.properties +126 -0
  117. package/dist/messagebundle_ro.properties +126 -0
  118. package/dist/messagebundle_ru.properties +128 -0
  119. package/dist/messagebundle_sk.properties +125 -0
  120. package/dist/messagebundle_sl.properties +126 -1
  121. package/dist/vscode.html-custom-data.json +16 -6
  122. package/dist/web-types.json +47 -14
  123. package/package.json +1 -1
  124. package/src/Carousel.ts +14 -1
  125. package/src/DatePicker.ts +29 -11
  126. package/src/DatePickerPopoverTemplate.tsx +2 -1
  127. package/src/DatePickerTemplate.tsx +1 -0
  128. package/src/FilterMenu.ts +23 -0
  129. package/src/ListItemBase.ts +2 -2
  130. package/src/MastheadItem.ts +24 -3
  131. package/src/MastheadMobileMenu.ts +6 -0
  132. package/src/MastheadProfileMenu.hbs +25 -25
  133. package/src/MastheadProfileMenu.ts +20 -16
  134. package/src/MastheadShell.hbs +3 -2
  135. package/src/MastheadShell.ts +11 -2
  136. package/src/SearchableListBase.ts +45 -4
  137. package/src/SelectBase.ts +23 -16
  138. package/src/SelectBoxTemplate.tsx +2 -2
  139. package/src/TagGroup.ts +69 -6
  140. package/src/TagGroupTemplate.tsx +2 -0
  141. package/src/TextField.ts +9 -0
  142. package/src/TextFieldTemplate.tsx +1 -0
  143. package/src/Tooltip.ts +2 -1
  144. package/src/generated/templates/MastheadProfileMenuTemplate.lit.ts +2 -2
  145. package/src/generated/templates/MastheadShellTemplate.lit.ts +2 -2
  146. package/src/generated/themes/CountrySelector.css.ts +1 -1
  147. package/src/generated/themes/ListItem.css.ts +1 -1
  148. package/src/generated/themes/MastheadCountrySelector.css.ts +1 -1
  149. package/src/generated/themes/MastheadItem.css.ts +1 -1
  150. package/src/generated/themes/MastheadMenu.css.ts +1 -1
  151. package/src/generated/themes/MastheadMobileMenu.css.ts +1 -1
  152. package/src/generated/themes/MastheadProfileMenu.css.ts +1 -1
  153. package/src/generated/themes/RadioButton.css.ts +1 -1
  154. package/src/generated/themes/TagGroup.css.ts +1 -1
  155. package/src/generated/themes/TextField.css.ts +1 -1
  156. package/src/i18n/messagebundle_pt.properties +126 -0
  157. package/src/i18n/messagebundle_ro.properties +126 -0
  158. package/src/i18n/messagebundle_ru.properties +128 -0
  159. package/src/i18n/messagebundle_sk.properties +125 -0
  160. package/src/i18n/messagebundle_sl.properties +126 -1
  161. package/src/themes/CountrySelector.css +5 -1
  162. package/src/themes/ListItem.css +4 -0
  163. package/src/themes/MastheadCountrySelector.css +29 -33
  164. package/src/themes/MastheadItem.css +53 -13
  165. package/src/themes/MastheadMenu.css +1 -11
  166. package/src/themes/MastheadMobileMenu.css +0 -12
  167. package/src/themes/MastheadProfileMenu.css +22 -16
  168. package/src/themes/RadioButton.css +9 -6
  169. package/src/themes/TagGroup.css +1 -2
  170. package/src/themes/TextField.css +2 -1
  171. package/dist/assets/messagebundle_pt-DPeE7NbC.js +0 -1
  172. package/dist/assets/messagebundle_ro-CN4kAAYE.js +0 -1
  173. package/dist/assets/messagebundle_ru-BT7zyn7i.js +0 -1
  174. package/dist/assets/messagebundle_sk-gmWb_4Wo.js +0 -1
  175. package/dist/assets/messagebundle_sl-B4p3OqyZ.js +0 -1
@@ -0,0 +1,259 @@
1
+ import { UDExDatePicker } from "../support/component-imports.js";
2
+
3
+ describe("Date Picker", () => {
4
+ describe("formatPattern as native placeholder", () => {
5
+ // Updated: TextField now only renders the placeholder on the native input while active (focused),
6
+ // to avoid overlap with the floating label. Each test asserts absent before focus, present after focus.
7
+ // The placeholder is derived from `_effectivePlaceholder` — which returns
8
+ // `getFormat().oFormatOptions.pattern || formatPattern`. With the default
9
+ // `formatPattern = "MMM dd, yyyy"`, UI5's DateFormat honors that pattern, so the
10
+ // resolved value matches the property value.
11
+ it("passes the default formatPattern as the inner text field's native input placeholder when active", () => {
12
+ cy.mount(<UDExDatePicker id="defaultDatePicker" label="Date"></UDExDatePicker>);
13
+
14
+ // Before focus: placeholder is not rendered on the native input.
15
+ cy.get("#defaultDatePicker")
16
+ .shadow()
17
+ .find("[udex-text-field]")
18
+ .shadow()
19
+ .find("input")
20
+ .then($input => {
21
+ const placeholder = $input.attr("placeholder");
22
+ expect(placeholder === undefined || placeholder === "").to.equal(true);
23
+ });
24
+
25
+ // Focus the native input to switch the field to active state.
26
+ cy.get("#defaultDatePicker")
27
+ .shadow()
28
+ .find("[udex-text-field]")
29
+ .shadow()
30
+ .find("input")
31
+ .focus();
32
+
33
+ // After focus: the default formatPattern is exposed as the native placeholder.
34
+ cy.get("#defaultDatePicker")
35
+ .shadow()
36
+ .find("[udex-text-field]")
37
+ .shadow()
38
+ .find("input")
39
+ .should("have.attr", "placeholder", "MMM dd, yyyy");
40
+ });
41
+
42
+ // Updated: same active-gate applies for explicit formatPattern.
43
+ it("passes an explicit formatPattern to the inner text field's native input placeholder when active", () => {
44
+ cy.mount(
45
+ <UDExDatePicker
46
+ id="customPatternDatePicker"
47
+ label="Date"
48
+ formatPattern="MM/dd/yyyy"
49
+ ></UDExDatePicker>,
50
+ );
51
+
52
+ // Before focus: placeholder is absent.
53
+ cy.get("#customPatternDatePicker")
54
+ .shadow()
55
+ .find("[udex-text-field]")
56
+ .shadow()
57
+ .find("input")
58
+ .then($input => {
59
+ const placeholder = $input.attr("placeholder");
60
+ expect(placeholder === undefined || placeholder === "").to.equal(true);
61
+ });
62
+
63
+ // Focus, then assert the explicit pattern is rendered.
64
+ cy.get("#customPatternDatePicker")
65
+ .shadow()
66
+ .find("[udex-text-field]")
67
+ .shadow()
68
+ .find("input")
69
+ .focus();
70
+
71
+ cy.get("#customPatternDatePicker")
72
+ .shadow()
73
+ .find("[udex-text-field]")
74
+ .shadow()
75
+ .find("input")
76
+ .should("have.attr", "placeholder", "MM/dd/yyyy");
77
+ });
78
+
79
+ // Updated: reactive update is only observable while the field is active.
80
+ it("updates the native input placeholder when formatPattern changes (while active)", () => {
81
+ cy.mount(
82
+ <UDExDatePicker
83
+ id="reactivePatternDatePicker"
84
+ formatPattern="MMM dd, yyyy"
85
+ ></UDExDatePicker>,
86
+ );
87
+
88
+ // Focus so the placeholder is actually rendered on the native input.
89
+ cy.get("#reactivePatternDatePicker")
90
+ .shadow()
91
+ .find("[udex-text-field]")
92
+ .shadow()
93
+ .find("input")
94
+ .focus();
95
+
96
+ cy.get("#reactivePatternDatePicker")
97
+ .shadow()
98
+ .find("[udex-text-field]")
99
+ .shadow()
100
+ .find("input")
101
+ .should("have.attr", "placeholder", "MMM dd, yyyy");
102
+
103
+ cy.get("#reactivePatternDatePicker").then($el => {
104
+ ($el[0] as unknown as { formatPattern: string }).formatPattern = "MM/dd/yyyy";
105
+ });
106
+
107
+ // Keep focus so the field remains active, then assert the updated pattern is reflected.
108
+ cy.get("#reactivePatternDatePicker")
109
+ .shadow()
110
+ .find("[udex-text-field]")
111
+ .shadow()
112
+ .find("input")
113
+ .focus();
114
+
115
+ cy.get("#reactivePatternDatePicker")
116
+ .shadow()
117
+ .find("[udex-text-field]")
118
+ .shadow()
119
+ .find("input")
120
+ .should("have.attr", "placeholder", "MM/dd/yyyy");
121
+ });
122
+
123
+ // Added: DatePicker forwards its resolved placeholder to the inner UDExTextField, which
124
+ // now renders the native placeholder without focus when no label is set. Assert visible
125
+ // before focus so the no-label branch of `active || !this.label` is exercised.
126
+ it("renders the default formatPattern as the native input placeholder without focus when no label is set", () => {
127
+ cy.mount(<UDExDatePicker id="noLabelDatePicker"></UDExDatePicker>);
128
+
129
+ // DatePicker has no `label` set, so the inner TextField skips the active gate.
130
+ cy.get("#noLabelDatePicker")
131
+ .shadow()
132
+ .find("[udex-text-field]")
133
+ .shadow()
134
+ .find("input")
135
+ .should("have.attr", "placeholder", "MMM dd, yyyy");
136
+ });
137
+
138
+ // Updated: `_effectivePlaceholder` now falls back to `UDExDatePicker.DEFAULT_FORMAT_PATTERN`
139
+ // ("MMM dd, yyyy") whenever `formatPattern` is falsy — covers both the "never set" case and
140
+ // the Storybook case where clearing the control sets `format-pattern=""` on the attribute,
141
+ // which would otherwise override the class-level default of the property.
142
+ it("uses the default formatPattern as placeholder when formatPattern is not explicitly set", () => {
143
+ cy.mount(<UDExDatePicker id="defaultPatternDatePicker" label="Date"></UDExDatePicker>);
144
+
145
+ cy.get("#defaultPatternDatePicker")
146
+ .shadow()
147
+ .find("[udex-text-field]")
148
+ .shadow()
149
+ .find("input")
150
+ .focus();
151
+
152
+ cy.get("#defaultPatternDatePicker")
153
+ .shadow()
154
+ .find("[udex-text-field]")
155
+ .shadow()
156
+ .find("input")
157
+ .should("have.attr", "placeholder", "MMM dd, yyyy");
158
+
159
+ // Case 2: explicit empty-string attribute (Storybook's cleared-control behavior).
160
+ // Mount with `format-pattern=""` set via the attribute so it overrides the class default,
161
+ // and confirm the getter still surfaces "MMM dd, yyyy" as the native placeholder.
162
+ cy.mount(
163
+ <UDExDatePicker
164
+ id="emptyPatternDatePicker"
165
+ label="Date"
166
+ format-pattern=""
167
+ ></UDExDatePicker>,
168
+ );
169
+
170
+ // Sanity check: the attribute is truly the empty string on the host, so the property
171
+ // is falsy and the fallback branch of `_effectivePlaceholder` is what's being exercised.
172
+ cy.get("#emptyPatternDatePicker").should("have.attr", "format-pattern", "");
173
+ cy.get("#emptyPatternDatePicker").then($el => {
174
+ const dp = $el[0] as unknown as { formatPattern: string };
175
+ expect(dp.formatPattern).to.equal("");
176
+ });
177
+
178
+ cy.get("#emptyPatternDatePicker")
179
+ .shadow()
180
+ .find("[udex-text-field]")
181
+ .shadow()
182
+ .find("input")
183
+ .focus();
184
+
185
+ cy.get("#emptyPatternDatePicker")
186
+ .shadow()
187
+ .find("[udex-text-field]")
188
+ .shadow()
189
+ .find("input")
190
+ .should("have.attr", "placeholder", "MMM dd, yyyy");
191
+ });
192
+ });
193
+
194
+ describe("calendar timestamp navigation", () => {
195
+ // Regression coverage for the fix that forwards `_calendarTimestamp` to the inner
196
+ // `udex-calendar`. UI5's Calendar guards `timestamp` behind an internal
197
+ // `_valueIsProcessed` flag after first render — without this forwarding, the calendar
198
+ // stays on the initial (empty) month when the popover opens after a date is typed.
199
+ // The visual month is a CSS/render concern, so these tests assert the JS-observable
200
+ // contract: the getter and the property flow-through to the child element.
201
+
202
+ it("_calendarTimestamp returns a non-zero UTC seconds timestamp for a valid value", () => {
203
+ cy.mount(
204
+ <UDExDatePicker id="tsDatePicker" value="May 16, 2026"></UDExDatePicker>,
205
+ );
206
+
207
+ cy.get("#tsDatePicker").then($el => {
208
+ const dp = $el[0] as unknown as { _calendarTimestamp: number | undefined };
209
+ expect(dp._calendarTimestamp).to.be.a("number");
210
+ expect(dp._calendarTimestamp).to.be.greaterThan(0);
211
+ });
212
+ });
213
+
214
+ it("_calendarTimestamp is undefined when value is empty", () => {
215
+ cy.mount(<UDExDatePicker id="emptyTsDatePicker"></UDExDatePicker>);
216
+
217
+ cy.get("#emptyTsDatePicker").then($el => {
218
+ const dp = $el[0] as unknown as { _calendarTimestamp: number | undefined };
219
+ // `_extractFirstTimestamp` returns undefined for empty/invalid values.
220
+ // eslint-disable-next-line no-unused-expressions
221
+ expect(dp._calendarTimestamp).to.be.undefined;
222
+ });
223
+ });
224
+
225
+ it("forwards _calendarTimestamp as the `timestamp` property on the inner udex-calendar", () => {
226
+ cy.mount(
227
+ <UDExDatePicker
228
+ id="forwardTsDatePicker"
229
+ value="May 16, 2026"
230
+ ></UDExDatePicker>,
231
+ );
232
+
233
+ // Open the popover via the public API so the calendar is rendered into the tree.
234
+ // Avoids coupling to the icon-button shadow DOM shape of the inner TextField.
235
+ cy.get("#forwardTsDatePicker").then($el => {
236
+ ($el[0] as unknown as { showCalendar: () => void }).showCalendar();
237
+ });
238
+
239
+ // Read the DatePicker's `_calendarTimestamp` first, then assert the inner
240
+ // udex-calendar's `timestamp` property matches.
241
+ cy.get("#forwardTsDatePicker").then($el => {
242
+ const dp = $el[0] as unknown as { _calendarTimestamp: number | undefined };
243
+ const expected = dp._calendarTimestamp;
244
+ expect(expected, "DatePicker should expose a numeric _calendarTimestamp").to.be.a(
245
+ "number",
246
+ );
247
+
248
+ cy.get("#forwardTsDatePicker")
249
+ .shadow()
250
+ .find("udex-calendar")
251
+ .should("exist")
252
+ .then($cal => {
253
+ const cal = $cal[0] as unknown as { timestamp: number | undefined };
254
+ expect(cal.timestamp).to.equal(expected);
255
+ });
256
+ });
257
+ });
258
+ });
259
+ });
@@ -1145,4 +1145,100 @@ describe("Filter Menu component", () => {
1145
1145
  .should("have.text", "No matches found.");
1146
1146
  });
1147
1147
  });
1148
+
1149
+ describe("Dropdown scroll on close", () => {
1150
+ const filterMenuSingleSelectScrollFixture = <UDExFilterMenu
1151
+ id="filter-menu"
1152
+ filter-name="Languages"
1153
+ height="200px">
1154
+ <UDExFilterMenuItem label="A"></UDExFilterMenuItem>
1155
+ <UDExFilterMenuItem label="B"></UDExFilterMenuItem>
1156
+ <UDExFilterMenuItem label="C"></UDExFilterMenuItem>
1157
+ <UDExFilterMenuItem label="D"></UDExFilterMenuItem>
1158
+ <UDExFilterMenuItem label="E"></UDExFilterMenuItem>
1159
+ <UDExFilterMenuItem label="F"></UDExFilterMenuItem>
1160
+ <UDExFilterMenuItem label="G"></UDExFilterMenuItem>
1161
+ <UDExFilterMenuItem label="H"></UDExFilterMenuItem>
1162
+ <UDExFilterMenuItem label="I"></UDExFilterMenuItem>
1163
+ <UDExFilterMenuItem label="J"></UDExFilterMenuItem>
1164
+ <UDExFilterMenuItem selected label="K"></UDExFilterMenuItem>
1165
+ </UDExFilterMenu>
1166
+
1167
+ const filterMenuScrollFixture = <UDExFilterMenu
1168
+ id="filter-menu"
1169
+ filter-name="Filter Name"
1170
+ mode={UDExListMode.MultiSelect}
1171
+ height="200px">
1172
+ <UDExFilterMenuItem label="JavaScript" supporting-text="Frontend web development"></UDExFilterMenuItem>
1173
+ <UDExFilterMenuItem label="Python" supporting-text="Used in data analysis"></UDExFilterMenuItem>
1174
+ <UDExFilterMenuItem label="Java" supporting-text="Building large-scale applications"></UDExFilterMenuItem>
1175
+ <UDExFilterMenuItem label="Swift" supporting-text="iOS app development"></UDExFilterMenuItem>
1176
+ <UDExFilterMenuItem label="C#" supporting-text="Object-oriented language"></UDExFilterMenuItem>
1177
+ <UDExFilterMenuItem label="Perl" supporting-text="General-purpose scripting language"></UDExFilterMenuItem>
1178
+ <UDExFilterMenuItem label="Rust" supporting-text="Focused on safety and speed"></UDExFilterMenuItem>
1179
+ <UDExFilterMenuItem label="PHP" supporting-text="Server-side scripting language"></UDExFilterMenuItem>
1180
+ <UDExFilterMenuItem label="Go" supporting-text="Statically typed language"></UDExFilterMenuItem>
1181
+ <UDExFilterMenuItem label="Kotlin" supporting-text="For Android development"></UDExFilterMenuItem>
1182
+ <UDExFilterMenuItem label="TypeScript" supporting-text="Super set of JS with static typing"></UDExFilterMenuItem>
1183
+ <UDExFilterMenuItem label="Ruby" supporting-text="Readable and easy-to-use"></UDExFilterMenuItem>
1184
+ <UDExFilterMenuItem label="C++" supporting-text="Efficient and flexible language"></UDExFilterMenuItem>
1185
+ <UDExFilterMenuItem label="R" supporting-text="Language for statistical computing"></UDExFilterMenuItem>
1186
+ </UDExFilterMenu>
1187
+
1188
+ it("SingleSelect mode - selected item is scrolled into view after closing and reopening the dropdown", () => {
1189
+ cy.mount(filterMenuSingleSelectScrollFixture);
1190
+ cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__trigger").as("trigger");
1191
+
1192
+ cy.get("@trigger").click();
1193
+
1194
+ cy.get("[udex-filter-menu]").shadow()
1195
+ .find(".udex-filter-menu__list")
1196
+ .find("udex-list-item").last()
1197
+ .shadow().find(".udex-list__item")
1198
+ .click();
1199
+
1200
+ cy.get("@trigger").should("have.attr", "aria-expanded", "false");
1201
+
1202
+ cy.get("@trigger").click();
1203
+
1204
+ cy.get("@trigger").should("have.attr", "aria-expanded", "true");
1205
+
1206
+ cy.get("#filter-menu").shadow()
1207
+ .find("ui5-popover").shadow()
1208
+ .find("[part=content]")
1209
+ .invoke("prop", "scrollTop")
1210
+ .should("be.greaterThan", 0);
1211
+ });
1212
+
1213
+ it("MultiSelect mode - scroll position resets to top after closing and reopening the dropdown", () => {
1214
+ cy.mount(filterMenuScrollFixture);
1215
+ cy.get("[udex-filter-menu]").shadow().find(".udex-filter-menu__trigger").as("trigger");
1216
+
1217
+ cy.get("@trigger").click();
1218
+
1219
+ cy.get("#filter-menu").shadow()
1220
+ .find("ui5-popover").shadow()
1221
+ .find("[part=content]").as("content");
1222
+
1223
+ cy.get("@content").then($el => {
1224
+ $el[0].scrollTop = 9999;
1225
+ });
1226
+
1227
+ cy.get("@content").invoke("prop", "scrollTop").should("be.greaterThan", 0);
1228
+
1229
+ cy.get("@trigger").click();
1230
+
1231
+ cy.get("@trigger").should("have.attr", "aria-expanded", "false");
1232
+
1233
+ cy.get("@trigger").click();
1234
+
1235
+ cy.get("@trigger").should("have.attr", "aria-expanded", "true");
1236
+
1237
+ cy.get("#filter-menu").shadow()
1238
+ .find("ui5-popover").shadow()
1239
+ .find("[part=content]")
1240
+ .invoke("prop", "scrollTop")
1241
+ .should("equal", 0);
1242
+ });
1243
+ });
1148
1244
  });
@@ -0,0 +1,88 @@
1
+ import {
2
+ UDExMastheadItem,
3
+ UDExMastheadMenu,
4
+ UDExMastheadMobileMenu,
5
+ } from "../support/component-imports.js";
6
+ import { subItemsProduct } from "../fixtures/masthead-shell.js";
7
+
8
+ describe("Masthead item dropdown scroll behavior", () => {
9
+ beforeEach(() => {
10
+ cy.viewport(800, 900);
11
+ cy.mount(
12
+ <UDExMastheadItem id="test-item" accessible-name="Products">
13
+ Products
14
+ <UDExMastheadMenu
15
+ id="test-menu"
16
+ slot="dropdown"
17
+ menu-title="Menu"
18
+ subItems={JSON.stringify(subItemsProduct)}
19
+ />
20
+ </UDExMastheadItem>,
21
+ );
22
+ });
23
+
24
+ it("scrolls dropdown-holder to top when item is deselected on mobile", () => {
25
+ cy.get("#test-item").then($item => {
26
+ ($item[0] as any).selected = true;
27
+ });
28
+
29
+ cy.get("#test-item")
30
+ .shadow()
31
+ .find(".udex-masthead-item__dropdown-holder")
32
+ .should("exist");
33
+
34
+ cy.get("#test-item").then($item => {
35
+ const item = $item[0] as any;
36
+ const holder = item.shadowRoot.querySelector(
37
+ ".udex-masthead-item__dropdown-holder",
38
+ ) as HTMLElement;
39
+ const scrollToSpy = cy.spy(holder, "scrollTo");
40
+ holder.scrollTop = 200;
41
+
42
+ item.selected = false;
43
+
44
+ expect(scrollToSpy).to.have.been.calledOnce;
45
+ expect(scrollToSpy).to.have.been.calledWith({ top: 0 });
46
+ });
47
+ });
48
+
49
+ it("does not scroll when item is deselected on desktop", () => {
50
+ cy.viewport(1920, 1080);
51
+
52
+ cy.get("#test-item").then($item => {
53
+ const item = $item[0] as any;
54
+ const scrollSpy = cy.spy(item, "dropdownScrollToTop");
55
+
56
+ item.selected = true;
57
+ item.selected = false;
58
+
59
+ expect(scrollSpy).to.not.have.been.called;
60
+ });
61
+ });
62
+
63
+ it("calls mobileMenuScrollToTop on the slotted mobile menu when deselected on mobile", () => {
64
+ cy.viewport(800, 900);
65
+
66
+ cy.mount(
67
+ <UDExMastheadItem id="menu-btn-item" accessible-name="menu" icon="menu2">
68
+ <UDExMastheadMobileMenu id="mobile-menu" slot="dropdown" />
69
+ </UDExMastheadItem>,
70
+ );
71
+
72
+ cy.get("#menu-btn-item").invoke("attr", "_is-menu-open-btn", "");
73
+
74
+ cy.get("#menu-btn-item").then($item => {
75
+ ($item[0] as any).selected = true;
76
+ });
77
+
78
+ cy.get("#mobile-menu").then($menu => {
79
+ const mobileMenu = $menu[0] as any;
80
+ const scrollSpy = cy.spy(mobileMenu, "mobileMenuScrollToTop");
81
+
82
+ const item = document.querySelector<any>("#menu-btn-item");
83
+ item.selected = false;
84
+
85
+ expect(scrollSpy).to.have.been.calledOnce;
86
+ });
87
+ });
88
+ });
@@ -577,7 +577,9 @@ describe("MastheadShell Default component", () => {
577
577
  });
578
578
 
579
579
  it("'ui5-profile-click' emits when clicking profile item", () => {
580
- cy.get("#profile-item").click();
580
+ cy.get("#profile-item").then($el => {
581
+ $el[0].dispatchEvent(new CustomEvent("click-default", { bubbles: true, composed: true }));
582
+ });
581
583
  cy.get("#eventComp").should("have.text", "ui5-profile-click");
582
584
  });
583
585
 
@@ -2222,4 +2222,86 @@ describe("SelectBox", () => {
2222
2222
  });
2223
2223
  });
2224
2224
  });
2225
+
2226
+ describe("Dropdown scroll", () => {
2227
+ const singleSelectScrollFixture = <div style="width: 350px; position:relative;">
2228
+ <UDExSelectBox id="select-box" height="200px" label="Languages">
2229
+ <UDExSelectBoxItem label="A"></UDExSelectBoxItem>
2230
+ <UDExSelectBoxItem label="B"></UDExSelectBoxItem>
2231
+ <UDExSelectBoxItem label="C"></UDExSelectBoxItem>
2232
+ <UDExSelectBoxItem label="D"></UDExSelectBoxItem>
2233
+ <UDExSelectBoxItem label="E"></UDExSelectBoxItem>
2234
+ <UDExSelectBoxItem label="F"></UDExSelectBoxItem>
2235
+ <UDExSelectBoxItem label="G"></UDExSelectBoxItem>
2236
+ <UDExSelectBoxItem label="H"></UDExSelectBoxItem>
2237
+ <UDExSelectBoxItem label="I"></UDExSelectBoxItem>
2238
+ <UDExSelectBoxItem label="J"></UDExSelectBoxItem>
2239
+ <UDExSelectBoxItem label="K" selected></UDExSelectBoxItem>
2240
+ </UDExSelectBox>
2241
+ </div>;
2242
+
2243
+ it("MultiSelect mode - scroll position resets to top after closing and reopening the dropdown", () => {
2244
+ cy.mount(multiSelect);
2245
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").as("trigger");
2246
+
2247
+ cy.get("@trigger").click();
2248
+
2249
+ cy.get("#select-box").shadow()
2250
+ .find("ui5-popover").shadow()
2251
+ .find("[part=content]").as("content");
2252
+
2253
+ cy.get("@content").then($el => {
2254
+ $el[0].scrollTop = 9999;
2255
+ });
2256
+
2257
+ cy.get("@content").invoke("prop", "scrollTop").should("be.greaterThan", 0);
2258
+
2259
+ cy.get("@trigger").click();
2260
+
2261
+ cy.get("#select-box").shadow()
2262
+ .find(".udex-select-box__dropdown")
2263
+ .should("not.have.attr", "open");
2264
+
2265
+ cy.get("@trigger").click();
2266
+
2267
+ cy.get("#select-box").shadow()
2268
+ .find(".udex-select-box__dropdown")
2269
+ .should("have.attr", "open");
2270
+
2271
+ cy.get("#select-box").shadow()
2272
+ .find("ui5-popover").shadow()
2273
+ .find("[part=content]")
2274
+ .invoke("prop", "scrollTop")
2275
+ .should("equal", 0);
2276
+ });
2277
+
2278
+ it("SingleSelect mode - selected item is scrolled into view after closing and reopening the dropdown", () => {
2279
+ cy.mount(singleSelectScrollFixture);
2280
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").as("trigger");
2281
+
2282
+ cy.get("@trigger").click();
2283
+
2284
+ cy.get("#select-box").shadow()
2285
+ .find(".udex-select-box__dropdown")
2286
+ .should("have.attr", "open");
2287
+
2288
+ cy.get("@trigger").click();
2289
+
2290
+ cy.get("#select-box").shadow()
2291
+ .find(".udex-select-box__dropdown")
2292
+ .should("not.have.attr", "open");
2293
+
2294
+ cy.get("@trigger").click();
2295
+
2296
+ cy.get("#select-box").shadow()
2297
+ .find(".udex-select-box__dropdown")
2298
+ .should("have.attr", "open");
2299
+
2300
+ cy.get("#select-box").shadow()
2301
+ .find("ui5-popover").shadow()
2302
+ .find("[part=content]")
2303
+ .invoke("prop", "scrollTop")
2304
+ .should("be.greaterThan", 0);
2305
+ });
2306
+ });
2225
2307
  });
@@ -95,4 +95,119 @@ describe("TagGroup", () => {
95
95
  cy.get("@tagClick").should("have.been.called");
96
96
  });
97
97
 
98
+ it("tooltip accessible name is set on the tooltip element", () => {
99
+ cy.mount(
100
+ <UDExTagGroup tooltipAccessibleName="Hidden tags">
101
+ {Array.from({ length: 16 }, (_, i) => <UDExTag text={`Tag ${i + 1}`}></UDExTag>)}
102
+ </UDExTagGroup>
103
+ );
104
+ const nMore = cy.get("[udex-tag-group]").shadow().find(".udex-tag-group__tokenizer").shadow().find(".ui5-tokenizer-more-text");
105
+ nMore.click();
106
+ cy.get("[udex-tag-group]").shadow().find("[udex-tooltip]").should("have.attr", "accessible-name", "Hidden tags");
107
+ });
108
+
109
+ describe("Keyboard focus with hidden tags", () => {
110
+ it("keeps the 'n more' button visible when a visible tag receives focus (overflow case)", () => {
111
+ cy.mount(<div><button id="before">before</button>{defaultTagGroup}</div>);
112
+
113
+ cy.get("[udex-tag-group]").shadow().find(".udex-tag-group__tokenizer").as("tokenizer");
114
+ // Wait until TagGroup has processed overflow state before navigating
115
+ cy.get("[udex-tag-group]").should("have.attr", "_is-hidden-tags");
116
+
117
+ cy.get("#before").focus();
118
+ cy.realPress("Tab");
119
+
120
+ cy.get("@tokenizer").should("not.have.attr", "expanded");
121
+ cy.get("@tokenizer").shadow().find(".ui5-tokenizer-more-text").should("be.visible");
122
+ });
123
+
124
+ it("keeps the 'n more' button reachable across successive focus moves between tags", () => {
125
+ cy.mount(<div><button id="before">before</button>{defaultTagGroup}</div>);
126
+
127
+ cy.get("[udex-tag-group]").shadow().find(".udex-tag-group__tokenizer").as("tokenizer");
128
+ cy.get("[udex-tag-group]").should("have.attr", "_is-hidden-tags");
129
+
130
+ cy.get("#before").focus();
131
+ cy.realPress("Tab");
132
+ cy.get("@tokenizer").should("not.have.attr", "expanded");
133
+
134
+ cy.realPress("Tab");
135
+ cy.get("@tokenizer").should("not.have.attr", "expanded");
136
+ cy.get("@tokenizer").shadow().find(".ui5-tokenizer-more-text").should("be.visible");
137
+ });
138
+
139
+ it("does not collapse the tokenizer on focusin when there are no hidden tags (no regression)", () => {
140
+ cy.mount(<div><button id="before">before</button>{expandedTagGroup}</div>);
141
+
142
+ cy.get("[udex-tag-group]").shadow().find(".udex-tag-group__tokenizer").as("tokenizer");
143
+ cy.get("@tokenizer").should("have.attr", "expanded");
144
+
145
+ cy.get("#before").focus();
146
+ cy.realPress("Tab");
147
+
148
+ cy.get("@tokenizer").should("have.attr", "expanded");
149
+ });
150
+
151
+ it("Tab on the last visible tag moves focus to the 'n more' button (overflow case)", () => {
152
+ cy.mount(<div><button id="before">before</button>{defaultTagGroup}</div>);
153
+
154
+ cy.get("[udex-tag-group]").shadow().find(".udex-tag-group__tokenizer").as("tokenizer");
155
+ cy.get("[udex-tag-group]").should("have.attr", "_is-hidden-tags");
156
+ cy.get("@tokenizer").shadow().find(".ui5-tokenizer-more-text").should("be.visible");
157
+
158
+ cy.get("@tokenizer").then($tokenizer => {
159
+ const nMoreCount = ($tokenizer[0] as any)._nMoreCount as number;
160
+ const totalCount = ($tokenizer[0] as any)._tokens.length as number;
161
+ const visibleCount = totalCount - nMoreCount;
162
+ cy.get("#before").focus();
163
+ // Tab to enter group (1) + Tab through each visible tag to reach the last
164
+ Cypress._.times(visibleCount, () => cy.realPress("Tab"));
165
+ });
166
+
167
+ cy.realPress("Tab");
168
+
169
+ cy.get("@tokenizer").shadow().find(".ui5-tokenizer-more-text").should("be.focused");
170
+ });
171
+
172
+ it("Shift+Tab on the 'n more' button moves focus back to the last visible tag", () => {
173
+ cy.mount(<div><button id="before">before</button>{defaultTagGroup}</div>);
174
+
175
+ cy.get("[udex-tag-group]").shadow().find(".udex-tag-group__tokenizer").as("tokenizer");
176
+ cy.get("[udex-tag-group]").should("have.attr", "_is-hidden-tags");
177
+
178
+ cy.get("@tokenizer").then($tokenizer => {
179
+ const nMoreCount = ($tokenizer[0] as any)._nMoreCount as number;
180
+ const totalCount = ($tokenizer[0] as any)._tokens.length as number;
181
+ const visibleCount = totalCount - nMoreCount;
182
+ cy.get("#before").focus();
183
+ // Tab through all visible tags + 1 to land on "n more"
184
+ Cypress._.times(visibleCount + 1, () => cy.realPress("Tab"));
185
+ });
186
+
187
+ cy.get("@tokenizer").shadow().find(".ui5-tokenizer-more-text").should("be.focused");
188
+
189
+ cy.realPress(["Shift", "Tab"]);
190
+
191
+ cy.get("@tokenizer").then($tokenizer => {
192
+ const nMoreCount = ($tokenizer[0] as any)._nMoreCount as number;
193
+ const totalCount = ($tokenizer[0] as any)._tokens.length as number;
194
+ const lastVisibleToken = ($tokenizer[0] as any)._tokens[totalCount - nMoreCount - 1];
195
+ cy.wrap(lastVisibleToken).shadow().find("a, [tabindex='0']").first().should("be.focused");
196
+ });
197
+ });
198
+
199
+ it("keeps the 'n more' button present in shadow DOM whenever a visible tag is focused", () => {
200
+ cy.mount(<div><button id="before">before</button>{defaultTagGroup}</div>);
201
+
202
+ cy.get("[udex-tag-group]").shadow().find(".udex-tag-group__tokenizer").as("tokenizer");
203
+ cy.get("[udex-tag-group]").should("have.attr", "_is-hidden-tags");
204
+
205
+ cy.get("#before").focus();
206
+ cy.realPress("Tab");
207
+
208
+ cy.get("@tokenizer").should("not.have.attr", "expanded");
209
+ cy.get("@tokenizer").shadow().find(".ui5-tokenizer-more-text").should("exist").and("be.visible");
210
+ });
211
+ });
212
+
98
213
  });