@sapudex/web-components 2.36.0 → 2.38.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 (215) hide show
  1. package/cypress/specs/List.cy.tsx +38 -0
  2. package/cypress/specs/Pagination.cy.tsx +52 -0
  3. package/cypress/specs/PasswordField.cy.tsx +36 -0
  4. package/cypress/specs/Search.cy.tsx +265 -0
  5. package/cypress/specs/SelectBox.cy.tsx +19 -1
  6. package/cypress/specs/TabNavigation.cy.tsx +314 -0
  7. package/cypress/specs/Table.cy.tsx +170 -0
  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 +2 -0
  11. package/dist/CountrySelector.js +12 -3
  12. package/dist/CountrySelector.js.map +1 -1
  13. package/dist/CountrySelectorTemplate.d.ts +2 -0
  14. package/dist/CountrySelectorTemplate.js +8 -0
  15. package/dist/CountrySelectorTemplate.js.map +1 -0
  16. package/dist/FilterMenu.d.ts +3 -3
  17. package/dist/FilterMenu.js +8 -9
  18. package/dist/FilterMenu.js.map +1 -1
  19. package/dist/FilterMenuTemplate.d.ts +2 -0
  20. package/dist/FilterMenuTemplate.js +27 -0
  21. package/dist/FilterMenuTemplate.js.map +1 -0
  22. package/dist/List.d.ts +1 -0
  23. package/dist/List.js +4 -0
  24. package/dist/List.js.map +1 -1
  25. package/dist/Pagination.js +1 -0
  26. package/dist/Pagination.js.map +1 -1
  27. package/dist/PasswordField.d.ts +16 -0
  28. package/dist/PasswordField.js +40 -0
  29. package/dist/PasswordField.js.map +1 -1
  30. package/dist/PasswordFieldTemplate.js +1 -1
  31. package/dist/PasswordFieldTemplate.js.map +1 -1
  32. package/dist/Search.d.ts +1 -0
  33. package/dist/Search.js +6 -0
  34. package/dist/Search.js.map +1 -1
  35. package/dist/SearchableListBaseTemplate.d.ts +2 -0
  36. package/dist/SearchableListBaseTemplate.js +32 -0
  37. package/dist/SearchableListBaseTemplate.js.map +1 -0
  38. package/dist/SecondaryNavigation.d.ts +2 -0
  39. package/dist/SecondaryNavigation.js +20 -0
  40. package/dist/SecondaryNavigation.js.map +1 -1
  41. package/dist/SelectBase.d.ts +7 -6
  42. package/dist/SelectBase.js +2 -2
  43. package/dist/SelectBase.js.map +1 -1
  44. package/dist/SelectBox.d.ts +2 -0
  45. package/dist/SelectBox.js +12 -3
  46. package/dist/SelectBox.js.map +1 -1
  47. package/dist/SelectBoxTemplate.d.ts +2 -0
  48. package/dist/SelectBoxTemplate.js +27 -0
  49. package/dist/SelectBoxTemplate.js.map +1 -0
  50. package/dist/Tab.d.ts +9 -0
  51. package/dist/Tab.js +6 -0
  52. package/dist/Tab.js.map +1 -1
  53. package/dist/TabNavigation.js +32 -12
  54. package/dist/TabNavigation.js.map +1 -1
  55. package/dist/TableGroupRowTemplate.js +1 -1
  56. package/dist/TableGroupRowTemplate.js.map +1 -1
  57. package/dist/TableTemplate.js +1 -1
  58. package/dist/TableTemplate.js.map +1 -1
  59. package/dist/TagTemplate.js +2 -4
  60. package/dist/TagTemplate.js.map +1 -1
  61. package/dist/TextField.js +1 -0
  62. package/dist/TextField.js.map +1 -1
  63. package/dist/Wizard.d.ts +2 -2
  64. package/dist/Wizard.js +7 -7
  65. package/dist/Wizard.js.map +1 -1
  66. package/dist/assets/{InputSuggestions-BIR_sdOv.js → InputSuggestions-BQp20KJM.js} +1 -1
  67. package/dist/assets/{ListItemStandardExpandableTextTemplate-5xOuY0en.js → ListItemStandardExpandableTextTemplate-D0w0u6f7.js} +1 -1
  68. package/dist/assets/{bundle.esm-C8GwJvJr.js → bundle.esm-DIFG6k5I.js} +240 -240
  69. package/dist/assets/messagebundle_bg-4ainC3nj.js +1 -0
  70. package/dist/assets/messagebundle_cs-CFtmLavu.js +1 -0
  71. package/dist/assets/messagebundle_en-BxjFAY2S.js +1 -0
  72. package/dist/css/themes/MastheadMenu.css +1 -1
  73. package/dist/css/themes/MastheadMobileMenu.css +1 -1
  74. package/dist/css/themes/MediaPlayer.css +1 -1
  75. package/dist/css/themes/RatingBar.css +1 -1
  76. package/dist/css/themes/RatingIndicator.css +1 -1
  77. package/dist/css/themes/SecondaryNavigationSubItem.css +1 -1
  78. package/dist/css/themes/Slider.css +1 -1
  79. package/dist/css/themes/TabNavigation.css +1 -1
  80. package/dist/css/themes/TableGroupRow.css +1 -1
  81. package/dist/css/themes/Wizard.css +1 -1
  82. package/dist/css/themes/WizardTab.css +1 -1
  83. package/dist/custom-elements-internal.json +65 -0
  84. package/dist/custom-elements.json +49 -0
  85. package/dist/features/SearchSuggestionsPopover.d.ts +4 -0
  86. package/dist/features/SearchSuggestionsPopover.js +56 -3
  87. package/dist/features/SearchSuggestionsPopover.js.map +1 -1
  88. package/dist/generated/assets/i18n/messagebundle_bg.json +1 -1
  89. package/dist/generated/assets/i18n/messagebundle_cs.json +1 -1
  90. package/dist/generated/assets/i18n/messagebundle_en.json +1 -1
  91. package/dist/generated/i18n/i18n-defaults.d.ts +2 -3
  92. package/dist/generated/i18n/i18n-defaults.js +2 -3
  93. package/dist/generated/i18n/i18n-defaults.js.map +1 -1
  94. package/dist/generated/templates/ListTemplate.lit.js +1 -1
  95. package/dist/generated/templates/ListTemplate.lit.js.map +1 -1
  96. package/dist/generated/templates/TabHeaderTemplate.lit.js +4 -4
  97. package/dist/generated/templates/TabHeaderTemplate.lit.js.map +1 -1
  98. package/dist/generated/templates/TabInOverflowTemplate.lit.js +2 -2
  99. package/dist/generated/templates/TabInOverflowTemplate.lit.js.map +1 -1
  100. package/dist/generated/themes/MastheadMenu.css.d.ts +1 -1
  101. package/dist/generated/themes/MastheadMenu.css.js +1 -1
  102. package/dist/generated/themes/MastheadMenu.css.js.map +1 -1
  103. package/dist/generated/themes/MastheadMobileMenu.css.d.ts +1 -1
  104. package/dist/generated/themes/MastheadMobileMenu.css.js +1 -1
  105. package/dist/generated/themes/MastheadMobileMenu.css.js.map +1 -1
  106. package/dist/generated/themes/MediaPlayer.css.d.ts +1 -1
  107. package/dist/generated/themes/MediaPlayer.css.js +1 -1
  108. package/dist/generated/themes/MediaPlayer.css.js.map +1 -1
  109. package/dist/generated/themes/RatingBar.css.d.ts +1 -1
  110. package/dist/generated/themes/RatingBar.css.js +1 -1
  111. package/dist/generated/themes/RatingBar.css.js.map +1 -1
  112. package/dist/generated/themes/RatingIndicator.css.d.ts +1 -1
  113. package/dist/generated/themes/RatingIndicator.css.js +1 -1
  114. package/dist/generated/themes/RatingIndicator.css.js.map +1 -1
  115. package/dist/generated/themes/SecondaryNavigationSubItem.css.d.ts +1 -1
  116. package/dist/generated/themes/SecondaryNavigationSubItem.css.js +1 -1
  117. package/dist/generated/themes/SecondaryNavigationSubItem.css.js.map +1 -1
  118. package/dist/generated/themes/Slider.css.d.ts +1 -1
  119. package/dist/generated/themes/Slider.css.js +1 -1
  120. package/dist/generated/themes/Slider.css.js.map +1 -1
  121. package/dist/generated/themes/TabNavigation.css.d.ts +1 -1
  122. package/dist/generated/themes/TabNavigation.css.js +1 -1
  123. package/dist/generated/themes/TabNavigation.css.js.map +1 -1
  124. package/dist/generated/themes/TableGroupRow.css.d.ts +1 -1
  125. package/dist/generated/themes/TableGroupRow.css.js +1 -1
  126. package/dist/generated/themes/TableGroupRow.css.js.map +1 -1
  127. package/dist/generated/themes/Wizard.css.d.ts +1 -1
  128. package/dist/generated/themes/Wizard.css.js +1 -1
  129. package/dist/generated/themes/Wizard.css.js.map +1 -1
  130. package/dist/generated/themes/WizardTab.css.d.ts +1 -1
  131. package/dist/generated/themes/WizardTab.css.js +1 -1
  132. package/dist/generated/themes/WizardTab.css.js.map +1 -1
  133. package/dist/messagebundle.properties +1 -6
  134. package/dist/messagebundle_bg.properties +129 -2
  135. package/dist/messagebundle_cs.properties +126 -0
  136. package/dist/messagebundle_en.properties +3 -1
  137. package/dist/vscode.html-custom-data.json +6 -1
  138. package/dist/web-types.json +42 -4
  139. package/package.json +1 -1
  140. package/src/CountrySelector.ts +14 -3
  141. package/src/CountrySelectorTemplate.tsx +12 -0
  142. package/src/FilterMenu.ts +9 -10
  143. package/src/FilterMenuTemplate.tsx +104 -0
  144. package/src/List.hbs +1 -1
  145. package/src/List.ts +5 -0
  146. package/src/Pagination.ts +1 -0
  147. package/src/PasswordField.ts +55 -0
  148. package/src/PasswordFieldTemplate.tsx +3 -0
  149. package/src/Search.ts +7 -0
  150. package/src/SearchableListBaseTemplate.tsx +157 -0
  151. package/src/SecondaryNavigation.ts +20 -0
  152. package/src/SelectBase.ts +7 -6
  153. package/src/SelectBox.ts +14 -3
  154. package/src/SelectBoxTemplate.tsx +165 -0
  155. package/src/Tab.ts +14 -0
  156. package/src/TabHeader.hbs +6 -2
  157. package/src/TabInOverflow.hbs +2 -0
  158. package/src/TabNavigation.ts +34 -12
  159. package/src/TableGroupRowTemplate.tsx +26 -17
  160. package/src/TableTemplate.tsx +1 -1
  161. package/src/TagTemplate.tsx +2 -4
  162. package/src/TextField.ts +1 -0
  163. package/src/Wizard.ts +15 -14
  164. package/src/features/SearchSuggestionsPopover.ts +59 -3
  165. package/src/generated/i18n/i18n-defaults.ts +2 -2
  166. package/src/generated/templates/ListTemplate.lit.ts +1 -1
  167. package/src/generated/templates/TabHeaderTemplate.lit.ts +4 -4
  168. package/src/generated/templates/TabInOverflowTemplate.lit.ts +2 -2
  169. package/src/generated/themes/MastheadMenu.css.ts +1 -1
  170. package/src/generated/themes/MastheadMobileMenu.css.ts +1 -1
  171. package/src/generated/themes/MediaPlayer.css.ts +1 -1
  172. package/src/generated/themes/RatingBar.css.ts +1 -1
  173. package/src/generated/themes/RatingIndicator.css.ts +1 -1
  174. package/src/generated/themes/SecondaryNavigationSubItem.css.ts +1 -1
  175. package/src/generated/themes/Slider.css.ts +1 -1
  176. package/src/generated/themes/TabNavigation.css.ts +1 -1
  177. package/src/generated/themes/TableGroupRow.css.ts +1 -1
  178. package/src/generated/themes/Wizard.css.ts +1 -1
  179. package/src/generated/themes/WizardTab.css.ts +1 -1
  180. package/src/i18n/messagebundle.properties +1 -6
  181. package/src/i18n/messagebundle_bg.properties +129 -2
  182. package/src/i18n/messagebundle_cs.properties +126 -0
  183. package/src/i18n/messagebundle_en.properties +3 -1
  184. package/src/themes/MastheadMenu.css +4 -2
  185. package/src/themes/MastheadMobileMenu.css +4 -2
  186. package/src/themes/MediaPlayer.css +12 -0
  187. package/src/themes/RatingBar.css +14 -5
  188. package/src/themes/SecondaryNavigationSubItem.css +1 -0
  189. package/src/themes/TabNavigation.css +6 -0
  190. package/src/themes/TableGroupRow.css +5 -0
  191. package/src/themes/Wizard.css +19 -15
  192. package/src/themes/WizardTab.css +43 -28
  193. package/dist/assets/messagebundle_bg-4X593TQq.js +0 -1
  194. package/dist/assets/messagebundle_cs-Bt8XtMib.js +0 -1
  195. package/dist/assets/messagebundle_en-iNqRB1VZ.js +0 -1
  196. package/dist/generated/templates/CountrySelectorTemplate.lit.d.ts +0 -3
  197. package/dist/generated/templates/CountrySelectorTemplate.lit.js +0 -8
  198. package/dist/generated/templates/CountrySelectorTemplate.lit.js.map +0 -1
  199. package/dist/generated/templates/FilterMenuTemplate.lit.d.ts +0 -3
  200. package/dist/generated/templates/FilterMenuTemplate.lit.js +0 -13
  201. package/dist/generated/templates/FilterMenuTemplate.lit.js.map +0 -1
  202. package/dist/generated/templates/SearchableListBaseTemplate.lit.d.ts +0 -3
  203. package/dist/generated/templates/SearchableListBaseTemplate.lit.js +0 -19
  204. package/dist/generated/templates/SearchableListBaseTemplate.lit.js.map +0 -1
  205. package/dist/generated/templates/SelectBoxTemplate.lit.d.ts +0 -3
  206. package/dist/generated/templates/SelectBoxTemplate.lit.js +0 -20
  207. package/dist/generated/templates/SelectBoxTemplate.lit.js.map +0 -1
  208. package/src/CountrySelector.hbs +0 -6
  209. package/src/FilterMenu.hbs +0 -88
  210. package/src/SearchableListBase.hbs +0 -148
  211. package/src/SelectBox.hbs +0 -138
  212. package/src/generated/templates/CountrySelectorTemplate.lit.ts +0 -12
  213. package/src/generated/templates/FilterMenuTemplate.lit.ts +0 -17
  214. package/src/generated/templates/SearchableListBaseTemplate.lit.ts +0 -23
  215. package/src/generated/templates/SelectBoxTemplate.lit.ts +0 -24
@@ -168,4 +168,42 @@ describe("List component - collapsible group header", () => {
168
168
  cy.get("@grouping-header").find('.udex-list__group-heading__icon').realClick();
169
169
  cy.get("#group-header-collapsible").should("not.have.attr", "collapsed", "");
170
170
  });
171
+ });
172
+
173
+ describe("List component - SingleContent accessible name", () => {
174
+ it("uses accessible-name as aria-label when only accessible-name is set", () => {
175
+ cy.mount(
176
+ <UDExList mode="SingleContent" accessible-name="My list">
177
+ <UDExListItem>Item 1</UDExListItem>
178
+ </UDExList>
179
+ );
180
+ cy.get("[udex-list]").shadow().find(".udex-list").should("have.attr", "aria-label", "My list");
181
+ });
182
+
183
+ it("uses header-text as aria-label when only header-text is set", () => {
184
+ cy.mount(
185
+ <UDExList mode="SingleContent" header-text="My header">
186
+ <UDExListItem>Item 1</UDExListItem>
187
+ </UDExList>
188
+ );
189
+ cy.get("[udex-list]").shadow().find(".udex-list").should("have.attr", "aria-label", "My header");
190
+ });
191
+
192
+ it("prioritizes accessible-name over header-text when both are set", () => {
193
+ cy.mount(
194
+ <UDExList mode="SingleContent" accessible-name="My accessible name" header-text="My header">
195
+ <UDExListItem>Item 1</UDExListItem>
196
+ </UDExList>
197
+ );
198
+ cy.get("[udex-list]").shadow().find(".udex-list").should("have.attr", "aria-label", "My accessible name");
199
+ });
200
+
201
+ it("does not have aria-label when neither accessible-name nor header-text is set", () => {
202
+ cy.mount(
203
+ <UDExList mode="SingleContent">
204
+ <UDExListItem>Item 1</UDExListItem>
205
+ </UDExList>
206
+ );
207
+ cy.get("[udex-list]").shadow().find(".udex-list").should("not.have.attr", "aria-label");
208
+ });
171
209
  });
@@ -0,0 +1,52 @@
1
+ import { UDExPagination } from "../support/component-imports.js";
2
+
3
+ describe("UDExPagination", () => {
4
+ describe("Focus management — _pageFocusedManually flag reset", () => {
5
+ it("does not call focusActivePage on programmatic currentPageIndex change after a user click", () => {
6
+ // Mount pagination with enough pages to navigate
7
+ cy.mount(
8
+ <div>
9
+ <button id="external-focus-target">External</button>
10
+ <UDExPagination id="pagination" pages={5} currentPageIndex={0} />
11
+ </div>,
12
+ );
13
+
14
+ // Wait for the component to render fully
15
+ cy.get("[udex-pagination]")
16
+ .shadow()
17
+ .find(".udex-pagination__page[data-index='0']")
18
+ .should("exist");
19
+
20
+ // Click on page 3 (index 2) — this sets _pageFocusedManually = true,
21
+ // triggers re-render, focusActivePage() runs and resets the flag
22
+ cy.get("[udex-pagination]")
23
+ .shadow()
24
+ .find(".udex-pagination__page[data-index='2']")
25
+ .click();
26
+
27
+ // Verify the click was registered (page 3 is now active)
28
+ cy.get("[udex-pagination]")
29
+ .shadow()
30
+ .find(".udex-pagination__page[data-index='2']")
31
+ .should("have.attr", "active");
32
+
33
+ // Move focus to an external element so we can detect if focus moves back
34
+ cy.get("#external-focus-target").focus();
35
+ cy.get("#external-focus-target").should("have.focus");
36
+
37
+ // Programmatically change currentPageIndex — this should NOT trigger focusActivePage
38
+ // because _pageFocusedManually was already consumed (reset to false) on the prior render
39
+ cy.get("[udex-pagination]").then(($el) => {
40
+ ($el[0] as any).currentPageIndex = 4;
41
+ });
42
+
43
+ // Allow time for re-render to complete
44
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
45
+ cy.wait(100);
46
+
47
+ // Assert that focus did NOT move to the pagination page button.
48
+ // Focus should remain on the external button.
49
+ cy.get("#external-focus-target").should("have.focus");
50
+ });
51
+ });
52
+ });
@@ -89,3 +89,39 @@ describe("General interaction", () => {
89
89
  });
90
90
  });
91
91
 
92
+ describe("Events", () => {
93
+ it("fires input event on typing", () => {
94
+ const inputHandler = cy.stub().as("inputHandler");
95
+ cy.mount(<UDExPasswordField onInput={inputHandler}></UDExPasswordField>);
96
+ cy.get("[udex-password-field]").shadow().find("[udex-text-field]").shadow().find("input").type("a");
97
+ cy.get("@inputHandler").should("have.been.called");
98
+ });
99
+
100
+ it("fires change event on Enter", () => {
101
+ const changeHandler = cy.stub().as("changeHandler");
102
+ cy.mount(<UDExPasswordField onChange={changeHandler}></UDExPasswordField>);
103
+ cy.get("[udex-password-field]").shadow().find("[udex-text-field]").shadow().find("input").type("test{enter}");
104
+ cy.get("@changeHandler").should("have.been.called");
105
+ });
106
+
107
+ it("fires blur event on focus out", () => {
108
+ const blurHandler = cy.stub().as("blurHandler");
109
+ cy.mount(<UDExPasswordField onBlur={blurHandler}></UDExPasswordField>);
110
+ cy.get("[udex-password-field]").shadow().find("[udex-text-field]").shadow().find("input").focus().blur();
111
+ cy.get("@blurHandler").should("have.been.called");
112
+ });
113
+
114
+ it("fires click-icon event when show/hide button is clicked", () => {
115
+ const clickIconHandler = cy.stub().as("clickIconHandler");
116
+ cy.mount(<UDExPasswordField onClickIcon={clickIconHandler} show-button-enabled></UDExPasswordField>);
117
+ cy.get("[udex-password-field]").shadow().find("[udex-text-field]").shadow().find("[udex-control-button]").click();
118
+ cy.get("@clickIconHandler").should("have.been.called");
119
+ });
120
+
121
+ it("updates value property on input", () => {
122
+ cy.mount(<UDExPasswordField></UDExPasswordField>);
123
+ cy.get("[udex-password-field]").shadow().find("[udex-text-field]").shadow().find("input").type("secret");
124
+ cy.get("[udex-password-field]").should("have.prop", "value", "secret");
125
+ });
126
+ });
127
+
@@ -0,0 +1,265 @@
1
+ import { UDExSearch } from "../support/component-imports.js";
2
+ // The SearchSuggestionsPopover feature must be imported so the popover is enabled.
3
+ import "../../src/features/SearchSuggestionsPopover.js";
4
+
5
+ const SUGGESTION_LIST_BOTH = JSON.stringify({
6
+ suggested: ["SAP hana", "SAP hana Enterprise Cloud"],
7
+ recommended: ["What is SAP hana", "SAP hana in Community"],
8
+ });
9
+
10
+ const SUGGESTION_LIST_RECOMMENDED_ONLY = JSON.stringify({
11
+ suggested: [],
12
+ recommended: ["What is SAP hana", "SAP hana in Community"],
13
+ });
14
+
15
+ const SEARCH_SHADOW = "[udex-search]";
16
+ const SEARCH_INPUT = "#search-input";
17
+ const SUGGESTIONS_LIST = ".udex-search-suggestions__list";
18
+
19
+ const focusInputThroughShadow = () => {
20
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).focus();
21
+ };
22
+
23
+ // suggestion-list must be set dynamically after mount.
24
+ // UI5Element._suppressInvalidation is true until after the first render, so
25
+ // attributes set by the JSX renderer never reach onInvalidation and the lists
26
+ // are never populated. Setting the attribute after mount (when
27
+ // _suppressInvalidation is false) allows onInvalidation → setSuggestedRecommendedLists
28
+ // to run and populate suggestedList / recommendedList.
29
+ const setSuggestionList = (list: string) => {
30
+ cy.get(SEARCH_SHADOW).invoke("attr", "suggestion-list", list);
31
+ };
32
+
33
+ const renderAlwaysOpen = () => (
34
+ <div>
35
+ <UDExSearch
36
+ id="search"
37
+ show-suggested-popover
38
+ open-suggestion-popup
39
+ />
40
+ <a id="next-focus" href="#">Next focus</a>
41
+ </div>
42
+ );
43
+
44
+ const renderAlwaysOpenSingleList = () => (
45
+ <div>
46
+ <UDExSearch
47
+ id="search"
48
+ show-suggested-popover
49
+ open-suggestion-popup
50
+ />
51
+ <a id="next-focus" href="#">Next focus</a>
52
+ </div>
53
+ );
54
+
55
+ describe("UDExSearch — keyboard focus management with openSuggestionPopup", () => {
56
+ describe("Always-open popover with two lists (suggested + recommended)", () => {
57
+ beforeEach(() => {
58
+ cy.mount(renderAlwaysOpen());
59
+ setSuggestionList(SUGGESTION_LIST_BOTH);
60
+ // Wait for the popover to be opened by onAfterRendering -> handleOpenSuggestionPopover.
61
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
62
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 2);
63
+ });
64
+
65
+ it("Tab traverses input -> lists[0] -> lists[1] -> input -> exits the component", () => {
66
+ focusInputThroughShadow();
67
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
68
+
69
+ // Tab 1: input -> lists[0]
70
+ cy.realPress("Tab");
71
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(0).should("be.focused");
72
+
73
+ // Tab 2: lists[0] -> lists[1]
74
+ cy.realPress("Tab");
75
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(1).should("be.focused");
76
+
77
+ // Tab 3: lists[1] -> input (escape flag is set; popover stays open)
78
+ cy.realPress("Tab");
79
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
80
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
81
+
82
+ // Tab 4: input + flag -> focus leaves the search component entirely
83
+ cy.realPress("Tab");
84
+ cy.get("#next-focus").should("be.focused");
85
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("not.be.focused");
86
+ });
87
+
88
+ it("Shift+Tab traverses input -> lists[1] -> lists[0] -> input -> exits the component backward", () => {
89
+ // Place a focusable element BEFORE the search so Shift+Tab past the input has somewhere to go.
90
+ cy.mount(
91
+ <div>
92
+ <a id="prev-focus" href="#">Previous focus</a>
93
+ <UDExSearch
94
+ id="search"
95
+ show-suggested-popover
96
+ open-suggestion-popup
97
+ />
98
+ </div>,
99
+ );
100
+ setSuggestionList(SUGGESTION_LIST_BOTH);
101
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
102
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 2);
103
+
104
+ focusInputThroughShadow();
105
+
106
+ // Shift+Tab 1: input -> lists[1] (last list)
107
+ cy.realPress(["Shift", "Tab"]);
108
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(1).should("be.focused");
109
+
110
+ // Shift+Tab 2: lists[1] -> lists[0]
111
+ cy.realPress(["Shift", "Tab"]);
112
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(0).should("be.focused");
113
+
114
+ // Shift+Tab 3: lists[0] -> input (escape flag is set; popover stays open)
115
+ cy.realPress(["Shift", "Tab"]);
116
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
117
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
118
+
119
+ // Shift+Tab 4: input + flag -> focus leaves the search component backward
120
+ cy.realPress(["Shift", "Tab"]);
121
+ cy.get("#prev-focus").should("be.focused");
122
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("not.be.focused");
123
+ });
124
+
125
+ it("Shift+Tab from reset button while popup is open moves focus to input (not trapped)", () => {
126
+ cy.mount(
127
+ <div>
128
+ <UDExSearch
129
+ id="search"
130
+ show-suggested-popover
131
+ open-suggestion-popup
132
+ value="SAP"
133
+ />
134
+ <a id="next-focus" href="#">Next focus</a>
135
+ </div>,
136
+ );
137
+ setSuggestionList(SUGGESTION_LIST_BOTH);
138
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
139
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 2);
140
+
141
+ focusInputThroughShadow();
142
+
143
+ // Navigate through popover lists with Tab and return to input via escape flag.
144
+ cy.realPress("Tab");
145
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(0).should("be.focused");
146
+ cy.realPress("Tab");
147
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(1).should("be.focused");
148
+ cy.realPress("Tab");
149
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
150
+
151
+ // Tab again: escape flag is consumed, natural Tab moves to reset button.
152
+ cy.realPress("Tab");
153
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("not.be.focused");
154
+
155
+ // Shift+Tab from reset button must return to input — not get trapped.
156
+ cy.realPress(["Shift", "Tab"]);
157
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
158
+ });
159
+
160
+ it("Down arrow from a focused list moves focus into individual item buttons; Up arrow reverses it", () => {
161
+ focusInputThroughShadow();
162
+
163
+ // Tab into lists[0].
164
+ cy.realPress("Tab");
165
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(0).should("be.focused");
166
+
167
+ // ArrowDown: first item in lists[0] receives focus & aria-selected.
168
+ cy.realPress("ArrowDown");
169
+ cy.get(SEARCH_SHADOW)
170
+ .shadow()
171
+ .find(SUGGESTIONS_LIST)
172
+ .eq(0)
173
+ .find("[ui5-button]")
174
+ .eq(0)
175
+ .should("have.attr", "aria-selected", "true");
176
+
177
+ // ArrowDown: focus moves to the second item in lists[0].
178
+ cy.realPress("ArrowDown");
179
+ cy.get(SEARCH_SHADOW)
180
+ .shadow()
181
+ .find(SUGGESTIONS_LIST)
182
+ .eq(0)
183
+ .find("[ui5-button]")
184
+ .eq(1)
185
+ .should("have.attr", "aria-selected", "true");
186
+
187
+ // ArrowDown from the LAST item of lists[0] hops focus to lists[1] (selectedItemIndex resets to -1).
188
+ cy.realPress("ArrowDown");
189
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(1).should("be.focused");
190
+
191
+ // ArrowDown inside lists[1]: focuses first item there.
192
+ cy.realPress("ArrowDown");
193
+ cy.get(SEARCH_SHADOW)
194
+ .shadow()
195
+ .find(SUGGESTIONS_LIST)
196
+ .eq(1)
197
+ .find("[ui5-button]")
198
+ .eq(0)
199
+ .should("have.attr", "aria-selected", "true");
200
+
201
+ // ArrowUp from the FIRST item of lists[1] hops focus back to lists[0].
202
+ cy.realPress("ArrowUp");
203
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(0).should("be.focused");
204
+ });
205
+ });
206
+
207
+ describe("Always-open popover with a single list (recommended only)", () => {
208
+ beforeEach(() => {
209
+ cy.mount(renderAlwaysOpenSingleList());
210
+ setSuggestionList(SUGGESTION_LIST_RECOMMENDED_ONLY);
211
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
212
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 1);
213
+ });
214
+
215
+ it("Tab traverses input -> lists[0] -> input -> exits the component", () => {
216
+ focusInputThroughShadow();
217
+
218
+ // Tab 1: input -> lists[0]
219
+ cy.realPress("Tab");
220
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(0).should("be.focused");
221
+
222
+ // Tab 2: lists[0] -> input (escape flag set; popover stays open)
223
+ cy.realPress("Tab");
224
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
225
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
226
+
227
+ // Tab 3: input + flag -> exits component
228
+ cy.realPress("Tab");
229
+ cy.get("#next-focus").should("be.focused");
230
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("not.be.focused");
231
+ });
232
+
233
+ it("Shift+Tab traverses input -> lists[0] -> input -> exits the component backward", () => {
234
+ cy.mount(
235
+ <div>
236
+ <a id="prev-focus" href="#">Previous focus</a>
237
+ <UDExSearch
238
+ id="search"
239
+ show-suggested-popover
240
+ open-suggestion-popup
241
+ />
242
+ </div>,
243
+ );
244
+ setSuggestionList(SUGGESTION_LIST_RECOMMENDED_ONLY);
245
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
246
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 1);
247
+
248
+ focusInputThroughShadow();
249
+
250
+ // Shift+Tab 1: input -> lists[0] (the only / last list)
251
+ cy.realPress(["Shift", "Tab"]);
252
+ cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).eq(0).should("be.focused");
253
+
254
+ // Shift+Tab 2: lists[0] -> input (escape flag set)
255
+ cy.realPress(["Shift", "Tab"]);
256
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
257
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
258
+
259
+ // Shift+Tab 3: input + flag -> exits component backward
260
+ cy.realPress(["Shift", "Tab"]);
261
+ cy.get("#prev-focus").should("be.focused");
262
+ cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("not.be.focused");
263
+ });
264
+ });
265
+ });
@@ -505,6 +505,24 @@ describe("SelectBox", () => {
505
505
  .should("have.text", "Python");
506
506
  });
507
507
 
508
+ it("MultiSelect mode - navigation label is cleared from input after Tab closes the dropdown", () => {
509
+ cy.mount(multiSelect);
510
+ cy.get("#select-box").shadow()
511
+ .find(".udex-text-field__search")
512
+ .click()
513
+ .realPress("ArrowDown");
514
+
515
+ cy.get("#select-box").shadow()
516
+ .find(".udex-text-field__single-label")
517
+ .should("have.text", "JavaScript");
518
+
519
+ cy.realPress("Tab");
520
+
521
+ cy.get("#select-box").shadow()
522
+ .find(".udex-text-field__single-label")
523
+ .should("not.exist");
524
+ });
525
+
508
526
  it("MultiSelect mode - after clicking on the item dropdown is opened", () => {
509
527
  cy.mount(multiSelect);
510
528
  cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
@@ -1155,7 +1173,7 @@ describe("SelectBox", () => {
1155
1173
  .find("udex-tag-group").shadow()
1156
1174
  .find("udex-tag")
1157
1175
  .first()
1158
- .should("have.attr", "text", "5 stars");
1176
+ .should("have.attr", "text", "5 stars");
1159
1177
  });
1160
1178
 
1161
1179
  it("MultiSelect mode - no single-label shown after opening dropdown and clicking outside", () => {