diff --git a/packages/fiori/cypress/specs/ShellBar.cy.tsx b/packages/fiori/cypress/specs/ShellBar.cy.tsx index 5527304ed42c..3f7c1ad3b4ca 100644 --- a/packages/fiori/cypress/specs/ShellBar.cy.tsx +++ b/packages/fiori/cypress/specs/ShellBar.cy.tsx @@ -11,6 +11,7 @@ import ToggleButton from "@ui5/webcomponents/dist/ToggleButton.js"; import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; import Avatar from "@ui5/webcomponents/dist/Avatar.js"; import Switch from "@ui5/webcomponents/dist/Switch.js"; +import Search from "../../src/Search.js"; const RESIZE_THROTTLE_RATE = 300; // ms @@ -324,13 +325,13 @@ describe("Slots", () => { function assertStartSeparatorVisibility(expectedExist: boolean) { cy.get("#shellbar") .shadow() - .find(".ui5-shellbar-overflow-container-right-inner > .ui5-shellbar-separator-start") + .find(".ui5-shellbar-content-items > .ui5-shellbar-separator-start") .should(expectedExist ? "exist" : "not.exist"); } function assertEndSeparatorVisibility(expectedExist: boolean) { cy.get("#shellbar") .shadow() - .find(".ui5-shellbar-overflow-container-right-inner > .ui5-shellbar-separator-end") + .find(".ui5-shellbar-content-items > .ui5-shellbar-separator-end") .should(expectedExist ? "exist" : "not.exist"); } @@ -390,4 +391,136 @@ describe("Slots", () => { .should("not.exist"); }); }); + + describe("Search field slot", () => { + it("Test search button is not visible when the search field slot is empty", () => { + cy.mount( + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .should("not.exist"); + }); + + it("Test search button is visible when the search field slot is not empty", () => { + cy.mount( + + + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .should("exist"); + }); + + it("Test search button is not visible when the hide-search-button property is set to true", () => { + cy.mount( + + + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .should("not.exist"); + }); + + it("Test search field is collapsed by default and expanded on click", () => { + cy.mount( + + + + ); + cy.get("#shellbar").shadow().as("shellbar"); + cy.get("@shellbar").find(".ui5-shellbar-search-field").should("not.exist"); + cy.get("@shellbar").find(".ui5-shellbar-search-button").click(); + cy.get("@shellbar").find(".ui5-shellbar-search-field").should("exist"); + }); + + it("Test search field is expanded by default when show-search-field is set to true", () => { + cy.mount( + + + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-field") + .should("exist"); + }); + + it("Test search button is not visible when a self-collapsible search field slot is empty", () => { + cy.mount( + + + + ); + cy.get("#shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .should("not.exist"); + }); + + it("Test self-collapsible search is expanded and collapsed by the show-search-field property", () => { + cy.mount( + + + + ); + cy.get("#search").should("have.prop", "collapsed", false); + cy.get("#shellbar").invoke("prop", "showSearchField", false); + cy.get("#search").should("have.prop", "collapsed", true); + }); + + + it("Test showSearchField property is true when using expanded search field", () => { + cy.mount( + + + + ); + cy.get("#search").should("have.prop", "collapsed", false); + cy.get("#shellbar").invoke("prop", "showSearchField").should("equal", true); + }); + + it("Test showSearchField property is false when using collapsed search field", () => { + cy.mount( + + + + ); + cy.get("#search").should("have.prop", "collapsed", true); + cy.get("#shellbar").invoke("prop", "showSearchField").should("equal", false); + }); + }); }); + +describe("Events", () => { + it("Test click on the search button fires search-button-click event", () => { + cy.mount( + + + + ); + cy.get("[ui5-shellbar") + .as("shellbar"); + + cy.get("@shellbar") + .then(shellbar => { + shellbar.get(0).addEventListener("ui5-search-button-click", cy.stub().as("searchButtonClick")); + }); + + cy.get("@shellbar") + .shadow() + .find(".ui5-shellbar-search-button") + .as("searchButton"); + + cy.get("@searchButton") + .click(); + + cy.get("@searchButtonClick") + .should("have.been.calledOnce"); + }); +}); \ No newline at end of file diff --git a/packages/fiori/src/ShellBar.ts b/packages/fiori/src/ShellBar.ts index b5eb271f9e80..e5ffd3fb2931 100644 --- a/packages/fiori/src/ShellBar.ts +++ b/packages/fiori/src/ShellBar.ts @@ -113,6 +113,10 @@ type ShellBarSearchButtonEventDetail = { searchFieldVisible: boolean; }; +type ShellBarSearchFieldToggleEventDetail = { + expanded: boolean; +}; + interface IShellBarHidableItem { classes: string, id: string, @@ -262,6 +266,16 @@ const PREDEFINED_PLACE_ACTIONS = ["feedback", "sys-help"]; bubbles: true, }) +/** + * Fired, when the search field is expanded or collapsed. + * @since 2.10.0 + * @param {Boolean} expanded whether the search field is expanded + * @public + */ +@event("search-field-toggle", { + bubbles: true, +}) + /** * Fired, when an item from the content slot is hidden or shown. * **Note:** The `content-item-visibility-change` event is in an experimental state and is a subject to change. @@ -282,8 +296,30 @@ class ShellBar extends UI5Element { "logo-click": ShellBarLogoClickEventDetail, "menu-item-click": ShellBarMenuItemClickEventDetail, "search-button-click": ShellBarSearchButtonEventDetail, + "search-field-toggle": ShellBarSearchFieldToggleEventDetail, "content-item-visibility-change": ShellBarContentItemVisibilityChangeEventDetail } + + /** + * Defines the visibility state of the search button. + * + * **Note:** The `hideSearchButton` property is in an experimental state and is a subject to change. + * @default false + * @public + */ + @property({ type: Boolean }) + hideSearchButton = false; + + /** + * Disables the automatic search field expansion/collapse when the available space is not enough. + * + * **Note:** The `disableAutoSearchField` property is in an experimental state and is a subject to change. + * @default false + * @public + */ + @property({ type: Boolean }) + disableAutoSearchField = false; + /** * Defines the `primaryTitle`. * @@ -329,16 +365,6 @@ class ShellBar extends UI5Element { @property({ type: Boolean }) showProductSwitch = false; - /** - * Defines, if the Search Field would be displayed when there is a valid `searchField` slot. - * - * **Note:** By default the Search Field is not displayed. - * @default false - * @public - */ - @property({ type: Boolean }) - showSearchField = false; - /** * Defines additional accessibility attributes on different areas of the component. * @@ -458,7 +484,10 @@ class ShellBar extends UI5Element { * Defines the `ui5-input`, that will be used as a search field. * @public */ - @slot() + @slot({ + type: HTMLElement, + invalidateOnChildChange: true, + }) searchField!: Array; /** @@ -508,6 +537,7 @@ class ShellBar extends UI5Element { _autoRestoreSearchField = false; _headerPress: () => void; + _showSearchField = false; static get FIORI_3_BREAKPOINTS() { return [ @@ -568,14 +598,14 @@ class ShellBar extends UI5Element { const spacerWidth = this.shadowRoot!.querySelector(".ui5-shellbar-spacer") ? this.shadowRoot!.querySelector(".ui5-shellbar-spacer")!.getBoundingClientRect().width : 0; const searchFieldWidth = this.domCalculatedValues("--_ui5_shellbar_search_field_width"); if (this.showFullWidthSearch) { - this.showSearchField = false; + this.setSearchState(true); return; } if ((spacerWidth <= searchFieldWidth && this.contentItemsHidden.length !== 0) && this.showSearchField) { - this.showSearchField = false; + this.setSearchState(false); this._autoRestoreSearchField = true; } else if (spacerWidth > searchFieldWidth && this._autoRestoreSearchField) { - this.showSearchField = true; + this.setSearchState(true); this._autoRestoreSearchField = false; } } @@ -711,10 +741,6 @@ class ShellBar extends UI5Element { return Number(styleSet.getPropertyValue(propertyName).replace("rem", "")) * parseInt(getComputedStyle(document.body).getPropertyValue("font-size")); } - _parsePxValue(styleSet: CSSStyleDeclaration, propertyName: string): number { - return Number(styleSet.getPropertyValue(propertyName).replace("px", "")); - } - domCalculatedValues(cssVar: string): number { const shellbarComputerStyle = getComputedStyle(this.getDomRef()!); return this._calculateCSSREMValue(shellbarComputerStyle, getScopedVarName(cssVar)); // px @@ -736,6 +762,37 @@ class ShellBar extends UI5Element { this._observeContentItems(); } + /** + * Defines, if the Search Field would be displayed when there is a valid `searchField` slot. + * + * **Note:** By default the Search Field is not displayed. + * @default false + * @public + */ + @property({ type: Boolean }) + set showSearchField(value: boolean) { + if (isSelfCollapsibleSearch(this.search)) { + this.search.collapsed = !value; + } + this._showSearchField = value; + } + + get showSearchField(): boolean { + if (isSelfCollapsibleSearch(this.search)) { + return !this.search.collapsed; + } + return this._showSearchField; + } + + /** + * Use this method to change the state of the search filed according to internal logic. + * An event is fired to notify the change. + */ + setSearchState(expanded: boolean) { + this.showSearchField = expanded; + this.fireDecoratorEvent("search-field-toggle", { expanded }); + } + onAfterRendering() { this._lastOffsetWidth = this.offsetWidth; this._overflowActions(); @@ -833,7 +890,7 @@ class ShellBar extends UI5Element { this._updateItemsInfo(itemsInfo); this._updateContentInfo(contentInfo); this._updateOverflowNotifications(); - this.showFullWidthSearch = this.overflowed; + this.showFullWidthSearch = this.overflowed && this.showSearchField; } _toggleActionPopover() { @@ -865,7 +922,7 @@ class ShellBar extends UI5Element { if (defaultPrevented) { return; } - this.showSearchField = !this.showSearchField; + this.setSearchState(!this.showSearchField); if (!this.showSearchField) { return; @@ -932,7 +989,8 @@ class ShellBar extends UI5Element { } _handleCancelButtonPress() { - this.showSearchField = false; + this.showFullWidthSearch = false; + this.setSearchState(false); } _handleProductSwitchPress(e: MouseEvent) { @@ -994,6 +1052,16 @@ class ShellBar extends UI5Element { return this.shadowRoot!.querySelector(`*[data-ui5-stable="product-switch"]`); } + /** + * Returns the `search` icon DOM ref. + * @public + * @default null + * @since 2.10.0 + */ + get searchButtonDomRef(): HTMLElement | null { + return this.shadowRoot!.querySelector(`*[data-ui5-stable="toggle-search"]`); + } + _getContentInfo(): Array { return [ ...this.contentItemsSorted.map(item => { @@ -1201,8 +1269,12 @@ class ShellBar extends UI5Element { get autoSearchField() { const onFocus = document.activeElement === this.searchField[0]; - const isEmpty = this.searchField[0]?.value.length === 0; - return (this.showSearchField || this._autoRestoreSearchField) && !onFocus && isEmpty; + const hasValue = this.searchField[0]?.value?.length > 0; + const disableAutoSearchField = this.disableAutoSearchField || onFocus || hasValue; + if (disableAutoSearchField) { + return false; + } + return this.showSearchField || this._autoRestoreSearchField; } get startContentInfoSorted() { @@ -1249,6 +1321,7 @@ class ShellBar extends UI5Element { }, search: { "ui5-shellbar-hidden-button": this.isIconHidden("search"), + "ui5-shellbar-search-toggle": true, }, overflow: { "ui5-shellbar-hidden-button": this._hiddenIcons.length === 0, @@ -1257,14 +1330,20 @@ class ShellBar extends UI5Element { "ui5-shellbar-hidden-button": this.isIconHidden("assistant"), "ui5-shellbar-assistant-button": true, }, + searchField: { + "ui5-shellbar-search-field": this.showSearchField, + "ui5-shellbar-search-toggle": isSelfCollapsibleSearch(this.search), + "ui5-shellbar-hidden-button": !this.showSearchField, + }, }; } get styles() { + const styles = { + "display": this.showSearchField ? "flex" : "none", + }; return { - searchField: { - "display": this.showSearchField ? "flex" : "none", - }, + searchField: isSelfCollapsibleSearch(this.search) ? {} : styles, }; } @@ -1340,7 +1419,7 @@ class ShellBar extends UI5Element { } get _contentItemsText() { - return ShellBar.i18nBundle.getText(SHELLBAR_ADDITIONAL_CONTEXT); + return this._enableContentAreaAccessibility ? ShellBar.i18nBundle.getText(SHELLBAR_ADDITIONAL_CONTEXT) : undefined; } get _searchFieldDescription() { @@ -1348,11 +1427,13 @@ class ShellBar extends UI5Element { } get _contentItemsRole() { - if (this.contentItems.length === 1) { - return; + if (this._enableContentAreaAccessibility) { + return "group"; } + } - return "group"; + get _enableContentAreaAccessibility() { + return this.contentItems.length > 1; } get contentItems() { @@ -1402,10 +1483,6 @@ class ShellBar extends UI5Element { return ShellBar.i18nBundle.getText(SHELLBAR_PRODUCT_SWITCH_BTN); } - get isSearchFieldVisible() { - return this.searchField[0]?.offsetWidth || 0; - } - get _profileText() { return this.accessibilityAttributes.profile?.name as string || ShellBar.i18nBundle.getText(SHELLBAR_PROFILE); } @@ -1429,7 +1506,7 @@ class ShellBar extends UI5Element { get hidableDomElements(): HTMLElement [] { const items = Array.from(this.shadowRoot!.querySelectorAll(".ui5-shellbar-button:not(.ui5-shellbar-search-button):not(.ui5-shellbar-overflow-button):not(.ui5-shellbar-cancel-button):not(.ui5-shellbar-no-overflow-button)")); const assistant = this.shadowRoot!.querySelector(".ui5-shellbar-assistant-button"); - const searchButton = this.shadowRoot!.querySelector(".ui5-shellbar-search-button"); + const searchToggle = this.shadowRoot!.querySelector(".ui5-shellbar-search-toggle"); const contentItems = this.contentItemsWrappersSorted; const firstContentItem = contentItems.pop(); const prioritizeContent = this.showSearchField && this.hasSearchField; @@ -1455,7 +1532,7 @@ class ShellBar extends UI5Element { ...items.toReversed(), assistant, ...contentItems, - searchButton, + searchToggle, firstContentItem, ]; } @@ -1531,8 +1608,27 @@ class ShellBar extends UI5Element { get isSBreakPoint() { return this.breakpointSize === "S"; } + + get hasSelfCollapsibleSearch() { + return isSelfCollapsibleSearch(this.search); + } + + get search() { + return this.searchField.length ? this.searchField[0] : null; + } } +interface IShellBarSelfCollapsibleSearch { + collapsed: boolean; +} + +const isSelfCollapsibleSearch = (searchField: any): searchField is IShellBarSelfCollapsibleSearch => { + if (searchField) { + return "collapsed" in searchField; + } + return false; +}; + ShellBar.define(); export default ShellBar; @@ -1546,4 +1642,6 @@ export type { ShellBarMenuItemClickEventDetail, ShellBarAccessibilityAttributes, ShellBarSearchButtonEventDetail, + ShellBarSearchFieldToggleEventDetail, + IShellBarSelfCollapsibleSearch, }; diff --git a/packages/fiori/src/ShellBarTemplate.tsx b/packages/fiori/src/ShellBarTemplate.tsx index 42ec7e756d85..7adbc1055312 100644 --- a/packages/fiori/src/ShellBarTemplate.tsx +++ b/packages/fiori/src/ShellBarTemplate.tsx @@ -75,7 +75,11 @@ export default function ShellBarTemplate(this: ShellBar) {
{this.hasContentItems && ( - <> +
{this.showStartSeparator && (
)} - +
)} {!this.hasContentItems &&
}
@@ -138,27 +142,29 @@ export default function ShellBarTemplate(this: ShellBar) {
)} -
+
- +
@@ -279,6 +278,42 @@ displayMenuOpener(hiddenItems.indexOf(btnOpenBasicDynamic) > -1); }); + function handleSearchEvent(e) { + if (e.target.collapsed) { + e.target.collapsed = false; + } else { + if (!e.target.value) { + e.target.collapsed = true; + } + } + } + const searches = Array.from(document.querySelectorAll('ui5-search')); + searches.forEach((searchField) => { + searchField.addEventListener('ui5-search', handleSearchEvent); + }); + + ["focus", "blur", "input"].forEach((event) => { + customSearchInput.addEventListener(event, () => { + shellbar_hide_search.disableAutoSearchField = + customSearchInput.value || customSearchInput === document.activeElement; + }); + }); + + shellbar_hide_search.addEventListener('ui5-search-field-toggle', async (e) => { + shellbar_hide_search.hideSearchButton = e.detail.expanded; + }); + + shellbar_hide_search.addEventListener('ui5-search-button-click', async (e) => { + await window["sap-ui-webcomponents-bundle"].renderFinished(); + customSearchInput.focus(); + }); + + customSearchClose.addEventListener('click', async () => { + shellbar_hide_search.showSearchField = false; + shellbar_hide_search.hideSearchButton = false; + await window["sap-ui-webcomponents-bundle"].renderFinished(); + shellbar_hide_search.searchButtonDomRef.focus(); + }); diff --git a/packages/fiori/test/specs/ShellBar.spec.js b/packages/fiori/test/specs/ShellBar.spec.js index f729d352e4e4..f816bd0d8d73 100644 --- a/packages/fiori/test/specs/ShellBar.spec.js +++ b/packages/fiori/test/specs/ShellBar.spec.js @@ -169,17 +169,6 @@ describe("Component Behavior", () => { assert.strictEqual(await input.getValue(), "Logo", "Input value is set by click event of Logo"); }); - it("tests search-button-click event", async () => { - await browser.setWindowSize(870, 1680); // search icon is not visible on XXL breakpoint - await browser.pause(HANDLE_RESIZE_DEBOUNCE_RATE_WAIT); - - const searchIcon = await browser.$("#shellbar").shadow$(".ui5-shellbar-search-button"); - const input = await browser.$("#press-input"); - - await searchIcon.click(); - assert.strictEqual(await input.getValue(), "Search Button", "Input value is set by click event of Search Button"); - }); - it("tests search-button-click event", async () => { await browser.setWindowSize(870, 1680); // search icon is not visible on XXL breakpoint await browser.pause(HANDLE_RESIZE_DEBOUNCE_RATE_WAIT);