Skip to content
Merged
40 changes: 40 additions & 0 deletions packages/fiori/cypress/specs/SearchField.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,44 @@ describe("SearchField general interaction", () => {
.should("not.exist");
});
});

describe("Field Loading", () => {
it("shows loading indicator on button in collapsed mode", () => {
cy.mount(<SearchField collapsed={true} fieldLoading={true}></SearchField>);

cy.get("[ui5-search-field]")
.shadow()
.find("[ui5-button]")
.should("have.attr", "loading");
});

it("does not show loading indicator on button when fieldLoading is false in collapsed mode", () => {
cy.mount(<SearchField collapsed={true} fieldLoading={false}></SearchField>);

cy.get("[ui5-search-field]")
.shadow()
.find("[ui5-button]")
.should("not.have.attr", "loading");
});

it("shows BusyIndicator in expanded mode when fieldLoading is true", () => {
cy.mount(<SearchField fieldLoading={true}></SearchField>);

cy.get("[ui5-search-field]")
.shadow()
.find("[ui5-busy-indicator]")
.should("exist")
.should("have.attr", "active");
});

it("BusyIndicator is not active in expanded mode when fieldLoading is false", () => {
cy.mount(<SearchField fieldLoading={false}></SearchField>);

cy.get("[ui5-search-field]")
.shadow()
.find("[ui5-busy-indicator]")
.should("exist")
.should("not.have.attr", "active");
});
});
});
9 changes: 9 additions & 0 deletions packages/fiori/src/SearchField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ class SearchField extends UI5Element {
"scope-change": SearchFieldScopeSelectionChangeDetails,
}

/**
* Indicates whether a loading indicator should be shown in the input field.
* @default false
* @since 2.19.0
* @public
*/
@property({ type: Boolean })
fieldLoading = false

/**
* Defines whether the clear icon of the search will be shown.
* @default false
Expand Down
130 changes: 67 additions & 63 deletions packages/fiori/src/SearchFieldTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type SearchField from "./SearchField.js";
import decline from "@ui5/webcomponents-icons/dist/decline.js";
import search from "@ui5/webcomponents-icons/dist/search.js";
import ButtonDesign from "@ui5/webcomponents/dist/types/ButtonDesign.js";
import BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js";

export type SearchFieldTemplateOptions = {
/**
Expand All @@ -22,81 +23,84 @@ export default function SearchFieldTemplate(this: SearchField, options?: SearchF
icon={search}
design={ButtonDesign.Transparent}
data-sap-focus-ref
loading={this.fieldLoading}
onClick={this._handleSearchIconPress}
tooltip={this._effectiveIconTooltip}
accessibleName={this._effectiveIconTooltip}
accessibilityAttributes={this._searchButtonAccessibilityAttributes}
></Button>
) : (
<div class="ui5-search-field-root" role="search" onFocusOut={this._onFocusOutSearch}>
<div class="ui5-search-field-content">
{this.scopes?.length ? (
<>
<Select
onChange={this._handleScopeChange}
class="sapUiSizeCompact ui5-search-field-select"
accessibleName={this._translations.scope}
tooltip={this._translations.scope}
value={this.scopeValue}
>
{this.scopes.map(scopeOption => (
<Option
value={scopeOption.value}
data-ui5-stable={scopeOption.stableDomRef}
ref={this.captureRef.bind(scopeOption)}
>{scopeOption.text}
</Option>
))}
</Select>
<div class="ui5-search-field-separator"></div>
</>
) : this.filterButton?.length ? (
<>
<div class="ui5-filter-wrapper" style="display: contents">
<slot name="filterButton"></slot>
</div>
<div class="ui5-search-field-separator"></div>
</>
) : null}
<BusyIndicator class="ui5-search-field-busy-indicator" active={this.fieldLoading}>
<div class="ui5-search-field-root" role="search" onFocusOut={this._onFocusOutSearch}>
<div class="ui5-search-field-content">
{this.scopes?.length ? (
<>
<Select
onChange={this._handleScopeChange}
class="sapUiSizeCompact ui5-search-field-select"
accessibleName={this._translations.scope}
tooltip={this._translations.scope}
value={this.scopeValue}
>
{this.scopes.map(scopeOption => (
<Option
value={scopeOption.value}
data-ui5-stable={scopeOption.stableDomRef}
ref={this.captureRef.bind(scopeOption)}
>{scopeOption.text}
</Option>
))}
</Select>
<div class="ui5-search-field-separator"></div>
</>
) : this.filterButton?.length ? (
<>
<div class="ui5-filter-wrapper" style="display: contents">
<slot name="filterButton"></slot>
</div>
<div class="ui5-search-field-separator"></div>
</>
) : null}

<input
class="ui5-search-field-inner-input"
role="searchbox"
aria-description={this.accessibleDescription}
aria-label={this.accessibleName || this._translations.searchFieldAriaLabel}
aria-autocomplete="both"
aria-controls="ui5-search-list"
value={this.value}
placeholder={this.placeholder}
data-sap-focus-ref
onInput={this._handleInput}
onFocusIn={this._onfocusin}
onFocusOut={this._onfocusout}
onKeyDown={this._onkeydown}
onClick={this._handleInnerClick} />
<input
class="ui5-search-field-inner-input"
role="searchbox"
aria-description={this.accessibleDescription}
aria-label={this.accessibleName || this._translations.searchFieldAriaLabel}
aria-autocomplete="both"
aria-controls="ui5-search-list"
value={this.value}
placeholder={this.placeholder}
data-sap-focus-ref
onInput={this._handleInput}
onFocusIn={this._onfocusin}
onFocusOut={this._onfocusout}
onKeyDown={this._onkeydown}
onClick={this._handleInnerClick} />

{this._effectiveShowClearIcon &&
<Icon
class="ui5-shell-search-field-icon"
name={decline}
showTooltip={true}
accessibleName={this._translations.clearIcon}
onClick={this._handleClear}
></Icon>
}

{this._effectiveShowClearIcon &&
<Icon
class="ui5-shell-search-field-icon"
name={decline}
class={{
"ui5-shell-search-field-icon": true,
"ui5-shell-search-field-search-icon": this._isSearchIcon,
}}
name={search}
showTooltip={true}
accessibleName={this._translations.clearIcon}
onClick={this._handleClear}
accessibleName={this._effectiveIconTooltip}
onClick={this._handleSearchIconPress}
></Icon>
}

<Icon
class={{
"ui5-shell-search-field-icon": true,
"ui5-shell-search-field-search-icon": this._isSearchIcon,
}}
name={search}
showTooltip={true}
accessibleName={this._effectiveIconTooltip}
onClick={this._handleSearchIconPress}
></Icon>
</div>
</div>
</div>
</BusyIndicator>
)
);
}
6 changes: 6 additions & 0 deletions packages/fiori/src/themes/SearchField.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
position: relative;
}

.ui5-search-field-busy-indicator {
width: 100%;
height: 100%;
border-radius: var(--_ui5_search_input_border_radius);
}

.ui5-shellbar-search-field-wrapper {
flex: 1;
min-width: auto;
Expand Down
14 changes: 14 additions & 0 deletions packages/fiori/test/pages/Search.html
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@
</ui5-search>
</div>

<div class="container" style="padding-top: 1rem;">
<ui5-label>Search with field loading state (type and press Enter)</ui5-label>
<ui5-search id="search-with-loading" show-clear-icon placeholder="Search..."></ui5-search>
</div>

<div class="container last" style="padding-top: 1rem;">
<ui5-label>Search with lazy loaded Suggestions - Autocomplete and highlighting</ui5-label>
<ui5-search id="search-lazy" show-clear-icon placeholder="Type 'a'..."></ui5-search>
Expand Down Expand Up @@ -450,6 +455,15 @@
}, 300);
}
});

const searchWithLoading = document.getElementById('search-with-loading');
searchWithLoading.addEventListener('ui5-search', async (e) => {
if (e.target.value) {
e.target.fieldLoading = true;
await new Promise(resolve => setTimeout(resolve, 2000));
e.target.fieldLoading = false;
}
});
</script>
</body>
</html>
11 changes: 11 additions & 0 deletions packages/fiori/test/pages/SearchField.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<ui5-search-field>
<ui5-button slot="filterButton" icon="filter"></ui5-button>
</ui5-search-field>
<div class="container" style="padding-top: 1rem; display: flex; flex-direction: column;">
<ui5-label>Search with loading state (click search icon)</ui5-label>
<ui5-search-field id="search-loading" placeholder="Search..." show-clear-icon></ui5-search-field>
</div>
<div class="container" style="padding-top: 1rem; display: flex; flex-direction: column;">
<ui5-label>Collapsed search</ui5-label>
<div class="container" style="border: 1px solid black; display: flex; padding: 4px; justify-content: flex-end;">
Expand Down Expand Up @@ -87,6 +91,13 @@
scopedSearch.addEventListener('ui5-scope-change', (event) => {
console.log('scope-change', event.detail.scope);
});

const searchLoading = document.getElementById('search-loading');
searchLoading.addEventListener('ui5-search', async () => {
searchLoading.fieldLoading = true;
await new Promise(resolve => setTimeout(resolve, 2000));
searchLoading.fieldLoading = false;
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Byline from "../../../_samples/fiori/Search/Byline/Byline.md";
import AdvancedFilter from "../../../_samples/fiori/Search/AdvancedFilter/AdvancedFilter.md"
import ShowMore from "../../../_samples/fiori/Search/ShowMore/ShowMore.md"
import Actions from "../../../_samples/fiori/Search/Actions/Actions.md"
import Loading from "../../../_samples/fiori/Search/Loading/Loading.md"

<%COMPONENT_OVERVIEW%>

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

<Actions />

### Loading State
This example shows the loading indicator during async search operations.

<Loading />

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import html from '!!raw-loader!./sample.html';
import js from '!!raw-loader!./main.js';

<Editor html={html} js={js} />
27 changes: 27 additions & 0 deletions packages/website/docs/_samples/fiori/Search/Loading/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import "@ui5/webcomponents-fiori/dist/SearchField.js";
import "@ui5/webcomponents/dist/Label.js";
import "@ui5/webcomponents/dist/Text.js";

const searchField = document.getElementById("search-loading");
const resultText = document.getElementById("result-text");

searchField.addEventListener("ui5-search", async (event) => {
const query = searchField.value;

// Show loading indicator
searchField.fieldLoading = true;
resultText.textContent = `Searching for "${query}"...`;

// Simulate async search operation
await new Promise(resolve => setTimeout(resolve, 2000));

// Hide loading indicator and show results
searchField.fieldLoading = false;
resultText.textContent = `Search completed for "${query}". Found 5 results.`;
});

searchField.addEventListener("ui5-input", () => {
if (!searchField.value) {
resultText.textContent = "Enter a search term and press Enter or click the search icon";
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<ui5-search-field id="search-loading" placeholder="Search..."></ui5-search-field>

<ui5-label style="margin-top: 1rem; display: block;">Result:</ui5-label>
<ui5-text id="result-text">Enter a search term and press Enter or click the search icon</ui5-text>
Loading