Skip to content

Commit b7e185f

Browse files
authored
feat(ui5-search): add new property fieldLoading (#12846)
## Description Adds a `fieldLoading` property to `ui5-search-field` that displays a loading indicator during async search operations. ## Changes - **SearchField component**: Added `fieldLoading` boolean property (default: false) - **Collapsed mode**: Shows loading state on the search button - **Expanded mode**: Activates BusyIndicator wrapper around the input field - **Tests**: Added Cypress tests covering both collapsed and expanded modes - **Documentation**: Added "Loading State" sample demonstrating async search with 2-second delay - **Test pages**: Added interactive examples to SearchField.html and Search.html ## Use Case When performing async search operations (API calls, database queries), developers can now indicate loading state to users: ```js searchField.addEventListener('ui5-search', async () => { searchField.fieldLoading = true; const results = await fetchSearchResults(searchField.value); searchField.fieldLoading = false; });
1 parent e93a0ad commit b7e185f

10 files changed

Lines changed: 188 additions & 63 deletions

File tree

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,4 +507,44 @@ describe("SearchField general interaction", () => {
507507
.should("not.exist");
508508
});
509509
});
510+
511+
describe("Field Loading", () => {
512+
it("shows loading indicator on button in collapsed mode", () => {
513+
cy.mount(<SearchField collapsed={true} fieldLoading={true}></SearchField>);
514+
515+
cy.get("[ui5-search-field]")
516+
.shadow()
517+
.find("[ui5-button]")
518+
.should("have.attr", "loading");
519+
});
520+
521+
it("does not show loading indicator on button when fieldLoading is false in collapsed mode", () => {
522+
cy.mount(<SearchField collapsed={true} fieldLoading={false}></SearchField>);
523+
524+
cy.get("[ui5-search-field]")
525+
.shadow()
526+
.find("[ui5-button]")
527+
.should("not.have.attr", "loading");
528+
});
529+
530+
it("shows BusyIndicator in expanded mode when fieldLoading is true", () => {
531+
cy.mount(<SearchField fieldLoading={true}></SearchField>);
532+
533+
cy.get("[ui5-search-field]")
534+
.shadow()
535+
.find("[ui5-busy-indicator]")
536+
.should("exist")
537+
.should("have.attr", "active");
538+
});
539+
540+
it("BusyIndicator is not active in expanded mode when fieldLoading is false", () => {
541+
cy.mount(<SearchField fieldLoading={false}></SearchField>);
542+
543+
cy.get("[ui5-search-field]")
544+
.shadow()
545+
.find("[ui5-busy-indicator]")
546+
.should("exist")
547+
.should("not.have.attr", "active");
548+
});
549+
});
510550
});

packages/fiori/src/SearchField.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ class SearchField extends UI5Element {
101101
"scope-change": SearchFieldScopeSelectionChangeDetails,
102102
}
103103

104+
/**
105+
* Indicates whether a loading indicator should be shown in the input field.
106+
* @default false
107+
* @since 2.19.0
108+
* @public
109+
*/
110+
@property({ type: Boolean })
111+
fieldLoading = false
112+
104113
/**
105114
* Defines whether the clear icon of the search will be shown.
106115
* @default false

packages/fiori/src/SearchFieldTemplate.tsx

Lines changed: 67 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type SearchField from "./SearchField.js";
66
import decline from "@ui5/webcomponents-icons/dist/decline.js";
77
import search from "@ui5/webcomponents-icons/dist/search.js";
88
import ButtonDesign from "@ui5/webcomponents/dist/types/ButtonDesign.js";
9+
import BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js";
910

1011
export type SearchFieldTemplateOptions = {
1112
/**
@@ -22,81 +23,84 @@ export default function SearchFieldTemplate(this: SearchField, options?: SearchF
2223
icon={search}
2324
design={ButtonDesign.Transparent}
2425
data-sap-focus-ref
26+
loading={this.fieldLoading}
2527
onClick={this._handleSearchIconPress}
2628
tooltip={this._effectiveIconTooltip}
2729
accessibleName={this._effectiveIconTooltip}
2830
accessibilityAttributes={this._searchButtonAccessibilityAttributes}
2931
></Button>
3032
) : (
31-
<div class="ui5-search-field-root" role="search" onFocusOut={this._onFocusOutSearch}>
32-
<div class="ui5-search-field-content">
33-
{this.scopes?.length ? (
34-
<>
35-
<Select
36-
onChange={this._handleScopeChange}
37-
class="sapUiSizeCompact ui5-search-field-select"
38-
accessibleName={this._translations.scope}
39-
tooltip={this._translations.scope}
40-
value={this.scopeValue}
41-
>
42-
{this.scopes.map(scopeOption => (
43-
<Option
44-
value={scopeOption.value}
45-
data-ui5-stable={scopeOption.stableDomRef}
46-
ref={this.captureRef.bind(scopeOption)}
47-
>{scopeOption.text}
48-
</Option>
49-
))}
50-
</Select>
51-
<div class="ui5-search-field-separator"></div>
52-
</>
53-
) : this.filterButton?.length ? (
54-
<>
55-
<div class="ui5-filter-wrapper" style="display: contents">
56-
<slot name="filterButton"></slot>
57-
</div>
58-
<div class="ui5-search-field-separator"></div>
59-
</>
60-
) : null}
33+
<BusyIndicator class="ui5-search-field-busy-indicator" active={this.fieldLoading}>
34+
<div class="ui5-search-field-root" role="search" onFocusOut={this._onFocusOutSearch}>
35+
<div class="ui5-search-field-content">
36+
{this.scopes?.length ? (
37+
<>
38+
<Select
39+
onChange={this._handleScopeChange}
40+
class="sapUiSizeCompact ui5-search-field-select"
41+
accessibleName={this._translations.scope}
42+
tooltip={this._translations.scope}
43+
value={this.scopeValue}
44+
>
45+
{this.scopes.map(scopeOption => (
46+
<Option
47+
value={scopeOption.value}
48+
data-ui5-stable={scopeOption.stableDomRef}
49+
ref={this.captureRef.bind(scopeOption)}
50+
>{scopeOption.text}
51+
</Option>
52+
))}
53+
</Select>
54+
<div class="ui5-search-field-separator"></div>
55+
</>
56+
) : this.filterButton?.length ? (
57+
<>
58+
<div class="ui5-filter-wrapper" style="display: contents">
59+
<slot name="filterButton"></slot>
60+
</div>
61+
<div class="ui5-search-field-separator"></div>
62+
</>
63+
) : null}
6164

62-
<input
63-
class="ui5-search-field-inner-input"
64-
role="searchbox"
65-
aria-description={this.accessibleDescription}
66-
aria-label={this.accessibleName || this._translations.searchFieldAriaLabel}
67-
aria-autocomplete="both"
68-
aria-controls="ui5-search-list"
69-
value={this.value}
70-
placeholder={this.placeholder}
71-
data-sap-focus-ref
72-
onInput={this._handleInput}
73-
onFocusIn={this._onfocusin}
74-
onFocusOut={this._onfocusout}
75-
onKeyDown={this._onkeydown}
76-
onClick={this._handleInnerClick} />
65+
<input
66+
class="ui5-search-field-inner-input"
67+
role="searchbox"
68+
aria-description={this.accessibleDescription}
69+
aria-label={this.accessibleName || this._translations.searchFieldAriaLabel}
70+
aria-autocomplete="both"
71+
aria-controls="ui5-search-list"
72+
value={this.value}
73+
placeholder={this.placeholder}
74+
data-sap-focus-ref
75+
onInput={this._handleInput}
76+
onFocusIn={this._onfocusin}
77+
onFocusOut={this._onfocusout}
78+
onKeyDown={this._onkeydown}
79+
onClick={this._handleInnerClick} />
80+
81+
{this._effectiveShowClearIcon &&
82+
<Icon
83+
class="ui5-shell-search-field-icon"
84+
name={decline}
85+
showTooltip={true}
86+
accessibleName={this._translations.clearIcon}
87+
onClick={this._handleClear}
88+
></Icon>
89+
}
7790

78-
{this._effectiveShowClearIcon &&
7991
<Icon
80-
class="ui5-shell-search-field-icon"
81-
name={decline}
92+
class={{
93+
"ui5-shell-search-field-icon": true,
94+
"ui5-shell-search-field-search-icon": this._isSearchIcon,
95+
}}
96+
name={search}
8297
showTooltip={true}
83-
accessibleName={this._translations.clearIcon}
84-
onClick={this._handleClear}
98+
accessibleName={this._effectiveIconTooltip}
99+
onClick={this._handleSearchIconPress}
85100
></Icon>
86-
}
87-
88-
<Icon
89-
class={{
90-
"ui5-shell-search-field-icon": true,
91-
"ui5-shell-search-field-search-icon": this._isSearchIcon,
92-
}}
93-
name={search}
94-
showTooltip={true}
95-
accessibleName={this._effectiveIconTooltip}
96-
onClick={this._handleSearchIconPress}
97-
></Icon>
101+
</div>
98102
</div>
99-
</div>
103+
</BusyIndicator>
100104
)
101105
);
102106
}

packages/fiori/src/themes/SearchField.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
position: relative;
2626
}
2727

28+
.ui5-search-field-busy-indicator {
29+
width: 100%;
30+
height: 100%;
31+
border-radius: var(--_ui5_search_input_border_radius);
32+
}
33+
2834
.ui5-shellbar-search-field-wrapper {
2935
flex: 1;
3036
min-width: auto;

packages/fiori/test/pages/Search.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@
253253
</ui5-search>
254254
</div>
255255

256+
<div class="container" style="padding-top: 1rem;">
257+
<ui5-label>Search with field loading state (type and press Enter)</ui5-label>
258+
<ui5-search id="search-with-loading" show-clear-icon placeholder="Search..."></ui5-search>
259+
</div>
260+
256261
<div class="container last" style="padding-top: 1rem;">
257262
<ui5-label>Search with lazy loaded Suggestions - Autocomplete and highlighting</ui5-label>
258263
<ui5-search id="search-lazy" show-clear-icon placeholder="Type 'a'..."></ui5-search>
@@ -450,6 +455,15 @@
450455
}, 300);
451456
}
452457
});
458+
459+
const searchWithLoading = document.getElementById('search-with-loading');
460+
searchWithLoading.addEventListener('ui5-search', async (e) => {
461+
if (e.target.value) {
462+
e.target.fieldLoading = true;
463+
await new Promise(resolve => setTimeout(resolve, 2000));
464+
e.target.fieldLoading = false;
465+
}
466+
});
453467
</script>
454468
</body>
455469
</html>

packages/fiori/test/pages/SearchField.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
<ui5-search-field>
4343
<ui5-button slot="filterButton" icon="filter"></ui5-button>
4444
</ui5-search-field>
45+
<div class="container" style="padding-top: 1rem; display: flex; flex-direction: column;">
46+
<ui5-label>Search with loading state (click search icon)</ui5-label>
47+
<ui5-search-field id="search-loading" placeholder="Search..." show-clear-icon></ui5-search-field>
48+
</div>
4549
<div class="container" style="padding-top: 1rem; display: flex; flex-direction: column;">
4650
<ui5-label>Collapsed search</ui5-label>
4751
<div class="container" style="border: 1px solid black; display: flex; padding: 4px; justify-content: flex-end;">
@@ -87,6 +91,13 @@
8791
scopedSearch.addEventListener('ui5-scope-change', (event) => {
8892
console.log('scope-change', event.detail.scope);
8993
});
94+
95+
const searchLoading = document.getElementById('search-loading');
96+
searchLoading.addEventListener('ui5-search', async () => {
97+
searchLoading.fieldLoading = true;
98+
await new Promise(resolve => setTimeout(resolve, 2000));
99+
searchLoading.fieldLoading = false;
100+
});
90101
</script>
91102
</body>
92103
</html>

packages/website/docs/_components_pages/fiori/Search/Search.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Byline from "../../../_samples/fiori/Search/Byline/Byline.md";
88
import AdvancedFilter from "../../../_samples/fiori/Search/AdvancedFilter/AdvancedFilter.md"
99
import ShowMore from "../../../_samples/fiori/Search/ShowMore/ShowMore.md"
1010
import Actions from "../../../_samples/fiori/Search/Actions/Actions.md"
11+
import Loading from "../../../_samples/fiori/Search/Loading/Loading.md"
1112

1213
<%COMPONENT_OVERVIEW%>
1314

@@ -45,3 +46,8 @@ This example shows how to use a interactive elements in search items.
4546

4647
<Actions />
4748

49+
### Loading State
50+
This example shows the loading indicator during async search operations.
51+
52+
<Loading />
53+
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import "@ui5/webcomponents-fiori/dist/SearchField.js";
2+
import "@ui5/webcomponents/dist/Label.js";
3+
import "@ui5/webcomponents/dist/Text.js";
4+
5+
const searchField = document.getElementById("search-loading");
6+
const resultText = document.getElementById("result-text");
7+
8+
searchField.addEventListener("ui5-search", async (event) => {
9+
const query = searchField.value;
10+
11+
// Show loading indicator
12+
searchField.fieldLoading = true;
13+
resultText.textContent = `Searching for "${query}"...`;
14+
15+
// Simulate async search operation
16+
await new Promise(resolve => setTimeout(resolve, 2000));
17+
18+
// Hide loading indicator and show results
19+
searchField.fieldLoading = false;
20+
resultText.textContent = `Search completed for "${query}". Found 5 results.`;
21+
});
22+
23+
searchField.addEventListener("ui5-input", () => {
24+
if (!searchField.value) {
25+
resultText.textContent = "Enter a search term and press Enter or click the search icon";
26+
}
27+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<ui5-search-field id="search-loading" placeholder="Search..."></ui5-search-field>
2+
3+
<ui5-label style="margin-top: 1rem; display: block;">Result:</ui5-label>
4+
<ui5-text id="result-text">Enter a search term and press Enter or click the search icon</ui5-text>

0 commit comments

Comments
 (0)