From 7b3bcdb1626ad1e52d3fde4dfe48b7ee366c1d88 Mon Sep 17 00:00:00 2001 From: aleksandar-terziev Date: Fri, 31 Oct 2025 14:27:47 +0200 Subject: [PATCH] feat(ui5-input,ui5-multi-input): add suggestions trigger --- packages/main/cypress/specs/Input.cy.tsx | 93 +++++++++++++- .../main/cypress/specs/Input.mobile.cy.tsx | 114 +++++++++++++++++- packages/main/cypress/specs/MultiInput.cy.tsx | 2 +- packages/main/src/Input.ts | 24 +++- .../src/features/InputSuggestionsTemplate.tsx | 2 +- packages/main/test/pages/Input.html | 8 ++ packages/main/test/pages/MultiInput.html | 15 +++ .../_components_pages/main/Input/Input.mdx | 8 ++ .../SuggestionsTrigger/SuggestionsTrigger.md | 4 + .../main/Input/SuggestionsTrigger/main.js | 28 +++++ .../main/Input/SuggestionsTrigger/sample.html | 20 +++ 11 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 packages/website/docs/_samples/main/Input/SuggestionsTrigger/SuggestionsTrigger.md create mode 100644 packages/website/docs/_samples/main/Input/SuggestionsTrigger/main.js create mode 100644 packages/website/docs/_samples/main/Input/SuggestionsTrigger/sample.html diff --git a/packages/main/cypress/specs/Input.cy.tsx b/packages/main/cypress/specs/Input.cy.tsx index 4081a16b5e9f..239475dbd981 100644 --- a/packages/main/cypress/specs/Input.cy.tsx +++ b/packages/main/cypress/specs/Input.cy.tsx @@ -2208,7 +2208,7 @@ describe("Input general interaction", () => { it("Should open suggestions popover if open is set on focusin", () => { cy.mount( - (e.target as Input).open = true}> + (e.target as Input).open = true}> @@ -2280,7 +2280,7 @@ describe("Input general interaction", () => { it("Changes text if cleared in change event handler", () => { cy.mount( - (e.target as Input).value = ""}> + (e.target as Input).value = ""}> @@ -2601,7 +2601,7 @@ describe("Selection-change event", () => { describe("Property open", () => { it("Suggestions picker is open when attribute open is set to true", () => { cy.mount( - + @@ -2797,3 +2797,90 @@ describe("Input Composition", () => { .should("have.attr", "value", "谢谢"); }); }); + +describe("startSuggestion threshold behavior", () => { + it("suggestions popover opens when startSuggestion threshold is met", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-input]") + .as("input") + .shadow() + .find("input") + .as("innerInput"); + + cy.get("@innerInput").realClick(); + + cy.get("@innerInput").realType("App"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .ui5ResponsivePopoverOpened(); + + cy.get("@innerInput").realPress("Backspace"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.be.visible"); + }); + + it("suggestions popover remains closed when typing below startSuggestion threshold", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-input]") + .as("input") + .shadow() + .find("input") + .as("innerInput"); + + cy.get("@innerInput").realClick(); + + cy.get("@innerInput").realType("A"); + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.be.visible"); + + cy.get("@innerInput").realType("p"); + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.be.visible"); + }); + + it("suggestions popover is opened when startSuggestion is not set", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-input]") + .as("input") + .shadow() + .find("input") + .as("innerInput"); + + cy.get("@innerInput").realClick(); + + cy.get("@innerInput").realType("A"); + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .ui5ResponsivePopoverOpened(); + }); +}); diff --git a/packages/main/cypress/specs/Input.mobile.cy.tsx b/packages/main/cypress/specs/Input.mobile.cy.tsx index 1549f598b846..49533dee00aa 100644 --- a/packages/main/cypress/specs/Input.mobile.cy.tsx +++ b/packages/main/cypress/specs/Input.mobile.cy.tsx @@ -183,7 +183,7 @@ describe("Eventing", () => { cy.mount( <> - + @@ -390,4 +390,114 @@ describe("Property open", () => { .find("[ui5-responsive-popover]") .ui5ResponsivePopoverClosed(); }); -}); \ No newline at end of file +}); + +describe("startSuggestion threshold behavior on mobile", () => { + beforeEach(() => { + cy.ui5SimulateDevice("phone"); + }); + + it("suggestions list should not display when startSuggestion threshold is not met", () => { + cy.mount( + + + + + + ); + + cy.get("#input-suggestions") + .as("input") + .realClick(); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + cy.get("@input") + .shadow() + .find(".ui5-input-inner-phone") + .as("innerInput") + .should("be.focused"); + + cy.get("@innerInput").realType("Ap"); + + cy.get("@input") + .find("[ui5-suggestion-item]") + .should("not.be.visible"); + + cy.get("@popover").ui5ResponsivePopoverOpened(); + }); + + it("suggestion list should display when startSuggestion threshold is met", () => { + cy.mount( + + + + + + ); + + cy.get("#input-suggestions") + .as("input") + .realClick(); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + cy.get("@input") + .shadow() + .find(".ui5-input-inner-phone") + .as("innerInput") + .should("be.focused"); + + cy.get("@innerInput").realType("Ap"); + + cy.get("@input") + .find("[ui5-suggestion-item]") + .should("be.visible"); + + cy.get("@input") + .find("[ui5-suggestion-item]") + .should("have.length", 3); + + cy.get("@popover").ui5ResponsivePopoverOpened(); + }); + + it("startSuggestion=0 shows suggestion list immediately on mobile", () => { + cy.mount( + + + + + + ); + + cy.get("#input-suggestions") + .as("input") + .realClick(); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .ui5ResponsivePopoverOpened(); + + cy.get("@input") + .shadow() + .find(".ui5-input-inner-phone") + .should("be.focused"); + + cy.get("@input") + .find("[ui5-suggestion-item]") + .should("be.visible"); + + cy.get("@input") + .find("[ui5-suggestion-item]") + .should("have.length", 3); + }); +}); diff --git a/packages/main/cypress/specs/MultiInput.cy.tsx b/packages/main/cypress/specs/MultiInput.cy.tsx index 280cad229959..bbfd4ba2250e 100644 --- a/packages/main/cypress/specs/MultiInput.cy.tsx +++ b/packages/main/cypress/specs/MultiInput.cy.tsx @@ -446,7 +446,7 @@ describe("MultiInput tokens", () => { it("should empty the field when value is cleared in the change handler", () => { cy.mount( - +
Token is already in the list
diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index 08da1a4eaa1b..51e2543460d7 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -426,6 +426,16 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement @property({ type: Boolean }) showSuggestions = false; + /** + * Defines the minimum number of typed characters required before suggestions become active + * + * @default 1 + * @public + * @since 2.16.0 + */ + @property({ type: Number }) + startSuggestion: number = 1; + /** * Sets the maximum number of characters available in the input field. * @@ -773,7 +783,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement if (preventOpenPicker) { this.open = false; } else if (!this._isPhone) { - this.open = hasItems && (this.open || (hasValue && isFocused && this.isTyping)); + this.open = this._shouldTriggerSuggest && hasItems && (this.open || (hasValue && isFocused && this.isTyping)); } const value = this.value; @@ -787,7 +797,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement // Typehead causes issues on Android devices, so we disable it for now // If there is already a selection the autocomplete has already been performed - if (this._shouldAutocomplete && !isAndroid() && !autoCompletedChars && !this._isKeyNavigation) { + if (this._shouldAutocomplete && this._shouldTriggerSuggest && !isAndroid() && !autoCompletedChars && !this._isKeyNavigation) { const item = this._getFirstMatchingItem(value); if (item) { if (!this._isComposing) { @@ -801,14 +811,14 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement onAfterRendering() { const innerInput = this.getInputDOMRefSync()!; - if (this.showSuggestions && this.Suggestions?._getPicker()) { + if (this._shouldTriggerSuggest && this.showSuggestions && this.Suggestions?._getPicker()) { this._listWidth = this.Suggestions._getListWidth(); // disabled ItemNavigation from the list since we are not using it this.Suggestions._getList()._itemNavigation._getItems = () => []; } - if (this._performTextSelection) { + if (this._shouldTriggerSuggest && this._performTextSelection) { // this is required to syncronize lit-html input's value and user's input // lit-html does not sync its stored value for the value property when the user is typing if (innerInput.value !== this._innerValue) { @@ -1138,7 +1148,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } _clearPopoverFocusAndSelection() { - if (!this.showSuggestions || !this.Suggestions) { + if (!this.showSuggestions || !this.Suggestions || !this._shouldTriggerSuggest) { return; } @@ -1939,6 +1949,10 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement return !this.focused && this.Suggestions?.isOpened(); } + get _shouldTriggerSuggest() { + return this.typedInValue.length >= this.startSuggestion; + } + /** * Returns the placeholder value. * @protected diff --git a/packages/main/src/features/InputSuggestionsTemplate.tsx b/packages/main/src/features/InputSuggestionsTemplate.tsx index bd3d917370fa..e18fee3ceba3 100644 --- a/packages/main/src/features/InputSuggestionsTemplate.tsx +++ b/packages/main/src/features/InputSuggestionsTemplate.tsx @@ -72,7 +72,7 @@ export default function InputSuggestionsTemplate(this: Input, hooks?: { suggesti } - { suggestionsList.call(this) } + { this._shouldTriggerSuggest && suggestionsList.call(this) } {this._isPhone && +
+

Input with startSuggestion trigger = 3

+ + +
+

Input in Compact

@@ -674,6 +680,7 @@

Input Composition

]; var input = document.getElementById('myInput'); + var inputSuggestionTrigger = document.getElementById('myInputSuggestionTrigger'); var inputChangeWithSuggestions = document.getElementById('inputChange-Suggestions'); var inputGrouping = document.getElementById('myInputGrouping'); var myInput2 = document.getElementById('myInput2'); @@ -831,6 +838,7 @@

Input Composition

var selectionChangeCounter = 0; input.addEventListener("ui5-input", suggest); + myInputSuggestionTrigger.addEventListener("ui5-input", suggest); inputCompact.addEventListener("ui5-input", suggest); inputError.addEventListener("ui5-input", suggest); customSuggestionsInput.addEventListener("ui5-input", suggestCustom); diff --git a/packages/main/test/pages/MultiInput.html b/packages/main/test/pages/MultiInput.html index cfbd08f60c7c..c56194a52634 100644 --- a/packages/main/test/pages/MultiInput.html +++ b/packages/main/test/pages/MultiInput.html @@ -302,6 +302,21 @@

Tokens + Suggestions

+
+

Tokens + Suggestions + Start Suggestion Trigger = 3

+ + + + + + + + + + + +
+

Composition

diff --git a/packages/website/docs/_components_pages/main/Input/Input.mdx b/packages/website/docs/_components_pages/main/Input/Input.mdx index 931b34c319c6..20f3e6ee8849 100644 --- a/packages/website/docs/_components_pages/main/Input/Input.mdx +++ b/packages/website/docs/_components_pages/main/Input/Input.mdx @@ -6,6 +6,7 @@ import Basic from "../../../_samples/main/Input/Basic/Basic.md"; import Suggestions from "../../../_samples/main/Input/Suggestions/Suggestions.md"; import ClearIcon from "../../../_samples/main/Input/ClearIcon/ClearIcon.md"; import SuggestionsWrapping from "../../../_samples/main/Input/SuggestionsWrapping/SuggestionsWrapping.md"; +import SuggestionsTrigger from "../../../_samples/main/Input/SuggestionsTrigger/SuggestionsTrigger.md"; import ValueStateMessage from "../../../_samples/main/Input/ValueStateMessage/ValueStateMessage.md"; import Label from "../../../_samples/main/Input/Label/Label.md"; import ValueHelpDialog from "../../../_samples/main/Input/ValueHelpDialog/ValueHelpDialog.md"; @@ -37,6 +38,13 @@ The sample demonstrates how the text of the suggestions wrap when too long. +### Suggestions Trigger +When startSuggestion property is set to a custom threshold number, +the suggestions list, auto completion and typeahead will not be triggered unless the threshold number is met. +See the startSuggestion property for more information. + + + ### Input and Label