/*! * SAPUI5 * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. */ import i18n from "../../i18n"; import Input, { $InputSettings, Input$SuggestionItemSelectedEvent } from "sap/m/Input"; import Label from "sap/m/Label"; import Text from "sap/m/Text"; import { FlexAlignItems, FlexDirection, FlexJustifyContent, ListType } from "sap/m/library"; import Column from "sap/m/Column"; import ColumnListItem from "sap/m/ColumnListItem"; import CustomListItem from "sap/m/CustomListItem"; import FlexItemData from "sap/m/FlexItemData"; import BusyIndicator from "sap/m/BusyIndicator"; import FlexBox from "sap/m/FlexBox"; import Icon from "sap/ui/core/Icon"; import HorizontalLayout from "sap/ui/layout/HorizontalLayout"; import VerticalLayout from "sap/ui/layout/VerticalLayout"; import BindingMode from "sap/ui/model/BindingMode"; import * as SearchHelper from "../../SearchHelper"; import SearchModel from "sap/esh/search/ui/SearchModel"; import Container from "sap/ushell/Container"; import Context from "sap/ui/model/Context"; import SuggestionType, { Suggestion, SuggestionHeader, Type as UISuggestionType, UISinaObjectSuggestion, isSearchSuggestion, } from "../../suggestions/SuggestionType"; import SearchShellHelperHorizonTheme from "../../SearchShellHelperHorizonTheme"; import { UserEventType } from "../../eventlogging/UserEvents"; import UIEvents, { IUiEventParametersSearchFinishedPublic } from "../../UIEvents"; import Device from "sap/ui/Device"; import Element from "sap/ui/core/Element"; import EventBus from "sap/ui/core/EventBus"; import Link from "sap/m/Link"; import MessagePopover from "sap/m/MessagePopover"; import Avatar from "sap/m/Avatar"; import AvatarShape from "sap/m/AvatarShape"; import AvatarSize from "sap/m/AvatarSize"; declare global { interface Window { hasher: { prependHash: string; getHash: () => string; setHash: (hash: string) => void; }; } } /** * @namespace sap.esh.search.ui.controls */ export default class SearchInput extends Input { private listNavigationMode: boolean; triggerSuggestionItemSelected = false; constructor(sId?: string, options?: $InputSettings) { // ugly hack disable fullscreen input on phone - start // ToDo: remove by switching search input to sap.m.SearchField // see also method isMobileDevice const phone = Device.system.phone; try { (Device.system as any).phone = false; // ToDo, 'phone' is a constant (read-only) super(sId, options); } finally { (Device.system as any).phone = phone; // ToDo, 'phone' is a constant (read-only) // ugly hack - end } this.decorateHandleListNavigation(); this.setStartSuggestion(0); this.setWidth("100%"); this.setShowValueStateMessage(false); this.setShowTableSuggestionValueHelp(false); this.setEnableSuggestionsHighlighting(false); this.setShowSuggestion(true); this.setFilterSuggests(false); this.addSuggestionColumn(new Column("")); this.attachSuggestionItemSelected(this.handleSuggestionItemSelected.bind(this)); this.setAutocomplete(false); this.setTooltip(i18n.getText("search")); this.bindProperty("placeholder", { path: "/searchTermPlaceholder", mode: BindingMode.OneWay, }); this.attachLiveChange(this.handleLiveChange.bind(this)); this.setValueLiveUpdate(true); this.bindProperty("enabled", { parts: [ { path: "/initializingObjSearch", }, { path: "/searchDeactivated", // example: In case of object search, if the basket panel is wider than a certain threshold, the search input is disabled in order to avoid that the search input updates the search results while result list/table/grid are only displayed partly (less than e.g. 50%) and thus are not usable anyway. The threshold is defined in BASKET_PANEL_DOMINANCE_THRESHOLD_PERCENT }, ], formatter: (initializingObjSearch: boolean, searchDeactivated: boolean) => !initializingObjSearch && !searchDeactivated, }); this.bindAggregation("suggestionRows", { path: "/suggestions", factory: (sId, oContext) => this.suggestionItemFactory(sId, oContext), }); this.bindProperty("value", { path: "/uiFilter/searchTerm" }); this.addStyleClass("searchInput"); // disable fullscreen input on phone this.listNavigationMode = false; } isMobileDevice(): boolean { // ugly hack disable fullscreen input on phone - start return false; // ugly hack disable fullscreen input on phone - end } onfocusin(oEvent: JQuery.Event): void { // keep type 'JQuery.Event' here, because of sap.m.Input, function 'onfocusin' super.onfocusin(oEvent); const relatedTarget = (oEvent as unknown as FocusEvent).relatedTarget as HTMLElement | null; if ( relatedTarget?.classList?.contains("searchSuggestionColumnListItem") || this["_oSuggPopover"].isOpen() ) { // this ignores focus events caused by clicking on suggestions items // focus events on suggestions items my cause side: // sometimes focus event sometimes is faster then suggestionItemSelected // in case suggestion list is cleared by focus event in suggestion handler // the subsequent suggestionsItem selected event fails return; } const oModel = this.getModel() as SearchModel; if ( oModel.getSearchBoxTerm().length === 0 && (oModel.config.bRecentSearches || oModel.config.aiSuggestions) ) { oModel.doSuggestion(); } } onsapenter(...args: Array): void { // if (!(this["_oSuggestionPopup"]?.isOpen() && this["_oSuggPopover"].getFocusedListItem())) { // // if (!(this._oSuggestionPopup && this._oSuggestionPopup.isOpen() && this.["_oSuggPopover"]._iPopupListSelectedIndex >= 0)) { // // check that enter happened in search input box and not on a suggestion item // // enter on a suggestion is not handled in onsapenter but in handleSuggestionItemSelected // (this.getModel() as SearchModel).invalidateQuery(); // this.triggerSearch(); // } const isSuggestionFocused = this["_oSuggestionPopup"]?.isOpen() && this["_oSuggPopover"].getFocusedListItem(); if (!isSuggestionFocused) { // no suggestion item focused -> trigger search (this.getModel() as SearchModel).invalidateQuery(); this.triggerSearch(); Input.prototype.onsapenter.apply(this, args); } else { // sugestion item focused -> trigger suggestion this.triggerSuggestionItemSelected = true; Input.prototype.onsapenter.apply(this, args); this.triggerSuggestionItemSelected = false; } // Input.prototype.onsapenter.apply(this, args); } // overwrite of UI5 base class method // this method is called by UI5 and triggers the suggestionItemSelected event // the method is called when // 1) suggestion popup is closed by focus out // 2) suggestion popup is closed by enter on suggestion item // for use case 1) we don't want that the suggestionItemSelectedEvent therefore we set triggerSelectionItemSelected=false // for use case 2) the suggestionItemSelectedEvent shall be created therefore in method onsapenter we set temporary triggerSelectionItemSelected=true updateSelectionFromList(item: unknown): void { if (this.triggerSuggestionItemSelected) { // default UI5 logic which triggers handleSuggestionItemSelected // super.updateSelectionFromList(item); (Input.prototype as any).updateSelectionFromList.apply(this, [item]); // alternative writing to avoid super private method call } else { // special logic in order to prevent that UI5 triggers handleSuggestionItemSelected // this.setSelectionUpdatedFromList(false); this["setSelectionUpdatedFromList"](false); // alternative writing to avoid super private method call } } triggerSearch(): void { // the footer is rerendered after each search in stand alone UI -> error popover losts parent and jumps to the screen top. // solution: if the error popover shows, set footer invisible before next search. // popover.close() is not working. It is closed after footer invisible, so it still jumps const msgPopupId = this.getModel().getProperty("/messagePopoverControlId"); const messagePopup = Element.getElementById(msgPopupId) as MessagePopover; if (this.getModel().getProperty("/errors").length > 0 && messagePopup?.isOpen()) { messagePopup.close(); messagePopup.setVisible(false); } // it is necessay to do this in search input (and not in search model) because otherwise navigating back from the app to the // search UI would result in a repeated navigation to the app SearchHelper.subscribeOnlyOnce( "triggerSearch", UIEvents.ESHSearchFinished, function () { (this.getModel() as SearchModel).autoStartApp(); }, this ); const oModel = this.getModel() as SearchModel; if (this.getValue().trim() === "" && oModel.config.isUshell) { this.setValue("*"); // special behaviour for S/4 } if (this.isNavigationAllowed()) { this.navigateToSearchApp(); } this.destroySuggestionRows(); oModel.abortSuggestions(); } isNavigationAllowed(): boolean { const oModel = this.getModel() as SearchModel; const searchBoxTerm = this.getValue(); for (const handler of oModel.searchTermHandlers) { const searchTermWasHandled = handler.handleSearchTerm(searchBoxTerm); if (searchTermWasHandled) { searchTermWasHandled.catch(() => { (this as any)._openSuggestionPopup(); // make sure that suggestions are visible in case there is an error message to show therein }); return false; } } return true; } decorateHandleListNavigation() { // ugly modifiaction: headers and busy indicator in suggestions shall not be selectable const suggestionPopover = this["_oSuggPopover"]; if ( suggestionPopover && suggestionPopover.handleListNavigation && !suggestionPopover.handleListNavigation.decorated ) { const handleListNavigation = suggestionPopover.handleListNavigation; suggestionPopover.handleListNavigation = (...args: unknown[]) => { this.listNavigationMode = true; handleListNavigation.apply(suggestionPopover, args); // -handleListNavigation calls getVisible on the suggestion items in order to determine to which suggestion items we can navigate // -for suggestion items of type 'header' or 'busy indicator' (to which no navigation shall take place) the getVisible function is overwritten // -the overwritten getVisible function returns false only for listNavigationMode=true // ==> this way suggestion items of type 'header' or 'busy indicator' are visible but we cannot navigate to them this.listNavigationMode = false; }; suggestionPopover.handleListNavigation.decorated = true; } } handleLiveChange(): void { const oModel = this.getModel() as SearchModel; if (oModel.getSearchBoxTerm().length > 0 || oModel.config.bRecentSearches) { oModel.doSuggestion(); } else { this.destroySuggestionRows(); oModel.abortSuggestions(); // set aria-expanded to false on the input element const domRef = this.getDomRef(); if (domRef) { const input = domRef.querySelector("input"); if (input) { input.setAttribute("aria-expanded", "false"); } } } } handleSuggestionItemSelected(oEvent: Input$SuggestionItemSelectedEvent): void { const oModel = this.getModel() as SearchModel; const searchBoxTerm = oModel.getSearchBoxTerm(); const suggestion = oEvent.getParameter("selectedRow").getBindingContext().getObject() as Suggestion; const suggestionTerm = suggestion.searchTerm || ""; const dataSource = suggestion.dataSource || oModel.getDataSource(); let targetURL = suggestion.url; const type = suggestion.uiSuggestionType; if (type === SuggestionType.Header || type === SuggestionType.BusyIndicator) { // workaroung in case someonw clicks on a header this.destroySuggestionRows(); this.setValue(""); return; } oModel.eventLogger.logEvent({ type: UserEventType.SUGGESTION_SELECT, suggestionTitle: suggestion.title, suggestionType: type, suggestionTerm: suggestionTerm, searchTerm: searchBoxTerm, targetUrl: targetURL, dataSourceKey: dataSource ? dataSource.id : "", }); if (oModel.config.bRecentSearches && oModel.recentlyUsedStorage && type === SuggestionType.Object) { // Object Suggestions in Repository Explorer open in new tab, thus we need to save it here. // All other suggestions trigger a search which will be added as a recent item through search model. oModel.recentlyUsedStorage.addItem(suggestion); } // remove any selection this.selectText(0, 0); switch (type) { case SuggestionType.Search: { if (isSearchSuggestion(suggestion)) { suggestion.titleNavigation.performNavigation(); } break; } case SuggestionType.Transaction: case SuggestionType.App: // app or transactions suggestions -> start app // starting the app by hash change closes the suggestion popup // closing the suggestion popup again triggers the suggestion item selected event // in order to avoid to receive the event twice the suggestions are destroyed this.destroySuggestionRows(); oModel.abortSuggestions(); if (targetURL?.[0] === "#") { if ( targetURL.indexOf("#Action-search") === 0 && (targetURL === SearchHelper.getHashFromUrl() || targetURL === decodeURIComponent(SearchHelper.getHashFromUrl())) ) { // ugly workaround // in case the app suggestion points to the search app with query identical to current query // --> do noting except: restore query term + focus again the first item in the result list oModel.setSearchBoxTerm(oModel.query.filter.searchTerm, false); oModel.setDataSource(oModel.query.filter.dataSource, false); oModel.notifySubscribers(UIEvents.ESHSearchFinished, null); EventBus.getInstance().publish(UIEvents.ESHSearchFinished, { searchTerm: oModel.getLastSearchTerm(), } as IUiEventParametersSearchFinishedPublic); return; } if (window["hasher"]) { if (targetURL[1] === window.hasher.prependHash) { // hasher preprends a "prependHash" character between "#" and the rest. // so we remove the same character to have the desired string in the end after hasher changed it // this avoids a wrong url if the application does not use window.hasher.getHash which removes prependHash again targetURL = targetURL.slice(0, 1) + targetURL.slice(2); } window["hasher"].setHash(targetURL); } else { window.location.href = targetURL; } } else { // special logging: only for urls started via suggestions // (apps/urls started via click ontile have logger in tile click handler) this.logRecentActivity(suggestion); if (targetURL) { window.open(targetURL, "_blank", "noopener,noreferrer"); oModel.setSearchBoxTerm("", false); } } // close the search field if suggestion is not search app if (oModel.config.isUshell && targetURL && targetURL.indexOf("#Action-search") !== 0) { // 1) navigate to an app <> search if (!SearchShellHelperHorizonTheme.isSearchFieldExpandedByDefault()) { sap.ui.require("sap/esh/search/ui/SearchShellHelper").collapseSearch(); } } else { // 2) navigate to search app this.focus(); } break; case SuggestionType.DataSource: // data source suggestions // -> change datasource in dropdown // -> do not start search oModel.setDataSource(dataSource, false); oModel.setSearchBoxTerm("", false); this.focus(); break; case SuggestionType.SearchTermData: case SuggestionType.SearchTermAI: // -> change search term + change datasource + start search oModel.setDataSource(dataSource, false); oModel.setSearchBoxTerm(suggestionTerm, false); oModel.invalidateQuery(); this.navigateToSearchApp(); break; case SuggestionType.SearchTermHistory: // history // -> change search term + change datasource + start search oModel.setDataSource(dataSource, false); oModel.setSearchBoxTerm(suggestionTerm, false); oModel.invalidateQuery(); this.navigateToSearchApp(); break; case SuggestionType.Object: { const objectSuggestion = suggestion as UISinaObjectSuggestion; if (objectSuggestion.titleNavigation) { objectSuggestion.titleNavigation.performNavigation(); } break; } default: break; } } logRecentActivity(suggestion: Suggestion): void { // load ushell deps lazy only in case of FLP sap.ui.require(["sap/ushell/Config", "sap/ushell/services/AppType"], function (Config, AppType) { const bLogRecentActivity = Config.last("/core/shell/enableRecentActivity") && Config.last("/core/shell/enableRecentActivityLogging"); if (bLogRecentActivity) { const oRecentEntry = { title: suggestion.title, appType: AppType.URL, url: suggestion.url, appId: suggestion.url, }; (Container.getRenderer("fiori2") as any).logRecentActivity(oRecentEntry); } }); } suggestionItemFactory(sId: string, oContext: Context): ColumnListItem { // set aria-expanded to true on the input element const domRef = this.getDomRef(); if (domRef) { const input = domRef.querySelector("input"); if (input) { input.setAttribute("aria-expanded", "true"); } } const suggestion = oContext.getObject() as Suggestion; switch (suggestion.uiSuggestionType) { case SuggestionType.Object: return this.objectSuggestionItemFactory(sId, oContext); case SuggestionType.Header: return this.headerSuggestionItemFactory(sId, oContext); case SuggestionType.BusyIndicator: return this.busyIndicatorSuggestionItemFactory(/* sId, oContext */); default: return this.regularSuggestionItemFactory(sId, oContext); } } busyIndicatorSuggestionItemFactory(/* sId: string, oContext: Context */): ColumnListItem { const cell = new VerticalLayout("", { content: [ new BusyIndicator("", { size: "0.6rem", }), ], }); cell["getText"] = () => { return this.getValue(); }; const listItem = new ColumnListItem("", { cells: [cell], type: ListType.Inactive, }); listItem.addStyleClass("searchSuggestion"); listItem.addStyleClass("searchBusyIndicatorSuggestion"); listItem.getVisible = this.assembleListNavigationModeGetVisibleFunction(); return listItem; } headerSuggestionItemFactory(sId: string, oContext: Context): ColumnListItem { const suggestion = oContext.getObject() as SuggestionHeader; const items = []; // left const leftItems = []; if (suggestion.uiSuggestionTypeOfSuggestionsInSection === UISuggestionType.SearchTermAI) { const icon = new Icon({ src: "sap-icon://ai" }); icon.addStyleClass("sapUiTinyMarginEnd"); leftItems.push(icon); } const label = new Label("", { text: { path: "label" }, }); leftItems.push(label); const leftContent = new FlexBox("", { direction: FlexDirection.Row, justifyContent: FlexJustifyContent.Start, items: leftItems, }); items.push(leftContent); // right if (suggestion.helpLink) { const rightContent = new Link({ text: i18n.getText("searchTermAILernMoreLink"), target: "_blank", href: suggestion.helpLink, }); items.push(rightContent); } // flexbox containing left and right const cell = new FlexBox("", { direction: FlexDirection.Row, justifyContent: FlexJustifyContent.SpaceBetween, items: items, }); cell["getText"] = () => { //return this.getValue(); return suggestion.label; // workaround: return " " -> header cannot be auto selected by sap.m.Input because // input field text != suggestion.label " " }; // column list item containing flexbo const listItem = new ColumnListItem("", { cells: [cell], type: ListType.Inactive, }); listItem.addStyleClass("searchSuggestion"); listItem.addStyleClass("searchHeaderSuggestion"); listItem.getVisible = this.assembleListNavigationModeGetVisibleFunction(); return listItem; } assembleListNavigationModeGetVisibleFunction(): () => boolean { return () => { if (!this.listNavigationMode) { return true; // without the special list navigation mode we return the default value true } // in list navigation mode return false; }; } assembleObjectSuggestionLabels(suggestion: { imageUrl?: string; imageExists?: true; imageIsCircular?: boolean; exists?: false; label2?: string; }): VerticalLayout { // first line: label 1 const labels = []; const label1 = new Label("", { text: { path: "label1" }, }); label1.addEventDelegate( { onAfterRendering: () => { SearchHelper.boldTagUnescaper(label1.getDomRef() as HTMLElement); }, }, label1 ); label1.addStyleClass("sapUshellSearchObjectSuggestion-Label1"); labels.push(label1); // second line: label 2 if (suggestion.label2) { const label2 = new Label("", { text: { path: "label2" }, }); label2.addEventDelegate( { onAfterRendering: () => { SearchHelper.boldTagUnescaper(label2.getDomRef() as HTMLElement); }, }, label2 ); label2.addStyleClass("sapUshellSearchObjectSuggestion-Label2"); labels.push(label2); } const vLayout = new VerticalLayout("", { content: labels, }); vLayout.addStyleClass("sapUshellSearchObjectSuggestion-Labels"); return vLayout; } objectSuggestionItemFactory(sId: string, oContext: Context): ColumnListItem { const suggestion = oContext.getObject() as { imageUrl?: string; imageExists?: true; imageIsCircular?: boolean; exists?: false; label2?: string; }; const suggestionParts = []; // image if (suggestion.imageExists && suggestion.imageUrl) { if (suggestion.imageUrl.startsWith("sap-icon://")) { suggestionParts.push( new Icon("", { src: suggestion.imageUrl, }).addStyleClass("sapUshellSearchObjectSuggestIcon") ); } else { suggestionParts.push( new Avatar("", { src: { path: "imageUrl" }, displayShape: { parts: [{ path: "imageIsCircular" }], formatter: (imageIsCircular: boolean) => imageIsCircular ? AvatarShape.Circle : AvatarShape.Square, }, displaySize: AvatarSize.XS, }) ); } } // labels const suggestionPartsToAdd = this.assembleObjectSuggestionLabels(suggestion); suggestionParts.push(suggestionPartsToAdd); // combine image and labels const cell = new HorizontalLayout("", { content: suggestionParts, }); cell.addStyleClass("sapUshellSearchObjectSuggestion-Container"); // there is an issue with focus on suggestions, which will be triggerd by loss of focus on the suggestion item // workaround: to avoid auto focus on object suggestions, we don't return the value of getText, so that the focus is not set // with solution above by overwriting of private method updateSelectionFromList, it could work without workaround cell["getText"] = () => { return this.getValue(); }; // suggestion list item const listItem = new ColumnListItem("", { cells: [cell], type: ListType.Active, }); listItem.addStyleClass("searchSuggestion"); listItem.addStyleClass("searchObjectSuggestion"); return listItem; } regularSuggestionItemFactory(sId: string, oContext: Context): ColumnListItem { // icon at the front: const icon = new Icon("", { src: { path: "icon" }, }) .addStyleClass("suggestIcon") .addStyleClass("sapUshellSearchSuggestAppIcon") .addStyleClass("suggestListItemCell"); // recent search suggestions which have a filter will get this additional icon // at the end of the line: const filterIcon = new Icon("", { src: { path: "filterIcon" }, }) .addStyleClass("suggestIcon") .addStyleClass("sapUshellSearchSuggestFilterIcon") .addStyleClass("suggestListItemCell"); const layoutData = new FlexItemData("", { shrinkFactor: 1, minWidth: "4rem", }); // label const label = new Text("", { text: { path: "label" }, layoutData: layoutData, wrapping: false, }) .addStyleClass("suggestText") .addStyleClass("suggestNavItem") .addStyleClass("suggestListItemCell"); const suggestion = oContext.getModel().getProperty(oContext.getPath()); label.addEventDelegate( { onAfterRendering: () => { const domref = label.getDomRef() as HTMLElement; const innerHTML = domref.innerHTML; const hasSpan = innerHTML.lastIndexOf("</span>") > 0; if (suggestion.uiSuggestionType === SuggestionType.Search && hasSpan) { // recent suggestions can be entered by the user. Thus he could also enter "bold" as search term // and the boldTagUnescaper would then render the search term as bold html. // TODO: call boldTagUnescaper for everything which is not in the const userEntered = innerHTML.slice( innerHTML.indexOf("<span>") + 12, innerHTML.lastIndexOf("</span>") ); const notUserEntered = innerHTML.slice(innerHTML.lastIndexOf("</span>") + 13); const notUserEnteredHTML = SearchHelper.boldTagUnescaperForStrings(notUserEntered); domref.innerHTML = "" + userEntered + "" + notUserEnteredHTML; } else { SearchHelper.boldTagUnescaper(label.getDomRef() as HTMLElement); } }, }, label ); const items = [icon, label]; if (suggestion.filterIcon) { items.push(filterIcon); } // combine app, icon and label into cell const cell = new CustomListItem("", { type: ListType.Active, content: new FlexBox("", { items, alignItems: FlexAlignItems.Center, }), }); cell.addStyleClass("searchSuggestionCustomListItem"); cell["getText"] = () => { return typeof suggestion?.filter?.searchTerm === "string" ? suggestion.filter.searchTerm : this.getValue(); }; const listItem = new ColumnListItem("", { cells: [cell], type: ListType.Active, }); cell.addStyleClass("searchSuggestionColumnListItem"); if (suggestion.uiSuggestionType === SuggestionType.App) { if (suggestion.title && suggestion.title.indexOf("combinedAppSuggestion") >= 0) { listItem.addStyleClass("searchCombinedAppSuggestion"); } else { listItem.addStyleClass("searchAppSuggestion"); } } if (suggestion.uiSuggestionType === SuggestionType.DataSource) { listItem.addStyleClass("searchDataSourceSuggestion"); } if (suggestion.uiSuggestionType === SuggestionType.SearchTermData) { listItem.addStyleClass("searchBOSuggestion"); } if (suggestion.uiSuggestionType === SuggestionType.SearchTermAI) { listItem.addStyleClass("searchAISuggestion"); } if (suggestion.uiSuggestionType === SuggestionType.SearchTermHistory) { listItem.addStyleClass("searchHistorySuggestion"); } if (suggestion.uiSuggestionType === SuggestionType.Search) { listItem.addStyleClass("searchRecentSuggestion"); } listItem.addStyleClass("searchSuggestion"); return listItem; } navigateToSearchApp(): void { const oSearchModel = this.getModel() as SearchModel; if (SearchHelper.isSearchAppActive() || !oSearchModel.config.isUshell) { // app running -> just fire query oSearchModel.fireSearchQuery(); } else { // app not running -> start via hash // change hash: // - do not use Searchhelper.hasher here // - this is starting the search app from outside oSearchModel.resetSearchResultItemMemory(); const sHash = oSearchModel.createSearchNavigationTargetCurrentState({ updateUrl: true, }).targetUrl; window.location.hash = sHash; } } onAfterRendering(oEvent): void { super.onAfterRendering(oEvent); const domRef = this.getDomRef(); if (!domRef) return; const input = domRef.querySelector("input"); if (!input) return; // Set input attributes input.setAttribute("autocomplete", "off"); input.setAttribute("autocorrect", "off"); // Additional hacks to show the "search" button on iOS keyboards: input.setAttribute("type", "search"); input.setAttribute("name", "search"); // Create form element const form = document.createElement("form"); form.style.setProperty("width", "100%"); form.setAttribute("action", ""); form.addEventListener("submit", (e) => { e.preventDefault(); return false; }); // Get parent of input const inputParent = input.parentElement; if (!inputParent) return; // Append form to parent and move input into form inputParent.appendChild(form); form.appendChild(input); // set ARIA attributes on the input input.setAttribute("role", "combobox"); input.setAttribute("aria-autocomplete", "list"); input.setAttribute("aria-expanded", "false"); input.setAttribute("aria-controls", this.getId() + "-popup"); input.setAttribute("aria-label", i18n.getText("search")); } onValueRevertedByEscape(): void { // this method is called if ESC was pressed and // the value in it was not empty if (SearchHelper.isSearchAppActive()) { // dont delete the value if search app is active return; } this.setValue(" "); // add space as a marker for following ESC handler } onsapescape(oEvent: JQuery.Event): void { // keep type 'JQuery.Event' here, because of sap.m.Input, function 'onsapescape' // set aria-expanded to false on the input element const domRef = this.getDomRef(); if (domRef) { const input = domRef.querySelector("input"); if (input) { input.setAttribute("aria-expanded", "false"); } } super.onsapescape(oEvent); } static renderer = { apiVersion: 2, }; }