Skip to content

Commit d006cd6

Browse files
feat(ui5-input,ui5-multi-input): dynamic suggestions triggering (#12597)
feat(ui5-input,ui5-multi-input): add suggestions trigger
1 parent bbda5a3 commit d006cd6

File tree

9 files changed

+263
-7
lines changed

9 files changed

+263
-7
lines changed

packages/main/cypress/specs/Input.cy.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,79 @@ describe("Input general interaction", () => {
544544
cy.get("@onChange").should("have.been.calledOnce");
545545
cy.get("@onSelectionChange").should("have.been.calledOnce");
546546
});
547+
548+
it("Should control suggestions dynamically based on threshold", () => {
549+
const THRESHOLD = 3;
550+
const countries = [
551+
"Argentina", "Albania", "Algeria", "Angola", "Austria", "Australia",
552+
"Bulgaria", "Belgium", "Brazil", "Canada", "Colombia", "Croatia"
553+
];
554+
555+
cy.mount(<Input id="threshold-input" showSuggestions />);
556+
557+
cy.document().then(doc => {
558+
const input = doc.querySelector<Input>("#threshold-input")!;
559+
560+
input.addEventListener("input", () => {
561+
const value = input.value;
562+
563+
while (input.lastChild) {
564+
input.removeChild(input.lastChild);
565+
}
566+
567+
if (value.length >= THRESHOLD) {
568+
input.showSuggestions = true;
569+
570+
const filtered = countries.filter(country =>
571+
country.toUpperCase().indexOf(value.toUpperCase()) === 0
572+
);
573+
574+
filtered.forEach(country => {
575+
const item = document.createElement("ui5-suggestion-item");
576+
item.setAttribute("text", country);
577+
input.appendChild(item);
578+
});
579+
} else {
580+
input.showSuggestions = false;
581+
}
582+
});
583+
});
584+
585+
cy.get("#threshold-input")
586+
.as("input")
587+
.realClick();
588+
589+
cy.get("@input")
590+
.should("be.focused");
591+
592+
cy.realType("B");
593+
594+
cy.get("@input")
595+
.shadow()
596+
.find<ResponsivePopover>("[ui5-responsive-popover]")
597+
.should("not.exist", "true");
598+
599+
cy.realType("ul");
600+
601+
cy.get("@input")
602+
.shadow()
603+
.find<ResponsivePopover>("[ui5-responsive-popover]")
604+
.ui5ResponsivePopoverOpened();
605+
606+
cy.get("@input")
607+
.find("[ui5-suggestion-item]")
608+
.should("have.length", 1)
609+
.first()
610+
.should("have.attr", "text", "Bulgaria");
611+
612+
cy.realPress("Backspace");
613+
cy.realPress("Backspace");
614+
615+
cy.get("@input")
616+
.shadow()
617+
.find<ResponsivePopover>("[ui5-responsive-popover]")
618+
.should("not.exist", "true");
619+
});
547620
});
548621

549622
describe("Input arrow navigation", () => {

packages/main/cypress/specs/Input.mobile.cy.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,57 @@ describe("Eventing", () => {
198198

199199
cy.get("@onSelectionChange").should("have.been.calledOnce");
200200
});
201+
202+
it("Should control suggestions dynamically based on threshold on mobile", () => {
203+
const THRESHOLD = 3;
204+
const countries = [
205+
"Argentina", "Albania", "Algeria", "Angola", "Austria", "Australia",
206+
"Bulgaria", "Belgium", "Brazil", "Canada", "Colombia", "Croatia"
207+
];
208+
209+
cy.mount(<Input id="mobile-threshold" showSuggestions />);
210+
211+
cy.document().then(doc => {
212+
const input = doc.querySelector<Input>("#mobile-threshold")!;
213+
214+
input.addEventListener("input", () => {
215+
const value = input.value;
216+
217+
while (input.lastChild) {
218+
input.removeChild(input.lastChild);
219+
}
220+
221+
if (value.length >= THRESHOLD) {
222+
input.showSuggestions = true;
223+
224+
const filtered = countries.filter(country =>
225+
country.toUpperCase().indexOf(value.toUpperCase()) === 0
226+
);
227+
228+
filtered.forEach(country => {
229+
const item = document.createElement("ui5-suggestion-item");
230+
item.setAttribute("text", country);
231+
input.appendChild(item);
232+
});
233+
} else {
234+
input.showSuggestions = false;
235+
}
236+
});
237+
});
238+
239+
cy.get("#mobile-threshold")
240+
.as("input")
241+
.realClick();
242+
243+
cy.get("@input")
244+
.shadow()
245+
.find<ResponsivePopover>("[ui5-responsive-popover]")
246+
.ui5ResponsivePopoverOpened();
247+
248+
cy.get("@input").shadow().find(".ui5-input-inner-phone").should("be.focused");
249+
cy.get("@input").shadow().find(".ui5-input-inner-phone").realType("Bu");
250+
cy.get("@input").shadow().find("ui5-suggestion-item").should("have.length", 0);
251+
});
201252
});
202253

203254
describe("Typeahead", () => {
@@ -390,4 +441,4 @@ describe("Property open", () => {
390441
.find<ResponsivePopover>("[ui5-responsive-popover]")
391442
.ui5ResponsivePopoverClosed();
392443
});
393-
});
444+
});

packages/main/src/Input.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
11101110
_onfocusout(e: FocusEvent) {
11111111
const toBeFocused = e.relatedTarget as HTMLElement;
11121112

1113-
if (this.Suggestions?._getPicker().contains(toBeFocused) || this.contains(toBeFocused) || this.getSlottedNodes("valueStateMessage").some(el => el.contains(toBeFocused))) {
1113+
if (this.Suggestions?._getPicker()?.contains(toBeFocused) || this.contains(toBeFocused) || this.getSlottedNodes("valueStateMessage").some(el => el.contains(toBeFocused))) {
11141114
return;
11151115
}
11161116

@@ -1174,7 +1174,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
11741174

11751175
if (this.previousValue !== this.getInputDOMRefSync()!.value) {
11761176
// if picker is open there might be a selected item, wait next tick to get the value applied
1177-
if (this.Suggestions?._getPicker().open && this._flattenItems.some(item => item.hasAttribute("ui5-suggestion-item") && (item as SuggestionItem).selected)) {
1177+
if (this.Suggestions?._getPicker()?.open && this._flattenItems.some(item => item.hasAttribute("ui5-suggestion-item") && (item as SuggestionItem).selected)) {
11781178
this._changeToBeFired = true;
11791179
} else {
11801180
fireChange();
@@ -1566,15 +1566,21 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
15661566

15671567
getInputDOMRef() {
15681568
if (isPhone() && this.Suggestions) {
1569-
return this.Suggestions._getPicker()!.querySelector<Input>(".ui5-input-inner-phone")!;
1569+
const picker = this.Suggestions._getPicker();
1570+
if (picker) {
1571+
return picker.querySelector<Input>(".ui5-input-inner-phone")!;
1572+
}
15701573
}
15711574

15721575
return this.nativeInput;
15731576
}
15741577

15751578
getInputDOMRefSync() {
1576-
if (isPhone() && this.Suggestions?._getPicker()) {
1577-
return this.Suggestions._getPicker().querySelector(".ui5-input-inner-phone")!.shadowRoot!.querySelector<HTMLInputElement>("input")!;
1579+
if (isPhone() && this.Suggestions) {
1580+
const picker = this.Suggestions._getPicker();
1581+
if (picker) {
1582+
return picker.querySelector(".ui5-input-inner-phone")!.shadowRoot!.querySelector<HTMLInputElement>("input")!;
1583+
}
15781584
}
15791585

15801586
return this.nativeInput;

packages/main/src/features/InputSuggestionsTemplate.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default function InputSuggestionsTemplate(this: Input, hooks?: { suggesti
7272
</div>
7373
}
7474

75-
{ suggestionsList.call(this) }
75+
{ this.showSuggestions && suggestionsList.call(this) }
7676

7777
{this._isPhone &&
7878
<div slot="footer" class="ui5-responsive-popover-footer">

packages/main/test/pages/Input.html

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,15 @@ <h3>Input with open suggestions on focusin</h3>
453453
<br>
454454
<br>
455455

456+
<h3>Input with Dynamic Suggestions Triggering (Threshold = 3)</h3>
457+
<ui5-input id="dynamicTriggerInput" placeholder="Type at least 3 characters to see suggestions..." show-suggestions></ui5-input>
458+
<div>
459+
<small>Suggestions will appear after typing 3 or more characters</small>
460+
</div>
461+
462+
<br>
463+
<br>
464+
456465
<h3>Input - showing wrapping</h3>
457466
<ui5-input show-suggestions id="input-change-wrapping">
458467
<ui5-suggestion-item text="Item that does not wrap"></ui5-suggestion-item>
@@ -1113,6 +1122,46 @@ <h3>Input Composition</h3>
11131122
document.getElementById("prevent-input-event-clear-icon").addEventListener("ui5-input", event => {
11141123
event.preventDefault();
11151124
});
1125+
1126+
// Dynamic Suggestions Triggering with Threshold
1127+
const dynamicTriggerInput = document.getElementById("dynamicTriggerInput");
1128+
const THRESHOLD = 3;
1129+
1130+
const countriesForDynamicTrigger = [
1131+
"Albania", "Andorra", "Armenia", "Austria", "Azerbaijan",
1132+
"Belarus", "Belgium", "Bosnia and Herzegovina", "Bulgaria",
1133+
"Croatia", "Cyprus", "Czech Republic", "Denmark",
1134+
"Estonia", "Finland", "France", "Georgia", "Germany", "Greece",
1135+
"Hungary", "Iceland", "Ireland", "Italy", "Kazakhstan",
1136+
"Kosovo", "Latvia", "Liechtenstein", "Lithuania", "Luxembourg",
1137+
"Malta", "Moldova", "Monaco", "Montenegro", "Netherlands",
1138+
"North Macedonia", "Norway", "Poland", "Portugal", "Romania",
1139+
"Russia", "San Marino", "Serbia", "Slovakia", "Slovenia",
1140+
"Spain", "Sweden", "Switzerland", "Turkey", "Ukraine",
1141+
"United Kingdom", "Vatican City"
1142+
];
1143+
1144+
dynamicTriggerInput.addEventListener("input", () => {
1145+
const value = dynamicTriggerInput.value;
1146+
1147+
while (dynamicTriggerInput.lastChild) {
1148+
dynamicTriggerInput.removeChild(dynamicTriggerInput.lastChild);
1149+
}
1150+
1151+
if (value.length >= THRESHOLD) {
1152+
dynamicTriggerInput.showSuggestions = true;
1153+
1154+
const filteredCountries = countriesForDynamicTrigger.filter(country =>
1155+
country.toLowerCase().startsWith(value.toLowerCase())
1156+
);
1157+
1158+
filteredCountries.forEach(country => {
1159+
const suggestion = document.createElement("ui5-suggestion-item");
1160+
suggestion.text = country;
1161+
dynamicTriggerInput.appendChild(suggestion);
1162+
});
1163+
}
1164+
});
11161165
</script>
11171166
</body>
11181167

packages/website/docs/_components_pages/main/Input/Input.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Basic from "../../../_samples/main/Input/Basic/Basic.md";
66
import Suggestions from "../../../_samples/main/Input/Suggestions/Suggestions.md";
77
import ClearIcon from "../../../_samples/main/Input/ClearIcon/ClearIcon.md";
88
import SuggestionsWrapping from "../../../_samples/main/Input/SuggestionsWrapping/SuggestionsWrapping.md";
9+
import DynamicSuggestions from "../../../_samples/main/Input/DynamicSuggestions/DynamicSuggestions.md";
910
import ValueStateMessage from "../../../_samples/main/Input/ValueStateMessage/ValueStateMessage.md";
1011
import Label from "../../../_samples/main/Input/Label/Label.md";
1112
import ValueHelpDialog from "../../../_samples/main/Input/ValueHelpDialog/ValueHelpDialog.md";
@@ -37,6 +38,12 @@ The sample demonstrates how the text of the suggestions wrap when too long.
3738

3839
<SuggestionsWrapping />
3940

41+
### Dynamic Suggestion Control
42+
This sample demonstrates how applications can control when suggestions appear by dynamically toggling the <b>showSuggestions</b> property.
43+
In this example, suggestions are only shown when the user has typed 3 or more characters.
44+
45+
<DynamicSuggestions />
46+
4047
### Input and Label
4148
<Label />
4249

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import html from '!!raw-loader!./sample.html';
2+
import js from '!!raw-loader!./main.js';
3+
4+
<Editor html={html} js={js} />
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import "@ui5/webcomponents/dist/Input.js";
2+
import "@ui5/webcomponents/dist/SuggestionItem.js";
3+
import "@ui5/webcomponents/dist/features/InputSuggestions.js";
4+
5+
const THRESHOLD = 3;
6+
7+
const countries = [
8+
"Argentina", "Albania", "Algeria", "Angola", "Austria", "Australia",
9+
"Bulgaria", "Belgium", "Brazil", "Canada", "Columbia", "Croatia",
10+
"Denmark", "England", "Finland", "France", "Germany", "Greece",
11+
"Hungary", "Ireland", "Italy", "Japan", "Kuwait", "Luxembourg",
12+
"Mexico", "Morocco", "Netherlands", "Norway", "Paraguay", "Philippines",
13+
"Portugal", "Romania", "Spain", "Sweden", "Switzerland", "Sri Lanka",
14+
"Senegal", "Thailand", "The United Kingdom of Great Britain and Northern Ireland",
15+
"USA", "Ukraine", "Vietnam"
16+
];
17+
18+
let suggestionItems = [];
19+
20+
const input = document.getElementById("input-threshold-3");
21+
22+
input.addEventListener("input", () => {
23+
const value = input.value;
24+
25+
// Clear existing suggestions
26+
while (input.lastChild) {
27+
input.removeChild(input.lastChild);
28+
}
29+
30+
if (value.length >= THRESHOLD) {
31+
// Enable suggestions and typeahead when threshold is met
32+
input.showSuggestions = true;
33+
34+
// Filter and add matching suggestions
35+
suggestionItems = countries.filter((item) => {
36+
return item.toUpperCase().indexOf(value.toUpperCase()) === 0;
37+
});
38+
39+
suggestionItems.forEach((item) => {
40+
const li = document.createElement("ui5-suggestion-item");
41+
li.text = item;
42+
input.appendChild(li);
43+
});
44+
}
45+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!-- playground-fold -->
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
5+
<head>
6+
<meta charset="UTF-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<title>Sample</title>
9+
</head>
10+
11+
<body style="background-color: var(--sapBackgroundColor); height: 400px;">
12+
<!-- playground-fold-end -->
13+
14+
<ui5-input id="input-threshold-3" placeholder="Start typing (threshold: 3 chars)" show-suggestions></ui5-input>
15+
16+
<!-- playground-fold -->
17+
<script type="module" src="main.js"></script>
18+
</body>
19+
20+
</html>
21+
<!-- playground-fold-end -->

0 commit comments

Comments
 (0)