Skip to content

Commit 32077a8

Browse files
authored
fix(ui5-search): fix opening and closing of the search items popover (#12241)
1 parent cd08a6a commit 32077a8

File tree

3 files changed

+225
-26
lines changed

3 files changed

+225
-26
lines changed

packages/fiori/cypress/specs/Search.cy.tsx

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Button from "@ui5/webcomponents/dist/Button.js";
1111
import ButtonDesign from "@ui5/webcomponents/dist/types/ButtonDesign.js";
1212
import Avatar from "@ui5/webcomponents/dist/Avatar.js";
1313
import AvatarSize from "@ui5/webcomponents/dist/types/AvatarSize.js";
14+
import type ResponsivePopover from "@ui5/webcomponents/dist/ResponsivePopover.js";
1415
import { SEARCH_ITEM_SHOW_MORE_COUNT, SEARCH_ITEM_SHOW_MORE_NO_COUNT } from "../../src/generated/i18n/i18n-defaults.js";
1516

1617
describe("Properties", () => {
@@ -791,7 +792,7 @@ describe("Events", () => {
791792

792793
cy.get("[ui5-search]")
793794
.then(search => {
794-
search.get(0).addEventListener("ui5-close", cy.stub().as("closed"));
795+
search.get(0).addEventListener("ui5-close", cy.spy().as("closed"));
795796
});
796797

797798
cy.get("[ui5-search]")
@@ -818,7 +819,7 @@ describe("Events", () => {
818819

819820
cy.get("[ui5-search]")
820821
.then(search => {
821-
search.get(0).addEventListener("ui5-close", cy.stub().as("closed"));
822+
search.get(0).addEventListener("ui5-close", cy.spy().as("closed"));
822823
});
823824

824825
cy.get("[ui5-search]")
@@ -1075,6 +1076,172 @@ describe("Events", () => {
10751076
cy.get("ui5-search-item").eq(0)
10761077
.should("not.have.attr", "selected");
10771078
});
1079+
1080+
it("should reset suggestions highlight on pressing 'clear' button", () => {
1081+
cy.mount(
1082+
<Search showClearIcon>
1083+
<SearchItem text="Item 1" />
1084+
</Search>
1085+
);
1086+
1087+
cy.get("[ui5-search]").as("search");
1088+
1089+
cy.get("@search")
1090+
.shadow()
1091+
.find("input")
1092+
.as("input");
1093+
1094+
cy.get("@input")
1095+
.realClick();
1096+
1097+
cy.get("@search")
1098+
.should("be.focused");
1099+
1100+
cy.get("@input")
1101+
.realPress("I");
1102+
1103+
cy.get("@search")
1104+
.should("have.value", "Item 1");
1105+
1106+
cy.get("[ui5-search-item]").eq(0)
1107+
.should("have.attr", "highlight-text", "I");
1108+
1109+
cy.get("@search")
1110+
.shadow()
1111+
.find("[ui5-icon][name='decline']")
1112+
.realClick();
1113+
1114+
cy.get("@search")
1115+
.should("have.value", "");
1116+
1117+
cy.get("@search")
1118+
.should("not.have.attr", "open");
1119+
1120+
cy.get("@search")
1121+
.invoke("prop", "open", true);
1122+
1123+
cy.get("ui5-search-item").eq(0)
1124+
.should("have.attr", "highlight-text", "");
1125+
});
1126+
1127+
it("should close the popover on search if no suggestion is selected", () => {
1128+
cy.mount(
1129+
<Search showClearIcon>
1130+
<SearchItem text="Item 1" />
1131+
</Search>
1132+
);
1133+
1134+
cy.get("[ui5-search]").as("search");
1135+
1136+
cy.get("@search")
1137+
.shadow()
1138+
.find("input")
1139+
.as("input");
1140+
1141+
cy.get("@input")
1142+
.realClick();
1143+
1144+
cy.get("@search")
1145+
.should("be.focused");
1146+
1147+
cy.get("@input")
1148+
.realPress("P"); // no matching suggestion
1149+
1150+
cy.get("@search")
1151+
.should("have.value", "P");
1152+
1153+
cy.get("@search")
1154+
.shadow()
1155+
.find("[ui5-icon][name='search']")
1156+
.realClick();
1157+
1158+
cy.get("@search")
1159+
.should("not.have.attr", "open");
1160+
});
1161+
1162+
it("should close the popover on 'search' if suggestion is selected", () => {
1163+
cy.mount(
1164+
<Search showClearIcon>
1165+
<SearchItem text="Item 1" />
1166+
</Search>
1167+
);
1168+
1169+
cy.get("[ui5-search]").as("search");
1170+
1171+
cy.get("@search")
1172+
.shadow()
1173+
.find("input")
1174+
.as("input");
1175+
1176+
cy.get("@input")
1177+
.realClick();
1178+
1179+
cy.get("@search")
1180+
.should("be.focused");
1181+
1182+
cy.get("@input")
1183+
.realPress("I"); // no matching suggestion
1184+
1185+
cy.get("@search")
1186+
.should("have.value", "Item 1");
1187+
1188+
cy.get("@search")
1189+
.shadow()
1190+
.find("[ui5-icon][name='search']")
1191+
.realClick();
1192+
1193+
cy.get("@search")
1194+
.should("not.have.attr", "open");
1195+
});
1196+
1197+
it("should open picker by default when 'open' property is set to true", () => {
1198+
cy.mount(
1199+
<Search open>
1200+
<SearchItem text="Item 1" />
1201+
</Search>
1202+
);
1203+
1204+
cy.get("[ui5-search]")
1205+
.shadow()
1206+
.find<ResponsivePopover>("[ui5-responsive-popover]")
1207+
.ui5ResponsivePopoverOpened();
1208+
});
1209+
1210+
it("should not open picker if text is deleted and there are no items", () => {
1211+
const handleInput = (e: any) => {
1212+
if (e.target.value) {
1213+
const item = document.createElement("ui5-search-item");
1214+
item.setAttribute("text", e.target.value);
1215+
e.target.appendChild(item);
1216+
} else {
1217+
e.target.innerHTML = "";
1218+
}
1219+
};
1220+
1221+
cy.mount(<Search showClearIcon noTypeahead onInput={handleInput}></Search>);
1222+
1223+
cy.get("[ui5-search]").as("search");
1224+
1225+
cy.get("@search")
1226+
.shadow()
1227+
.find("input")
1228+
.as("input");
1229+
1230+
cy.get("@input")
1231+
.realClick();
1232+
1233+
cy.get("@input")
1234+
.realPress("I");
1235+
1236+
cy.get("@input")
1237+
.realPress("Backspace");
1238+
1239+
cy.get("@search")
1240+
.should("have.value", "");
1241+
1242+
cy.get("@search")
1243+
.should("not.have.attr", "open");
1244+
});
10781245
});
10791246

10801247
describe("Accessibility", () => {

packages/fiori/src/Search.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ class Search extends SearchField {
213213
*/
214214
_proposedItem?: ISearchSuggestionItem;
215215

216+
/**
217+
* This property is used during rendering to indicate that the user has started typing in the input
218+
* @private
219+
*/
220+
_isTyping: boolean;
221+
216222
@i18n("@ui5/webcomponents-fiori")
217223
static i18nBundle: I18nBundle;
218224

@@ -222,26 +228,32 @@ class Search extends SearchField {
222228
// The typed in value.
223229
this._typedInValue = "";
224230
this._valueBeforeOpen = this.getAttribute("value") || "";
231+
this._isTyping = false;
225232
}
226233

227234
onBeforeRendering() {
228235
super.onBeforeRendering();
229236

237+
if (this.collapsed && !isPhone()) {
238+
this.open = false;
239+
return;
240+
}
241+
230242
const innerInput = this.nativeInput;
231243
const autoCompletedChars = innerInput && (innerInput.selectionEnd! - innerInput.selectionStart!);
232244

245+
this.open = this.open || (this._popoupHasAnyContent() && this._isTyping && innerInput!.value.length > 0);
246+
233247
// If there is already a selection the autocomplete has already been performed
234248
if (this._shouldAutocomplete && !autoCompletedChars) {
235249
const item = this._getFirstMatchingItem(this.value);
236250
this._proposedItem = item;
237251

238-
if (!isPhone()) {
239-
this.open = this._popoupHasAnyContent();
240-
}
241-
242252
if (item) {
243253
this._handleTypeAhead(item);
244254
this._selectMatchingItem(item);
255+
} else {
256+
this._deselectItems();
245257
}
246258
}
247259

@@ -311,8 +323,6 @@ class Search extends SearchField {
311323
this._innerValue = originalValue;
312324
this._performTextSelection = true;
313325
this.value = originalValue;
314-
315-
this._shouldAutocomplete = false;
316326
}
317327

318328
_startsWithMatchingItems(str: string): Array<ISearchSuggestionItem> {
@@ -383,6 +393,7 @@ class Search extends SearchField {
383393

384394
innerInput.setSelectionRange(this.value.length, this.value.length);
385395
this.open = false;
396+
this._isTyping = false;
386397
}
387398

388399
_onMobileInputKeydown(e: KeyboardEvent) {
@@ -401,6 +412,7 @@ class Search extends SearchField {
401412
_handleEscape() {
402413
this.value = this._typedInValue || this.value;
403414
this._innerValue = this.value;
415+
this._isTyping = false;
404416
}
405417

406418
_handleInput(e: InputEvent) {
@@ -411,7 +423,17 @@ class Search extends SearchField {
411423
return;
412424
}
413425

414-
this.open = ((e.currentTarget as HTMLInputElement).value.length > 0) && this._popoupHasAnyContent();
426+
this._isTyping = true;
427+
this.open = this.value.length > 0;
428+
}
429+
430+
_handleClear(): void {
431+
super._handleClear();
432+
433+
this._typedInValue = "";
434+
this._innerValue = "";
435+
this._shouldAutocomplete = false;
436+
this.open = false;
415437
}
416438

417439
_popoupHasAnyContent() {
@@ -466,7 +488,10 @@ class Search extends SearchField {
466488
this.value = item.text;
467489
this._innerValue = this.value;
468490
this._typedInValue = this.value;
491+
this._shouldAutocomplete = false;
492+
this._performTextSelection = true;
469493
this.open = false;
494+
this._isTyping = false;
470495
this.focus();
471496
}
472497

@@ -502,6 +527,7 @@ class Search extends SearchField {
502527
}
503528

504529
this.open = false;
530+
this._isTyping = false;
505531
}
506532

507533
_handleBeforeClose(e: CustomEvent<PopupBeforeCloseEventDetail>) {
@@ -518,6 +544,7 @@ class Search extends SearchField {
518544

519545
_handleClose() {
520546
this.open = false;
547+
this._isTyping = false;
521548
this.fireDecoratorEvent("close");
522549
}
523550

packages/fiori/test/pages/Search.html

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -403,24 +403,29 @@
403403

404404
const searchLazy = document.getElementById('search-lazy');
405405
searchLazy.addEventListener('ui5-input', (e) => {
406-
// clear search items
407-
searchLazy.innerHTML = '';
408-
409-
searchLazy.getSlottedNodes("items").forEach(item => {
410-
item.remove();
411-
});
412-
413-
// simulate lazy loading
414406
setTimeout(() => {
415-
const lazyData = [
416-
{ name: 'Red Apple', category: 'Fruit' },
417-
{ name: 'Apple', category: 'Fruit' },
418-
{ name: 'Banana', category: 'Fruit' },
419-
{ name: 'Orange', category: 'Fruit' },
420-
{ name: 'Grapes', category: 'Fruit' },
421-
];
422-
createItems(searchLazy, lazyData);
423-
}, 100);
407+
// clear search items
408+
searchLazy.innerHTML = '';
409+
410+
searchLazy.getSlottedNodes("items").forEach(item => {
411+
item.remove();
412+
});
413+
}, 100)
414+
415+
416+
if(e.target.value.length){
417+
// simulate lazy loading
418+
setTimeout(() => {
419+
const lazyData = [
420+
{ name: `Red Apple ${Math.random()}`, category: 'Fruit' },
421+
{ name: `Apple ${Math.random()}`, category: 'Fruit' },
422+
{ name: `Banana ${Math.random()}`, category: 'Fruit' },
423+
{ name: `Orange ${Math.random()}`, category: 'Fruit' },
424+
{ name: `Grapes ${Math.random()}`, category: 'Fruit' },
425+
];
426+
createItems(searchLazy, lazyData);
427+
}, 300);
428+
}
424429
});
425430
</script>
426431
</body>

0 commit comments

Comments
 (0)