@sapudex/web-components 2.44.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 (125) hide show
  1. package/cypress/specs/DatePicker.cy.tsx +259 -0
  2. package/cypress/specs/MastheadItem.cy.tsx +88 -0
  3. package/cypress/specs/MastheadShell.cy.tsx +3 -1
  4. package/cypress/specs/TagGroup.cy.tsx +104 -0
  5. package/cypress/specs/TextField.cy.tsx +64 -1
  6. package/cypress/support/component-imports.instrumented.ts +2 -0
  7. package/cypress/support/component-imports.src.ts +2 -0
  8. package/dist/Carousel.d.ts +5 -1
  9. package/dist/Carousel.js +10 -1
  10. package/dist/Carousel.js.map +1 -1
  11. package/dist/DatePicker.d.ts +4 -0
  12. package/dist/DatePicker.js +19 -13
  13. package/dist/DatePicker.js.map +1 -1
  14. package/dist/DatePickerPopoverTemplate.js +1 -1
  15. package/dist/DatePickerPopoverTemplate.js.map +1 -1
  16. package/dist/DatePickerTemplate.js +1 -1
  17. package/dist/DatePickerTemplate.js.map +1 -1
  18. package/dist/MastheadItem.d.ts +6 -3
  19. package/dist/MastheadItem.js +18 -0
  20. package/dist/MastheadItem.js.map +1 -1
  21. package/dist/MastheadMobileMenu.d.ts +1 -0
  22. package/dist/MastheadMobileMenu.js +5 -0
  23. package/dist/MastheadMobileMenu.js.map +1 -1
  24. package/dist/MastheadProfileMenu.d.ts +5 -4
  25. package/dist/MastheadProfileMenu.js +15 -12
  26. package/dist/MastheadProfileMenu.js.map +1 -1
  27. package/dist/MastheadShell.d.ts +2 -0
  28. package/dist/MastheadShell.js +9 -2
  29. package/dist/MastheadShell.js.map +1 -1
  30. package/dist/SelectBoxTemplate.js +1 -1
  31. package/dist/SelectBoxTemplate.js.map +1 -1
  32. package/dist/TagGroup.d.ts +5 -1
  33. package/dist/TagGroup.js +51 -7
  34. package/dist/TagGroup.js.map +1 -1
  35. package/dist/TagGroupTemplate.js +1 -1
  36. package/dist/TagGroupTemplate.js.map +1 -1
  37. package/dist/TextField.d.ts +7 -0
  38. package/dist/TextField.js +10 -0
  39. package/dist/TextField.js.map +1 -1
  40. package/dist/TextFieldTemplate.js +1 -1
  41. package/dist/TextFieldTemplate.js.map +1 -1
  42. package/dist/Tooltip.js +2 -1
  43. package/dist/Tooltip.js.map +1 -1
  44. package/dist/assets/{InputSuggestions-B3D8lxAS.js → InputSuggestions-_syMxKYw.js} +1 -1
  45. package/dist/assets/{ListItemStandardExpandableTextTemplate-Is8_hDTt.js → ListItemStandardExpandableTextTemplate-CKRbK6UW.js} +1 -1
  46. package/dist/assets/{bundle.esm-6rH7NAG7.js → bundle.esm-DMmyJHkn.js} +191 -191
  47. package/dist/assets/messagebundle_pt-D9rap3lj.js +1 -0
  48. package/dist/assets/messagebundle_ro-BTNJ9YdP.js +1 -0
  49. package/dist/css/themes/CountrySelector.css +1 -1
  50. package/dist/css/themes/MastheadCountrySelector.css +1 -1
  51. package/dist/css/themes/MastheadItem.css +1 -1
  52. package/dist/css/themes/MastheadMenu.css +1 -1
  53. package/dist/css/themes/MastheadMobileMenu.css +1 -1
  54. package/dist/css/themes/MastheadProfileMenu.css +1 -1
  55. package/dist/css/themes/TagGroup.css +1 -1
  56. package/dist/custom-elements-internal.json +68 -17
  57. package/dist/custom-elements.json +75 -17
  58. package/dist/generated/assets/i18n/messagebundle_pt.json +1 -1
  59. package/dist/generated/assets/i18n/messagebundle_ro.json +1 -1
  60. package/dist/generated/templates/MastheadProfileMenuTemplate.lit.js +2 -2
  61. package/dist/generated/templates/MastheadProfileMenuTemplate.lit.js.map +1 -1
  62. package/dist/generated/templates/MastheadShellTemplate.lit.js +2 -2
  63. package/dist/generated/templates/MastheadShellTemplate.lit.js.map +1 -1
  64. package/dist/generated/themes/CountrySelector.css.d.ts +1 -1
  65. package/dist/generated/themes/CountrySelector.css.js +1 -1
  66. package/dist/generated/themes/CountrySelector.css.js.map +1 -1
  67. package/dist/generated/themes/MastheadCountrySelector.css.d.ts +1 -1
  68. package/dist/generated/themes/MastheadCountrySelector.css.js +1 -1
  69. package/dist/generated/themes/MastheadCountrySelector.css.js.map +1 -1
  70. package/dist/generated/themes/MastheadItem.css.d.ts +1 -1
  71. package/dist/generated/themes/MastheadItem.css.js +1 -1
  72. package/dist/generated/themes/MastheadItem.css.js.map +1 -1
  73. package/dist/generated/themes/MastheadMenu.css.d.ts +1 -1
  74. package/dist/generated/themes/MastheadMenu.css.js +1 -1
  75. package/dist/generated/themes/MastheadMenu.css.js.map +1 -1
  76. package/dist/generated/themes/MastheadMobileMenu.css.d.ts +1 -1
  77. package/dist/generated/themes/MastheadMobileMenu.css.js +1 -1
  78. package/dist/generated/themes/MastheadMobileMenu.css.js.map +1 -1
  79. package/dist/generated/themes/MastheadProfileMenu.css.d.ts +1 -1
  80. package/dist/generated/themes/MastheadProfileMenu.css.js +1 -1
  81. package/dist/generated/themes/MastheadProfileMenu.css.js.map +1 -1
  82. package/dist/generated/themes/TagGroup.css.d.ts +1 -1
  83. package/dist/generated/themes/TagGroup.css.js +1 -1
  84. package/dist/generated/themes/TagGroup.css.js.map +1 -1
  85. package/dist/messagebundle_pt.properties +126 -0
  86. package/dist/messagebundle_ro.properties +126 -0
  87. package/dist/vscode.html-custom-data.json +11 -6
  88. package/dist/web-types.json +37 -14
  89. package/package.json +1 -1
  90. package/src/Carousel.ts +14 -1
  91. package/src/DatePicker.ts +29 -11
  92. package/src/DatePickerPopoverTemplate.tsx +2 -1
  93. package/src/DatePickerTemplate.tsx +1 -0
  94. package/src/MastheadItem.ts +24 -3
  95. package/src/MastheadMobileMenu.ts +6 -0
  96. package/src/MastheadProfileMenu.hbs +25 -25
  97. package/src/MastheadProfileMenu.ts +20 -16
  98. package/src/MastheadShell.hbs +3 -2
  99. package/src/MastheadShell.ts +11 -2
  100. package/src/SelectBoxTemplate.tsx +1 -1
  101. package/src/TagGroup.ts +59 -6
  102. package/src/TagGroupTemplate.tsx +1 -0
  103. package/src/TextField.ts +9 -0
  104. package/src/TextFieldTemplate.tsx +1 -0
  105. package/src/Tooltip.ts +2 -1
  106. package/src/generated/templates/MastheadProfileMenuTemplate.lit.ts +2 -2
  107. package/src/generated/templates/MastheadShellTemplate.lit.ts +2 -2
  108. package/src/generated/themes/CountrySelector.css.ts +1 -1
  109. package/src/generated/themes/MastheadCountrySelector.css.ts +1 -1
  110. package/src/generated/themes/MastheadItem.css.ts +1 -1
  111. package/src/generated/themes/MastheadMenu.css.ts +1 -1
  112. package/src/generated/themes/MastheadMobileMenu.css.ts +1 -1
  113. package/src/generated/themes/MastheadProfileMenu.css.ts +1 -1
  114. package/src/generated/themes/TagGroup.css.ts +1 -1
  115. package/src/i18n/messagebundle_pt.properties +126 -0
  116. package/src/i18n/messagebundle_ro.properties +126 -0
  117. package/src/themes/CountrySelector.css +5 -1
  118. package/src/themes/MastheadCountrySelector.css +29 -33
  119. package/src/themes/MastheadItem.css +53 -13
  120. package/src/themes/MastheadMenu.css +1 -11
  121. package/src/themes/MastheadMobileMenu.css +0 -12
  122. package/src/themes/MastheadProfileMenu.css +22 -16
  123. package/src/themes/TagGroup.css +1 -2
  124. package/dist/assets/messagebundle_pt-DPeE7NbC.js +0 -1
  125. package/dist/assets/messagebundle_ro-CN4kAAYE.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
+ });
@@ -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
 
@@ -106,4 +106,108 @@ describe("TagGroup", () => {
106
106
  cy.get("[udex-tag-group]").shadow().find("[udex-tooltip]").should("have.attr", "accessible-name", "Hidden tags");
107
107
  });
108
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
+
109
213
  });
@@ -103,7 +103,7 @@ describe("Text Field", () => {
103
103
  });
104
104
 
105
105
  it("Supporting text span doesn't appear by default", () => {
106
- cy.get("#defaultTextField").shadow().get("span").should("not.exist",);
106
+ cy.get("#defaultTextField").shadow().find("span").should("not.exist");
107
107
  });
108
108
 
109
109
  it("\"aria-describedby\" attribute is set", () => {
@@ -138,6 +138,69 @@ describe("Text Field", () => {
138
138
  })
139
139
  });
140
140
 
141
+ describe("placeholder", () => {
142
+ // Updated: placeholder now only renders when the field is active (focused) so
143
+ // it does not visually overlap the floating label when the field is empty and blurred.
144
+ it("does not render a non-empty placeholder on the native input by default", () => {
145
+ cy.get("#defaultTextField").shadow().find("input").then($input => {
146
+ const placeholder = $input.attr("placeholder");
147
+ // Placeholder attribute may be absent or present as empty string when unset
148
+ expect(placeholder === undefined || placeholder === "").to.equal(true);
149
+ });
150
+ });
151
+
152
+ // Updated: placeholder is now gated on `active`. Assert absent before focus, present after focus.
153
+ it("does not render the placeholder on the native input until the field is active (focused)", () => {
154
+ const PLACEHOLDER_VALUE = "Enter your name";
155
+ cy.mount(<UDExTextField placeholder={PLACEHOLDER_VALUE} label="Name" id="placeholderTextField"></UDExTextField>);
156
+
157
+ // Before focus: placeholder should not be present (attribute absent or empty).
158
+ cy.get("#placeholderTextField").shadow().find("input").then($input => {
159
+ const placeholder = $input.attr("placeholder");
160
+ expect(placeholder === undefined || placeholder === "").to.equal(true);
161
+ });
162
+
163
+ // Focus the native input to switch the field to active state.
164
+ cy.get("#placeholderTextField").shadow().find("input").focus();
165
+
166
+ // After focus: placeholder should be rendered on the native input.
167
+ cy.get("#placeholderTextField").shadow().find("input")
168
+ .should("have.attr", "placeholder", PLACEHOLDER_VALUE);
169
+ });
170
+
171
+ // Updated: when no label is present, there is nothing to overlap the placeholder area,
172
+ // so the placeholder is rendered on the native input at all times (not gated on `active`).
173
+ it("renders the placeholder on the native input without focus when no label is set", () => {
174
+ const PLACEHOLDER_VALUE = "Enter your name";
175
+ cy.mount(
176
+ <UDExTextField placeholder={PLACEHOLDER_VALUE} id="noLabelPlaceholderTextField"></UDExTextField>,
177
+ );
178
+
179
+ // No label => `active || !this.label` is true even before focus.
180
+ cy.get("#noLabelPlaceholderTextField").shadow().find("input")
181
+ .should("have.attr", "placeholder", PLACEHOLDER_VALUE);
182
+ });
183
+
184
+ // Updated: propagation now only observable while the field is active — focus, then assert reactive update.
185
+ it("updates the native input placeholder when the property changes (while active)", () => {
186
+ cy.mount(<UDExTextField placeholder="Original" id="reactivePlaceholder"></UDExTextField>);
187
+
188
+ // Focus first so the placeholder is actually rendered on the native input.
189
+ cy.get("#reactivePlaceholder").shadow().find("input").focus();
190
+ cy.get("#reactivePlaceholder").shadow().find("input")
191
+ .should("have.attr", "placeholder", "Original");
192
+
193
+ cy.get("#reactivePlaceholder").then($el => {
194
+ ($el[0] as unknown as { placeholder: string }).placeholder = "Updated";
195
+ });
196
+
197
+ // Keep focus on the input so `active` stays true, and assert the new value is reflected.
198
+ cy.get("#reactivePlaceholder").shadow().find("input").focus();
199
+ cy.get("#reactivePlaceholder").shadow().find("input")
200
+ .should("have.attr", "placeholder", "Updated");
201
+ });
202
+ });
203
+
141
204
  it("sets object fieldRef current to input element inside shadow root", () => {
142
205
  const fieldRefObj = { current: null as HTMLInputElement | null };
143
206
  cy.mount(<UDExTextField fieldRef={fieldRefObj}></UDExTextField>);
@@ -11,6 +11,7 @@ import UDExCheckbox from "../../instrumented/Checkbox.js";
11
11
  import UDExContact from "../../instrumented/Contact.js";
12
12
  import UDExContactItem from "../../instrumented/ContactItem.js";
13
13
  import UDExCountrySelector from "../../instrumented/CountrySelector.js";
14
+ import UDExDatePicker from "../../instrumented/DatePicker.js";
14
15
  import UDExDivider from "../../instrumented/Divider.js";
15
16
  import UDExDropdown from "../../instrumented/Dropdown.js";
16
17
  import UDExEventRibbon from "../../instrumented/EventRibbon.js";
@@ -99,6 +100,7 @@ export {
99
100
  UDExContact,
100
101
  UDExContactItem,
101
102
  UDExCountrySelector,
103
+ UDExDatePicker,
102
104
  UDExDivider,
103
105
  UDExDropdown,
104
106
  UDExEventRibbon,
@@ -11,6 +11,7 @@ import UDExCheckbox from "../../src/Checkbox.js";
11
11
  import UDExContact from "../../src/Contact.js";
12
12
  import UDExContactItem from "../../src/ContactItem.js";
13
13
  import UDExCountrySelector from "../../src/CountrySelector.js";
14
+ import UDExDatePicker from "../../src/DatePicker.js";
14
15
  import UDExDivider from "../../src/Divider.js";
15
16
  import UDExDropdown from "../../src/Dropdown.js";
16
17
  import UDExEventRibbon from "../../src/EventRibbon.js";
@@ -99,6 +100,7 @@ export {
99
100
  UDExContact,
100
101
  UDExContactItem,
101
102
  UDExCountrySelector,
103
+ UDExDatePicker,
102
104
  UDExDivider,
103
105
  UDExDropdown,
104
106
  UDExEventRibbon,
@@ -2,6 +2,7 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
2
2
  import type { DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js";
3
3
  import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
4
4
  import type UI5Carousel from "@ui5/webcomponents/dist/Carousel.js";
5
+ import type { CarouselNavigateEventDetail } from "@ui5/webcomponents/dist/Carousel.js";
5
6
  import type { UI5CustomEvent } from "@ui5/webcomponents-base";
6
7
  import type CarouselPageIndicatorType from "@ui5/webcomponents/dist/types/CarouselPageIndicatorType.js";
7
8
  import type UDExButton from "./Button.js";
@@ -32,6 +33,9 @@ export declare enum UDExCarouselButtonDesign {
32
33
  * @public
33
34
  */
34
35
  declare class UDExCarousel extends UI5Element {
36
+ eventDetails: {
37
+ "navigate": CarouselNavigateEventDetail;
38
+ };
35
39
  /**
36
40
  * Defines the position of the navigation.
37
41
  * <br><br>
@@ -112,7 +116,7 @@ declare class UDExCarousel extends UI5Element {
112
116
  _prevButtonClick(e: UI5CustomEvent<UDExButton, "click">): void;
113
117
  _nextButtonClick(e: UI5CustomEvent<UDExButton, "click">): void;
114
118
  _onkeydown(e: KeyboardEvent): void;
115
- _handleNavigate(): void;
119
+ _handleNavigate(e: UI5CustomEvent<UI5Carousel, "navigate">): void;
116
120
  onAfterRendering(): void;
117
121
  }
118
122
  export default UDExCarousel;
package/dist/Carousel.js CHANGED
@@ -9,6 +9,7 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
9
9
  import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
10
10
  import property from "@ui5/webcomponents-base/dist/decorators/property.js";
11
11
  import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
12
+ import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
12
13
  import jsxRenderer from "@ui5/webcomponents-base/renderer/JsxRenderer.js";
13
14
  import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
14
15
  import { isUp, isDown, isRight, isLeft, } from "@ui5/webcomponents-base/dist/Keys.js";
@@ -144,8 +145,9 @@ let UDExCarousel = UDExCarousel_1 = class UDExCarousel extends UI5Element {
144
145
  this._calculateDisabledButtonsState();
145
146
  }
146
147
  }
147
- _handleNavigate() {
148
+ _handleNavigate(e) {
148
149
  this._calculateDisabledButtonsState();
150
+ this.fireDecoratorEvent("navigate", e.detail);
149
151
  }
150
152
  onAfterRendering() {
151
153
  redirectAttributes(this, "carousel");
@@ -183,6 +185,13 @@ UDExCarousel = UDExCarousel_1 = __decorate([
183
185
  styles: CarouselCss,
184
186
  template: CarouselTemplate,
185
187
  })
188
+ /**
189
+ * Triggered when carousel navigates to a new slide
190
+ * @public
191
+ * @param { CustomEvent }
192
+ */
193
+ ,
194
+ event("navigate")
186
195
  ], UDExCarousel);
187
196
  UDExCarousel.define();
188
197
  export default UDExCarousel;