@sapudex/web-components 2.33.0 → 2.35.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/FilterMenu.cy.tsx +53 -0
  2. package/cypress/specs/ListItem.cy.tsx +73 -21
  3. package/cypress/specs/MastheadMenu.cy.tsx +645 -0
  4. package/cypress/specs/MastheadMobileMenu.cy.tsx +573 -0
  5. package/cypress/specs/MastheadShell.cy.tsx +79 -0
  6. package/cypress/specs/SecondaryNavigationDesktop.cy.tsx +14 -0
  7. package/cypress/specs/SelectBox.cy.tsx +229 -1
  8. package/cypress/support/component-imports.instrumented.ts +2 -0
  9. package/cypress/support/component-imports.src.ts +2 -0
  10. package/dist/CountrySelector.d.ts +1 -0
  11. package/dist/CountrySelector.js +3 -0
  12. package/dist/CountrySelector.js.map +1 -1
  13. package/dist/FilterMenu.d.ts +1 -0
  14. package/dist/FilterMenu.js +3 -0
  15. package/dist/FilterMenu.js.map +1 -1
  16. package/dist/ListItem.d.ts +3 -0
  17. package/dist/ListItem.js +10 -1
  18. package/dist/ListItem.js.map +1 -1
  19. package/dist/ListItemBase.d.ts +31 -0
  20. package/dist/ListItemBase.js +28 -0
  21. package/dist/ListItemBase.js.map +1 -1
  22. package/dist/MastheadItem.d.ts +12 -1
  23. package/dist/MastheadItem.js +33 -1
  24. package/dist/MastheadItem.js.map +1 -1
  25. package/dist/MastheadMenu.d.ts +5 -1
  26. package/dist/MastheadMenu.js +16 -6
  27. package/dist/MastheadMenu.js.map +1 -1
  28. package/dist/SearchableListBase.d.ts +3 -1
  29. package/dist/SearchableListBase.js +7 -1
  30. package/dist/SearchableListBase.js.map +1 -1
  31. package/dist/SecondaryNavigationItem.d.ts +5 -1
  32. package/dist/SecondaryNavigationItem.js +35 -5
  33. package/dist/SecondaryNavigationItem.js.map +1 -1
  34. package/dist/SelectBase.js +14 -3
  35. package/dist/SelectBase.js.map +1 -1
  36. package/dist/SelectBox.d.ts +1 -0
  37. package/dist/SelectBox.js +3 -0
  38. package/dist/SelectBox.js.map +1 -1
  39. package/dist/assets/{InputSuggestions-B_Dl2L6w.js → InputSuggestions-CrSNIz-l.js} +1 -1
  40. package/dist/assets/{ListItemStandardExpandableTextTemplate-9Z-aZmJx.js → ListItemStandardExpandableTextTemplate-BUgxFxi7.js} +1 -1
  41. package/dist/assets/{bundle-DbTEs-Za.css → bundle-JYo3bV8v.css} +1 -1
  42. package/dist/assets/{bundle.esm-BlIxH9k1.js → bundle.esm-BZJm6rNI.js} +633 -219
  43. package/dist/css/themes/Breadcrumbs.css +1 -1
  44. package/dist/css/themes/BreadcrumbsPopover.css +1 -1
  45. package/dist/css/themes/FilterMenu.css +1 -1
  46. package/dist/css/themes/ListGroupHeader.css +1 -1
  47. package/dist/css/themes/ListItem.css +1 -1
  48. package/dist/css/themes/MastheadItem.css +1 -1
  49. package/dist/css/themes/MastheadMenu.css +1 -1
  50. package/dist/css/themes/MastheadMobileMenu.css +1 -1
  51. package/dist/css/themes/RatingIndicator.css +1 -1
  52. package/dist/css/themes/SearchableList.css +1 -1
  53. package/dist/css/themes/SecondaryNavigation.css +1 -1
  54. package/dist/css/themes/SecondaryNavigationItem.css +1 -1
  55. package/dist/css/themes/SelectBase.css +1 -1
  56. package/dist/css/themes/Slider.css +1 -1
  57. package/dist/custom-elements-internal.json +1125 -35
  58. package/dist/custom-elements.json +1126 -36
  59. package/dist/generated/templates/ListGroupHeaderTemplate.lit.js +16 -12
  60. package/dist/generated/templates/ListGroupHeaderTemplate.lit.js.map +1 -1
  61. package/dist/generated/templates/ListItemTemplate.lit.js +40 -32
  62. package/dist/generated/templates/ListItemTemplate.lit.js.map +1 -1
  63. package/dist/generated/templates/MastheadItemTemplate.lit.js +2 -2
  64. package/dist/generated/templates/MastheadItemTemplate.lit.js.map +1 -1
  65. package/dist/generated/templates/MastheadMenuTemplate.lit.js +8 -1
  66. package/dist/generated/templates/MastheadMenuTemplate.lit.js.map +1 -1
  67. package/dist/generated/templates/MastheadMobileMenuTemplate.lit.js +10 -4
  68. package/dist/generated/templates/MastheadMobileMenuTemplate.lit.js.map +1 -1
  69. package/dist/generated/templates/MastheadShellTemplate.lit.js +3 -3
  70. package/dist/generated/templates/MastheadShellTemplate.lit.js.map +1 -1
  71. package/dist/generated/templates/SearchableListBaseTemplate.lit.js +4 -4
  72. package/dist/generated/templates/SearchableListBaseTemplate.lit.js.map +1 -1
  73. package/dist/generated/templates/SelectBoxTemplate.lit.js +12 -10
  74. package/dist/generated/templates/SelectBoxTemplate.lit.js.map +1 -1
  75. package/dist/generated/themes/Breadcrumbs.css.d.ts +1 -1
  76. package/dist/generated/themes/Breadcrumbs.css.js +1 -1
  77. package/dist/generated/themes/Breadcrumbs.css.js.map +1 -1
  78. package/dist/generated/themes/BreadcrumbsPopover.css.d.ts +1 -1
  79. package/dist/generated/themes/BreadcrumbsPopover.css.js +1 -1
  80. package/dist/generated/themes/BreadcrumbsPopover.css.js.map +1 -1
  81. package/dist/generated/themes/FilterMenu.css.d.ts +1 -1
  82. package/dist/generated/themes/FilterMenu.css.js +1 -1
  83. package/dist/generated/themes/FilterMenu.css.js.map +1 -1
  84. package/dist/generated/themes/ListGroupHeader.css.d.ts +1 -1
  85. package/dist/generated/themes/ListGroupHeader.css.js +1 -1
  86. package/dist/generated/themes/ListGroupHeader.css.js.map +1 -1
  87. package/dist/generated/themes/ListItem.css.d.ts +1 -1
  88. package/dist/generated/themes/ListItem.css.js +1 -1
  89. package/dist/generated/themes/ListItem.css.js.map +1 -1
  90. package/dist/generated/themes/MastheadItem.css.d.ts +1 -1
  91. package/dist/generated/themes/MastheadItem.css.js +1 -1
  92. package/dist/generated/themes/MastheadItem.css.js.map +1 -1
  93. package/dist/generated/themes/MastheadMenu.css.d.ts +1 -1
  94. package/dist/generated/themes/MastheadMenu.css.js +1 -1
  95. package/dist/generated/themes/MastheadMenu.css.js.map +1 -1
  96. package/dist/generated/themes/MastheadMobileMenu.css.d.ts +1 -1
  97. package/dist/generated/themes/MastheadMobileMenu.css.js +1 -1
  98. package/dist/generated/themes/MastheadMobileMenu.css.js.map +1 -1
  99. package/dist/generated/themes/RatingIndicator.css.d.ts +1 -1
  100. package/dist/generated/themes/RatingIndicator.css.js +1 -1
  101. package/dist/generated/themes/RatingIndicator.css.js.map +1 -1
  102. package/dist/generated/themes/SearchableList.css.d.ts +1 -1
  103. package/dist/generated/themes/SearchableList.css.js +1 -1
  104. package/dist/generated/themes/SearchableList.css.js.map +1 -1
  105. package/dist/generated/themes/SecondaryNavigation.css.d.ts +1 -1
  106. package/dist/generated/themes/SecondaryNavigation.css.js +1 -1
  107. package/dist/generated/themes/SecondaryNavigation.css.js.map +1 -1
  108. package/dist/generated/themes/SecondaryNavigationItem.css.d.ts +1 -1
  109. package/dist/generated/themes/SecondaryNavigationItem.css.js +1 -1
  110. package/dist/generated/themes/SecondaryNavigationItem.css.js.map +1 -1
  111. package/dist/generated/themes/SelectBase.css.d.ts +1 -1
  112. package/dist/generated/themes/SelectBase.css.js +1 -1
  113. package/dist/generated/themes/SelectBase.css.js.map +1 -1
  114. package/dist/generated/themes/Slider.css.d.ts +1 -1
  115. package/dist/generated/themes/Slider.css.js +1 -1
  116. package/dist/generated/themes/Slider.css.js.map +1 -1
  117. package/dist/types/List.d.ts +2 -0
  118. package/dist/types/List.js +2 -0
  119. package/dist/types/List.js.map +1 -1
  120. package/dist/vscode.html-custom-data.json +292 -12
  121. package/dist/web-types.json +431 -21
  122. package/package.json +1 -1
  123. package/src/CountrySelector.ts +3 -0
  124. package/src/FilterMenu.ts +3 -0
  125. package/src/ListGroupHeader.hbs +11 -7
  126. package/src/ListItem.hbs +28 -2
  127. package/src/ListItem.ts +12 -1
  128. package/src/ListItemBase.ts +48 -0
  129. package/src/MastheadItem.hbs +2 -0
  130. package/src/MastheadItem.ts +37 -2
  131. package/src/MastheadMenu.hbs +35 -0
  132. package/src/MastheadMenu.ts +21 -7
  133. package/src/MastheadMobileMenu.hbs +40 -9
  134. package/src/MastheadShell.hbs +3 -0
  135. package/src/SearchableListBase.hbs +21 -1
  136. package/src/SearchableListBase.ts +11 -2
  137. package/src/SecondaryNavigationItem.ts +37 -5
  138. package/src/SelectBase.ts +15 -3
  139. package/src/SelectBox.hbs +13 -5
  140. package/src/SelectBox.ts +3 -0
  141. package/src/generated/templates/ListGroupHeaderTemplate.lit.ts +16 -12
  142. package/src/generated/templates/ListItemTemplate.lit.ts +40 -32
  143. package/src/generated/templates/MastheadItemTemplate.lit.ts +2 -2
  144. package/src/generated/templates/MastheadMenuTemplate.lit.ts +8 -1
  145. package/src/generated/templates/MastheadMobileMenuTemplate.lit.ts +10 -4
  146. package/src/generated/templates/MastheadShellTemplate.lit.ts +3 -3
  147. package/src/generated/templates/SearchableListBaseTemplate.lit.ts +4 -4
  148. package/src/generated/templates/SelectBoxTemplate.lit.ts +12 -10
  149. package/src/generated/themes/Breadcrumbs.css.ts +1 -1
  150. package/src/generated/themes/BreadcrumbsPopover.css.ts +1 -1
  151. package/src/generated/themes/FilterMenu.css.ts +1 -1
  152. package/src/generated/themes/ListGroupHeader.css.ts +1 -1
  153. package/src/generated/themes/ListItem.css.ts +1 -1
  154. package/src/generated/themes/MastheadItem.css.ts +1 -1
  155. package/src/generated/themes/MastheadMenu.css.ts +1 -1
  156. package/src/generated/themes/MastheadMobileMenu.css.ts +1 -1
  157. package/src/generated/themes/RatingIndicator.css.ts +1 -1
  158. package/src/generated/themes/SearchableList.css.ts +1 -1
  159. package/src/generated/themes/SecondaryNavigation.css.ts +1 -1
  160. package/src/generated/themes/SecondaryNavigationItem.css.ts +1 -1
  161. package/src/generated/themes/SelectBase.css.ts +1 -1
  162. package/src/generated/themes/Slider.css.ts +1 -1
  163. package/src/themes/Breadcrumbs.css +16 -2
  164. package/src/themes/BreadcrumbsPopover.css +18 -3
  165. package/src/themes/FilterMenu.css +4 -9
  166. package/src/themes/ListGroupHeader.css +11 -0
  167. package/src/themes/ListItem.css +6 -4
  168. package/src/themes/MastheadItem.css +41 -9
  169. package/src/themes/MastheadMenu.css +45 -0
  170. package/src/themes/MastheadMobileMenu.css +45 -0
  171. package/src/themes/SearchableList.css +18 -3
  172. package/src/themes/SecondaryNavigation.css +1 -1
  173. package/src/themes/SecondaryNavigationItem.css +5 -2
  174. package/src/themes/SelectBase.css +0 -9
  175. package/src/types/List.ts +2 -0
@@ -0,0 +1,573 @@
1
+ import { UDExMastheadMobileMenu } from "../support/component-imports.js";
2
+
3
+ // Fixture: category with 3rd-level sub-items
4
+ const categoryWithSubItems = {
5
+ category: "Cloud Products",
6
+ index: 0,
7
+ items: [
8
+ {
9
+ title: "Platform Services",
10
+ url: "",
11
+ externalLink: false,
12
+ subItems: [
13
+ {
14
+ title: "SAP BTP",
15
+ url: "https://example.com/btp",
16
+ externalLink: false,
17
+ },
18
+ {
19
+ title: "SAP Integration Suite",
20
+ url: "https://example.com/integration",
21
+ externalLink: false,
22
+ },
23
+ ],
24
+ },
25
+ {
26
+ title: "ERP Solutions",
27
+ url: "",
28
+ externalLink: false,
29
+ subItems: [
30
+ {
31
+ title: "SAP S/4HANA Cloud",
32
+ url: "https://example.com/s4hana",
33
+ externalLink: false,
34
+ },
35
+ ],
36
+ },
37
+ ],
38
+ };
39
+
40
+ // Fixture: category with only flat items (no subItems)
41
+ const categoryWithoutSubItems = {
42
+ category: "Services",
43
+ index: 0,
44
+ items: [
45
+ {
46
+ title: "Consulting",
47
+ url: "https://example.com/consulting",
48
+ externalLink: false,
49
+ },
50
+ {
51
+ title: "Training",
52
+ url: "https://example.com/training",
53
+ externalLink: false,
54
+ },
55
+ ],
56
+ };
57
+
58
+ // Fixture: mixed — one item with subItems, one without
59
+ const categoryMixed = {
60
+ category: "Solutions",
61
+ index: 0,
62
+ items: [
63
+ {
64
+ title: "Cloud Solutions",
65
+ url: "",
66
+ externalLink: false,
67
+ subItems: [
68
+ {
69
+ title: "SAP Analytics Cloud",
70
+ url: "https://example.com/analytics",
71
+ externalLink: false,
72
+ },
73
+ ],
74
+ },
75
+ {
76
+ title: "Direct Link Item",
77
+ url: "https://example.com/direct",
78
+ externalLink: false,
79
+ },
80
+ ],
81
+ };
82
+
83
+ // Fixture: sub-category parent items all have urls (linked sub-categories)
84
+ const categoryWithLinkedSubCategories = {
85
+ category: "AI Platform",
86
+ index: 0,
87
+ items: [
88
+ {
89
+ title: "Build",
90
+ url: "https://example.com/build",
91
+ externalLink: false,
92
+ subItems: [
93
+ {
94
+ title: "Develop",
95
+ url: "https://example.com/develop",
96
+ externalLink: false,
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ title: "Deploy",
102
+ url: "https://example.com/deploy",
103
+ externalLink: false,
104
+ subItems: [
105
+ {
106
+ title: "Staging",
107
+ url: "https://example.com/staging",
108
+ externalLink: false,
109
+ },
110
+ {
111
+ title: "Production",
112
+ url: "https://example.com/production",
113
+ externalLink: false,
114
+ },
115
+ ],
116
+ },
117
+ ],
118
+ };
119
+
120
+ // Fixture: mixed linked and unlinked sub-category parents
121
+ const categoryMixedLinkedAndUnlinked = {
122
+ category: "Data Services",
123
+ index: 0,
124
+ items: [
125
+ {
126
+ title: "Integration",
127
+ url: "https://example.com/integration",
128
+ externalLink: false,
129
+ subItems: [
130
+ {
131
+ title: "API Management",
132
+ url: "https://example.com/api-mgmt",
133
+ externalLink: false,
134
+ },
135
+ ],
136
+ },
137
+ {
138
+ title: "Analytics",
139
+ url: "",
140
+ externalLink: false,
141
+ subItems: [
142
+ {
143
+ title: "Dashboards",
144
+ url: "https://example.com/dashboards",
145
+ externalLink: false,
146
+ },
147
+ ],
148
+ },
149
+ ],
150
+ };
151
+
152
+ /**
153
+ * Helper: mount the mobile menu and activate category-items view.
154
+ * Sets _showCategoryItems = true and _category to the provided fixture,
155
+ * which triggers the category-items inline partial template.
156
+ */
157
+ function mountWithCategoryItems(category: typeof categoryWithSubItems) {
158
+ cy.mount(<UDExMastheadMobileMenu id="mobile-menu" />);
159
+ cy.get("[udex-masthead-mobile-menu]").then(($el) => {
160
+ const el = $el[0] as any;
161
+ el._category = category;
162
+ el._showCategoryItems = true;
163
+ });
164
+ // Wait for re-render after property changes
165
+ cy.get("[udex-masthead-mobile-menu]").shadow().as("shadow");
166
+ }
167
+
168
+ describe("UDExMastheadMobileMenu -- 3rd-level navigation (sub-items)", () => {
169
+ describe("Rendering sub-categories (no url — span)", () => {
170
+ beforeEach(() => {
171
+ mountWithCategoryItems(categoryWithSubItems);
172
+ });
173
+
174
+ it("renders a .udex-masthead-mobile-menu__sub-category span for each item with subItems", () => {
175
+ cy.get("@shadow")
176
+ .find(".udex-masthead-mobile-menu__sub-category")
177
+ .should("have.length", 2);
178
+ });
179
+
180
+ it("renders sub-category as a <span> element when the item has no url", () => {
181
+ cy.get("@shadow")
182
+ .find(".udex-masthead-mobile-menu__sub-category")
183
+ .first()
184
+ .should("have.prop", "tagName", "SPAN");
185
+ });
186
+
187
+ it("displays the correct title text for each sub-category span", () => {
188
+ cy.get("@shadow")
189
+ .find(".udex-masthead-mobile-menu__sub-category")
190
+ .eq(0)
191
+ .should("have.text", "Platform Services");
192
+
193
+ cy.get("@shadow")
194
+ .find(".udex-masthead-mobile-menu__sub-category")
195
+ .eq(1)
196
+ .should("have.text", "ERP Solutions");
197
+ });
198
+ });
199
+
200
+ describe("Rendering sub-list and sub-items", () => {
201
+ beforeEach(() => {
202
+ mountWithCategoryItems(categoryWithSubItems);
203
+ });
204
+
205
+ it("renders a .udex-masthead-mobile-menu__sub-list for each item with subItems", () => {
206
+ cy.get("@shadow")
207
+ .find(".udex-masthead-mobile-menu__sub-list")
208
+ .should("have.length", 2);
209
+ });
210
+
211
+ it("renders udex-masthead-menu-item elements inside each sub-list with correct count", () => {
212
+ // First sub-list: "Platform Services" has 2 sub-items
213
+ cy.get("@shadow")
214
+ .find(".udex-masthead-mobile-menu__sub-list")
215
+ .eq(0)
216
+ .find("udex-masthead-menu-item")
217
+ .should("have.length", 2);
218
+
219
+ // Second sub-list: "ERP Solutions" has 1 sub-item
220
+ cy.get("@shadow")
221
+ .find(".udex-masthead-mobile-menu__sub-list")
222
+ .eq(1)
223
+ .find("udex-masthead-menu-item")
224
+ .should("have.length", 1);
225
+ });
226
+
227
+ it("renders the correct label attribute for each sub-item menu-item", () => {
228
+ cy.get("@shadow")
229
+ .find(".udex-masthead-mobile-menu__sub-list")
230
+ .eq(0)
231
+ .find("udex-masthead-menu-item")
232
+ .eq(0)
233
+ .should("have.attr", "label", "SAP BTP");
234
+
235
+ cy.get("@shadow")
236
+ .find(".udex-masthead-mobile-menu__sub-list")
237
+ .eq(0)
238
+ .find("udex-masthead-menu-item")
239
+ .eq(1)
240
+ .should("have.attr", "label", "SAP Integration Suite");
241
+ });
242
+
243
+ it("sets the correct url attribute on sub-item menu-items", () => {
244
+ cy.get("@shadow")
245
+ .find(".udex-masthead-mobile-menu__sub-list")
246
+ .eq(0)
247
+ .find("udex-masthead-menu-item")
248
+ .eq(0)
249
+ .should("have.attr", "url", "https://example.com/btp");
250
+
251
+ cy.get("@shadow")
252
+ .find(".udex-masthead-mobile-menu__sub-list")
253
+ .eq(1)
254
+ .find("udex-masthead-menu-item")
255
+ .eq(0)
256
+ .should("have.attr", "url", "https://example.com/s4hana");
257
+ });
258
+
259
+ it("sets third-level attribute on sub-item menu-items", () => {
260
+ cy.get("@shadow")
261
+ .find(".udex-masthead-mobile-menu__sub-list")
262
+ .eq(0)
263
+ .find("udex-masthead-menu-item")
264
+ .first()
265
+ .should("have.attr", "third-level");
266
+ });
267
+
268
+ // Updated: has-border removed from sub-item menu-items in template
269
+ it("does not set has-border attribute on sub-item menu-items", () => {
270
+ cy.get("@shadow")
271
+ .find(".udex-masthead-mobile-menu__sub-list")
272
+ .eq(0)
273
+ .find("udex-masthead-menu-item")
274
+ .first()
275
+ .should("not.have.attr", "has-border");
276
+ });
277
+ });
278
+
279
+ describe("Sub-list accessibility", () => {
280
+ beforeEach(() => {
281
+ mountWithCategoryItems(categoryWithSubItems);
282
+ });
283
+
284
+ it("sets aria-label on the sub-list matching the sub-category title", () => {
285
+ cy.get("@shadow")
286
+ .find(".udex-masthead-mobile-menu__sub-list")
287
+ .eq(0)
288
+ .should("have.attr", "aria-label", "Platform Services");
289
+
290
+ cy.get("@shadow")
291
+ .find(".udex-masthead-mobile-menu__sub-list")
292
+ .eq(1)
293
+ .should("have.attr", "aria-label", "ERP Solutions");
294
+ });
295
+
296
+ it("wraps sub-items in a <ul> element for correct list semantics", () => {
297
+ cy.get("@shadow")
298
+ .find(".udex-masthead-mobile-menu__sub-list")
299
+ .first()
300
+ .should("have.prop", "tagName", "UL");
301
+ });
302
+ });
303
+
304
+ describe("Regression: items without subItems still render directly", () => {
305
+ beforeEach(() => {
306
+ mountWithCategoryItems(categoryWithoutSubItems);
307
+ });
308
+
309
+ it("does not render any .udex-masthead-mobile-menu__sub-category when no items have subItems", () => {
310
+ cy.get("@shadow")
311
+ .find(".udex-masthead-mobile-menu__sub-category")
312
+ .should("not.exist");
313
+ });
314
+
315
+ it("does not render any .udex-masthead-mobile-menu__sub-list when no items have subItems", () => {
316
+ cy.get("@shadow")
317
+ .find(".udex-masthead-mobile-menu__sub-list")
318
+ .should("not.exist");
319
+ });
320
+
321
+ it("renders udex-masthead-menu-item elements directly for flat items", () => {
322
+ cy.get("@shadow")
323
+ .find(".udex-masthead-mobile-menu__list udex-masthead-menu-item[third-level]")
324
+ .should("have.length", 2);
325
+ });
326
+
327
+ it("renders the correct labels for flat items", () => {
328
+ cy.get("@shadow")
329
+ .find(".udex-masthead-mobile-menu__list udex-masthead-menu-item[third-level]")
330
+ .eq(0)
331
+ .should("have.attr", "label", "Consulting");
332
+
333
+ cy.get("@shadow")
334
+ .find(".udex-masthead-mobile-menu__list udex-masthead-menu-item[third-level]")
335
+ .eq(1)
336
+ .should("have.attr", "label", "Training");
337
+ });
338
+
339
+ it("renders the correct url for flat items", () => {
340
+ cy.get("@shadow")
341
+ .find(".udex-masthead-mobile-menu__list udex-masthead-menu-item[third-level]")
342
+ .eq(0)
343
+ .should("have.attr", "url", "https://example.com/consulting");
344
+ });
345
+ });
346
+
347
+ describe("Mixed content: items with and without subItems in the same category", () => {
348
+ beforeEach(() => {
349
+ mountWithCategoryItems(categoryMixed);
350
+ });
351
+
352
+ it("renders a sub-category span only for the item that has subItems", () => {
353
+ cy.get("@shadow")
354
+ .find(".udex-masthead-mobile-menu__sub-category")
355
+ .should("have.length", 1)
356
+ .and("have.text", "Cloud Solutions");
357
+ });
358
+
359
+ it("renders a sub-list with the correct sub-item for the grouped item", () => {
360
+ cy.get("@shadow")
361
+ .find(".udex-masthead-mobile-menu__sub-list")
362
+ .should("have.length", 1)
363
+ .find("udex-masthead-menu-item")
364
+ .should("have.length", 1)
365
+ .and("have.attr", "label", "SAP Analytics Cloud");
366
+ });
367
+
368
+ it("renders the flat item as a direct udex-masthead-menu-item outside any sub-list", () => {
369
+ // The direct link item sits at top level in the main list, not inside a sub-list
370
+ cy.get("@shadow")
371
+ .find(
372
+ ".udex-masthead-mobile-menu__list > li > udex-masthead-menu-item[third-level]"
373
+ )
374
+ .should("have.length", 1)
375
+ .and("have.attr", "label", "Direct Link Item")
376
+ .and("have.attr", "url", "https://example.com/direct");
377
+ });
378
+ });
379
+
380
+ // Updated: linked sub-categories (with url) now have the
381
+ // .udex-masthead-mobile-menu__sub-category class restored. CSS variable
382
+ // remapping is done via the `udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category`
383
+ // rule (no padding/border — those only apply to the span variant).
384
+ describe("Rendering linked sub-categories (item has url + subItems)", () => {
385
+ beforeEach(() => {
386
+ mountWithCategoryItems(categoryWithLinkedSubCategories);
387
+ });
388
+
389
+ // Updated: linked sub-categories now carry the __sub-category class again
390
+ it("renders .udex-masthead-mobile-menu__sub-category for linked items (udex-masthead-menu-item, not span)", () => {
391
+ cy.get("@shadow")
392
+ .find("udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category")
393
+ .should("have.length", 2);
394
+ });
395
+
396
+ it("renders linked sub-category as a udex-masthead-menu-item (not a span) when the item has a url", () => {
397
+ cy.get("@shadow")
398
+ .find("udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category")
399
+ .first()
400
+ .should("have.prop", "tagName")
401
+ .then((tagName: string) => {
402
+ expect(tagName.toLowerCase()).to.include("udex-masthead-menu-item");
403
+ });
404
+ });
405
+
406
+ it("sets the correct label attribute on the linked sub-category", () => {
407
+ cy.get("@shadow")
408
+ .find("udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category")
409
+ .eq(0)
410
+ .should("have.attr", "label", "Build");
411
+
412
+ cy.get("@shadow")
413
+ .find("udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category")
414
+ .eq(1)
415
+ .should("have.attr", "label", "Deploy");
416
+ });
417
+
418
+ it("sets the correct url attribute on the linked sub-category", () => {
419
+ cy.get("@shadow")
420
+ .find("udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category")
421
+ .eq(0)
422
+ .should("have.attr", "url", "https://example.com/build");
423
+
424
+ cy.get("@shadow")
425
+ .find("udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category")
426
+ .eq(1)
427
+ .should("have.attr", "url", "https://example.com/deploy");
428
+ });
429
+
430
+ it("sets the third-level attribute on the linked sub-category", () => {
431
+ cy.get("@shadow")
432
+ .find("udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category")
433
+ .first()
434
+ .should("have.attr", "third-level");
435
+ });
436
+
437
+ // Updated: linked sub-category has the __sub-category class but does NOT
438
+ // have has-border — padding/border styles only apply to the span variant
439
+ it("does not set has-border attribute on the linked sub-category", () => {
440
+ cy.get("@shadow")
441
+ .find("udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category")
442
+ .first()
443
+ .should("not.have.attr", "has-border");
444
+ });
445
+
446
+ it("still renders the sub-list beneath the linked sub-category", () => {
447
+ cy.get("@shadow")
448
+ .find(".udex-masthead-mobile-menu__sub-list")
449
+ .should("have.length", 2);
450
+ });
451
+
452
+ it("renders the correct sub-items inside the sub-list of a linked sub-category", () => {
453
+ // First sub-list (under "Build") has 1 sub-item: "Develop"
454
+ cy.get("@shadow")
455
+ .find(".udex-masthead-mobile-menu__sub-list")
456
+ .eq(0)
457
+ .find("udex-masthead-menu-item")
458
+ .should("have.length", 1)
459
+ .and("have.attr", "label", "Develop");
460
+
461
+ // Second sub-list (under "Deploy") has 2 sub-items: "Staging", "Production"
462
+ cy.get("@shadow")
463
+ .find(".udex-masthead-mobile-menu__sub-list")
464
+ .eq(1)
465
+ .find("udex-masthead-menu-item")
466
+ .should("have.length", 2);
467
+
468
+ cy.get("@shadow")
469
+ .find(".udex-masthead-mobile-menu__sub-list")
470
+ .eq(1)
471
+ .find("udex-masthead-menu-item")
472
+ .eq(0)
473
+ .should("have.attr", "label", "Staging");
474
+
475
+ cy.get("@shadow")
476
+ .find(".udex-masthead-mobile-menu__sub-list")
477
+ .eq(1)
478
+ .find("udex-masthead-menu-item")
479
+ .eq(1)
480
+ .should("have.attr", "label", "Production");
481
+ });
482
+
483
+ it("preserves aria-label on the sub-list matching the linked sub-category title", () => {
484
+ cy.get("@shadow")
485
+ .find(".udex-masthead-mobile-menu__sub-list")
486
+ .eq(0)
487
+ .should("have.attr", "aria-label", "Build");
488
+
489
+ cy.get("@shadow")
490
+ .find(".udex-masthead-mobile-menu__sub-list")
491
+ .eq(1)
492
+ .should("have.attr", "aria-label", "Deploy");
493
+ });
494
+ });
495
+
496
+ describe("Mixed linked and unlinked sub-categories in the same category", () => {
497
+ beforeEach(() => {
498
+ mountWithCategoryItems(categoryMixedLinkedAndUnlinked);
499
+ });
500
+
501
+ // Updated: linked sub-category now has the __sub-category class restored;
502
+ // target it via udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category
503
+ it("renders the linked sub-category as a udex-masthead-menu-item with __sub-category class", () => {
504
+ // "Integration" has a url — should be a menu-item with the class
505
+ cy.get("@shadow")
506
+ .find(
507
+ "udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category"
508
+ )
509
+ .should("have.length", 1)
510
+ .and("have.attr", "label", "Integration")
511
+ .and("have.attr", "url", "https://example.com/integration");
512
+ });
513
+
514
+ it("renders the unlinked sub-category as a span", () => {
515
+ // "Analytics" has no url — should be a span
516
+ cy.get("@shadow")
517
+ .find("span.udex-masthead-mobile-menu__sub-category")
518
+ .should("have.length", 1)
519
+ .and("have.text", "Analytics");
520
+ });
521
+
522
+ // Updated: both linked and unlinked sub-categories now have __sub-category class
523
+ it("renders two .udex-masthead-mobile-menu__sub-category elements (one linked, one unlinked)", () => {
524
+ cy.get("@shadow")
525
+ .find(".udex-masthead-mobile-menu__sub-category")
526
+ .should("have.length", 2);
527
+ });
528
+
529
+ it("renders sub-lists for both linked and unlinked sub-categories", () => {
530
+ cy.get("@shadow")
531
+ .find(".udex-masthead-mobile-menu__sub-list")
532
+ .should("have.length", 2);
533
+ });
534
+
535
+ it("renders the linked sub-category's sub-item correctly", () => {
536
+ cy.get("@shadow")
537
+ .find(".udex-masthead-mobile-menu__sub-list")
538
+ .eq(0)
539
+ .find("udex-masthead-menu-item")
540
+ .should("have.length", 1)
541
+ .and("have.attr", "label", "API Management")
542
+ .and("have.attr", "url", "https://example.com/api-mgmt");
543
+ });
544
+
545
+ it("renders the unlinked sub-category's sub-item correctly", () => {
546
+ cy.get("@shadow")
547
+ .find(".udex-masthead-mobile-menu__sub-list")
548
+ .eq(1)
549
+ .find("udex-masthead-menu-item")
550
+ .should("have.length", 1)
551
+ .and("have.attr", "label", "Dashboards")
552
+ .and("have.attr", "url", "https://example.com/dashboards");
553
+ });
554
+
555
+ // Updated: linked sub-category has __sub-category class but NOT has-border;
556
+ // padding/border styles only apply to the span variant via CSS rule split
557
+ it("sets third-level but not has-border on the linked sub-category menu-item", () => {
558
+ cy.get("@shadow")
559
+ .find(
560
+ "udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category"
561
+ )
562
+ .first()
563
+ .should("have.attr", "third-level");
564
+
565
+ cy.get("@shadow")
566
+ .find(
567
+ "udex-masthead-menu-item.udex-masthead-mobile-menu__sub-category"
568
+ )
569
+ .first()
570
+ .should("not.have.attr", "has-border");
571
+ });
572
+ });
573
+ });
@@ -407,6 +407,85 @@ describe("MastheadShell Default component", () => {
407
407
  cy.realPress(["Shift", "Tab"]);
408
408
  cy.get("#support-item").should("be.focused");
409
409
  });
410
+
411
+ // Added: verifies Safari Tab-into-dropdown fix — when dropdown is open, Tab moves focus into the first menu item
412
+ it("Tab moves focus into the first dropdown menu item when dropdown is open", () => {
413
+ cy.get("#product-item").realClick();
414
+ cy.get("#product-item").shadow().find(".udex-masthead-item__dropdown").should("be.visible");
415
+ cy.realPress("Tab");
416
+ cy.get("#product-menu")
417
+ .shadow()
418
+ .find("[udex-masthead-menu-item]")
419
+ .first()
420
+ .shadow()
421
+ .find(".udex-masthead-menu-item")
422
+ .should("be.focused");
423
+ });
424
+
425
+ // Updated: unified handleKeyDown uses composedPath() guard to let Tab within dropdown advance normally
426
+ it("Tab on first dropdown item moves focus to second item, not back to first", () => {
427
+ cy.get("#product-item").realClick();
428
+ cy.get("#product-item").shadow().find(".udex-masthead-item__dropdown").should("be.visible");
429
+ // Tab into the dropdown — focuses first item
430
+ cy.realPress("Tab");
431
+ cy.get("#product-menu")
432
+ .shadow()
433
+ .find("[udex-masthead-menu-item]")
434
+ .first()
435
+ .shadow()
436
+ .find(".udex-masthead-menu-item")
437
+ .should("be.focused");
438
+ // Tab again — should advance to second item (not reset to first)
439
+ cy.realPress("Tab");
440
+ cy.get("#product-menu")
441
+ .shadow()
442
+ .find("[udex-masthead-menu-item]")
443
+ .eq(1)
444
+ .shadow()
445
+ .find(".udex-masthead-menu-item")
446
+ .should("be.focused");
447
+ });
448
+
449
+ // Added: verifies Tab on last dropdown item exits dropdown instead of looping back to first item
450
+ it("Tab on last dropdown item exits dropdown and moves focus to the next top-level menu item", () => {
451
+ cy.get("#product-item").realClick();
452
+ cy.get("#product-item").shadow().find(".udex-masthead-item__dropdown").should("be.visible");
453
+
454
+ // Focus the last focusable menu item in the dropdown (items with url only)
455
+ cy.get("#product-menu")
456
+ .shadow()
457
+ .find("[udex-masthead-menu-item]")
458
+ .filter((_, el) => !!el.getAttribute("url"))
459
+ .last()
460
+ .shadow()
461
+ .find(".udex-masthead-menu-item")
462
+ .focus();
463
+
464
+ // Verify we are on the last item before pressing Tab
465
+ cy.get("#product-menu")
466
+ .shadow()
467
+ .find("[udex-masthead-menu-item]")
468
+ .filter((_, el) => !!el.getAttribute("url"))
469
+ .last()
470
+ .shadow()
471
+ .find(".udex-masthead-menu-item")
472
+ .should("be.focused");
473
+
474
+ // Tab should exit the dropdown — NOT loop back to the first item
475
+ cy.realPress("Tab");
476
+
477
+ // The first dropdown item should NOT be focused (no looping)
478
+ cy.get("#product-menu")
479
+ .shadow()
480
+ .find("[udex-masthead-menu-item]")
481
+ .first()
482
+ .shadow()
483
+ .find(".udex-masthead-menu-item")
484
+ .should("not.be.focused");
485
+
486
+ // The dropdown should close because focus left the masthead item
487
+ cy.get("#product-item").shadow().find(".udex-masthead-item__dropdown").should("not.exist");
488
+ });
410
489
  });
411
490
 
412
491
  describe("Component events", () => {
@@ -329,6 +329,20 @@ describe("Desktop", () => {
329
329
  .its("lastCall.args.0.detail").should("not.have.property", "isMore");
330
330
  });
331
331
 
332
+ it("sets CSS variable when dropdown opens on desktop", () => {
333
+ cy.get("#secondary-nav").shadow().find("udex-secondary-navigation-item").first().click()
334
+ .shadow().find(".udex-secondary-navigation__dropdown").should("be.visible");
335
+
336
+ cy.get("#secondary-nav").shadow().find("udex-secondary-navigation-item").first()
337
+ .then($item => {
338
+ const item = $item[0] as HTMLElement;
339
+ const dropdown = item.shadowRoot!.querySelector<HTMLElement>(".udex-secondary-navigation__dropdown")!;
340
+ const { top } = dropdown.getBoundingClientRect();
341
+ const expected = `${window.innerHeight - top - 24}px`;
342
+ expect(item.style.getPropertyValue("--udex-secondary-navigation-item-dropdown-desktop-max-height")).to.eq(expected);
343
+ });
344
+ });
345
+
332
346
  describe("Keyboard Navigation (Disclosure pattern)", () => {
333
347
  it("Tab key moves focus to the next top-level item", () => {
334
348
  cy.get("#secondary-nav").shadow().find("udex-secondary-navigation-item").eq(2)