From a265a65d8b48b4fcef84931a3ca0b7ad4278283e Mon Sep 17 00:00:00 2001 From: Nikoleta Ivanova <31706628+nikoletavnv@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:59:34 +0200 Subject: [PATCH] feat(ui5-input): prevent suggestion-item-select event per suggestion item (#7940) * feat(ui5-input): prevent suggestion-item-select * feat(ui5-input): prevent suggestion item select * feat(ui5-input): close suggestions popover on group item enter * feat(ui5-input): close suggestions popover on group item enter * feat(ui5-input): add tests for preventing suggestion-item-select event * feat(ui5-input): keep old order of firing events when suggestion item is selected * feat(ui5-input): keep old order of firing events when suggestion item is selected --- packages/main/src/Input.ts | 52 +++++++++++---- .../main/src/features/InputSuggestions.ts | 5 ++ packages/main/test/pages/Input.html | 16 +++++ packages/main/test/specs/Input.spec.js | 64 ++++++++++++++++++- 4 files changed, 121 insertions(+), 16 deletions(-) diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index 0ff4359f1edf..5b2a0b089b01 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -626,7 +626,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { hasSuggestionItemSelected: boolean; valueBeforeItemSelection: string; valueBeforeItemPreview: string - suggestionSelectionCanceled: boolean; + suggestionSelectionCancelled: boolean; previousValue: string; firstRendering: boolean; typedInValue: string; @@ -664,7 +664,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { this.valueBeforeItemPreview = ""; // Indicates if the user selection has been canceled with [ESC]. - this.suggestionSelectionCanceled = false; + this.suggestionSelectionCancelled = false; // tracks the value between focus in and focus out to detect that change event should be fired. this.previousValue = ""; @@ -870,7 +870,8 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { } _handleEnter(e: KeyboardEvent) { - const itemPressed = !!(this.Suggestions && this.Suggestions.onEnter(e)); + const suggestionItemPressed = !!(this.Suggestions && this.Suggestions.onEnter(e)); + const innerInput = this.getInputDOMRefSync()!; // Check for autocompleted item const matchingItem = this.suggestionItems.find(item => { @@ -881,7 +882,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { const itemText = matchingItem.text ? matchingItem.text : (matchingItem.textContent || ""); innerInput.setSelectionRange(itemText.length, itemText.length); - if (!itemPressed) { + if (!suggestionItemPressed) { this.selectSuggestion(matchingItem, true); this.open = false; } @@ -891,7 +892,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { innerInput.setSelectionRange(this.value.length, this.value.length); } - if (!itemPressed) { + if (!suggestionItemPressed) { this.lastConfirmedValue = this.value; if (this.FormSupport) { @@ -949,9 +950,9 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { // Restore the value. this.value = this.typedInValue || this.valueBeforeItemPreview; - // Mark that the selection has been canceled, so the popover can close + // Mark that the selection has been cancelled, so the popover can close // and not reopen, due to receiving focus. - this.suggestionSelectionCanceled = true; + this.suggestionSelectionCancelled = true; this.focused = true; return; @@ -1100,7 +1101,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { ]; this._shouldAutocomplete = !allowedEventTypes.includes(eventType) && !this.noTypeahead; - this.suggestionSelectionCanceled = false; + this.suggestionSelectionCancelled = false; if (e instanceof InputEvent) { // ---- Special cases of numeric Input ---- @@ -1292,14 +1293,19 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { this.hasSuggestionItemSelected = true; + const valueOriginal = this.value; + const valueBeforeItemSelectionOriginal = this.valueBeforeItemSelection; + const lastConfirmedValueOriginal = this.lastConfirmedValue; + const performTextSelectionOriginal = this._performTextSelection; + const typedInValueOriginal = this.typedInValue; + const previousValueOriginal = this.previousValue; + if (fireInput) { this.value = itemText; this.valueBeforeItemSelection = itemText; this.lastConfirmedValue = itemText; this._performTextSelection = true; - this.hasSuggestionItemSelected = true; - this.value = itemText; this.fireEvent(INPUT_EVENTS.CHANGE); @@ -1313,9 +1319,29 @@ class Input extends UI5Element implements SuggestionComponent, IFormElement { } this.valueBeforeItemPreview = ""; - this.suggestionSelectionCanceled = false; - - this.fireEvent(INPUT_EVENTS.SUGGESTION_ITEM_SELECT, { item }); + this.suggestionSelectionCancelled = false; + + // Fire suggestion-item-select event after input change events for backward compatibility, but revert all input properties set before suggestion was prevented. + // For v2.0 this code will be reworked. + const isCancelledByUser = !this.fireEvent(INPUT_EVENTS.SUGGESTION_ITEM_SELECT, { item }, true); + + if (isCancelledByUser) { + this.Suggestions?._clearSelectedSuggestionAndAccInfo(); + this.hasSuggestionItemSelected = false; + this.suggestionSelectionCancelled = true; + + if (fireInput) { + // revert properties set during fireInput + if (itemText === this.value) { // If no chnages were made to the input value after suggestion-item-select was prevented - revert value to the original text + this.value = valueOriginal; + } + this.valueBeforeItemSelection = valueBeforeItemSelectionOriginal; + this.lastConfirmedValue = lastConfirmedValueOriginal; + this._performTextSelection = performTextSelectionOriginal; + this.typedInValue = typedInValueOriginal; + this.previousValue = previousValueOriginal; + } + } this.isTyping = false; this.openOnMobile = false; diff --git a/packages/main/src/features/InputSuggestions.ts b/packages/main/src/features/InputSuggestions.ts index 847b2429429c..4ae46ea0fcbd 100644 --- a/packages/main/src/features/InputSuggestions.ts +++ b/packages/main/src/features/InputSuggestions.ts @@ -656,6 +656,11 @@ class Suggestions { this.component._isValueStateFocused = false; } + _clearSelectedSuggestionAndAccInfo() { + this.accInfo = undefined; + this.selectedItemIndex = 0; + } + static get dependencies() { return [ SuggestionItem, diff --git a/packages/main/test/pages/Input.html b/packages/main/test/pages/Input.html index b628c2707797..60e40d016982 100644 --- a/packages/main/test/pages/Input.html +++ b/packages/main/test/pages/Input.html @@ -122,6 +122,13 @@

Input with disabled autocomplete (type-ahead)

Condensed +

Input with disabled autocomplete and preventable suggestion select

+ + Cozy + Compact + Condensed + +

'change' event result

@@ -752,6 +759,15 @@

Input - change event handling

}); document.getElementById("change-event-value").addEventListener("ui5-change", event => event.target.value = ""); + + const inputWithPreventableSelection = document.getElementById("input-prevent-suggestion-select"); + inputWithPreventableSelection.addEventListener("ui5-suggestionItemSelect", (e) => { + const selectedItemText = e.detail.item.text || e.detail.item.textContent; + if(selectedItemText === "Cozy"){ + e.preventDefault(); + inputWithPreventableSelection.value = "test test"; + } + }); diff --git a/packages/main/test/specs/Input.spec.js b/packages/main/test/specs/Input.spec.js index ff27b38cebab..336276fa17cf 100644 --- a/packages/main/test/specs/Input.spec.js +++ b/packages/main/test/specs/Input.spec.js @@ -735,8 +735,6 @@ describe("Input general interaction", () => { }); it("Tests disabled autocomplete(type-ahead)", async () => { - let hasSelection; - const input = await browser.$("#input-disabled-autocomplete").shadow$("input"); await input.click(); @@ -1519,6 +1517,66 @@ describe("XSS tests for suggestions", () => { }); }); +describe("Prevent suggestion-item-select event", () => { + let input; + let SUGGESTION_TEXT; + const INPUT_ID_SELECTOR = "#input-prevent-suggestion-select"; + + beforeEach(async () => { + await browser.url(`test/pages/Input.html`); + + input = await browser.$(INPUT_ID_SELECTOR); + }); + + it("User can prevent suggested-item-select on desired item", async () => { + SUGGESTION_TEXT = "Cozy"; + + await input.click(); + await input.keys(SUGGESTION_TEXT.at(0)); + + const staticAreaItemClassName = + await browser.getStaticAreaItemClassName(INPUT_ID_SELECTOR); + const respPopover = await browser + .$(`.${staticAreaItemClassName}`) + .shadow$("ui5-responsive-popover"); + + // Select first suggestion item that has event prevent + const firstSuggestion = await respPopover + .$("ui5-list") + .$("ui5-li-suggestion-item"); + await firstSuggestion.click(); + + assert.strictEqual( + await input.getProperty("value"), + "test test", + "Prevent suggestion-item-select event does not work" + ); + }); + + it("Suggestion selection works as usual for items that do not match event prevent criterias defined by user", async () => { + SUGGESTION_TEXT = "Compact"; + + await input.click(); + await input.keys(SUGGESTION_TEXT.at(0)); + + const staticAreaItemClassName = + await browser.getStaticAreaItemClassName(INPUT_ID_SELECTOR); + const respPopover = await browser + .$(`.${staticAreaItemClassName}`) + .shadow$("ui5-responsive-popover"); + + const secondSuggestion = await respPopover + .$("ui5-list") + .$$("ui5-li-suggestion-item")[1]; + await secondSuggestion.click(); + + assert.strictEqual( + await input.getProperty("value"), + SUGGESTION_TEXT, + "Event suggestion-item-select works as expected for items without event prevention" + ); + }); +}); describe("Lazy loading", () => { beforeEach(async () => { @@ -1578,4 +1636,4 @@ describe("Lazy loading", () => { assert.strictEqual(await respPopover.getProperty("opened"), true, "Picker should not be open"); }); -}); +}); \ No newline at end of file