Skip to content

feat(ui5-shellbar-search): initial implementation #11398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/fiori/cypress/specs/Search.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import SearchItem from "../../src/SearchItem.js";
import SearchItemGroup from "../../src/SearchItemGroup.js";
import history from "@ui5/webcomponents-icons/dist/history.js";
import IllustratedMessage from "../../src/IllustratedMessage.js";
import SearchPopupMode from "@ui5/webcomponents/dist/types/SearchPopupMode.js";
import searchIcon from "@ui5/webcomponents-icons/dist/search.js";
import SearchMessageArea from "../../src/SearchMessageArea.js";
import Button from "@ui5/webcomponents/dist/Button.js";
Expand Down
19 changes: 19 additions & 0 deletions packages/fiori/cypress/specs/SearchField.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
SEARCH_FIELD_SCOPE_SELECT_LABEL,
SEARCH_FIELD_CLEAR_ICON,
SEARCH_FIELD_SEARCH_ICON,
SEARCH_FIELD_LABEL
} from "../../src/generated/i18n/i18n-defaults.js";

describe("SearchField general interaction", () => {
Expand Down Expand Up @@ -40,6 +41,24 @@ describe("SearchField general interaction", () => {
.find("input")
.should("have.attr", "aria-label", attributeValue);
});

it("accessibleName should have default value if not set", () => {
cy.mount(<SearchField></SearchField>);

cy.get("[ui5-search-field]")
.shadow()
.find("input")
.should("have.attr", "aria-label", SEARCH_FIELD_LABEL.defaultText);
});

it("accessibleDescription should propagate if set", () => {
cy.mount(<SearchField accessibleDescription="Test"></SearchField>);

cy.get("[ui5-search-field]")
.shadow()
.find("input")
.should("have.attr", "aria-description", "Test");
});
});

describe("Collapsed Search Field", () => {
Expand Down
65 changes: 65 additions & 0 deletions packages/fiori/cypress/specs/ShellBarSearch.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import ShellBarSearch from "../../src/ShellBarSearch.js";
import {
SHELLBAR_SEARCH_COLLAPSED,
SEARCH_FIELD_SEARCH_ICON,
SHELLBAR_SEARCH_EXPANDED,
} from "../../src/generated/i18n/i18n-defaults.js";

describe("Behaviour", () => {
it ("Toggles collapsed property upon icon press", () => {
cy.mount(<ShellBarSearch />);

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-icon]")
.as("searchIcon");

cy.get("@searchIcon")
.realClick();

cy.get("[ui5-shellbar-search]")
.should("have.prop", "collapsed", true);

cy.get("@searchIcon")
.realClick();

cy.get("[ui5-shellbar-search]")
.should("not.have.a.property", "collapsed");
});

it ("Tests icon tooltips for diffrent states", () => {
cy.mount(<ShellBarSearch />);

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-icon]")
.as("searchIcon");

cy.get("@searchIcon")
.should("have.attr", "accessible-name", SHELLBAR_SEARCH_EXPANDED.defaultText);

cy.get("@searchIcon")
.realClick();

cy.get("[ui5-shellbar-search]")
.should("have.prop", "collapsed", true);

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-button]")
.should("have.attr", "accessible-name", SHELLBAR_SEARCH_COLLAPSED.defaultText);

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-button]")
.realClick();

cy.get("[ui5-shellbar-search]")
.shadow()
.find("input")
.type("test");

cy.get("@searchIcon")
.should("have.attr", "accessible-name", SEARCH_FIELD_SEARCH_ICON.defaultText);
});
});
98 changes: 98 additions & 0 deletions packages/fiori/cypress/specs/ShellBarSearch.mobile.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import SearchItem from "../../src/SearchItem.js";
import ShellBarSearch from "../../src/ShellBarSearch.js";

describe("Mobile Behaviour", () => {
beforeEach(() => {
cy.ui5SimulateDevice();
});

it("Should not close dialog upon focus out", () => {
cy.mount(<ShellBarSearch />);

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-button]")
.realClick();

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-responsive-popover] header input")
.type("test");

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-responsive-popover] header input")
.blur();

cy.get("[ui5-shellbar-search]")
.should("have.prop", "open", true);
});

it("Should select typed ahead item when typing", () => {
cy.mount(
<>
<ShellBarSearch showClearIcon={true}>
<SearchItem text="Item 1" />
<SearchItem text="Item 2" />
<SearchItem text="Item 3" />
<SearchItem text="Item 4" />
<SearchItem text="Item 5" />
<SearchItem text="Item 6" />
</ShellBarSearch>
</>
);

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-button]")
.realClick();

cy.get("[ui5-shellbar-search]")
.should("have.prop", "open", true);

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-responsive-popover] header input")
.type("item 1");

cy.get("[ui5-search-item]:first")
.should("have.attr", "selected");
});

it("Should type ahead internal input", () => {
cy.mount(
<>
<ShellBarSearch showClearIcon={true}>
<SearchItem text="Item 1" />
<SearchItem text="Item 2" />
<SearchItem text="Item 3" />
<SearchItem text="Item 4" />
<SearchItem text="Item 5" />
<SearchItem text="Item 6" />
</ShellBarSearch>
</>
);

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-button]")
.realClick();

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-responsive-popover] header input")
.type("ite");

cy.get("[ui5-shellbar-search]")
.shadow()
.find("[ui5-responsive-popover] header input")
.should("have.value", "Item 1");
});

it("is collapsed by default", () => {
cy.mount(<ShellBarSearch />);

cy.get("[ui5-shellbar-search]")
.should("have.prop", "collapsed", true);
});
});
18 changes: 10 additions & 8 deletions packages/fiori/src/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ type SearchEventDetails = {
*
* ### ES6 Module Import
*
* `import "@ui5/webcomponents/fiori/dist/Search.js";`
* `import "@ui5/webcomponents-fiori/dist/Search.js";`
*
* @constructor
* @extends SearchField
Expand Down Expand Up @@ -287,13 +287,11 @@ class Search extends SearchField {

_handleMobileInput(e: CustomEvent<InputEventDetail>) {
this.value = (e.target as Input).value;
this._performItemSelectionOnMobile = this._shouldPerformSelectionOnMobile(e);
this._performItemSelectionOnMobile = this._shouldPerformSelectionOnMobile(e.detail.inputType);

this.fireDecoratorEvent("input");
}

_shouldPerformSelectionOnMobile(e: CustomEvent<InputEventDetail>): boolean {
const eventType = e.detail.inputType;
_shouldPerformSelectionOnMobile(inputType: string): boolean {
const allowedEventTypes = [
"deleteWordBackward",
"deleteWordForward",
Expand All @@ -310,7 +308,7 @@ class Search extends SearchField {
"historyUndo",
];

return !this.noTypeahead && !allowedEventTypes.includes(eventType || "");
return !this.noTypeahead && !allowedEventTypes.includes(inputType || "");
}

_handleTypeAhead(item: ISearchSuggestionItem) {
Expand Down Expand Up @@ -430,7 +428,11 @@ class Search extends SearchField {
_handleInput(e: InputEvent) {
super._handleInput(e);

this.open = !isPhone() && ((e.currentTarget as HTMLInputElement).value.length > 0) && this._popoupHasAnyContent();
if (isPhone()) {
return;
}

this.open = ((e.currentTarget as HTMLInputElement).value.length > 0) && this._popoupHasAnyContent();
}

_popoupHasAnyContent() {
Expand Down Expand Up @@ -606,7 +608,7 @@ class Search extends SearchField {
get nativeInput() {
const domRef = this.getDomRef();

return domRef ? domRef.querySelector<HTMLInputElement>(`input`) : null;
return domRef?.querySelector<HTMLInputElement>(`input`);
}

get mobileInput() {
Expand Down
12 changes: 7 additions & 5 deletions packages/fiori/src/SearchField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SEARCH_FIELD_SCOPE_SELECT_LABEL,
SEARCH_FIELD_CLEAR_ICON,
SEARCH_FIELD_SEARCH_ICON,
SEARCH_FIELD_LABEL,
} from "./generated/i18n/i18n-defaults.js";

/**
Expand Down Expand Up @@ -49,7 +50,7 @@ type SearchFieldScopeSelectionChangeDetails = {
*
* ### ES6 Module Import
*
* `import "@ui5/webcomponents/fiori/dist/SearchField.js";`
* `import "@ui5/webcomponents-fiori/dist/SearchField.js";`
*
* @constructor
* @extends UI5Element
Expand Down Expand Up @@ -111,7 +112,7 @@ class SearchField extends UI5Element {
* Defines whether the component is collapsed.
*
* @default false
* @public
* @private
*/
@property({ type: Boolean })
collapsed = false;
Expand Down Expand Up @@ -144,12 +145,12 @@ class SearchField extends UI5Element {
accessibleName?: string;

/**
* Defines the tooltip of the search icon component.
* Defines the accessible ARIA description of the field.
* @public
* @default undefined
*/
@property()
searchIconTooltip?: string;
accessibleDescription?: string;

/**
* Defines the component scope options.
Expand Down Expand Up @@ -248,11 +249,12 @@ class SearchField extends UI5Element {
scope: SearchField.i18nBundle.getText(SEARCH_FIELD_SCOPE_SELECT_LABEL),
searchIcon: SearchField.i18nBundle.getText(SEARCH_FIELD_SEARCH_ICON),
clearIcon: SearchField.i18nBundle.getText(SEARCH_FIELD_CLEAR_ICON),
searchFieldAriaLabel: SearchField.i18nBundle.getText(SEARCH_FIELD_LABEL),
};
}

get _effectiveIconTooltip() {
return this.searchIconTooltip || this._translations.searchIcon;
return this._translations.searchIcon;
}

captureRef(ref: HTMLElement & { scopeOption?: UI5Element} | null) {
Expand Down
14 changes: 11 additions & 3 deletions packages/fiori/src/SearchFieldTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ 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";

export default function SearchFieldTemplate(this: SearchField) {
export type SearchFieldTemplateOptions = {
/**
* If set to true, the search field will be expanded.
*/
forceExpanded?: boolean;
};

export default function SearchFieldTemplate(this: SearchField, options?: SearchFieldTemplateOptions) {
return (
this.collapsed ? (
!options?.forceExpanded && this.collapsed ? (
<Button
class="ui5-shell-search-field-button"
icon={search}
Expand Down Expand Up @@ -46,7 +53,8 @@ export default function SearchFieldTemplate(this: SearchField) {
<input
class="ui5-search-field-inner-input"
role="searchbox"
aria-label={this.accessibleName}
aria-description={this.accessibleDescription}
aria-label={this.accessibleName || this._translations.searchFieldAriaLabel}
value={this.value}
placeholder={this.placeholder}
data-sap-focus-ref
Expand Down
2 changes: 1 addition & 1 deletion packages/fiori/src/SearchItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
*
* ### ES6 Module Import
*
* `import "@ui5/webcomponents/fiori/dist/SearchItem.js";`
* `import "@ui5/webcomponents-fiori/dist/SearchItem.js";`
*
* @constructor
* @extends ListItemBase
Expand Down
7 changes: 4 additions & 3 deletions packages/fiori/src/SearchPopoverTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import InputKeyHint from "@ui5/webcomponents/dist/types/InputKeyHint.js";
import Button from "@ui5/webcomponents/dist/Button.js";
import ButtonDesign from "@ui5/webcomponents/dist/types/ButtonDesign.js";
import ListAccessibleRole from "@ui5/webcomponents/dist/types/ListAccessibleRole.js";
import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js";

export default function SearchPopoverTemplate(this: Search) {
export default function SearchPopoverTemplate(this: Search, headerTemplate?: JsxTemplate) {
return (
<ResponsivePopover
hideArrow={true}
Expand All @@ -34,7 +35,7 @@ export default function SearchPopoverTemplate(this: Search) {
}}
>

{isPhone() ? (
{isPhone() ? (headerTemplate ? headerTemplate.call(this) : (
<>
<header slot="header" class="ui5-search-popup-searching-header">
<Input class="ui5-search-popover-search-field" onInput={this._handleMobileInput} showClearIcon={this.showClearIcon} noTypeahead={this.noTypeahead} hint={InputKeyHint.Search} onKeyDown={this._onMobileInputKeydown}>
Expand All @@ -45,7 +46,7 @@ export default function SearchPopoverTemplate(this: Search) {
<Button design={ButtonDesign.Transparent} onClick={this._handleCancel}>{this.cancelButtonText}</Button>
</header>
</>
) : null }
)) : null }

<main class="ui5-search-popover-content">
<slot name="messageArea"></slot>
Expand Down
Loading
Loading