From 276c9c7101f5d3549581818e73a7e4f6a71976b4 Mon Sep 17 00:00:00 2001 From: Stefan Dimitrov Date: Mon, 27 Jan 2025 15:35:51 +0200 Subject: [PATCH 1/4] feat(ui5-input): visual selection on type-ahead --- packages/main/cypress/specs/Input.cy.ts | 355 ++++++++++++++++++ packages/main/src/Input.ts | 34 +- .../main/src/features/InputSuggestions.ts | 18 +- 3 files changed, 396 insertions(+), 11 deletions(-) diff --git a/packages/main/cypress/specs/Input.cy.ts b/packages/main/cypress/specs/Input.cy.ts index 1c3d4a8cd51d..6e6f53256a26 100644 --- a/packages/main/cypress/specs/Input.cy.ts +++ b/packages/main/cypress/specs/Input.cy.ts @@ -128,3 +128,358 @@ describe("Input Tests", () => { .should("not.have.attr", "tabindex", "0"); }); }); + +describe("Input general interaction", () => { + it("handles suggestions selection cancel with ESC", () => { + cy.mount(` + + + + + + + + `); + + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .realClick(); + cy.get("@input") + .realType("C"); + cy.get("@input") + .realPress("ArrowDown"); + + cy.get("@input") + .should("have.attr", "value", "Titanium"); + + cy.get("@input") + .realPress("Escape"); + + cy.get("@input") + .should("have.value", "C"); + }); + + it("tests selection-change with custom items", () => { + cy.mount(` + + + + + + `); + + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .realClick(); + cy.get("@input") + .realPress("c"); + cy.get("@input") + .realPress("ArrowDown"); + + cy.get("@input") + .should("have.value", "Compact") + .should("not.have.attr", "focused"); + + cy.get("ui5-suggestion-item") + .eq(1) + .should("have.attr", "focused"); + + cy.get("@input") + .realPress("ArrowDown"); + + cy.get("ui5-suggestion-item") + .eq(2) + .should("have.attr", "focused"); + + cy.get("ui5-suggestion-item") + .eq(1) + .should("not.have.attr", "focused"); + }); +}); + +describe("Input arrow navigation", () => { + it("Value state header and group headers should be included in the arrow navigation", () => { + cy.mount(` +
Custom error value state message with a Link.
+ + + + + +
`); + + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .realClick() + .realType("a") + .realPress("ArrowDown"); + + cy.get("@input") + .should("not.have.attr", "focused"); + + cy.get("@input") + .shadow() + .find("ui5-responsive-popover") + .as("ui5-responsive-popover"); + + cy.get("@ui5-responsive-popover") + .find("div") + .as("valueMessage") + .should("have.class", "ui5-responsive-popover-header--focused"); + + cy.get("@input") + .realPress("ArrowDown"); + + cy.get("@valueMessage") + .should("not.have.class", "ui5-responsive-popover-header--focused"); + + cy.get("ui5-suggestion-item") + .eq(0) + .should("have.attr", "focused"); + }); + + it("Should navigate up and down through the suggestions popover with arrow keys", () => { + cy.mount(` + + + + + + `); + + cy.get("#myInput2").click(); + cy.get("#myInput2").realType("c"); + cy.get("#myInput2").realPress("ArrowDown"); + + cy.get("ui5-suggestion-item").eq(1).should("have.attr", "text", "Compact"); + cy.get("#myInput2").should("not.have.attr", "focused"); + cy.get("ui5-suggestion-item").eq(1).should("have.attr", "focused"); + + cy.get("#myInput2") + .realPress("ArrowDown"); + + cy.get("ui5-suggestion-item").eq(2).should("have.attr", "focused"); + cy.get("ui5-suggestion-item").eq(1).should("not.have.attr", "focused"); + + cy.get("#myInput2") + .realPress("ArrowUp"); + + cy.get("ui5-suggestion-item").eq(1).should("have.attr", "focused"); + cy.get("ui5-suggestion-item").eq(2).should("not.have.attr", "focused"); + + cy.get("#myInput2").realPress("ArrowUp"); + cy.get("#myInput2").realPress("ArrowUp"); + + cy.get("#myInput2").should("have.attr", "focused"); + cy.get("ui5-suggestion-item").first().should("not.have.attr", "focused"); + }); +}); + +describe("Input PAGEUP/PAGEDOWN navigation", () => { + beforeEach(() => { + cy.mount(` + + + + + + + + + + + + + + + `); + }); + it("Should focus the tenth item from the suggestions popover with PAGEDOWN", () => { + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .realClick(); + cy.get("@input") + .realType("a"); + cy.get("@input") + .realPress("ArrowDown"); + cy.get("@input") + .realPress("PageDown"); + + cy.get("ui5-suggestion-item") + .eq(11) + .should("have.attr", "text", "Antigua and Barbuda"); + + cy.get("ui5-suggestion-item") + .eq(11) + .should("have.attr", "focused"); + }); + + it("Should focus the -10 item/group header from the suggestions popover with PAGEUP", () => { + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .realClick(); + cy.get("@input") + .realType("a"); + cy.get("@input") + .realPress("ArrowUp"); + + cy.get("ui5-suggestion-item-group") + .eq(0) + .should("have.attr", "focused"); + + cy.get("@input") + .realPress("PageDown"); + cy.get("@input") + .realPress("PageUp"); + + cy.get("ui5-suggestion-item-group") + .eq(0) + .should("have.attr", "focused"); + }); +}); + +describe("Selection-change event", () => { + it("Selection-change event fires with null arguments when suggestion was selected but user alters input value to something else", () => { + cy.mount(` + + + + `); + + cy.get("ui5-input") + .as("input"); + + cy.get("ui5-input") + .shadow() + .find("input") + .as("innerInput"); + + let eventCount = 0; + + cy.get("@input").then($input => { + $input[0].addEventListener("ui5-selection-change", () => { + eventCount++; + }); + }); + + cy.get("@innerInput") + .click(); + cy.get("@innerInput") + .type("C"); + cy.get("@innerInput") + .realPress("ArrowDown"); + cy.get("@innerInput") + .realPress("Enter"); + + cy.get("@innerInput") + .should("have.value", "Compact"); + + cy.get("@innerInput") + .click(); + cy.get("@innerInput") + .clear(); + cy.get("@innerInput") + .type("N"); + cy.get("@innerInput") + .realPress("Enter"); + + cy.get("@innerInput") + .should("have.value", "N"); + + cy.then(() => { + expect(eventCount).to.equal(2); + }); + }); +}); + +describe("Change event behavior when selecting the same suggestion item", () => { + let changeCount = 0; + + beforeEach(() => { + cy.mount(` + + + + + + + + `); + + cy.get("#myInput").then($el => { + $el[0].addEventListener("change", () => { + changeCount++; + }); + }); + }); + + it("Change event is not fired when the same suggestion item is selected (with typeahead)", () => { + cy.get("#myInput") + .click(); + + cy.get("#myInput") + .realType("a"); + + cy.get("#myInput").realPress("Enter"); + cy.get("#myInput").should("have.value", "Afghanistan"); + + cy.get("#myInput").realPress("Backspace"); + cy.get("#myInput").realPress("ArrowDown"); + cy.get("#myInput").realPress("ArrowDown"); + cy.get("#myInput").realPress("Enter"); + + cy.get("#myInput").should("have.value", "Afghanistan"); + cy.then(() => { + expect(changeCount).to.equal(1); + }); + }); + + it("Change event is not fired when the same suggestion item is selected (no-typeahead)", () => { + cy.get("#myInput").invoke("attr", "value", "Afghanistan"); + cy.get("#myInput").invoke("attr", "no-typeahead", true); + + cy.get("#myInput").realPress("Backspace"); + + cy.get("#myInput").realPress("ArrowDown"); + cy.get("#myInput").realPress("ArrowDown"); + cy.get("#myInput").realPress("Enter"); + + cy.get("#myInput").should("have.value", "Afghanistan"); + cy.then(() => { + expect(changeCount).to.equal(1); + }); + }); + + it("Change event is not fired when the same suggestion item is selected after focus out and selecting suggestion again", () => { + cy.get("#myInput") + .invoke("attr", "value", "Afghanistan"); + + cy.get("#myInput") + .realPress("Tab"); + + cy.get("#myInput") + .realClick(); + cy.get("#myInput") + .realPress("ArrowDown"); + cy.get("#myInput") + .realPress("ArrowDown"); + cy.get("#myInput") + .realPress("Enter"); + + cy.get("#myInput").should("have.value", "Afghanistan"); + cy.then(() => { + expect(changeCount).to.equal(1); + }); + }); +}); diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index bdc3d63e79cb..2d818602ba5f 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -573,6 +573,8 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _shouldAutocomplete?: boolean; _keyDown?: boolean; _isKeyNavigation?: boolean; + _indexOfSelectedItem: number; + // Suggestions?: InputSuggestions; _selectedText?: string; _clearIconClicked?: boolean; _focusedAfterClear: boolean; @@ -639,6 +641,8 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this._isChangeTriggeredBySuggestion = false; + this._indexOfSelectedItem = -1; + this._handleResizeBound = this._handleResize.bind(this); this._keepInnerValue = false; @@ -717,6 +721,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement const item = this._getFirstMatchingItem(value); if (item) { this._handleTypeAhead(item); + this._selectMatchingItem(item); } } } @@ -810,15 +815,23 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this._keyDown = false; } + get currentItemIndex() { + const allItems = this.Suggestions?._getItems() as IInputSuggestionItemSelectable[]; + const currentItem = allItems.find(item => { return item.selected || item.focused; }); + const indexOfCurrentItem = currentItem ? allItems.indexOf(currentItem) : -1; + return indexOfCurrentItem; + } + _handleUp(e: KeyboardEvent) { if (this.Suggestions?.isOpened()) { - this.Suggestions.onUp(e); + // const itemIndex = this.currentItemIndex; + this.Suggestions.onUp(e, this.currentItemIndex); } } _handleDown(e: KeyboardEvent) { if (this.Suggestions?.isOpened()) { - this.Suggestions.onDown(e); + this.Suggestions.onDown(e, this.currentItemIndex); } } @@ -1160,6 +1173,16 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this.Suggestions?.onItemPress(e); } + _selectMatchingItem(item: IInputSuggestionItemSelectable) { + const items = this.Suggestions?._getItems(); + const index = items?.findIndex(suggItem => suggItem.id === item.id); + if (index) { + this._indexOfSelectedItem = index; + } + item.selected = true; + this.fireDecoratorEvent("select"); + } + _handleTypeAhead(item: IInputSuggestionItemSelectable) { const value = item.text ? item.text : ""; @@ -1201,10 +1224,12 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this.focused = false; } - if (this._changeToBeFired) { + if (this._changeToBeFired && this._isChangeTriggeredBySuggestion === false) { this.fireDecoratorEvent(INPUT_EVENTS.CHANGE); - this._changeToBeFired = false; + } else { + this._isChangeTriggeredBySuggestion = false; } + this._changeToBeFired = false; this.open = false; this.isTyping = false; @@ -1402,7 +1427,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement if (shouldFireSelectionChange) { this.fireSelectionChange(suggestionItem, true); } - this.acceptSuggestion(suggestionItem, keyboardUsed); } diff --git a/packages/main/src/features/InputSuggestions.ts b/packages/main/src/features/InputSuggestions.ts index 5705e954bffe..efc655a9136b 100644 --- a/packages/main/src/features/InputSuggestions.ts +++ b/packages/main/src/features/InputSuggestions.ts @@ -81,15 +81,19 @@ class Suggestions { this.selectedItemIndex = -1; } - onUp(e: KeyboardEvent) { + onUp(e: KeyboardEvent, indexOfItem: number) { + // this.selectedItemIndex = indexOfSelectedItem; e.preventDefault(); - this._handleItemNavigation(false /* forward */); + indexOfItem = !this.isOpened && this._hasValueState && indexOfItem === -1 ? 0 : indexOfItem; + this._handleItemNavigation(false /* forward */, indexOfItem); return true; } - onDown(e: KeyboardEvent) { + onDown(e: KeyboardEvent, indexOfItem: number) { + // this.selectedItemIndex = indexOfSelectedItem; e.preventDefault(); - this._handleItemNavigation(true /* forward */); + indexOfItem = !this.isOpened && this._hasValueState && indexOfItem === -1 ? 0 : indexOfItem; + this._handleItemNavigation(true /* forward */, indexOfItem); return true; } @@ -298,11 +302,12 @@ class Suggestions { return !!(this._getPicker()?.open); } - _handleItemNavigation(forward: boolean) { + _handleItemNavigation(forward: boolean, indexOfItem: number) { + this.selectedItemIndex = indexOfItem; + if (!this._getItems().length) { return; } - if (forward) { this._selectNextItem(); } else { @@ -312,6 +317,7 @@ class Suggestions { _selectNextItem() { const itemsCount = this._getItems().length; + const previousSelectedIdx = this.selectedItemIndex; if (this._hasValueState && previousSelectedIdx === -1 && !this.component._isValueStateFocused) { From c9823a4eb6c0d1e3deb30ea30125a31a3761f620 Mon Sep 17 00:00:00 2001 From: Stefan Dimitrov Date: Tue, 28 Jan 2025 09:27:19 +0200 Subject: [PATCH 2/4] feat(ui5-input): fixed tests --- packages/main/cypress/specs/Input.cy.ts | 59 +++ packages/main/src/Input.ts | 6 - .../main/src/features/InputSuggestions.ts | 2 - packages/main/test/specs/Input.spec.js | 414 +----------------- 4 files changed, 60 insertions(+), 421 deletions(-) diff --git a/packages/main/cypress/specs/Input.cy.ts b/packages/main/cypress/specs/Input.cy.ts index 6e6f53256a26..9a727e8563e1 100644 --- a/packages/main/cypress/specs/Input.cy.ts +++ b/packages/main/cypress/specs/Input.cy.ts @@ -4,6 +4,7 @@ import type Input from "../../src/Input.js"; import "../../src/SuggestionItem.js"; import "../../src/SuggestionItemCustom.js"; import "../../src/SuggestionItemGroup.js"; +import "../../src/features/InputSuggestions.js"; describe("Input Tests", () => { it("tets input event prevention", () => { @@ -146,16 +147,26 @@ describe("Input general interaction", () => { cy.get("@input") .realClick(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realType("C"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("@input") .should("have.attr", "value", "Titanium"); cy.get("@input") .realPress("Escape"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("@input") .should("have.value", "C"); @@ -175,10 +186,18 @@ describe("Input general interaction", () => { cy.get("@input") .realClick(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realPress("c"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("@input") .should("have.value", "Compact") @@ -190,6 +209,8 @@ describe("Input general interaction", () => { cy.get("@input") .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("ui5-suggestion-item") .eq(2) @@ -255,8 +276,14 @@ describe("Input arrow navigation", () => { `); cy.get("#myInput2").click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("#myInput2").realType("c"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("#myInput2").realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("ui5-suggestion-item").eq(1).should("have.attr", "text", "Compact"); cy.get("#myInput2").should("not.have.attr", "focused"); @@ -264,18 +291,26 @@ describe("Input arrow navigation", () => { cy.get("#myInput2") .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("ui5-suggestion-item").eq(2).should("have.attr", "focused"); cy.get("ui5-suggestion-item").eq(1).should("not.have.attr", "focused"); cy.get("#myInput2") .realPress("ArrowUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("ui5-suggestion-item").eq(1).should("have.attr", "focused"); cy.get("ui5-suggestion-item").eq(2).should("not.have.attr", "focused"); cy.get("#myInput2").realPress("ArrowUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("#myInput2").realPress("ArrowUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("#myInput2").should("have.attr", "focused"); cy.get("ui5-suggestion-item").first().should("not.have.attr", "focused"); @@ -307,12 +342,23 @@ describe("Input PAGEUP/PAGEDOWN navigation", () => { cy.get("@input") .realClick(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realType("a"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realPress("PageDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("ui5-suggestion-item") .eq(11) @@ -329,10 +375,18 @@ describe("Input PAGEUP/PAGEDOWN navigation", () => { cy.get("@input") .realClick(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realType("a"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realPress("ArrowUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("ui5-suggestion-item-group") .eq(0) @@ -340,8 +394,13 @@ describe("Input PAGEUP/PAGEDOWN navigation", () => { cy.get("@input") .realPress("PageDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input") .realPress("PageUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); cy.get("ui5-suggestion-item-group") .eq(0) diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index 2d818602ba5f..b5fdcdfc389b 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -1174,13 +1174,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } _selectMatchingItem(item: IInputSuggestionItemSelectable) { - const items = this.Suggestions?._getItems(); - const index = items?.findIndex(suggItem => suggItem.id === item.id); - if (index) { - this._indexOfSelectedItem = index; - } item.selected = true; - this.fireDecoratorEvent("select"); } _handleTypeAhead(item: IInputSuggestionItemSelectable) { diff --git a/packages/main/src/features/InputSuggestions.ts b/packages/main/src/features/InputSuggestions.ts index efc655a9136b..b6cbaa1e7ef5 100644 --- a/packages/main/src/features/InputSuggestions.ts +++ b/packages/main/src/features/InputSuggestions.ts @@ -82,7 +82,6 @@ class Suggestions { } onUp(e: KeyboardEvent, indexOfItem: number) { - // this.selectedItemIndex = indexOfSelectedItem; e.preventDefault(); indexOfItem = !this.isOpened && this._hasValueState && indexOfItem === -1 ? 0 : indexOfItem; this._handleItemNavigation(false /* forward */, indexOfItem); @@ -90,7 +89,6 @@ class Suggestions { } onDown(e: KeyboardEvent, indexOfItem: number) { - // this.selectedItemIndex = indexOfSelectedItem; e.preventDefault(); indexOfItem = !this.isOpened && this._hasValueState && indexOfItem === -1 ? 0 : indexOfItem; this._handleItemNavigation(true /* forward */, indexOfItem); diff --git a/packages/main/test/specs/Input.spec.js b/packages/main/test/specs/Input.spec.js index 486cc2327ed3..8425aa111d89 100644 --- a/packages/main/test/specs/Input.spec.js +++ b/packages/main/test/specs/Input.spec.js @@ -390,28 +390,6 @@ describe("Input general interaction", () => { // assert.strictEqual(await suggestionsInput.getValue(), "Portugal", "First item has been selected again"); }); - it("handles suggestions selection cancel with ESC", async () => { - await browser.url(`test/pages/Input.html`); - - const suggestionsInput = await browser.$("#myInputEsc"); - - // act - await suggestionsInput.click(); - await suggestionsInput.keys("c"); - await suggestionsInput.keys("ArrowDown"); - - // assert - assert.strictEqual(await suggestionsInput.getValue(), "Chromium", - "The value is updated as the item has been previewed."); - - // act - await suggestionsInput.keys("Escape"); - - // assert - assert.strictEqual(await suggestionsInput.getProperty("value"), "c", - "The value is restored as ESC has been pressed."); - }); - it("Input value should correspond to suggestion item when it is clicked", async () => { await browser.url(`test/pages/Input.html`); @@ -759,27 +737,6 @@ describe("Input general interaction", () => { assert.ok(await helpPopover.isDisplayedInViewport(), "The help popover remains open as the focus is within."); }); - it("tests selection-change with custom items", async () => { - await browser.url(`test/pages/Input.html`); - - const selChangeFireCount = $("#custom-items-selection-change-count"); - const selChangeItemIndex = $("#custom-items-selection-item-index"); - const input = await $("#input-custom-flat").shadow$("input"); - - await input.click(); - await input.keys("a"); - - await input.keys("ArrowDown"); - - assert.strictEqual(await selChangeFireCount.getHTML(false), "1", "The selection-change event is fired once"); - assert.strictEqual(await selChangeItemIndex.getHTML(false), "0", "The selected item index is correct"); - - await input.keys("ArrowDown"); - - assert.strictEqual(await selChangeFireCount.getHTML(false), "2", "The selection-change event is fired twice"); - assert.strictEqual(await selChangeItemIndex.getHTML(false), "1", "The selected item index is correct"); - }); - it("fires open event when suggestions picker is opened on typing", async () => { await browser.url(`test/pages/Input.html`); const input = await browser.$("#myInput"); @@ -864,50 +821,6 @@ describe("Input general interaction", () => { assert.strictEqual(await suggestionsCount.getText(), "5 results are available", "Suggestions count is available since the suggestions popover is opened"); }); - it("Suggestions announcement", async () => { - await browser.url(`test/pages/Input.html`); - - const inputWithGroups = await $("#inputCompact"); - const inputSuggestions = await $("#myInput2"); - const inputWithGroupsInnerInput = await inputWithGroups.shadow$("input"); - const inputWithGroupsAnnouncement = await inputWithGroups.shadow$(`#selectionText`); - const suggestionsAnnouncement = await inputSuggestions.shadow$(`#selectionText`); - - //act - await inputWithGroups.click(); - - //assert - assert.strictEqual(await inputWithGroupsAnnouncement.getText(), "", "Suggestions announcement is not available on initially"); - - //act - await inputWithGroupsInnerInput.keys("a"); - await inputWithGroupsInnerInput.keys("ArrowDown"); - - //assert - assert.strictEqual(await inputWithGroupsAnnouncement.getText(), "Group Header A", "Group item announcement should not contain the position and total count."); - - //act - await inputWithGroupsInnerInput.keys("ArrowDown"); - - const announcementText = await inputWithGroupsAnnouncement.getText(); - - //assert - assert.ok(announcementText.includes("List item 1 of 12"), "The total count announcement and position of items should exclude group items."); - assert.strictEqual(await inputWithGroupsAnnouncement.getText(), "explore List item 1 of 12", "The additional text is announced"); - await inputWithGroupsInnerInput.keys("Backspace"); - - // Close suggestions to not intercept the click in the input below for the last test - await inputWithGroupsInnerInput.keys("Escape"); - - //act - await inputSuggestions.click(); - await (await inputSuggestions.shadow$("input")).keys("c"); - await inputWithGroupsInnerInput.keys("ArrowDown"); - - //assert - assert.strictEqual(await suggestionsAnnouncement.getText(), "List item 1 of 5", "Item position and count is announced correctly"); - }); - it("Should close the Popover when no suggestions are available", async () => { await browser.url(`test/pages/Input.html`); @@ -1128,81 +1041,12 @@ describe("Input general interaction", () => { assert.strictEqual(await inner.getValue(), "", "Inner input's value should be empty"); }); - it("Change event is not fired when the same suggestion item is selected (with typeahead) - #8912", async () => { - const suggestionsInput = await browser.$("#myInput"); - - await suggestionsInput.click(); - await suggestionsInput.keys("a"); - - await suggestionsInput.keys("ArrowDown"); - await suggestionsInput.keys("ArrowDown"); - await suggestionsInput.keys("Enter"); - - const changeCount = await browser.$("#myInput-change-count"); - - // Assert - assert.strictEqual(await changeCount.getHTML(false), "1", "The change event is called once"); - assert.strictEqual(await suggestionsInput.getValue(), "Afghanistan", "Input's value should be the text of the selected item"); - - await suggestionsInput.keys("Backspace"); - - await suggestionsInput.keys("ArrowDown"); - await suggestionsInput.keys("ArrowDown"); - await suggestionsInput.keys("Enter"); - - assert.strictEqual(await changeCount.getHTML(false), "1", "The change event is still called once"); - assert.strictEqual(await suggestionsInput.getValue(), "Afghanistan", "Input's value should be the text of the selected item"); - }); - - it("Change event is not fired when the same suggestion item is selected (no-typeahead) - #8912", async () => { - const suggestionsInput = await browser.$("#myInput"); - await browser.execute(() => { - document.querySelector("#myInput").noTypeahead = true; - }); - - await suggestionsInput.keys("Backspace"); - - await suggestionsInput.keys("ArrowDown"); - await suggestionsInput.keys("ArrowDown"); - await suggestionsInput.keys("Enter"); - - const changeCount = await browser.$("#myInput-change-count"); - - assert.strictEqual(await changeCount.getHTML(false), "1", "The change event is still called once"); - assert.strictEqual(await suggestionsInput.getValue(), "Afghanistan", "Input's value should be the text of the selected item"); - - // restore the default property value - await browser.execute(() => { - document.querySelector("#myInput").noTypeahead = false; - }); - }); - - it("Change event is not fired when the same suggestion item is selected after focus out and selecting suggestion again - #8912", async () => { - const suggestionsInput = await browser.$("#myInput"); - const changeCount = await browser.$("#myInput-change-count"); - - assert.strictEqual(await changeCount.getHTML(false), "1", "The change event is called once"); - assert.strictEqual(await suggestionsInput.getValue(), "Afghanistan", "Input's value should be the text of the selected item"); - - await suggestionsInput.keys("Tab"); - - await suggestionsInput.click(); - await suggestionsInput.keys("Backspace"); - await suggestionsInput.keys("ArrowDown"); - await suggestionsInput.keys("ArrowDown"); - await suggestionsInput.keys("Enter"); - - // Assert - assert.strictEqual(await changeCount.getHTML(false), "1", "The change event is still called once"); - assert.strictEqual(await suggestionsInput.getValue(), "Afghanistan", "Input's value should be the text of the selected item"); - }); - it("Tests prevented input event", async () => { const input = await $("#prevent-input-event"); const innerInput = await input.shadow$("input"); await input.click(); - + await innerInput.keys("a"); await innerInput.keys("b"); await innerInput.keys("c"); @@ -1222,196 +1066,6 @@ describe("Input general interaction", () => { }); }); -describe("Input arrow navigation", () => { - - it("Should navigate up and down through the suggestions popover with arrow keys", async () => { - await browser.url(`test/pages/Input.html`); - - const suggestionsInput = await browser.$("#myInput2"); - - await suggestionsInput.click(); - await suggestionsInput.keys("c"); - await suggestionsInput.keys("ArrowDown"); - - const firstListItem = await suggestionsInput.$("ui5-suggestion-item"); - - assert.strictEqual(await suggestionsInput.getValue(), "Cozy", "First item has been selected"); - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await firstListItem.getProperty("focused"), true, "First list item is focused"); - - await suggestionsInput.keys("ArrowDown"); - const secondListItem = await suggestionsInput.$$("ui5-suggestion-item")[1]; - - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await secondListItem.getProperty("focused"), true, "Second list item is focused"); - - await suggestionsInput.keys("ArrowUp"); - - assert.strictEqual(await firstListItem.getProperty("focused"), true, "First list item is focused"); - assert.strictEqual(await secondListItem.getProperty("focused"), false, "Second list item is not focused"); - - await suggestionsInput.keys("ArrowUp"); - - assert.strictEqual(await suggestionsInput.getProperty("focused"), true, "Input is focused"); - assert.strictEqual(await firstListItem.getProperty("focused"), false, "First list item is not focused"); - }); - - it("Value state header and group headers should be included in the arrow navigation", async () => { - await browser.url(`test/pages/Input.html`); - - const suggestionsInput = await browser.$("#inputError"); - - await suggestionsInput.click(); - await suggestionsInput.keys("a"); - await suggestionsInput.keys("ArrowDown"); - - const respPopover = await suggestionsInput.shadow$("ui5-responsive-popover"); - const valueStateHeader = await respPopover.$(".ui5-responsive-popover-header.ui5-valuestatemessage-root"); - const firstListItem = suggestionsInput.$("ui5-suggestion-item"); - const groupHeader = suggestionsInput.$("ui5-suggestion-item-group"); - - assert.strictEqual(await suggestionsInput.getValue(), "a", "Input's value should be the typed-in value"); - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await firstListItem.getProperty("focused"), false, "First list item is not focused"); - assert.strictEqual(await groupHeader.getProperty("focused"), false, "Group header is not focused"); - assert.strictEqual(await suggestionsInput.getProperty("_isValueStateFocused"), true, "Value State should not be focused"); - assert.strictEqual(await valueStateHeader.hasClass("ui5-responsive-popover-header--focused"), true, "Value state header is focused"); - - await suggestionsInput.keys("ArrowDown"); - - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await firstListItem.getProperty("focused"), false, "First list item is not focused"); - assert.strictEqual(await groupHeader.getProperty("focused"), true, "Group header is focused"); - assert.strictEqual(await valueStateHeader.hasClass("ui5-responsive-popover-header--focused"), false, "Value state header is not focused"); - - await suggestionsInput.keys("ArrowDown"); - - assert.strictEqual(await suggestionsInput.getValue(), "Afghanistan", "Input's value should be the text of the selected item"); - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await firstListItem.getProperty("focused"), true, "First list item is focused"); - assert.strictEqual(await groupHeader.getProperty("focused"), false, "Group header is no longer focused"); - assert.strictEqual(await valueStateHeader.hasClass("ui5-responsive-popover-header--focused"), false, "Value state header is not focused"); - - await suggestionsInput.keys("ArrowUp"); - - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await firstListItem.getProperty("focused"), false, "First list item is not focused"); - assert.strictEqual(await groupHeader.getProperty("focused"), true, "Group header is focused"); - assert.strictEqual(await valueStateHeader.hasClass("ui5-responsive-popover-header--focused"), false, "Value state header is not focused"); - - - await suggestionsInput.keys("ArrowUp"); - - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await firstListItem.getProperty("focused"), false, "First list item is not focused"); - assert.strictEqual(await groupHeader.getProperty("focused"), false, "Group header is not focused"); - assert.strictEqual(await suggestionsInput.getProperty("_isValueStateFocused"), true, "Value State should not be focused"); - assert.strictEqual(await valueStateHeader.hasClass("ui5-responsive-popover-header--focused"), true, "Value state header is focused"); - - await suggestionsInput.keys("ArrowUp"); - - assert.strictEqual(await suggestionsInput.getProperty("focused"), true, "Input is focused"); - assert.strictEqual(await firstListItem.getProperty("focused"), false, "First list item is not focused"); - assert.strictEqual(await groupHeader.getProperty("focused"), false, "Group header is not focused"); - assert.strictEqual(await valueStateHeader.hasClass("ui5-responsive-popover-header--focused"), false, "Value state header is not focused"); - }); - - it("Items should not hide behind value state header on arrow up navigation", async () => { - await browser.url(`test/pages/Input.html`); - await browser.setWindowSize(1000, 400); - - const input = await browser.$("#inputError"); - await input.scrollIntoView(); - await input.click(); - await input.keys("a"); - - let isInVisibleArea = await browser.executeAsync(async done => { - const input = document.getElementById("inputError"); - const listItems = input.querySelectorAll("ui5-suggestion-item"); - const elementRect = listItems[6].getBoundingClientRect(); //Suggestion item "Angola" - - const popover = input.shadowRoot.querySelector("ui5-responsive-popover"); - const scrollableRect = popover.shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); - - // Check if the element is within the visible area - const isElementAboveViewport = elementRect.bottom < scrollableRect.top; - const isElementBelowViewport = elementRect.top > scrollableRect.bottom; - const isElementLeftOfViewport = elementRect.right < scrollableRect.left; - const isElementRightOfViewport = elementRect.left > scrollableRect.right; - - const isListItemInVisibleArea = ( - !isElementAboveViewport && - !isElementBelowViewport && - !isElementLeftOfViewport && - !isElementRightOfViewport - ); - - done(isListItemInVisibleArea); - }); - - assert.notOk(isInVisibleArea, "Item 'Angola' should not be displayed in the viewport"); - - // click ArrowDown 9 times - 1 time through VSH and 1 through group header and 7 times through the list items - await input.keys(["ArrowDown", "ArrowDown", "ArrowDown", "ArrowDown", "ArrowDown", "ArrowDown", "ArrowDown", "ArrowDown", "ArrowDown"]); - - isInVisibleArea = isInVisibleArea = await browser.executeAsync(async done => { - const input = document.getElementById("inputError"); - const listItems = input.querySelectorAll("ui5-suggestion-item"); - const elementRect = listItems[6].getBoundingClientRect(); //Suggestion item "Angola" - - const popover = input.shadowRoot.querySelector("ui5-responsive-popover"); - const scrollableRect = popover.shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); - - // Check if the element is within the visible area - const isElementAboveViewport = elementRect.bottom < scrollableRect.top; - const isElementBelowViewport = elementRect.top > scrollableRect.bottom; - const isElementLeftOfViewport = elementRect.right < scrollableRect.left; - const isElementRightOfViewport = elementRect.left > scrollableRect.right; - - const isListItemInVisibleArea = ( - !isElementAboveViewport && - !isElementBelowViewport && - !isElementLeftOfViewport && - !isElementRightOfViewport - ); - - done(isListItemInVisibleArea); - }); - - assert.ok(isInVisibleArea, "Item 'Angola' should be displayed in the viewport"); - - // click ArrowUp 6 times through the list items to trigger scrolling and reach the first suggestion item - await input.keys(["ArrowUp", "ArrowUp", "ArrowUp", "ArrowUp", "ArrowUp", "ArrowUp"]); - - isInVisibleArea = await browser.executeAsync(async done => { - const input = document.getElementById("inputError"); - const listItems = input.querySelectorAll("ui5-suggestion-item"); - const elementRect = listItems[0].getBoundingClientRect(); //Suggestion item "Afganistan" - - const popover = input.shadowRoot.querySelector("ui5-responsive-popover"); - const scrollableRect = popover.shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); - - // Check if the element is within the visible area - const isElementAboveViewport = elementRect.bottom < scrollableRect.top; - const isElementBelowViewport = elementRect.top > scrollableRect.bottom; - const isElementLeftOfViewport = elementRect.right < scrollableRect.left; - const isElementRightOfViewport = elementRect.left > scrollableRect.right; - - const isListItemInVisibleArea = ( - !isElementAboveViewport && - !isElementBelowViewport && - !isElementLeftOfViewport && - !isElementRightOfViewport - ); - - done(isListItemInVisibleArea); - }); - - assert.ok(isInVisibleArea, "Item 'Afganistan' should be displayed in the viewport"); - }); -}); - describe("Input HOME navigation", () => { it("Should move caret to beginning of input with HOME if focus is on Input", async () => { await browser.url(`test/pages/Input.html`); @@ -1587,48 +1241,6 @@ describe("Input PAGEUP/PAGEDOWN navigation", () => { assert.strictEqual(await firstListItem.getProperty("focused"), false, "Responsive popover remains open and first list item is not focused"); }); - - it("Should focus the tenth item from the suggestions popover with PAGEDOWN", async () => { - await browser.url(`test/pages/Input.html`); - - const suggestionsInput = await browser.$("#myInput"); - - await suggestionsInput.click(); - await suggestionsInput.keys("a"); - - // Moving focus to suggestions popover, because by design PAGEDOWN does nothing if focus is on input - await suggestionsInput.keys("ArrowDown"); - - await suggestionsInput.keys("PageDown"); - - const tenthListItem = await suggestionsInput.$("ui5-suggestion-item:nth-of-type(10)"); - - assert.strictEqual(await suggestionsInput.getValue(), "Azerbaijan", "Tenth item has been selected"); - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await tenthListItem.getProperty("focused"), true, "Tenth list item is focused"); - }); - - it("Should focus the -10 item/group header from the suggestions popover with PAGEUP", async () => { - await browser.url(`test/pages/Input.html`); - - const suggestionsInput = await browser.$("#myInput"); - await suggestionsInput.scrollIntoView(); - - await suggestionsInput.click(); - await suggestionsInput.keys("a"); - - // Moving focus to suggestions popover, because by design PAGEUP does nothing if focus is on input - await suggestionsInput.keys("ArrowDown"); - - await suggestionsInput.keys("PageDown"); - await suggestionsInput.keys("PageUp"); - - const groupHeader = await suggestionsInput.$("ui5-suggestion-item-group"); - - assert.strictEqual(await suggestionsInput.getValue(), "a", "No item has been selected"); - assert.strictEqual(await suggestionsInput.getProperty("focused"), false, "Input is not focused"); - assert.strictEqual(await groupHeader.getProperty("focused"), true, "Group header is focused"); - }); }); describe("XSS tests for suggestions", () => { @@ -1780,30 +1392,6 @@ describe("Selection-change event", () => { assert.strictEqual(await selectionChangeCount.getText(), "1", "Selection-change event was fired once"); }); - - it("Selection-change event fires with null arguments when suggestion was selected but user alters input value to something else", async () => { - await browser.url(`test/pages/Input.html`); - - const input = await $("#input-selection-change"); - const inner = await input.shadow$("input"); - const selectionChangeCount = await $("#input-selection-change-count"); - const selectionChangeValue = await $("#input-selection-change-value"); - - await inner.click(); - await inner.keys("C"); - - // select first item - await input.keys("ArrowDown"); - assert.strictEqual(await selectionChangeCount.getText(), "1", "Selection-change event was fired once"); - assert.strictEqual(await selectionChangeValue.getText(), "Cozy", "Selection-change event was fired with arguments"); - - await inner.click(); - await inner.keys("N"); // this value is not in the suggestions - await inner.keys("Enter"); - - assert.strictEqual(await selectionChangeCount.getText(), "2", "Selection-change event was fired twice"); - assert.strictEqual(await selectionChangeValue.getText(), "", "Selection-change event was fired with null arguments"); - }); }); describe("Property open", () => { From 31c7c035ebd9e1f548514bd3abbd01e9bd93cc21 Mon Sep 17 00:00:00 2001 From: Stefan Dimitrov Date: Sun, 2 Feb 2025 19:08:32 +0200 Subject: [PATCH 3/4] feat(ui5-input): fix tests and resolve comments --- packages/main/cypress/specs/Input.cy.tsx | 457 ++++++++++++++++++ packages/main/src/Input.ts | 4 +- .../main/src/features/InputSuggestions.ts | 12 +- 3 files changed, 464 insertions(+), 9 deletions(-) diff --git a/packages/main/cypress/specs/Input.cy.tsx b/packages/main/cypress/specs/Input.cy.tsx index 67c036c394bb..79ee083c0b8b 100644 --- a/packages/main/cypress/specs/Input.cy.tsx +++ b/packages/main/cypress/specs/Input.cy.tsx @@ -126,3 +126,460 @@ describe("Input Tests", () => { .should("not.have.attr", "tabindex", "0"); }); }); + +describe("Input general interaction", () => { + it("handles suggestions selection cancel with ESC", () => { + cy.mount( + + + + + + + + ); + + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .shadow() + .find("ui5-responsive-popover") + .as("popover"); + + cy.get("@input") + .realClick(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .realType("C"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@popover") + .should("have.attr", "open"); + + cy.get("@input") + .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .should("have.attr", "value", "Titanium"); + + cy.get("@input") + .realPress("Escape"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .should("have.value", "C"); + }); + + it("tests selection-change with custom items", () => { + cy.mount( + + + + + + + + ); + + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .shadow() + .find("ui5-responsive-popover") + .as("popover"); + + cy.get("@input") + .realClick(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .realType("c"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@popover") + .should("have.attr", "open"); + + cy.get("@input") + .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .should("have.value", "Compact") + .should("not.have.attr", "focused"); + + cy.get("ui5-suggestion-item") + .eq(1) + .should("have.attr", "focused"); + + cy.get("@input") + .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("ui5-suggestion-item") + .eq(2) + .should("have.attr", "focused"); + + cy.get("ui5-suggestion-item") + .eq(1) + .should("not.have.attr", "focused"); + }); +}); + +describe("Input arrow navigation", () => { + it("Value state header and group headers should be included in the arrow navigation", () => { + cy.mount( + +
+ Custom error value state message with a Link. +
+ + + + + + + ); + + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .realClick() + .realType("a") + .realPress("ArrowDown"); + + cy.get("@input") + .should("not.have.attr", "focused"); + + cy.get("@input") + .shadow() + .find("ui5-responsive-popover") + .as("ui5-responsive-popover"); + + cy.get("@ui5-responsive-popover") + .find("div") + .as("valueMessage") + .should("have.class", "ui5-responsive-popover-header--focused"); + + cy.get("@input") + .realPress("ArrowDown"); + + cy.get("@valueMessage") + .should("not.have.class", "ui5-responsive-popover-header--focused"); + + cy.get("ui5-suggestion-item") + .eq(0) + .should("have.attr", "focused"); + }); + + it("Should navigate up and down through the suggestions popover with arrow keys", () => { + cy.mount( + + + + + + + + ); + + cy.get("#myInput2") + .as("input"); + + cy.get("@input") + .shadow() + .find("ui5-responsive-popover") + .as("popover"); + + cy.get("@input").click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input").realType("c"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@popover") + .should("have.attr", "open"); + + cy.get("@input").realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("ui5-suggestion-item").eq(1).should("have.attr", "text", "Compact"); + cy.get("@input").should("not.have.attr", "focused"); + cy.get("ui5-suggestion-item").eq(1).should("have.attr", "focused"); + + cy.get("@input") + .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("ui5-suggestion-item").eq(2).should("have.attr", "focused"); + cy.get("ui5-suggestion-item").eq(1).should("not.have.attr", "focused"); + + cy.get("@input") + .realPress("ArrowUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("ui5-suggestion-item").eq(1).should("have.attr", "focused"); + cy.get("ui5-suggestion-item").eq(2).should("not.have.attr", "focused"); + + cy.get("@input").realPress("ArrowUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.get("@input").realPress("ArrowUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input").should("have.attr", "focused"); + cy.get("ui5-suggestion-item").first().should("not.have.attr", "focused"); + }); +}); + +describe("Input PAGEUP/PAGEDOWN navigation", () => { + beforeEach(() => { + cy.mount( + + + + + + + + + + + + + + + + + ); + }); + it("Should focus the tenth item from the suggestions popover with PAGEDOWN", () => { + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .realClick(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .realType("a"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .realPress("ArrowDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .realPress("PageDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("ui5-suggestion-item") + .eq(11) + .should("have.attr", "text", "Antigua and Barbuda"); + + cy.get("ui5-suggestion-item") + .eq(11) + .should("have.attr", "focused"); + }); + + it("Should focus the -10 item/group header from the suggestions popover with PAGEUP", () => { + cy.get("ui5-input") + .as("input"); + + cy.get("@input") + .realClick(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .realType("a"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .realPress("ArrowUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("ui5-suggestion-item-group") + .eq(0) + .should("have.attr", "focused"); + + cy.get("@input") + .realPress("PageDown"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("@input") + .realPress("PageUp"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + + cy.get("ui5-suggestion-item-group") + .eq(0) + .should("have.attr", "focused"); + }); +}); + +describe("Selection-change event", () => { + it("Selection-change event fires with null arguments when suggestion was selected but user alters input value to something else", () => { + cy.mount( + + + + + + ); + + cy.get("ui5-input") + .as("input"); + + cy.get("ui5-input") + .shadow() + .find("input") + .as("innerInput"); + + let eventCount = 0; + + cy.get("@input").then($input => { + $input[0].addEventListener("ui5-selection-change", () => { + eventCount++; + }); + }); + + cy.get("@innerInput") + .click(); + cy.get("@innerInput") + .type("C"); + cy.get("@innerInput") + .realPress("ArrowDown"); + cy.get("@innerInput") + .realPress("Enter"); + + cy.get("@innerInput") + .should("have.value", "Compact"); + + cy.get("@innerInput") + .click(); + cy.get("@innerInput") + .clear(); + cy.get("@innerInput") + .type("N"); + cy.get("@innerInput") + .realPress("Enter"); + + cy.get("@innerInput") + .should("have.value", "N"); + + cy.then(() => { + expect(eventCount).to.equal(2); + }); + }); +}); + +describe("Change event behavior when selecting the same suggestion item", () => { + let changeCount = 0; + + beforeEach(() => { + cy.mount( + + + + + + + + + + ); + + cy.get("#myInput") + .as("input"); + + cy.get("@input").then($el => { + $el[0].addEventListener("change", () => { + changeCount++; + }); + }); + }); + + it("Change event is not fired when the same suggestion item is selected (with typeahead)", () => { + cy.get("@input") + .click(); + + cy.get("@input") + .realType("a"); + + cy.get("@input").realPress("Enter"); + cy.get("@input").should("have.value", "Afghanistan"); + + cy.get("@input").realPress("Backspace"); + cy.get("@input").realPress("ArrowDown"); + cy.get("@input").realPress("ArrowDown"); + cy.get("@input").realPress("Enter"); + + cy.get("@input").should("have.value", "Afghanistan"); + cy.then(() => { + expect(changeCount).to.equal(1); + }); + }); + + it("Change event is not fired when the same suggestion item is selected (no-typeahead)", () => { + cy.get("@input").invoke("attr", "value", "Afghanistan"); + cy.get("@input").invoke("attr", "no-typeahead", true); + + cy.get("@input").realPress("Backspace"); + + cy.get("@input").realPress("ArrowDown"); + cy.get("@input").realPress("ArrowDown"); + cy.get("@input").realPress("Enter"); + + cy.get("@input").should("have.value", "Afghanistan"); + cy.then(() => { + expect(changeCount).to.equal(1); + }); + }); + + it("Change event is not fired when the same suggestion item is selected after focus out and selecting suggestion again", () => { + cy.get("@input") + .invoke("attr", "value", "Afghanistan"); + + cy.get("@input") + .realPress("Tab"); + + cy.get("@input") + .realClick(); + cy.get("@input") + .realPress("ArrowDown"); + cy.get("@input") + .realPress("ArrowDown"); + cy.get("@input") + .realPress("Enter"); + + cy.get("@input").should("have.value", "Afghanistan"); + cy.then(() => { + expect(changeCount).to.equal(1); + }); + }); +}); diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index b5fdcdfc389b..dc07c69154b1 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -574,7 +574,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _keyDown?: boolean; _isKeyNavigation?: boolean; _indexOfSelectedItem: number; - // Suggestions?: InputSuggestions; _selectedText?: string; _clearIconClicked?: boolean; _focusedAfterClear: boolean; @@ -824,7 +823,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _handleUp(e: KeyboardEvent) { if (this.Suggestions?.isOpened()) { - // const itemIndex = this.currentItemIndex; this.Suggestions.onUp(e, this.currentItemIndex); } } @@ -1218,7 +1216,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this.focused = false; } - if (this._changeToBeFired && this._isChangeTriggeredBySuggestion === false) { + if (this._changeToBeFired && !this._isChangeTriggeredBySuggestion) { this.fireDecoratorEvent(INPUT_EVENTS.CHANGE); } else { this._isChangeTriggeredBySuggestion = false; diff --git a/packages/main/src/features/InputSuggestions.ts b/packages/main/src/features/InputSuggestions.ts index b6cbaa1e7ef5..045b60ba8918 100644 --- a/packages/main/src/features/InputSuggestions.ts +++ b/packages/main/src/features/InputSuggestions.ts @@ -83,15 +83,15 @@ class Suggestions { onUp(e: KeyboardEvent, indexOfItem: number) { e.preventDefault(); - indexOfItem = !this.isOpened && this._hasValueState && indexOfItem === -1 ? 0 : indexOfItem; - this._handleItemNavigation(false /* forward */, indexOfItem); + const index = !this.isOpened && this._hasValueState && indexOfItem === -1 ? 0 : indexOfItem; + this._handleItemNavigation(false /* forward */, index); return true; } onDown(e: KeyboardEvent, indexOfItem: number) { e.preventDefault(); - indexOfItem = !this.isOpened && this._hasValueState && indexOfItem === -1 ? 0 : indexOfItem; - this._handleItemNavigation(true /* forward */, indexOfItem); + const index = !this.isOpened && this._hasValueState && indexOfItem === -1 ? 0 : indexOfItem; + this._handleItemNavigation(true /* forward */, index); return true; } @@ -300,8 +300,8 @@ class Suggestions { return !!(this._getPicker()?.open); } - _handleItemNavigation(forward: boolean, indexOfItem: number) { - this.selectedItemIndex = indexOfItem; + _handleItemNavigation(forward: boolean, index: number) { + this.selectedItemIndex = index; if (!this._getItems().length) { return; From 097b006bb1cc9245f0639d2e160cc3c9efc0fcbb Mon Sep 17 00:00:00 2001 From: Stefan Dimitrov Date: Mon, 3 Feb 2025 20:45:11 +0200 Subject: [PATCH 4/4] feat(ui5-input): fix tests --- packages/main/cypress/specs/Input.cy.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/main/cypress/specs/Input.cy.tsx b/packages/main/cypress/specs/Input.cy.tsx index 79ee083c0b8b..1f5dd8f886e2 100644 --- a/packages/main/cypress/specs/Input.cy.tsx +++ b/packages/main/cypress/specs/Input.cy.tsx @@ -303,7 +303,7 @@ describe("Input arrow navigation", () => { .find("ui5-responsive-popover") .as("popover"); - cy.get("@input").click(); + cy.get("@input").realClick(); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(200); cy.get("@input").realType("c"); @@ -469,7 +469,7 @@ describe("Selection-change event", () => { }); cy.get("@innerInput") - .click(); + .realClick(); cy.get("@innerInput") .type("C"); cy.get("@innerInput") @@ -481,7 +481,7 @@ describe("Selection-change event", () => { .should("have.value", "Compact"); cy.get("@innerInput") - .click(); + .realClick(); cy.get("@innerInput") .clear(); cy.get("@innerInput") @@ -526,7 +526,7 @@ describe("Change event behavior when selecting the same suggestion item", () => it("Change event is not fired when the same suggestion item is selected (with typeahead)", () => { cy.get("@input") - .click(); + .realClick(); cy.get("@input") .realType("a");