@sapudex/web-components 2.39.0 → 2.41.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.
- package/cypress/specs/Dropdown.cy.tsx +90 -0
- package/cypress/specs/PasswordField.cy.tsx +37 -0
- package/cypress/specs/Search.cy.tsx +244 -138
- package/cypress/specs/SelectBox.cy.tsx +457 -0
- package/cypress/specs/Tile.cy.tsx +96 -0
- package/cypress/support/component-imports.instrumented.ts +4 -0
- package/cypress/support/component-imports.src.ts +4 -0
- package/dist/Dropdown.d.ts +5 -0
- package/dist/Dropdown.js +56 -0
- package/dist/Dropdown.js.map +1 -1
- package/dist/List.d.ts +1 -6
- package/dist/List.js +2 -10
- package/dist/List.js.map +1 -1
- package/dist/MastheadCountrySelector.d.ts +7 -0
- package/dist/MastheadCountrySelector.js +3 -0
- package/dist/MastheadCountrySelector.js.map +1 -1
- package/dist/PasswordField.d.ts +10 -0
- package/dist/PasswordField.js +17 -2
- package/dist/PasswordField.js.map +1 -1
- package/dist/PasswordFieldTemplate.js +1 -1
- package/dist/PasswordFieldTemplate.js.map +1 -1
- package/dist/Search.d.ts +1 -1
- package/dist/Search.js +13 -9
- package/dist/Search.js.map +1 -1
- package/dist/SearchTemplate.js +3 -3
- package/dist/SearchTemplate.js.map +1 -1
- package/dist/SearchableListBase.d.ts +2 -6
- package/dist/SearchableListBase.js +6 -3
- package/dist/SearchableListBase.js.map +1 -1
- package/dist/SearchableListBaseTemplate.js +1 -1
- package/dist/SearchableListBaseTemplate.js.map +1 -1
- package/dist/SelectBase.d.ts +20 -2
- package/dist/SelectBase.js +189 -13
- package/dist/SelectBase.js.map +1 -1
- package/dist/SelectBox.js +2 -1
- package/dist/SelectBox.js.map +1 -1
- package/dist/SelectBoxTemplate.js +4 -4
- package/dist/SelectBoxTemplate.js.map +1 -1
- package/dist/TextField.d.ts +1 -1
- package/dist/TextField.js +2 -2
- package/dist/TextField.js.map +1 -1
- package/dist/TextFieldTemplate.js +1 -1
- package/dist/TextFieldTemplate.js.map +1 -1
- package/dist/assets/{InputSuggestions-jTW8u1vD.js → InputSuggestions-BSg7bjcx.js} +1 -1
- package/dist/assets/{ListItemStandardExpandableTextTemplate-ShRY1eSJ.js → ListItemStandardExpandableTextTemplate-CCEQur_J.js} +1 -1
- package/dist/assets/{bundle.esm-VI-guBTG.js → bundle.esm-I7iOjLgn.js} +232 -232
- package/dist/assets/messagebundle_en-BZqDcZHN.js +1 -0
- package/dist/assets/messagebundle_et-yYsmBPXN.js +1 -0
- package/dist/assets/messagebundle_fi-DeMNrHC9.js +1 -0
- package/dist/assets/messagebundle_fr-qEm5Eykz.js +1 -0
- package/dist/assets/messagebundle_hr-NBdiYo-T.js +1 -0
- package/dist/assets/messagebundle_hu-CBV3ISbF.js +1 -0
- package/dist/css/themes/MastheadCountrySelector.css +1 -1
- package/dist/css/themes/MastheadProfileMenu.css +1 -1
- package/dist/css/themes/SelectBase.css +1 -1
- package/dist/css/themes/SelectBox.css +1 -1
- package/dist/css/themes/StatusMessage.css +1 -1
- package/dist/css/themes/TextField.css +1 -1
- package/dist/css/themes/Tile.css +1 -1
- package/dist/css/themes/Toast.css +1 -1
- package/dist/custom-elements-internal.json +85 -161
- package/dist/custom-elements.json +56 -120
- package/dist/features/SearchSuggestionsPopover.d.ts +6 -9
- package/dist/features/SearchSuggestionsPopover.js +47 -163
- package/dist/features/SearchSuggestionsPopover.js.map +1 -1
- package/dist/generated/assets/i18n/messagebundle_en.json +1 -1
- package/dist/generated/assets/i18n/messagebundle_et.json +1 -1
- package/dist/generated/assets/i18n/messagebundle_fi.json +1 -1
- package/dist/generated/assets/i18n/messagebundle_fr.json +1 -1
- package/dist/generated/assets/i18n/messagebundle_hr.json +1 -1
- package/dist/generated/assets/i18n/messagebundle_hu.json +1 -1
- package/dist/generated/i18n/i18n-defaults.d.ts +3 -2
- package/dist/generated/i18n/i18n-defaults.js +3 -2
- package/dist/generated/i18n/i18n-defaults.js.map +1 -1
- package/dist/generated/templates/MastheadCountrySelectorTemplate.lit.js +6 -5
- package/dist/generated/templates/MastheadCountrySelectorTemplate.lit.js.map +1 -1
- package/dist/generated/templates/MastheadMobileMenuTemplate.lit.js +1 -1
- package/dist/generated/templates/MastheadMobileMenuTemplate.lit.js.map +1 -1
- package/dist/generated/themes/MastheadCountrySelector.css.d.ts +1 -1
- package/dist/generated/themes/MastheadCountrySelector.css.js +1 -1
- package/dist/generated/themes/MastheadCountrySelector.css.js.map +1 -1
- package/dist/generated/themes/MastheadProfileMenu.css.d.ts +1 -1
- package/dist/generated/themes/MastheadProfileMenu.css.js +1 -1
- package/dist/generated/themes/MastheadProfileMenu.css.js.map +1 -1
- package/dist/generated/themes/SelectBase.css.d.ts +1 -1
- package/dist/generated/themes/SelectBase.css.js +1 -1
- package/dist/generated/themes/SelectBase.css.js.map +1 -1
- package/dist/generated/themes/SelectBox.css.d.ts +1 -1
- package/dist/generated/themes/SelectBox.css.js +1 -1
- package/dist/generated/themes/SelectBox.css.js.map +1 -1
- package/dist/generated/themes/StatusMessage.css.d.ts +1 -1
- package/dist/generated/themes/StatusMessage.css.js +1 -1
- package/dist/generated/themes/StatusMessage.css.js.map +1 -1
- package/dist/generated/themes/TextField.css.d.ts +1 -1
- package/dist/generated/themes/TextField.css.js +1 -1
- package/dist/generated/themes/TextField.css.js.map +1 -1
- package/dist/generated/themes/Tile.css.d.ts +1 -1
- package/dist/generated/themes/Tile.css.js +1 -1
- package/dist/generated/themes/Tile.css.js.map +1 -1
- package/dist/generated/themes/Toast.css.d.ts +1 -1
- package/dist/generated/themes/Toast.css.js +1 -1
- package/dist/generated/themes/Toast.css.js.map +1 -1
- package/dist/messagebundle.properties +2 -1
- package/dist/messagebundle_en.properties +2 -1
- package/dist/messagebundle_et.properties +126 -0
- package/dist/messagebundle_fi.properties +127 -0
- package/dist/messagebundle_fr.properties +120 -0
- package/dist/messagebundle_hr.properties +125 -0
- package/dist/messagebundle_hu.properties +125 -0
- package/dist/vscode.html-custom-data.json +12 -27
- package/dist/web-types.json +36 -44
- package/package.json +1 -1
- package/src/Dropdown.ts +62 -0
- package/src/List.ts +3 -18
- package/src/MastheadCountrySelector.hbs +2 -0
- package/src/MastheadCountrySelector.ts +9 -0
- package/src/MastheadMobileMenu.hbs +1 -0
- package/src/PasswordField.ts +24 -0
- package/src/PasswordFieldTemplate.tsx +2 -2
- package/src/Search.ts +13 -9
- package/src/SearchTemplate.tsx +0 -3
- package/src/SearchableListBase.ts +8 -9
- package/src/SearchableListBaseTemplate.tsx +0 -1
- package/src/SelectBase.ts +203 -16
- package/src/SelectBox.ts +2 -1
- package/src/SelectBoxTemplate.tsx +1 -0
- package/src/TextField.ts +1 -1
- package/src/TextFieldTemplate.tsx +1 -1
- package/src/features/SearchSuggestionsPopover.ts +55 -177
- package/src/generated/i18n/i18n-defaults.ts +2 -2
- package/src/generated/templates/MastheadCountrySelectorTemplate.lit.ts +6 -5
- package/src/generated/templates/MastheadMobileMenuTemplate.lit.ts +1 -1
- package/src/generated/themes/MastheadCountrySelector.css.ts +1 -1
- package/src/generated/themes/MastheadProfileMenu.css.ts +1 -1
- package/src/generated/themes/SelectBase.css.ts +1 -1
- package/src/generated/themes/SelectBox.css.ts +1 -1
- package/src/generated/themes/StatusMessage.css.ts +1 -1
- package/src/generated/themes/TextField.css.ts +1 -1
- package/src/generated/themes/Tile.css.ts +1 -1
- package/src/generated/themes/Toast.css.ts +1 -1
- package/src/i18n/messagebundle.properties +2 -1
- package/src/i18n/messagebundle_en.properties +2 -1
- package/src/i18n/messagebundle_et.properties +126 -0
- package/src/i18n/messagebundle_fi.properties +127 -0
- package/src/i18n/messagebundle_fr.properties +120 -0
- package/src/i18n/messagebundle_hr.properties +125 -0
- package/src/i18n/messagebundle_hu.properties +125 -0
- package/src/themes/MastheadCountrySelector.css +5 -2
- package/src/themes/MastheadProfileMenu.css +0 -5
- package/src/themes/SelectBase.css +7 -0
- package/src/themes/SelectBox.css +4 -0
- package/src/themes/StatusMessage.css +4 -4
- package/src/themes/TextField.css +3 -0
- package/src/themes/Tile.css +11 -3
- package/src/themes/Toast.css +14 -2
- package/dist/assets/messagebundle_en-BxjFAY2S.js +0 -1
- package/dist/assets/messagebundle_et-BCYOcg2t.js +0 -1
- package/dist/assets/messagebundle_fi-CssCIjmp.js +0 -1
- package/dist/assets/messagebundle_fr-CvOyuAVw.js +0 -1
- package/dist/assets/messagebundle_hr-CashhWbN.js +0 -1
- package/dist/assets/messagebundle_hu-BEMbu-EE.js +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UDExDropdown,
|
|
3
|
+
UDExButton,
|
|
4
|
+
UDExList,
|
|
5
|
+
UDExListItem,
|
|
6
|
+
UDExSearch,
|
|
7
|
+
} from "../support/component-imports.js";
|
|
8
|
+
|
|
9
|
+
const dropdownFixture = (
|
|
10
|
+
<div style="position:relative; padding: 24px;">
|
|
11
|
+
<a id="prev-link" href="#">Previous focusable element</a>
|
|
12
|
+
{" "}
|
|
13
|
+
<UDExButton id="opener">Open dropdown</UDExButton>
|
|
14
|
+
{" "}
|
|
15
|
+
<a id="next-link" href="#">Next focusable element</a>
|
|
16
|
+
<UDExDropdown id="dropdown" opener="opener">
|
|
17
|
+
<div slot="header">
|
|
18
|
+
<UDExSearch id="dropdown-search"></UDExSearch>
|
|
19
|
+
</div>
|
|
20
|
+
<UDExList id="dropdown-list">
|
|
21
|
+
<UDExListItem id="item-1">List Item 1</UDExListItem>
|
|
22
|
+
<UDExListItem id="item-2">List Item 2</UDExListItem>
|
|
23
|
+
<UDExListItem id="item-3">List Item 3</UDExListItem>
|
|
24
|
+
</UDExList>
|
|
25
|
+
<div slot="footer">
|
|
26
|
+
<UDExButton id="footer-button" design="Tertiary">Apply</UDExButton>
|
|
27
|
+
</div>
|
|
28
|
+
</UDExDropdown>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const openDropdown = () => {
|
|
33
|
+
cy.get("#dropdown").then($el => {
|
|
34
|
+
($el[0] as any).open = true;
|
|
35
|
+
});
|
|
36
|
+
cy.get("[udex-dropdown]").should("have.attr", "open");
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("UDExDropdown", () => {
|
|
40
|
+
describe("Rendering", () => {
|
|
41
|
+
it("registers the udex-dropdown custom element", () => {
|
|
42
|
+
cy.mount(dropdownFixture);
|
|
43
|
+
cy.get("[udex-dropdown]").should("exist");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("Dropdown keyboard navigation", () => {
|
|
48
|
+
it("Shift+Tab from first focusable element closes dropdown and returns focus to opener", () => {
|
|
49
|
+
cy.mount(dropdownFixture);
|
|
50
|
+
openDropdown();
|
|
51
|
+
|
|
52
|
+
// Opening the dropdown auto-focuses the search input in the header.
|
|
53
|
+
// forwardToLast() (Shift+Tab from the first focusable element) calls
|
|
54
|
+
// closePopup() which resets focus back to the opener.
|
|
55
|
+
cy.realPress(["Shift", "Tab"]);
|
|
56
|
+
|
|
57
|
+
cy.get("[udex-dropdown]").should("not.have.attr", "open");
|
|
58
|
+
cy.get("#opener").should("be.focused");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("Tab from last focusable element closes dropdown and moves focus past opener", () => {
|
|
62
|
+
cy.mount(dropdownFixture);
|
|
63
|
+
openDropdown();
|
|
64
|
+
|
|
65
|
+
// Directly focus the last-fe sentinel — this is exactly what the browser
|
|
66
|
+
// does when Tab is pressed from the last focusable element inside the
|
|
67
|
+
// popover. The sentinel's onFocusIn calls forwardToFirst(), which closes
|
|
68
|
+
// the dropdown and moves focus to the next element after the opener.
|
|
69
|
+
cy.get("[udex-dropdown]").shadow().find(".last-fe").then($el => {
|
|
70
|
+
$el[0].focus();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
cy.get("[udex-dropdown]").should("not.have.attr", "open");
|
|
74
|
+
cy.get("#next-link").should("be.focused");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("Tab navigates forward through focusable elements without closing the dropdown", () => {
|
|
78
|
+
cy.mount(dropdownFixture);
|
|
79
|
+
openDropdown();
|
|
80
|
+
|
|
81
|
+
// The search input is auto-focused on open. A single forward Tab should
|
|
82
|
+
// move focus to the next focusable element inside the popover (list /
|
|
83
|
+
// footer) — the dropdown should remain open because we have not yet
|
|
84
|
+
// crossed the last-fe sentinel.
|
|
85
|
+
cy.realPress("Tab");
|
|
86
|
+
|
|
87
|
+
cy.get("[udex-dropdown]").should("have.attr", "open");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -237,6 +237,43 @@ describe("passwordVisible property", () => {
|
|
|
237
237
|
});
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
describe("activeButtonTooltip", () => {
|
|
241
|
+
const controlBtn = () =>
|
|
242
|
+
cy.get("[udex-password-field]")
|
|
243
|
+
.shadow().find("[udex-text-field]")
|
|
244
|
+
.shadow().find("[udex-control-button]");
|
|
245
|
+
|
|
246
|
+
it("uses showButtonTooltip when password is hidden", () => {
|
|
247
|
+
cy.mount(<UDExPasswordField show-button-enabled show-button-tooltip="Show it" hide-button-tooltip="Hide it"></UDExPasswordField>);
|
|
248
|
+
controlBtn().should("have.attr", "accessible-name", "Show it");
|
|
249
|
+
controlBtn().shadow().find("[data-sap-focus-ref]").should("have.attr", "title", "Show it");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("uses hideButtonTooltip when password is visible", () => {
|
|
253
|
+
cy.mount(<UDExPasswordField show-button-enabled show-button-tooltip="Show it" hide-button-tooltip="Hide it"></UDExPasswordField>);
|
|
254
|
+
controlBtn().click();
|
|
255
|
+
controlBtn().should("have.attr", "accessible-name", "Hide it");
|
|
256
|
+
controlBtn().shadow().find("[data-sap-focus-ref]").should("have.attr", "title", "Hide it");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("falls back to showButtonTooltip when hideButtonTooltip is not set", () => {
|
|
260
|
+
cy.mount(<UDExPasswordField show-button-enabled show-button-tooltip="Show it"></UDExPasswordField>);
|
|
261
|
+
controlBtn().click();
|
|
262
|
+
controlBtn().should("have.attr", "accessible-name", "Hide password");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("falls back to i18n default 'Show password' when no tooltips are set and password is hidden", () => {
|
|
266
|
+
cy.mount(<UDExPasswordField show-button-enabled></UDExPasswordField>);
|
|
267
|
+
controlBtn().should("have.attr", "accessible-name", "Show password");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("falls back to i18n default 'Hide password' when no tooltips are set and password is visible", () => {
|
|
271
|
+
cy.mount(<UDExPasswordField show-button-enabled></UDExPasswordField>);
|
|
272
|
+
controlBtn().click();
|
|
273
|
+
controlBtn().should("have.attr", "accessible-name", "Hide password");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
240
277
|
describe("Events", () => {
|
|
241
278
|
it("fires input event on typing", () => {
|
|
242
279
|
const inputHandler = cy.stub().as("inputHandler");
|
|
@@ -15,26 +15,40 @@ const SUGGESTION_LIST_RECOMMENDED_ONLY = JSON.stringify({
|
|
|
15
15
|
const SEARCH_SHADOW = "[udex-search]";
|
|
16
16
|
const SEARCH_INPUT = "#search-input";
|
|
17
17
|
const SUGGESTIONS_LIST = ".udex-search-suggestions__list";
|
|
18
|
+
const ITEM_BUTTON = "[ui5-button]";
|
|
19
|
+
const SUBMIT_BUTTON = "#submit-button";
|
|
18
20
|
|
|
19
21
|
const focusInputThroughShadow = () => {
|
|
20
22
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).focus();
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// (
|
|
26
|
-
// pseudo-class
|
|
27
|
-
const
|
|
28
|
-
cy.get(SEARCH_SHADOW)
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
// Items are focused via item.focus() inside the popover (light-DOM children of
|
|
26
|
+
// ui5-popover that lives inside the search shadow root). Cypress's
|
|
27
|
+
// should("be.focused") fails on such cross-shadow chains, so use the native
|
|
28
|
+
// :focus pseudo-class which the browser applies to the actually-focused node.
|
|
29
|
+
const assertNthFlatItemFocused = (flatIndex: number) => {
|
|
30
|
+
cy.get(SEARCH_SHADOW)
|
|
31
|
+
.shadow()
|
|
32
|
+
.find(`${SUGGESTIONS_LIST} ${ITEM_BUTTON}`)
|
|
33
|
+
.eq(flatIndex)
|
|
34
|
+
.should($el => {
|
|
35
|
+
expect($el[0].matches(":focus")).to.be.true;
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const assertNthFlatItemAriaSelected = (flatIndex: number) => {
|
|
40
|
+
cy.get(SEARCH_SHADOW)
|
|
41
|
+
.shadow()
|
|
42
|
+
.find(`${SUGGESTIONS_LIST} ${ITEM_BUTTON}`)
|
|
43
|
+
.eq(flatIndex)
|
|
44
|
+
.should("have.attr", "aria-selected", "true");
|
|
31
45
|
};
|
|
32
46
|
|
|
33
47
|
// suggestion-list must be set dynamically after mount.
|
|
34
48
|
// UI5Element._suppressInvalidation is true until after the first render, so
|
|
35
49
|
// attributes set by the JSX renderer never reach onInvalidation and the lists
|
|
36
50
|
// are never populated. Setting the attribute after mount (when
|
|
37
|
-
// _suppressInvalidation is false) allows onInvalidation
|
|
51
|
+
// _suppressInvalidation is false) allows onInvalidation -> setSuggestedRecommendedLists
|
|
38
52
|
// to run and populate suggestedList / recommendedList.
|
|
39
53
|
const setSuggestionList = (list: string) => {
|
|
40
54
|
cy.get(SEARCH_SHADOW).invoke("attr", "suggestion-list", list);
|
|
@@ -51,18 +65,31 @@ const renderAlwaysOpen = () => (
|
|
|
51
65
|
</div>
|
|
52
66
|
);
|
|
53
67
|
|
|
54
|
-
const
|
|
68
|
+
const renderAlwaysOpenWithSubmit = () => (
|
|
55
69
|
<div>
|
|
56
70
|
<UDExSearch
|
|
57
71
|
id="search"
|
|
58
72
|
show-suggested-popover
|
|
59
73
|
open-suggestion-popup
|
|
74
|
+
show-submit-button
|
|
60
75
|
/>
|
|
61
76
|
<a id="next-focus" href="#">Next focus</a>
|
|
62
77
|
</div>
|
|
63
78
|
);
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
const renderAlwaysOpenWithPrev = () => (
|
|
81
|
+
<div>
|
|
82
|
+
<a id="prev-focus" href="#">Previous focus</a>
|
|
83
|
+
<UDExSearch
|
|
84
|
+
id="search"
|
|
85
|
+
show-suggested-popover
|
|
86
|
+
open-suggestion-popup
|
|
87
|
+
/>
|
|
88
|
+
<a id="next-focus" href="#">Next focus</a>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
describe("UDExSearch — keyboard focus management (rewritten)", () => {
|
|
66
93
|
describe("Always-open popover with two lists (suggested + recommended)", () => {
|
|
67
94
|
beforeEach(() => {
|
|
68
95
|
cy.mount(renderAlwaysOpen());
|
|
@@ -70,211 +97,290 @@ describe("UDExSearch — keyboard focus management with openSuggestionPopup", ()
|
|
|
70
97
|
// Wait for the popover to be opened by onAfterRendering -> handleOpenSuggestionPopover.
|
|
71
98
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
72
99
|
cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 2);
|
|
100
|
+
// 2 + 2 = 4 items across both lists.
|
|
101
|
+
cy.get(SEARCH_SHADOW).shadow().find(`${SUGGESTIONS_LIST} ${ITEM_BUTTON}`).should("have.length", 4);
|
|
73
102
|
});
|
|
74
103
|
|
|
75
|
-
it("
|
|
104
|
+
it("ArrowDown from the input opens the dropdown and focuses the first item", () => {
|
|
76
105
|
focusInputThroughShadow();
|
|
77
|
-
cy.
|
|
106
|
+
cy.realPress("ArrowDown");
|
|
78
107
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
assertNthFlatItemFocused(0);
|
|
109
|
+
assertNthFlatItemAriaSelected(0);
|
|
110
|
+
});
|
|
82
111
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
it("ArrowDown navigates a flat list across both suggested and recommended items", () => {
|
|
113
|
+
focusInputThroughShadow();
|
|
114
|
+
// Arrow down four times moves through index 0, 1, 2, 3.
|
|
115
|
+
cy.realPress("ArrowDown"); // -> first item of suggested
|
|
116
|
+
assertNthFlatItemFocused(0);
|
|
86
117
|
|
|
87
|
-
//
|
|
88
|
-
|
|
118
|
+
cy.realPress("ArrowDown"); // -> second item of suggested
|
|
119
|
+
assertNthFlatItemFocused(1);
|
|
120
|
+
|
|
121
|
+
cy.realPress("ArrowDown"); // -> first item of recommended
|
|
122
|
+
assertNthFlatItemFocused(2);
|
|
123
|
+
|
|
124
|
+
cy.realPress("ArrowDown"); // -> second item of recommended (last)
|
|
125
|
+
assertNthFlatItemFocused(3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("ArrowDown on the last item wraps to the first item (circular)", () => {
|
|
129
|
+
focusInputThroughShadow();
|
|
130
|
+
cy.realPress("ArrowDown");
|
|
131
|
+
cy.realPress("ArrowDown");
|
|
132
|
+
cy.realPress("ArrowDown");
|
|
133
|
+
cy.realPress("ArrowDown");
|
|
134
|
+
assertNthFlatItemFocused(3);
|
|
135
|
+
|
|
136
|
+
// Wrap forward: last -> first.
|
|
137
|
+
cy.realPress("ArrowDown");
|
|
138
|
+
assertNthFlatItemFocused(0);
|
|
139
|
+
assertNthFlatItemAriaSelected(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("ArrowUp on the first item wraps to the last item (circular)", () => {
|
|
143
|
+
focusInputThroughShadow();
|
|
144
|
+
// Initial state: selectedItemIndex = -1. ArrowUp wraps to last.
|
|
145
|
+
cy.realPress("ArrowDown");
|
|
146
|
+
assertNthFlatItemFocused(0);
|
|
147
|
+
|
|
148
|
+
// From first item, ArrowUp wraps to last item.
|
|
149
|
+
cy.realPress("ArrowUp");
|
|
150
|
+
assertNthFlatItemFocused(3);
|
|
151
|
+
assertNthFlatItemAriaSelected(3);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("ArrowUp navigates backward across both lists as one flat sequence", () => {
|
|
155
|
+
focusInputThroughShadow();
|
|
156
|
+
// Move to last item.
|
|
157
|
+
cy.realPress("ArrowDown");
|
|
158
|
+
cy.realPress("ArrowDown");
|
|
159
|
+
cy.realPress("ArrowDown");
|
|
160
|
+
cy.realPress("ArrowDown");
|
|
161
|
+
assertNthFlatItemFocused(3);
|
|
162
|
+
|
|
163
|
+
// Step backward: last -> 2 -> 1 -> 0.
|
|
164
|
+
cy.realPress("ArrowUp");
|
|
165
|
+
assertNthFlatItemFocused(2);
|
|
166
|
+
cy.realPress("ArrowUp");
|
|
167
|
+
assertNthFlatItemFocused(1);
|
|
168
|
+
cy.realPress("ArrowUp");
|
|
169
|
+
assertNthFlatItemFocused(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("Escape from a focused dropdown item returns focus to the input", () => {
|
|
173
|
+
focusInputThroughShadow();
|
|
174
|
+
cy.realPress("ArrowDown");
|
|
175
|
+
assertNthFlatItemFocused(0);
|
|
176
|
+
|
|
177
|
+
cy.realPress("Escape");
|
|
178
|
+
// With openSuggestionPopup=true the popover re-opens immediately after close,
|
|
179
|
+
// so we only verify that focus returned to the input.
|
|
89
180
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
90
|
-
|
|
181
|
+
});
|
|
91
182
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
cy.
|
|
95
|
-
|
|
183
|
+
it("aria-selected is removed from items after the dropdown closes", () => {
|
|
184
|
+
focusInputThroughShadow();
|
|
185
|
+
cy.realPress("ArrowDown");
|
|
186
|
+
assertNthFlatItemAriaSelected(0);
|
|
187
|
+
|
|
188
|
+
cy.realPress("Escape");
|
|
189
|
+
|
|
190
|
+
// After close, no item should retain aria-selected.
|
|
191
|
+
cy.get(SEARCH_SHADOW)
|
|
192
|
+
.shadow()
|
|
193
|
+
.find(`${SUGGESTIONS_LIST} ${ITEM_BUTTON}[aria-selected="true"]`)
|
|
194
|
+
.should("have.length", 0);
|
|
96
195
|
});
|
|
196
|
+
});
|
|
97
197
|
|
|
98
|
-
|
|
99
|
-
|
|
198
|
+
describe("Tab / Shift+Tab no-trap behaviour from the input", () => {
|
|
199
|
+
it("Tab from the input while dropdown is open closes the dropdown and proceeds to the next focusable", () => {
|
|
200
|
+
// Use a search WITHOUT openSuggestionPopup so handleTabKey actually closes.
|
|
100
201
|
cy.mount(
|
|
101
202
|
<div>
|
|
102
|
-
<a id="prev-focus" href="#">Previous focus</a>
|
|
103
203
|
<UDExSearch
|
|
104
204
|
id="search"
|
|
105
205
|
show-suggested-popover
|
|
106
|
-
|
|
206
|
+
value="SAP hana"
|
|
107
207
|
/>
|
|
208
|
+
<a id="next-focus" href="#">Next focus</a>
|
|
108
209
|
</div>,
|
|
109
210
|
);
|
|
110
211
|
setSuggestionList(SUGGESTION_LIST_BOTH);
|
|
212
|
+
// Open the popover by focusing the input and triggering ArrowDown,
|
|
213
|
+
// which calls handleDownKey -> onOpenPopover.
|
|
214
|
+
focusInputThroughShadow();
|
|
215
|
+
cy.realPress("ArrowDown");
|
|
111
216
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
112
|
-
cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 2);
|
|
113
217
|
|
|
218
|
+
// Move focus back to the input (the popover stays open).
|
|
114
219
|
focusInputThroughShadow();
|
|
115
220
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
116
221
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
cy.realPress(["Shift", "Tab"]);
|
|
123
|
-
assertListFocused(0);
|
|
124
|
-
|
|
125
|
-
// Shift+Tab 3: lists[0] -> input (escape flag is set; popover stays open)
|
|
126
|
-
cy.realPress(["Shift", "Tab"]);
|
|
127
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
128
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
129
|
-
|
|
130
|
-
// Shift+Tab 4: input + flag -> focus leaves the search component backward
|
|
131
|
-
cy.realPress(["Shift", "Tab"]);
|
|
132
|
-
cy.get("#prev-focus").should("be.focused");
|
|
222
|
+
// Tab from input: handleTabKey calls onClosePopover (no preventDefault),
|
|
223
|
+
// so default Tab moves focus to the next focusable in the document.
|
|
224
|
+
cy.realPress("Tab");
|
|
225
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "false");
|
|
226
|
+
// Default Tab proceeded — the input is no longer the focused element.
|
|
133
227
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("not.be.focused");
|
|
134
228
|
});
|
|
135
229
|
|
|
136
|
-
it("Shift+Tab from
|
|
230
|
+
it("Shift+Tab from the input while dropdown is open closes the dropdown and proceeds backward", () => {
|
|
137
231
|
cy.mount(
|
|
138
232
|
<div>
|
|
233
|
+
<a id="prev-focus" href="#">Previous focus</a>
|
|
139
234
|
<UDExSearch
|
|
140
235
|
id="search"
|
|
141
236
|
show-suggested-popover
|
|
142
|
-
|
|
143
|
-
value="SAP"
|
|
237
|
+
value="SAP hana"
|
|
144
238
|
/>
|
|
145
|
-
<a id="next-focus" href="#">Next focus</a>
|
|
146
239
|
</div>,
|
|
147
240
|
);
|
|
148
241
|
setSuggestionList(SUGGESTION_LIST_BOTH);
|
|
242
|
+
focusInputThroughShadow();
|
|
243
|
+
cy.realPress("ArrowDown");
|
|
149
244
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
150
|
-
cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 2);
|
|
151
245
|
|
|
246
|
+
// Return focus to the input.
|
|
152
247
|
focusInputThroughShadow();
|
|
153
248
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
154
249
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
cy.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
250
|
+
// Shift+Tab from input: handleTabKey closes (no preventDefault), default
|
|
251
|
+
// Shift+Tab proceeds backward to the previous focusable.
|
|
252
|
+
cy.realPress(["Shift", "Tab"]);
|
|
253
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "false");
|
|
254
|
+
cy.get("#prev-focus").should("be.focused");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("with openSuggestionPopup=true, Tab from the input does not close the popover (no preventDefault)", () => {
|
|
258
|
+
cy.mount(renderAlwaysOpen());
|
|
259
|
+
setSuggestionList(SUGGESTION_LIST_BOTH);
|
|
260
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
162
261
|
|
|
163
|
-
|
|
262
|
+
focusInputThroughShadow();
|
|
164
263
|
cy.realPress("Tab");
|
|
165
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("not.be.focused");
|
|
166
264
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("
|
|
265
|
+
// handleTabKey returned early because openSuggestionPopup is true.
|
|
266
|
+
// The popover remains open (or is re-opened by handleOpenSuggestionPopover on next render).
|
|
267
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
170
268
|
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("Tab / Shift+Tab from a focused dropdown item", () => {
|
|
272
|
+
it("Tab from a focused item closes the dropdown and moves focus to #submit-button when present", () => {
|
|
273
|
+
cy.mount(renderAlwaysOpenWithSubmit());
|
|
274
|
+
setSuggestionList(SUGGESTION_LIST_BOTH);
|
|
275
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
276
|
+
cy.get(SEARCH_SHADOW).shadow().find(`${SUGGESTIONS_LIST} ${ITEM_BUTTON}`).should("have.length", 4);
|
|
277
|
+
cy.get(SEARCH_SHADOW).shadow().find(SUBMIT_BUTTON).should("exist");
|
|
171
278
|
|
|
172
|
-
it("Down arrow from a focused list moves focus into individual item buttons; Up arrow reverses it", () => {
|
|
173
279
|
focusInputThroughShadow();
|
|
174
|
-
cy.
|
|
280
|
+
cy.realPress("ArrowDown");
|
|
281
|
+
assertNthFlatItemFocused(0);
|
|
175
282
|
|
|
176
|
-
// Tab into lists[0].
|
|
177
283
|
cy.realPress("Tab");
|
|
178
|
-
assertListFocused(0);
|
|
179
284
|
|
|
180
|
-
//
|
|
181
|
-
cy.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.eq(0)
|
|
186
|
-
.find("[ui5-button]")
|
|
187
|
-
.eq(0)
|
|
188
|
-
.should("have.attr", "aria-selected", "true");
|
|
285
|
+
// Focus moved to submit button (programmatic focus by onKeyTabList).
|
|
286
|
+
cy.get(SEARCH_SHADOW).shadow().find(SUBMIT_BUTTON).should($el => {
|
|
287
|
+
expect($el[0].matches(":focus")).to.be.true;
|
|
288
|
+
});
|
|
289
|
+
});
|
|
189
290
|
|
|
190
|
-
|
|
191
|
-
cy.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.find("[ui5-button]")
|
|
197
|
-
.eq(1)
|
|
198
|
-
.should("have.attr", "aria-selected", "true");
|
|
291
|
+
it("Tab from a focused item closes the dropdown and falls back to the input when there is no submit button", () => {
|
|
292
|
+
cy.mount(renderAlwaysOpen());
|
|
293
|
+
setSuggestionList(SUGGESTION_LIST_BOTH);
|
|
294
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
295
|
+
cy.get(SEARCH_SHADOW).shadow().find(`${SUGGESTIONS_LIST} ${ITEM_BUTTON}`).should("have.length", 4);
|
|
296
|
+
cy.get(SEARCH_SHADOW).shadow().find(SUBMIT_BUTTON).should("not.exist");
|
|
199
297
|
|
|
200
|
-
|
|
298
|
+
focusInputThroughShadow();
|
|
201
299
|
cy.realPress("ArrowDown");
|
|
202
|
-
|
|
300
|
+
assertNthFlatItemFocused(0);
|
|
301
|
+
|
|
302
|
+
cy.realPress("Tab");
|
|
303
|
+
|
|
304
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("Shift+Tab from a focused item closes the dropdown and moves focus to the input", () => {
|
|
308
|
+
cy.mount(renderAlwaysOpenWithSubmit());
|
|
309
|
+
setSuggestionList(SUGGESTION_LIST_BOTH);
|
|
310
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
311
|
+
cy.get(SEARCH_SHADOW).shadow().find(`${SUGGESTIONS_LIST} ${ITEM_BUTTON}`).should("have.length", 4);
|
|
203
312
|
|
|
204
|
-
|
|
313
|
+
focusInputThroughShadow();
|
|
205
314
|
cy.realPress("ArrowDown");
|
|
206
|
-
|
|
207
|
-
.shadow()
|
|
208
|
-
.find(SUGGESTIONS_LIST)
|
|
209
|
-
.eq(1)
|
|
210
|
-
.find("[ui5-button]")
|
|
211
|
-
.eq(0)
|
|
212
|
-
.should("have.attr", "aria-selected", "true");
|
|
315
|
+
assertNthFlatItemFocused(0);
|
|
213
316
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
317
|
+
cy.realPress(["Shift", "Tab"]);
|
|
318
|
+
|
|
319
|
+
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
217
320
|
});
|
|
218
321
|
});
|
|
219
322
|
|
|
220
323
|
describe("Always-open popover with a single list (recommended only)", () => {
|
|
221
324
|
beforeEach(() => {
|
|
222
|
-
cy.mount(
|
|
325
|
+
cy.mount(renderAlwaysOpen());
|
|
223
326
|
setSuggestionList(SUGGESTION_LIST_RECOMMENDED_ONLY);
|
|
224
327
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
225
328
|
cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 1);
|
|
329
|
+
cy.get(SEARCH_SHADOW).shadow().find(`${SUGGESTIONS_LIST} ${ITEM_BUTTON}`).should("have.length", 2);
|
|
226
330
|
});
|
|
227
331
|
|
|
228
|
-
it("
|
|
332
|
+
it("ArrowDown moves through the single list, ArrowDown on last item wraps to first", () => {
|
|
229
333
|
focusInputThroughShadow();
|
|
230
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
231
334
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
assertListFocused(0);
|
|
335
|
+
cy.realPress("ArrowDown");
|
|
336
|
+
assertNthFlatItemFocused(0);
|
|
235
337
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
239
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
338
|
+
cy.realPress("ArrowDown");
|
|
339
|
+
assertNthFlatItemFocused(1);
|
|
240
340
|
|
|
241
|
-
//
|
|
242
|
-
cy.realPress("
|
|
243
|
-
|
|
244
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("not.be.focused");
|
|
341
|
+
// Last item -> wrap to first.
|
|
342
|
+
cy.realPress("ArrowDown");
|
|
343
|
+
assertNthFlatItemFocused(0);
|
|
245
344
|
});
|
|
246
345
|
|
|
247
|
-
it("
|
|
248
|
-
cy.mount(
|
|
249
|
-
<div>
|
|
250
|
-
<a id="prev-focus" href="#">Previous focus</a>
|
|
251
|
-
<UDExSearch
|
|
252
|
-
id="search"
|
|
253
|
-
show-suggested-popover
|
|
254
|
-
open-suggestion-popup
|
|
255
|
-
/>
|
|
256
|
-
</div>,
|
|
257
|
-
);
|
|
258
|
-
setSuggestionList(SUGGESTION_LIST_RECOMMENDED_ONLY);
|
|
259
|
-
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
260
|
-
cy.get(SEARCH_SHADOW).shadow().find(SUGGESTIONS_LIST).should("have.length", 1);
|
|
261
|
-
|
|
346
|
+
it("ArrowUp from the first item wraps to the last item", () => {
|
|
262
347
|
focusInputThroughShadow();
|
|
263
|
-
cy.
|
|
348
|
+
cy.realPress("ArrowDown");
|
|
349
|
+
assertNthFlatItemFocused(0);
|
|
264
350
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
351
|
+
cy.realPress("ArrowUp");
|
|
352
|
+
assertNthFlatItemFocused(1);
|
|
353
|
+
});
|
|
268
354
|
|
|
269
|
-
|
|
270
|
-
|
|
355
|
+
it("Tab from a focused item closes the dropdown and returns focus to the input (no submit button)", () => {
|
|
356
|
+
focusInputThroughShadow();
|
|
357
|
+
cy.realPress("ArrowDown");
|
|
358
|
+
assertNthFlatItemFocused(0);
|
|
359
|
+
|
|
360
|
+
cy.realPress("Tab");
|
|
361
|
+
// With openSuggestionPopup=true the popover re-opens immediately after close,
|
|
362
|
+
// so we only verify that focus returned to the input.
|
|
271
363
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("be.focused");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("Suggestion list divs are no longer Tab-reachable", () => {
|
|
368
|
+
it("the list divs do not carry tabindex=0 and are skipped in the Tab order", () => {
|
|
369
|
+
cy.mount(renderAlwaysOpenWithPrev());
|
|
370
|
+
setSuggestionList(SUGGESTION_LIST_BOTH);
|
|
272
371
|
cy.get(SEARCH_SHADOW).shadow().find(SEARCH_INPUT).should("have.attr", "aria-expanded", "true");
|
|
273
372
|
|
|
274
|
-
//
|
|
275
|
-
cy.
|
|
276
|
-
|
|
277
|
-
|
|
373
|
+
// Confirm the lists themselves are not exposed in the Tab order.
|
|
374
|
+
cy.get(SEARCH_SHADOW)
|
|
375
|
+
.shadow()
|
|
376
|
+
.find(SUGGESTIONS_LIST)
|
|
377
|
+
.each($el => {
|
|
378
|
+
// Either no tabindex attribute at all, or not a positive/zero value.
|
|
379
|
+
const tabindex = $el.attr("tabindex");
|
|
380
|
+
if (tabindex !== undefined) {
|
|
381
|
+
expect(Number(tabindex)).to.be.lessThan(0);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
278
384
|
});
|
|
279
385
|
});
|
|
280
386
|
});
|