diff --git a/packages/fiori/src/SideNavigationTemplate.tsx b/packages/fiori/src/SideNavigationTemplate.tsx
index 5f7a41a43a0ee..f9786aa793a7f 100644
--- a/packages/fiori/src/SideNavigationTemplate.tsx
+++ b/packages/fiori/src/SideNavigationTemplate.tsx
@@ -14,6 +14,8 @@ export default function SideNavigationTemplate(this: SideNavigation) {
>
{header.call(this)}
+ {filterSection.call(this)}
+
{this.collapsed ?
+ {this.showNoMatchText &&
+
+ }
}
@@ -73,3 +83,11 @@ function header(this: SideNavigation) {
) : undefined;
}
+
+function filterSection(this: SideNavigation) {
+ return !this.collapsed && this.filterSection.length ? (
+
+
+
+ ) : undefined;
+}
diff --git a/packages/fiori/src/bundle.esm.ts b/packages/fiori/src/bundle.esm.ts
index 8bcd67655e6c6..2978ba115bc24 100644
--- a/packages/fiori/src/bundle.esm.ts
+++ b/packages/fiori/src/bundle.esm.ts
@@ -47,6 +47,7 @@ import SideNavigationItem from "./SideNavigationItem.js";
import SideNavigationSubItem from "./SideNavigationSubItem.js";
import SideNavigationGroup from "./SideNavigationGroup.js";
import SideNavigation from "./SideNavigation.js";
+import SideNavigationSearchField from "./SideNavigationSearchField.js";
import SortItem from "./SortItem.js";
import UserSettingsItem from "./UserSettingsItem.js";
import SettingsDialog from "./UserSettingsDialog.js";
diff --git a/packages/fiori/src/i18n/messagebundle.properties b/packages/fiori/src/i18n/messagebundle.properties
index 1943a22d906c1..b2af72f926923 100644
--- a/packages/fiori/src/i18n/messagebundle.properties
+++ b/packages/fiori/src/i18n/messagebundle.properties
@@ -509,6 +509,15 @@ SEARCH_FIELD_LABEL=Search field
#XFLD: Placeholder text for search field with scope on mobile
SEARCH_FIELD_PLACEHOLDER_WITH_SCOPE=Search in: {0}
+#XFLD: Default placeholder text for the side navigation search field
+SIDE_NAVIGATION_SEARCH_FIELD_PLACEHOLDER=Find in Navigation
+
+#XACT: Default ARIA label for the side navigation search field
+SIDE_NAVIGATION_SEARCH_FIELD_LABEL=Side navigation filter
+
+#XACT: Default ARIA description for the side navigation search field
+SIDE_NAVIGATION_SEARCH_DESCRIPTION=Results update as you type. Press Enter for the number of results
+
#XACT: ARIA announcement for search field clear icon
SEARCH_FIELD_CLEAR_ICON=Clear search
@@ -563,6 +572,12 @@ SIDE_NAVIGATION_FLEXIBLE_LIST_LABEL=Primary Navigation Menu
#XTXT: Text for the Side Navigation Footer list
SIDE_NAVIGATION_FIXED_LIST_LABEL=Footer Navigation Menu
+#XACT: Screen reader announcement when the applied filter matches a single navigation item
+SIDE_NAVIGATION_SEARCH_MATCH_COUNT_SINGULAR={0} match found
+
+#XACT: Screen reader announcement when the applied filter matches multiple navigation items
+SIDE_NAVIGATION_SEARCH_MATCH_COUNT_PLURAL={0} matches found
+
#XTXT
SIDE_NAVIGATION_ICON_COLLAPSE=Colapse
diff --git a/packages/fiori/src/themes/SideNavigation.css b/packages/fiori/src/themes/SideNavigation.css
index 3f5c159c31288..4cc2578204112 100644
--- a/packages/fiori/src/themes/SideNavigation.css
+++ b/packages/fiori/src/themes/SideNavigation.css
@@ -26,7 +26,6 @@
}
}
-
.ui5-sn-root {
height: 100%;
display: flex;
@@ -93,4 +92,8 @@
min-height: var(--_ui5_side_navigation_navigation_separator_height);
background-color: var(--_ui5_side_navigation_navigation_separator_background_color);
border-radius: var(--_ui5_side_navigation_navigation_separator_radius);
+}
+
+.ui5-sn-filter-section {
+ padding: 0.5rem 0.5rem 0.25rem 0.5rem;
}
\ No newline at end of file
diff --git a/packages/fiori/src/themes/SideNavigationItem.css b/packages/fiori/src/themes/SideNavigationItem.css
index 06d353fb46616..df99e0eda5aa0 100644
--- a/packages/fiori/src/themes/SideNavigationItem.css
+++ b/packages/fiori/src/themes/SideNavigationItem.css
@@ -27,4 +27,15 @@
.ui5-sn-item-level1 .ui5-sn-item-text {
font-family: var(--_ui5_side_navigation_parent_item_font_family);
+}
+
+:host(.ui5-sn-item-no-match) .ui5-sn-item {
+ cursor: default;
+ padding: 0;
+}
+
+:host(.ui5-sn-item-no-match) .ui5-sn-item-text {
+ text-align: center;
+ flex: auto;
+ font-family: var(--_ui5_side_navigation_item_font_family);
}
\ No newline at end of file
diff --git a/packages/fiori/src/themes/SideNavigationItemBase.css b/packages/fiori/src/themes/SideNavigationItemBase.css
index 0645081b1bc7b..206f489c6e0e7 100644
--- a/packages/fiori/src/themes/SideNavigationItemBase.css
+++ b/packages/fiori/src/themes/SideNavigationItemBase.css
@@ -125,7 +125,7 @@ and there is an additional border that appears on hover. */
cursor: unset;
}
-:host(:not([ui5-side-navigation-item][in-popover][unselectable])) .ui5-sn-item:not(.ui5-sn-item-disabled):not(.ui5-sn-item-selected):hover {
+:host(:not([ui5-side-navigation-item][in-popover][unselectable]):not(.ui5-sn-item-no-match)) .ui5-sn-item:not(.ui5-sn-item-disabled):not(.ui5-sn-item-selected):hover {
background: var(--sapList_Hover_Background);
}
@@ -149,7 +149,7 @@ and there is an additional border that appears on hover. */
.ui5-sn-item:not(.ui5-sn-item-disabled).ui5-sn-item-selected:active,
.ui5-sn-item:not(.ui5-sn-item-disabled).ui5-sn-item-selected.ui5-sn-item-active,
-:host(:not([ui5-side-navigation-item][in-popover][unselectable])) .ui5-sn-item:not(.ui5-sn-item-disabled):active,
+:host(:not([ui5-side-navigation-item][in-popover][unselectable]):not(.ui5-sn-item-no-match)) .ui5-sn-item:not(.ui5-sn-item-disabled):active,
.ui5-sn-item:not(.ui5-sn-item-disabled).ui5-sn-item-active,
:host(.ui5-sn-item-overflow.ui5-sn-item-active) .ui5-sn-item,
:host([side-nav-collapsed]) .ui5-sn-item:not(.ui5-sn-item-disabled).ui5-sn-item-selected:active,
@@ -408,3 +408,9 @@ and there is an additional border that appears on hover. */
.ui5-sn-item-separator {
min-height: 0.625rem;
}
+
+.ui5-sn-item-highlight {
+ background-color: var(--sapContent_SearchHighlightColor);
+ font-family: var(--sapFontBoldFamily);
+ font-size: var(--sapFontSize);
+}
diff --git a/packages/fiori/src/themes/SideNavigationSearchField.css b/packages/fiori/src/themes/SideNavigationSearchField.css
new file mode 100644
index 0000000000000..d5282b93bba44
--- /dev/null
+++ b/packages/fiori/src/themes/SideNavigationSearchField.css
@@ -0,0 +1,12 @@
+:host(:not([hidden])) {
+ display: flex;
+}
+
+.ui5-side-navigation-search-field-root {
+ display: flex;
+ width: 100%;
+}
+
+.ui5-side-navigation-search-field-input {
+ width: 100%;
+}
diff --git a/packages/fiori/test/pages/SideNavigationSearch.html b/packages/fiori/test/pages/SideNavigationSearch.html
new file mode 100644
index 0000000000000..7b5b46fc55e42
--- /dev/null
+++ b/packages/fiori/test/pages/SideNavigationSearch.html
@@ -0,0 +1,42 @@
+
+
+
+
+
Navigation Layout
+
+
+
+
+
+
+
+
+
+
+ UI5 Web Components
+
+
+
+
+
+
+
+
+
+
Home
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+
+
+
diff --git a/packages/fiori/test/pages/SideNavigationSearch.js b/packages/fiori/test/pages/SideNavigationSearch.js
new file mode 100644
index 0000000000000..27dca21edf9db
--- /dev/null
+++ b/packages/fiori/test/pages/SideNavigationSearch.js
@@ -0,0 +1,480 @@
+const nl1 = document.getElementById("nl1");
+const rtlCb = document.getElementById("rtlCb");
+const searchField = document.getElementById("searchField");
+const sideNav = document.getElementById("sn1");
+
+document.getElementById("startButton").addEventListener("click", function () {
+ if (nl1.isSideCollapsed()) {
+ nl1.mode = "Expanded";
+ } else {
+ searchField.value = "";
+ executeSearch();
+ nl1.mode = "Collapsed";
+ }
+});
+
+rtlCb.addEventListener("ui5-change", function () {
+ nl1.setAttribute("dir", rtlCb.checked ? "rtl" : "");
+});
+
+const data = {
+ navigation: [{
+ title: 'Home',
+ icon: 'sap-icon://home',
+ key: 'home',
+ href: '#/home'
+ }, {
+ type: 'group',
+ title: 'Business Operations',
+ expanded: true,
+ items: [{
+ title: 'Favorites',
+ icon: 'sap-icon://unfavorite',
+ expanded: true,
+ selectable: false,
+ tagText: '3 Items',
+ tagState: 'Indication17',
+ items: [{
+ title: 'My Accounts',
+ key: 'myAccounts',
+ href: '#/myAccounts'
+ }, {
+ title: 'My Orders',
+ key: 'myOrders',
+ href: '#/myOrders',
+ tagText: '5 Pending',
+ tagState: 'Indication20'
+ }]
+ }, {
+ title: 'Customer Management',
+ icon: 'sap-icon://account',
+ expanded: true,
+ selectable: false,
+ key: 'CustomerManagement',
+ href: '#CustomerManagement',
+ items: [{
+ title: 'Contacts',
+ key: 'contacts',
+ href: '#/contacts'
+ }, {
+ title: 'Companies',
+ key: 'companies',
+ href: '#/companies'
+ }, {
+ title: 'Partners',
+ key: 'partners',
+ href: '#/partners'
+ }]
+ }, {
+ title: 'SAP Best Practices',
+ icon: 'sap-icon://learning-assistant',
+ key: 'sapBestPractices',
+ href: 'https://sap.com',
+ target: '_blank',
+ selectable: false
+ }, {
+ title: 'Sales',
+ icon: 'sap-icon://crm-sales',
+ expanded: true,
+ selectable: false,
+ key: 'Sales',
+ href: '#/Sales',
+ items: [{
+ title: 'Leads',
+ key: 'leads',
+ href: '#/leads'
+ }, {
+ title: 'Opportunities',
+ key: 'opportunities',
+ href: '#/opportunities'
+ }, {
+ title: 'Quotes',
+ key: 'quotes',
+ href: '#/quotes'
+ }, {
+ title: 'Orders',
+ key: 'orders',
+ href: '#/orders'
+ }, {
+ title: 'Invoices',
+ key: 'invoices',
+ href: '#/invoices'
+ }]
+ }, {
+ title: 'Products',
+ icon: 'sap-icon://customer-view',
+ expanded: true,
+ key: 'products',
+ href: '#/products',
+ tagText: 'Low Stock',
+ tagState: 'Indication18',
+ items: [{
+ title: 'Product Catalog',
+ key: 'productCatalog',
+ href: '#/productCatalog'
+ }, {
+ title: 'Pricing',
+ key: 'pricing',
+ href: '#/pricing'
+ }, {
+ title: 'Inventory Management',
+ key: 'inventoryManagement',
+ href: '#/inventoryManagement'
+ }]
+ }, {
+ title: 'Marketing',
+ icon: 'sap-icon://customer-view',
+ expanded: true,
+ selectable: false,
+ key: 'marketing',
+ href: '#/marketing',
+ items: [{
+ title: 'Campaigns',
+ key: 'campaigns',
+ href: '#/campaigns'
+ }, {
+ title: 'E-mail Marketing',
+ key: 'emailMarketing',
+ href: '#/emailMarketing'
+ }, {
+ title: 'MArketing Automation',
+ key: 'marketingAutomation',
+ href: '#/marketingAutomation'
+ }]
+ }, {
+ title: 'Finance',
+ icon: 'sap-icon://money-bills',
+ expanded: true,
+ selectable: false,
+ items: [{
+ title: 'Accounts Receivable',
+ key: 'accountsReceivable',
+ href: '#/accountsReceivable'
+ }, {
+ title: 'Accounts Payable',
+ key: 'accountsPayable',
+ href: '#/accountsPayable'
+ }, {
+ title: 'Budget Planning',
+ key: 'budgetPlanning',
+ href: '#/budgetPlanning'
+ }, {
+ title: 'Tax Management',
+ key: 'taxManagement',
+ href: '#/taxManagement'
+ }]
+ }, {
+ title: 'Year-End Financial Reports',
+ icon: 'sap-icon://manager-insight',
+ expanded: true,
+ key: 'reports',
+ href: '#/reports',
+ items: [{
+ title: 'Sales Reports',
+ key: 'salesReports',
+ href: '#/salesReports'
+ }, {
+ title: 'Customer reports',
+ key: 'customerReports',
+ href: '#/customerReports'
+ }]
+ }]
+ }, {
+ type: 'group',
+ title: 'System & Administration',
+ expanded: true,
+ items: [{
+ title: 'Analytics',
+ icon: 'sap-icon://bar-chart',
+ key: 'analytics',
+ href: '#/analytics',
+ tagText: 'Beta',
+ tagState: 'Indication15'
+ }, {
+ title: 'SAP Community',
+ icon: 'sap-icon://discussion-2',
+ key: 'sapCommunity',
+ href: 'https://sap.com',
+ target: '_blank',
+ selectable: false
+ }, {
+ title: 'Administration',
+ icon: 'sap-icon://settings',
+ expanded: false,
+ selectable: false,
+ items: [{
+ title: 'User Management',
+ key: 'userManagement',
+ href: '#/userManagement'
+ }, {
+ title: 'System Configuration',
+ key: 'systemConfig',
+ href: '#/systemConfig'
+ }, {
+ title: 'Audit Log',
+ key: 'auditLog',
+ href: '#/auditLog'
+ }]
+ }, {
+ title: 'Service Management',
+ icon: 'sap-icon://customer-and-supplier',
+ expanded: false,
+ selectable: false,
+ items: [{
+ title: 'Service Tickets',
+ key: 'serviceTickets',
+ href: '#/serviceTickets'
+ }, {
+ title: 'Knowledge Base',
+ key: 'knowledgeBase',
+ href: '#/knowledgeBase'
+ }, {
+ title: 'Service Contracts',
+ key: 'serviceContracts',
+ href: '#/serviceContracts'
+ }]
+ }, {
+ title: 'Notifications',
+ icon: 'sap-icon://message-information',
+ key: 'notifications',
+ href: '#/notifications',
+ tagText: '8 New',
+ tagState: 'Indication18'
+ }, {
+ title: 'SAP Training',
+ icon: 'sap-icon://course-book',
+ key: 'sapTraining',
+ href: 'https://sap.com',
+ target: '_blank',
+ selectable: false
+ }, {
+ title: 'Integration Hub',
+ icon: 'sap-icon://connected',
+ expanded: false,
+ key: 'integrationHub',
+ href: '#/integrationHub',
+ items: [{
+ title: 'API Management',
+ key: 'apiManagement',
+ href: '#/apiManagement'
+ }, {
+ title: 'Data Sync',
+ key: 'dataSync',
+ href: '#/dataSync'
+ }]
+ }]
+ }],
+ fixedNavigation: [{
+ title: 'Quick Create',
+ icon: 'sap-icon://add',
+ ariaHasPopup: "Dialog",
+ design: "Action",
+ selectable: false,
+ key: 'quickCreate'
+ }, {
+ title: 'Product Settings',
+ icon: 'sap-icon://settings',
+ key: 'productSettings',
+ href: '#/productSettings'
+ }, {
+ title: 'SAP Support Portal',
+ icon: 'sap-icon://sys-help',
+ key: 'sapSupport',
+ tagText: '24/7',
+ tagState: 'Indication16',
+ href: 'https://sap.com',
+ target: '_blank',
+ selectable: false
+ }]
+};
+
+// Map the "IndicationNN" tag states to the recommended ui5-tag colorScheme (5-10).
+function tagColorScheme(tagState) {
+ const map = {
+ Indication15: "5",
+ Indication16: "6",
+ Indication17: "7",
+ Indication18: "8",
+ Indication19: "9",
+ Indication20: "10",
+ };
+ return map[tagState] || "5";
+}
+
+// Strips the "sap-icon://" prefix expected by the icon attribute.
+function iconName(icon) {
+ return icon ? icon.replace("sap-icon://", "") : "";
+}
+
+// Escapes a value so it is safe to inject as HTML text / attribute content.
+function escapeHtml(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function tagMarkup(node) {
+ if (!node.tagText) {
+ return "";
+ }
+ return `
${escapeHtml(node.tagText)} `;
+}
+
+// Second-level (child) navigation entry.
+function subItemMarkup(node) {
+ const attrs = [`text="${escapeHtml(node.title)}"`];
+ if (node.href) {
+ attrs.push(`href="${escapeHtml(node.href)}"`);
+ }
+ if (node.target) {
+ attrs.push(`target="${escapeHtml(node.target)}"`);
+ }
+ if (node.selectable === false) {
+ attrs.push("unselectable");
+ }
+ return `
${tagMarkup(node)} `;
+}
+
+// Top-level navigation entry (optionally with sub-items). `slot` places it in a
+// named slot (e.g. "fixedItems") when provided.
+function itemMarkup(node, slot) {
+ const attrs = [`text="${escapeHtml(node.title)}"`];
+ if (slot) {
+ attrs.push(`slot="${escapeHtml(slot)}"`);
+ }
+ if (node.icon) {
+ attrs.push(`icon="${escapeHtml(iconName(node.icon))}"`);
+ }
+ if (node.href) {
+ attrs.push(`href="${escapeHtml(node.href)}"`);
+ }
+ if (node.target) {
+ attrs.push(`target="${escapeHtml(node.target)}"`);
+ }
+ if (node.design) {
+ attrs.push(`design="${escapeHtml(node.design)}"`);
+ }
+ if (node.expanded) {
+ attrs.push("expanded");
+ }
+ // Unselectable when explicitly marked, external (_blank), or an action item.
+ if (node.selectable === false || node.target === "_blank" || node.design === "Action") {
+ attrs.push("unselectable");
+ }
+ if (node.ariaHasPopup) {
+ const accAttrs = escapeHtml(JSON.stringify({ hasPopup: node.ariaHasPopup.toLowerCase() }));
+ attrs.push(`accessibility-attributes="${accAttrs}"`);
+ }
+ const children = tagMarkup(node) + (node.items || []).map(subItemMarkup).join("");
+ return `
${children} `;
+}
+
+// A "group" node maps to ui5-side-navigation-group; anything else is a plain item.
+function nodeMarkup(node) {
+ if (node.type === "group") {
+ const attrs = [`text="${escapeHtml(node.title)}"`];
+ if (node.expanded) {
+ attrs.push("expanded");
+ }
+ const children = (node.items || []).map(child => itemMarkup(child)).join("");
+ return `
${children} `;
+ }
+ return itemMarkup(node);
+}
+
+// Renders the given data into the side navigation. Keep this pure so a filtered
+// copy of `data` can be passed in later to re-render the matching items only.
+function renderNavigation(navData) {
+ // Remove only previously-rendered navigation items/groups, keeping any
+ // default slotted content (e.g. the ui5-search-field in the "filter-section" slot).
+ sideNav.querySelectorAll("ui5-side-navigation-item, ui5-side-navigation-group").forEach(el => el.remove());
+
+ const markup = (navData.navigation || []).map(nodeMarkup).join("")
+ + (navData.fixedNavigation || []).map(node => itemMarkup(node, "fixedItems")).join("");
+
+ sideNav.insertAdjacentHTML("beforeend", markup);
+
+ // Select the first selectable top-level item by default.
+ const firstSelectable = sideNav.querySelector("ui5-side-navigation-item:not([unselectable])");
+ if (firstSelectable) {
+ firstSelectable.selected = true;
+ }
+}
+
+renderNavigation(data);
+
+function filterItems(items, value) {
+ if (!items) {
+ return [];
+ }
+
+ const lowerCaseValue = value.toLowerCase();
+
+ return items.reduce(function (result, item) {
+ const titleMatches = item.title && item.title.toLowerCase().indexOf(lowerCaseValue) > -1;
+ const tagTextMatches = item.tagText && item.tagText.toLowerCase().indexOf(lowerCaseValue) > -1;
+
+ if (titleMatches || tagTextMatches) {
+ result.push(item);
+ } else if (item.items) {
+ const filteredChildren = filterItems(item.items, value);
+ if (filteredChildren.length > 0) {
+ result.push(Object.assign({}, item, { items: filteredChildren }));
+ }
+ }
+
+ return result;
+ }, []);
+}
+
+function countItems(items, value) {
+ if (!items) {
+ return 0;
+ }
+
+ const lowerCaseValue = value.toLowerCase();
+
+ return items.reduce((count, item) => {
+ const titleMatches = item.title?.toLowerCase().includes(lowerCaseValue);
+ const tagTextMatches = item.tagText?.toLowerCase().includes(lowerCaseValue);
+ return count + (titleMatches || tagTextMatches ? 1 : 0) + countItems(item.items, value);
+ }, 0);
+}
+
+function executeSearch(value) {
+ // Highlight the matching portions of the rendered item texts.
+ sideNav.highlightedText = value || "";
+
+ if (!value) {
+ renderNavigation(data);
+ return 0;
+ }
+
+ const combinedItems = data.navigation.concat(data.fixedNavigation);
+
+ const filteredData = {
+ navigation: filterItems(combinedItems, value),
+ fixedNavigation: []
+ };
+
+ renderNavigation(filteredData);
+
+ return countItems(combinedItems, value);
+}
+
+searchField.addEventListener("input", function () {
+ executeSearch(searchField.value);
+});
+
+searchField.addEventListener("search", function () {
+ const value = searchField.value;
+ if (!value) {
+ return;
+ }
+
+ const matchCount = executeSearch(value);
+ sideNav.announceSearchMatchCount(matchCount);
+});
diff --git a/packages/main/cypress/specs/Input.cy.tsx b/packages/main/cypress/specs/Input.cy.tsx
index 5c52f0fa3bb6a..5b6959d3950a4 100644
--- a/packages/main/cypress/specs/Input.cy.tsx
+++ b/packages/main/cypress/specs/Input.cy.tsx
@@ -3719,5 +3719,41 @@ describe("Input built-in filtering", () => {
expect(styles.getPropertyValue("--_ui5_input_icon_state_opacity").trim()).to.equal("");
});
});
+
+ it("should render the title tooltip from accessibleName when hideTooltip is not set", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-input]")
+ .find("[ui5-input-icon]")
+ .shadow()
+ .find(".ui5-input-icon-root")
+ .should("have.attr", "title", "Search");
+ });
+
+ it("should not render the title tooltip when hideTooltip is set", () => {
+ cy.mount(
+
+
+
+ );
+
+ // accessibleName is still exposed to screen readers via aria-label...
+ cy.get("[ui5-input]")
+ .find("[ui5-input-icon]")
+ .shadow()
+ .find(".ui5-input-icon-root")
+ .should("have.attr", "aria-label", "Search");
+
+ // ...but no native title tooltip is rendered.
+ cy.get("[ui5-input]")
+ .find("[ui5-input-icon]")
+ .shadow()
+ .find(".ui5-input-icon-root")
+ .should("not.have.attr", "title");
+ });
});
});
diff --git a/packages/main/src/InputIcon.ts b/packages/main/src/InputIcon.ts
index 9bb98166af19a..9a0928139d175 100644
--- a/packages/main/src/InputIcon.ts
+++ b/packages/main/src/InputIcon.ts
@@ -62,7 +62,7 @@ class InputIcon extends UI5Element {
* Defines the accessible name of the icon.
*
* **Note:** This property is used for accessibility purposes and will be announced by screen readers.
- * When set, it is also rendered as a native `title` tooltip.
+ * When set, it is also rendered as a native `title` tooltip, unless `hideTooltip` is set.
*
* @default undefined
* @public
@@ -70,6 +70,19 @@ class InputIcon extends UI5Element {
@property()
accessibleName?: string;
+ /**
+ * Defines whether the native `title` tooltip, derived from `accessibleName`, is displayed.
+ *
+ * **Note:** When set, the `accessibleName` is still announced by screen readers, but no
+ * `title` tooltip is rendered.
+ *
+ * @since 2.28.0
+ * @default false
+ * @public
+ */
+ @property({ type: Boolean })
+ hideTooltip = false;
+
/**
* @private
*/
@@ -127,7 +140,7 @@ class InputIcon extends UI5Element {
}
get effectiveTitle() {
- return this.accessibleName || undefined;
+ return this.hideTooltip ? undefined : (this.accessibleName || undefined);
}
}
diff --git a/packages/website/docs/_samples/fiori/SideNavigation/Search/Search.md b/packages/website/docs/_samples/fiori/SideNavigation/Search/Search.md
new file mode 100644
index 0000000000000..92d575f55b63c
--- /dev/null
+++ b/packages/website/docs/_samples/fiori/SideNavigation/Search/Search.md
@@ -0,0 +1,6 @@
+import html from '!!raw-loader!./sample.html';
+import js from '!!raw-loader!./main.js';
+import css from '!!raw-loader!./main.css';
+import react from '!!raw-loader!./sample.tsx';
+
+
diff --git a/packages/website/docs/_samples/fiori/SideNavigation/Search/main.css b/packages/website/docs/_samples/fiori/SideNavigation/Search/main.css
new file mode 100644
index 0000000000000..58e3dc3664418
--- /dev/null
+++ b/packages/website/docs/_samples/fiori/SideNavigation/Search/main.css
@@ -0,0 +1,3 @@
+.content {
+ padding: 1rem;
+}
diff --git a/packages/website/docs/_samples/fiori/SideNavigation/Search/main.js b/packages/website/docs/_samples/fiori/SideNavigation/Search/main.js
new file mode 100644
index 0000000000000..aa6aa1e7d28ed
--- /dev/null
+++ b/packages/website/docs/_samples/fiori/SideNavigation/Search/main.js
@@ -0,0 +1,485 @@
+import "@ui5/webcomponents-fiori/dist/NavigationLayout.js";
+import "@ui5/webcomponents-fiori/dist/ShellBar.js";
+import "@ui5/webcomponents-fiori/dist/ShellBarBranding.js";
+import "@ui5/webcomponents-fiori/dist/SideNavigation.js";
+import "@ui5/webcomponents-fiori/dist/SideNavigationGroup.js";
+import "@ui5/webcomponents-fiori/dist/SideNavigationItem.js";
+import "@ui5/webcomponents-fiori/dist/SideNavigationSubItem.js";
+import "@ui5/webcomponents-fiori/dist/SideNavigationSearchField.js";
+import "@ui5/webcomponents/dist/Button.js";
+import "@ui5/webcomponents/dist/Tag.js";
+
+import "@ui5/webcomponents-icons/dist/menu.js";
+import "@ui5/webcomponents-icons/dist/home.js";
+import "@ui5/webcomponents-icons/dist/unfavorite.js";
+import "@ui5/webcomponents-icons/dist/account.js";
+import "@ui5/webcomponents-icons/dist/learning-assistant.js";
+import "@ui5/webcomponents-icons/dist/crm-sales.js";
+import "@ui5/webcomponents-icons/dist/customer-view.js";
+import "@ui5/webcomponents-icons/dist/money-bills.js";
+import "@ui5/webcomponents-icons/dist/manager-insight.js";
+import "@ui5/webcomponents-icons/dist/bar-chart.js";
+import "@ui5/webcomponents-icons/dist/discussion-2.js";
+import "@ui5/webcomponents-icons/dist/settings.js";
+import "@ui5/webcomponents-icons/dist/customer-and-supplier.js";
+import "@ui5/webcomponents-icons/dist/message-information.js";
+import "@ui5/webcomponents-icons/dist/course-book.js";
+import "@ui5/webcomponents-icons/dist/connected.js";
+import "@ui5/webcomponents-icons/dist/sys-help.js";
+
+const nl1 = document.getElementById("nl1");
+const searchField = document.getElementById("searchField");
+const sideNav = document.getElementById("sn1");
+
+document.getElementById("startButton").addEventListener("click", function () {
+ if (nl1.isSideCollapsed()) {
+ nl1.mode = "Expanded";
+ } else {
+ searchField.value = "";
+ executeSearch();
+ nl1.mode = "Collapsed";
+ }
+});
+
+const data = {
+ navigation: [{
+ title: "Home",
+ icon: "home",
+ key: "home",
+ href: "#/home"
+ }, {
+ type: "group",
+ title: "Business Operations",
+ expanded: true,
+ items: [{
+ title: "Favorites",
+ icon: "unfavorite",
+ expanded: true,
+ selectable: false,
+ tagText: "3 Items",
+ tagState: "Indication17",
+ items: [{
+ title: "My Accounts",
+ key: "myAccounts",
+ href: "#/myAccounts"
+ }, {
+ title: "My Orders",
+ key: "myOrders",
+ href: "#/myOrders",
+ tagText: "5 Pending",
+ tagState: "Indication20"
+ }]
+ }, {
+ title: "Customer Management",
+ icon: "account",
+ expanded: true,
+ selectable: false,
+ key: "CustomerManagement",
+ href: "#CustomerManagement",
+ items: [{
+ title: "Contacts",
+ key: "contacts",
+ href: "#/contacts"
+ }, {
+ title: "Companies",
+ key: "companies",
+ href: "#/companies"
+ }, {
+ title: "Partners",
+ key: "partners",
+ href: "#/partners"
+ }]
+ }, {
+ title: "SAP Best Practices",
+ icon: "learning-assistant",
+ key: "sapBestPractices",
+ href: "https://sap.com",
+ target: "_blank",
+ selectable: false
+ }, {
+ title: "Sales",
+ icon: "crm-sales",
+ expanded: true,
+ selectable: false,
+ key: "Sales",
+ href: "#/Sales",
+ items: [{
+ title: "Leads",
+ key: "leads",
+ href: "#/leads"
+ }, {
+ title: "Opportunities",
+ key: "opportunities",
+ href: "#/opportunities"
+ }, {
+ title: "Quotes",
+ key: "quotes",
+ href: "#/quotes"
+ }, {
+ title: "Orders",
+ key: "orders",
+ href: "#/orders"
+ }, {
+ title: "Invoices",
+ key: "invoices",
+ href: "#/invoices"
+ }]
+ }, {
+ title: "Products",
+ icon: "customer-view",
+ expanded: true,
+ key: "products",
+ href: "#/products",
+ tagText: "Low Stock",
+ tagState: "Indication18",
+ items: [{
+ title: "Product Catalog",
+ key: "productCatalog",
+ href: "#/productCatalog"
+ }, {
+ title: "Pricing",
+ key: "pricing",
+ href: "#/pricing"
+ }, {
+ title: "Inventory Management",
+ key: "inventoryManagement",
+ href: "#/inventoryManagement"
+ }]
+ }, {
+ title: "Marketing",
+ icon: "customer-view",
+ expanded: true,
+ selectable: false,
+ key: "marketing",
+ href: "#/marketing",
+ items: [{
+ title: "Campaigns",
+ key: "campaigns",
+ href: "#/campaigns"
+ }, {
+ title: "E-mail Marketing",
+ key: "emailMarketing",
+ href: "#/emailMarketing"
+ }, {
+ title: "Marketing Automation",
+ key: "marketingAutomation",
+ href: "#/marketingAutomation"
+ }]
+ }, {
+ title: "Finance",
+ icon: "money-bills",
+ expanded: true,
+ selectable: false,
+ items: [{
+ title: "Accounts Receivable",
+ key: "accountsReceivable",
+ href: "#/accountsReceivable"
+ }, {
+ title: "Accounts Payable",
+ key: "accountsPayable",
+ href: "#/accountsPayable"
+ }, {
+ title: "Budget Planning",
+ key: "budgetPlanning",
+ href: "#/budgetPlanning"
+ }, {
+ title: "Tax Management",
+ key: "taxManagement",
+ href: "#/taxManagement"
+ }]
+ }, {
+ title: "Year-End Financial Reports",
+ icon: "manager-insight",
+ expanded: true,
+ key: "reports",
+ href: "#/reports",
+ items: [{
+ title: "Sales Reports",
+ key: "salesReports",
+ href: "#/salesReports"
+ }, {
+ title: "Customer reports",
+ key: "customerReports",
+ href: "#/customerReports"
+ }]
+ }]
+ }, {
+ type: "group",
+ title: "System & Administration",
+ expanded: true,
+ items: [{
+ title: "Analytics",
+ icon: "bar-chart",
+ key: "analytics",
+ href: "#/analytics",
+ tagText: "Beta",
+ tagState: "Indication15"
+ }, {
+ title: "SAP Community",
+ icon: "discussion-2",
+ key: "sapCommunity",
+ href: "https://sap.com",
+ target: "_blank",
+ selectable: false
+ }, {
+ title: "Administration",
+ icon: "settings",
+ expanded: false,
+ selectable: false,
+ items: [{
+ title: "User Management",
+ key: "userManagement",
+ href: "#/userManagement"
+ }, {
+ title: "System Configuration",
+ key: "systemConfig",
+ href: "#/systemConfig"
+ }, {
+ title: "Audit Log",
+ key: "auditLog",
+ href: "#/auditLog"
+ }]
+ }, {
+ title: "Service Management",
+ icon: "customer-and-supplier",
+ expanded: false,
+ selectable: false,
+ items: [{
+ title: "Service Tickets",
+ key: "serviceTickets",
+ href: "#/serviceTickets"
+ }, {
+ title: "Knowledge Base",
+ key: "knowledgeBase",
+ href: "#/knowledgeBase"
+ }, {
+ title: "Service Contracts",
+ key: "serviceContracts",
+ href: "#/serviceContracts"
+ }]
+ }, {
+ title: "Notifications",
+ icon: "message-information",
+ key: "notifications",
+ href: "#/notifications",
+ tagText: "8 New",
+ tagState: "Indication18"
+ }, {
+ title: "SAP Training",
+ icon: "course-book",
+ key: "sapTraining",
+ href: "https://sap.com",
+ target: "_blank",
+ selectable: false
+ }, {
+ title: "Integration Hub",
+ icon: "connected",
+ expanded: false,
+ key: "integrationHub",
+ href: "#/integrationHub",
+ items: [{
+ title: "API Management",
+ key: "apiManagement",
+ href: "#/apiManagement"
+ }, {
+ title: "Data Sync",
+ key: "dataSync",
+ href: "#/dataSync"
+ }]
+ }]
+ }],
+ fixedNavigation: [{
+ title: "Product Settings",
+ icon: "settings",
+ key: "productSettings",
+ href: "#/productSettings"
+ }, {
+ title: "SAP Support Portal",
+ icon: "sys-help",
+ key: "sapSupport",
+ tagText: "24/7",
+ tagState: "Indication16",
+ href: "https://sap.com",
+ target: "_blank",
+ selectable: false
+ }]
+};
+
+// Map the "IndicationNN" tag states to the recommended ui5-tag colorScheme (5-10).
+function tagColorScheme(tagState) {
+ const map = {
+ Indication15: "5",
+ Indication16: "6",
+ Indication17: "7",
+ Indication18: "8",
+ Indication19: "9",
+ Indication20: "10",
+ };
+ return map[tagState] || "5";
+}
+
+// Escapes a value so it is safe to inject as HTML text / attribute content.
+function escapeHtml(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function tagMarkup(node) {
+ if (!node.tagText) {
+ return "";
+ }
+ return `
${escapeHtml(node.tagText)} `;
+}
+
+// Second-level (child) navigation entry.
+function subItemMarkup(node) {
+ const attrs = [`text="${escapeHtml(node.title)}"`];
+ if (node.href) {
+ attrs.push(`href="${escapeHtml(node.href)}"`);
+ }
+ if (node.target) {
+ attrs.push(`target="${escapeHtml(node.target)}"`);
+ }
+ if (node.selectable === false) {
+ attrs.push("unselectable");
+ }
+ return `
${tagMarkup(node)} `;
+}
+
+// Top-level navigation entry (optionally with sub-items). `slot` places it in a
+// named slot (e.g. "fixedItems") when provided.
+function itemMarkup(node, slot) {
+ const attrs = [`text="${escapeHtml(node.title)}"`];
+ if (slot) {
+ attrs.push(`slot="${escapeHtml(slot)}"`);
+ }
+ if (node.icon) {
+ attrs.push(`icon="${escapeHtml(node.icon)}"`);
+ }
+ if (node.href) {
+ attrs.push(`href="${escapeHtml(node.href)}"`);
+ }
+ if (node.target) {
+ attrs.push(`target="${escapeHtml(node.target)}"`);
+ }
+ if (node.expanded) {
+ attrs.push("expanded");
+ }
+ // Unselectable when explicitly marked or external (_blank).
+ if (node.selectable === false || node.target === "_blank") {
+ attrs.push("unselectable");
+ }
+ const children = tagMarkup(node) + (node.items || []).map(subItemMarkup).join("");
+ return `
${children} `;
+}
+
+// A "group" node maps to ui5-side-navigation-group; anything else is a plain item.
+function nodeMarkup(node) {
+ if (node.type === "group") {
+ const attrs = [`text="${escapeHtml(node.title)}"`];
+ if (node.expanded) {
+ attrs.push("expanded");
+ }
+ const children = (node.items || []).map(child => itemMarkup(child)).join("");
+ return `
${children} `;
+ }
+ return itemMarkup(node);
+}
+
+// Renders the given data into the side navigation. Keep this pure so a filtered
+// copy of `data` can be passed in later to re-render the matching items only.
+function renderNavigation(navData) {
+ // Remove only previously-rendered navigation items/groups, keeping any
+ // default slotted content (e.g. the search field in the "filterSection" slot).
+ sideNav.querySelectorAll("ui5-side-navigation-item, ui5-side-navigation-group").forEach(el => el.remove());
+
+ const markup = (navData.navigation || []).map(nodeMarkup).join("")
+ + (navData.fixedNavigation || []).map(node => itemMarkup(node, "fixedItems")).join("");
+
+ sideNav.insertAdjacentHTML("beforeend", markup);
+
+ // Select the first selectable top-level item by default.
+ const firstSelectable = sideNav.querySelector("ui5-side-navigation-item:not([unselectable])");
+ if (firstSelectable) {
+ firstSelectable.selected = true;
+ }
+}
+
+renderNavigation(data);
+
+function filterItems(items, value) {
+ if (!items) {
+ return [];
+ }
+
+ const lowerCaseValue = value.toLowerCase();
+
+ return items.reduce(function (result, item) {
+ const titleMatches = item.title && item.title.toLowerCase().indexOf(lowerCaseValue) > -1;
+ const tagTextMatches = item.tagText && item.tagText.toLowerCase().indexOf(lowerCaseValue) > -1;
+
+ if (titleMatches || tagTextMatches) {
+ result.push(item);
+ } else if (item.items) {
+ const filteredChildren = filterItems(item.items, value);
+ if (filteredChildren.length > 0) {
+ result.push(Object.assign({}, item, { items: filteredChildren }));
+ }
+ }
+
+ return result;
+ }, []);
+}
+
+function countItems(items, value) {
+ if (!items) {
+ return 0;
+ }
+
+ const lowerCaseValue = value.toLowerCase();
+
+ return items.reduce((count, item) => {
+ const titleMatches = item.title?.toLowerCase().includes(lowerCaseValue);
+ const tagTextMatches = item.tagText?.toLowerCase().includes(lowerCaseValue);
+ return count + (titleMatches || tagTextMatches ? 1 : 0) + countItems(item.items, value);
+ }, 0);
+}
+
+function executeSearch(value) {
+ // Highlight the matching portions of the rendered item texts.
+ sideNav.highlightedText = value || "";
+
+ if (!value) {
+ renderNavigation(data);
+ return 0;
+ }
+
+ const combinedItems = data.navigation.concat(data.fixedNavigation);
+
+ const filteredData = {
+ navigation: filterItems(combinedItems, value),
+ fixedNavigation: []
+ };
+
+ renderNavigation(filteredData);
+
+ return countItems(combinedItems, value);
+}
+
+searchField.addEventListener("input", function () {
+ executeSearch(searchField.value);
+});
+
+searchField.addEventListener("search", function () {
+ const value = searchField.value;
+ if (!value) {
+ return;
+ }
+
+ const matchCount = executeSearch(value);
+ sideNav.announceSearchMatchCount(matchCount);
+});
diff --git a/packages/website/docs/_samples/fiori/SideNavigation/Search/sample.html b/packages/website/docs/_samples/fiori/SideNavigation/Search/sample.html
new file mode 100644
index 0000000000000..b00d89e9dd8cb
--- /dev/null
+++ b/packages/website/docs/_samples/fiori/SideNavigation/Search/sample.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
Sample
+
+
+
+
+
+
+
+
+ UI5 Web Components
+
+
+
+
+
+
+
+
+
+
Home
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/website/docs/_samples/fiori/SideNavigation/Search/sample.tsx b/packages/website/docs/_samples/fiori/SideNavigation/Search/sample.tsx
new file mode 100644
index 0000000000000..7e19f6fc5a864
--- /dev/null
+++ b/packages/website/docs/_samples/fiori/SideNavigation/Search/sample.tsx
@@ -0,0 +1,443 @@
+import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js";
+import { useState, useRef, useCallback } from "react";
+import NavigationLayoutClass from "@ui5/webcomponents-fiori/dist/NavigationLayout.js";
+import ShellBarClass from "@ui5/webcomponents-fiori/dist/ShellBar.js";
+import ShellBarBrandingClass from "@ui5/webcomponents-fiori/dist/ShellBarBranding.js";
+import SideNavigationClass from "@ui5/webcomponents-fiori/dist/SideNavigation.js";
+import SideNavigationGroupClass from "@ui5/webcomponents-fiori/dist/SideNavigationGroup.js";
+import SideNavigationItemClass from "@ui5/webcomponents-fiori/dist/SideNavigationItem.js";
+import SideNavigationSubItemClass from "@ui5/webcomponents-fiori/dist/SideNavigationSubItem.js";
+import SideNavigationSearchFieldClass from "@ui5/webcomponents-fiori/dist/SideNavigationSearchField.js";
+import ButtonClass from "@ui5/webcomponents/dist/Button.js";
+import TagClass from "@ui5/webcomponents/dist/Tag.js";
+import "@ui5/webcomponents-icons/dist/menu.js";
+import "@ui5/webcomponents-icons/dist/home.js";
+import "@ui5/webcomponents-icons/dist/unfavorite.js";
+import "@ui5/webcomponents-icons/dist/account.js";
+import "@ui5/webcomponents-icons/dist/learning-assistant.js";
+import "@ui5/webcomponents-icons/dist/crm-sales.js";
+import "@ui5/webcomponents-icons/dist/customer-view.js";
+import "@ui5/webcomponents-icons/dist/money-bills.js";
+import "@ui5/webcomponents-icons/dist/manager-insight.js";
+import "@ui5/webcomponents-icons/dist/bar-chart.js";
+import "@ui5/webcomponents-icons/dist/discussion-2.js";
+import "@ui5/webcomponents-icons/dist/settings.js";
+import "@ui5/webcomponents-icons/dist/customer-and-supplier.js";
+import "@ui5/webcomponents-icons/dist/message-information.js";
+import "@ui5/webcomponents-icons/dist/course-book.js";
+import "@ui5/webcomponents-icons/dist/connected.js";
+import "@ui5/webcomponents-icons/dist/sys-help.js";
+
+const NavigationLayout = createReactComponent(NavigationLayoutClass);
+const ShellBar = createReactComponent(ShellBarClass);
+const ShellBarBranding = createReactComponent(ShellBarBrandingClass);
+const SideNavigation = createReactComponent(SideNavigationClass);
+const SideNavigationGroup = createReactComponent(SideNavigationGroupClass);
+const SideNavigationItem = createReactComponent(SideNavigationItemClass);
+const SideNavigationSubItem = createReactComponent(SideNavigationSubItemClass);
+const SideNavigationSearchField = createReactComponent(SideNavigationSearchFieldClass);
+const Button = createReactComponent(ButtonClass);
+const Tag = createReactComponent(TagClass);
+
+interface NavNode {
+ title: string;
+ icon?: string;
+ href?: string;
+ target?: string;
+ type?: "group";
+ expanded?: boolean;
+ selectable?: boolean;
+ tagText?: string;
+ tagState?: string;
+ items?: NavNode[];
+}
+
+const data: { navigation: NavNode[]; fixedNavigation: NavNode[] } = {
+ navigation: [
+ { title: "Home", icon: "home", href: "#/home" },
+ {
+ type: "group",
+ title: "Business Operations",
+ expanded: true,
+ items: [
+ {
+ title: "Favorites",
+ icon: "unfavorite",
+ expanded: true,
+ selectable: false,
+ tagText: "3 Items",
+ tagState: "Indication17",
+ items: [
+ { title: "My Accounts", href: "#/myAccounts" },
+ {
+ title: "My Orders",
+ href: "#/myOrders",
+ tagText: "5 Pending",
+ tagState: "Indication20",
+ },
+ ],
+ },
+ {
+ title: "Customer Management",
+ icon: "account",
+ expanded: true,
+ selectable: false,
+ href: "#CustomerManagement",
+ items: [
+ { title: "Contacts", href: "#/contacts" },
+ { title: "Companies", href: "#/companies" },
+ { title: "Partners", href: "#/partners" },
+ ],
+ },
+ {
+ title: "SAP Best Practices",
+ icon: "learning-assistant",
+ href: "https://sap.com",
+ target: "_blank",
+ selectable: false,
+ },
+ {
+ title: "Sales",
+ icon: "crm-sales",
+ expanded: true,
+ selectable: false,
+ href: "#/Sales",
+ items: [
+ { title: "Leads", href: "#/leads" },
+ { title: "Opportunities", href: "#/opportunities" },
+ { title: "Quotes", href: "#/quotes" },
+ { title: "Orders", href: "#/orders" },
+ { title: "Invoices", href: "#/invoices" },
+ ],
+ },
+ {
+ title: "Products",
+ icon: "customer-view",
+ expanded: true,
+ href: "#/products",
+ tagText: "Low Stock",
+ tagState: "Indication18",
+ items: [
+ { title: "Product Catalog", href: "#/productCatalog" },
+ { title: "Pricing", href: "#/pricing" },
+ { title: "Inventory Management", href: "#/inventoryManagement" },
+ ],
+ },
+ {
+ title: "Marketing",
+ icon: "customer-view",
+ expanded: true,
+ selectable: false,
+ href: "#/marketing",
+ items: [
+ { title: "Campaigns", href: "#/campaigns" },
+ { title: "E-mail Marketing", href: "#/emailMarketing" },
+ { title: "Marketing Automation", href: "#/marketingAutomation" },
+ ],
+ },
+ {
+ title: "Finance",
+ icon: "money-bills",
+ expanded: true,
+ selectable: false,
+ items: [
+ { title: "Accounts Receivable", href: "#/accountsReceivable" },
+ { title: "Accounts Payable", href: "#/accountsPayable" },
+ { title: "Budget Planning", href: "#/budgetPlanning" },
+ { title: "Tax Management", href: "#/taxManagement" },
+ ],
+ },
+ {
+ title: "Year-End Financial Reports",
+ icon: "manager-insight",
+ expanded: true,
+ href: "#/reports",
+ items: [
+ { title: "Sales Reports", href: "#/salesReports" },
+ { title: "Customer reports", href: "#/customerReports" },
+ ],
+ },
+ ],
+ },
+ {
+ type: "group",
+ title: "System & Administration",
+ expanded: true,
+ items: [
+ {
+ title: "Analytics",
+ icon: "bar-chart",
+ href: "#/analytics",
+ tagText: "Beta",
+ tagState: "Indication15",
+ },
+ {
+ title: "SAP Community",
+ icon: "discussion-2",
+ href: "https://sap.com",
+ target: "_blank",
+ selectable: false,
+ },
+ {
+ title: "Administration",
+ icon: "settings",
+ selectable: false,
+ items: [
+ { title: "User Management", href: "#/userManagement" },
+ { title: "System Configuration", href: "#/systemConfig" },
+ { title: "Audit Log", href: "#/auditLog" },
+ ],
+ },
+ {
+ title: "Service Management",
+ icon: "customer-and-supplier",
+ selectable: false,
+ items: [
+ { title: "Service Tickets", href: "#/serviceTickets" },
+ { title: "Knowledge Base", href: "#/knowledgeBase" },
+ { title: "Service Contracts", href: "#/serviceContracts" },
+ ],
+ },
+ {
+ title: "Notifications",
+ icon: "message-information",
+ href: "#/notifications",
+ tagText: "8 New",
+ tagState: "Indication18",
+ },
+ {
+ title: "SAP Training",
+ icon: "course-book",
+ href: "https://sap.com",
+ target: "_blank",
+ selectable: false,
+ },
+ {
+ title: "Integration Hub",
+ icon: "connected",
+ href: "#/integrationHub",
+ items: [
+ { title: "API Management", href: "#/apiManagement" },
+ { title: "Data Sync", href: "#/dataSync" },
+ ],
+ },
+ ],
+ },
+ ],
+ fixedNavigation: [
+ { title: "Product Settings", icon: "settings", href: "#/productSettings" },
+ {
+ title: "SAP Support Portal",
+ icon: "sys-help",
+ tagText: "24/7",
+ tagState: "Indication16",
+ href: "https://sap.com",
+ target: "_blank",
+ selectable: false,
+ },
+ ],
+};
+
+// Map the "IndicationNN" tag states to the recommended ui5-tag colorScheme (5-10).
+function tagColorScheme(tagState?: string): string {
+ const map: Record
= {
+ Indication15: "5",
+ Indication16: "6",
+ Indication17: "7",
+ Indication18: "8",
+ Indication19: "9",
+ Indication20: "10",
+ };
+ return (tagState && map[tagState]) || "5";
+}
+
+function renderTag(node: NavNode) {
+ if (!node.tagText) {
+ return null;
+ }
+ return (
+
+ {node.tagText}
+
+ );
+}
+
+// Keeps only the branches whose title/tag matches, preserving matching ancestors.
+function filterItems(items: NavNode[] | undefined, value: string): NavNode[] {
+ if (!items) {
+ return [];
+ }
+
+ const lowerCaseValue = value.toLowerCase();
+
+ return items.reduce((result, item) => {
+ const titleMatches = item.title.toLowerCase().includes(lowerCaseValue);
+ const tagTextMatches = !!item.tagText?.toLowerCase().includes(lowerCaseValue);
+
+ if (titleMatches || tagTextMatches) {
+ result.push(item);
+ } else if (item.items) {
+ const filteredChildren = filterItems(item.items, value);
+ if (filteredChildren.length > 0) {
+ result.push({ ...item, items: filteredChildren });
+ }
+ }
+
+ return result;
+ }, []);
+}
+
+function countItems(items: NavNode[] | undefined, value: string): number {
+ if (!items) {
+ return 0;
+ }
+
+ const lowerCaseValue = value.toLowerCase();
+
+ return items.reduce((count, item) => {
+ const titleMatches = item.title.toLowerCase().includes(lowerCaseValue);
+ const tagTextMatches = !!item.tagText?.toLowerCase().includes(lowerCaseValue);
+ return count + (titleMatches || tagTextMatches ? 1 : 0) + countItems(item.items, value);
+ }, 0);
+}
+
+function renderSubItem(node: NavNode, index: number) {
+ return (
+
+ {renderTag(node)}
+
+ );
+}
+
+function renderItem(node: NavNode, index: number, slot?: string) {
+ // Unselectable when explicitly marked, external (_blank), or an action item.
+ const unselectable =
+ node.selectable === false ||
+ node.target === "_blank";
+
+ return (
+
+ {renderTag(node)}
+ {(node.items || []).map(renderSubItem)}
+
+ );
+}
+
+function renderNode(node: NavNode, index: number) {
+ if (node.type === "group") {
+ return (
+
+ {(node.items || []).map((child, childIndex) => renderItem(child, childIndex))}
+
+ );
+ }
+ return renderItem(node, index);
+}
+
+function App() {
+ const [query, setQuery] = useState("");
+ const layoutRef = useRef>(null);
+ const sideNavRef = useRef>(null);
+
+ const filteredNavigation = query
+ ? filterItems(data.navigation.concat(data.fixedNavigation), query)
+ : data.navigation;
+ const filteredFixed = query ? [] : data.fixedNavigation;
+
+ const handleStartButtonClick = useCallback(() => {
+ const layout = layoutRef.current;
+ if (!layout) {
+ return;
+ }
+ if (layout.isSideCollapsed()) {
+ layout.mode = "Expanded";
+ } else {
+ setQuery("");
+ layout.mode = "Collapsed";
+ }
+ }, []);
+
+ const handleInput = useCallback((e: Event) => {
+ setQuery((e.target as HTMLInputElement).value);
+ }, []);
+
+ const handleSearch = useCallback((e: Event) => {
+ const value = (e.target as HTMLInputElement).value;
+ if (!value) {
+ return;
+ }
+ sideNavRef.current?.announceSearchMatchCount(
+ countItems(data.navigation.concat(data.fixedNavigation), value)
+ );
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ UI5 Web Components
+
+
+
+
+
+ {filteredNavigation.map(renderNode)}
+ {filteredFixed.map((node, index) => renderItem(node, index, "fixedItems"))}
+
+
+
+
Home
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
+ ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat.
+
+
+
+
+ >
+ );
+}
+
+export default App;
diff --git a/packages/website/docs/components/fiori/samples.json b/packages/website/docs/components/fiori/samples.json
index 8f2bcdc9c52fe..cdc9d1c9356d0 100644
--- a/packages/website/docs/components/fiori/samples.json
+++ b/packages/website/docs/components/fiori/samples.json
@@ -281,6 +281,11 @@
"name": "Tags",
"title": "Indication Tags",
"description": "Navigation items can display indication tags to highlight status or phases such as \"New\", \"Beta\", or \"Deprecated\"."
+ },
+ {
+ "name": "Search",
+ "title": "Search",
+ "description": "A search field in the filter section and filterable navigation items."
}
]
},
diff --git a/packages/website/src/components/Editor/ReactPlayground.tsx b/packages/website/src/components/Editor/ReactPlayground.tsx
index e8dcd4eb9721a..ad15f37f1ad63 100644
--- a/packages/website/src/components/Editor/ReactPlayground.tsx
+++ b/packages/website/src/components/Editor/ReactPlayground.tsx
@@ -200,6 +200,7 @@ import SideNavigationClass from "@ui5/webcomponents-fiori/dist/SideNavigation.js
import SideNavigationItemClass from "@ui5/webcomponents-fiori/dist/SideNavigationItem.js";
import SideNavigationSubItemClass from "@ui5/webcomponents-fiori/dist/SideNavigationSubItem.js";
import SideNavigationGroupClass from "@ui5/webcomponents-fiori/dist/SideNavigationGroup.js";
+import SideNavigationSearchFieldClass from "@ui5/webcomponents-fiori/dist/SideNavigationSearchField.js";
import NotificationListClass from "@ui5/webcomponents-fiori/dist/NotificationList.js";
import NotificationListItemClass from "@ui5/webcomponents-fiori/dist/NotificationListItem.js";
import NotificationListGroupItemClass from "@ui5/webcomponents-fiori/dist/NotificationListGroupItem.js";
@@ -323,7 +324,7 @@ const ComponentClasses: Record = {
TableHeaderCellActionAIClass, ToolbarItemClass, DateRangeClass,
// fiori package
ShellBarClass, ShellBarItemClass, SideNavigationClass, SideNavigationItemClass,
- SideNavigationSubItemClass, SideNavigationGroupClass, NotificationListClass, NotificationListItemClass,
+ SideNavigationSubItemClass, SideNavigationGroupClass, SideNavigationSearchFieldClass, NotificationListClass, NotificationListItemClass,
NotificationListGroupItemClass, UploadCollectionClass, UploadCollectionItemClass, WizardClass,
WizardStepClass, IllustratedMessageClass, TimelineClass, TimelineItemClass, TimelineGroupItemClass,
PageClass, DynamicPageClass, DynamicPageTitleClass, DynamicPageHeaderClass, DynamicSideContentClass,
diff --git a/packages/website/src/components/Editor/monaco-ui5-types.d.ts b/packages/website/src/components/Editor/monaco-ui5-types.d.ts
index feef0342d9b99..7ae7d88d28af3 100644
--- a/packages/website/src/components/Editor/monaco-ui5-types.d.ts
+++ b/packages/website/src/components/Editor/monaco-ui5-types.d.ts
@@ -103,6 +103,7 @@ interface UI5BaseProps {
id?: string;
class?: string;
className?: string;
+ dir?: string;
style?: React.CSSProperties | string;
slot?: string;
key?: string | number;
@@ -2005,8 +2006,10 @@ interface ShellBarSpacerProps extends UI5BaseProps {
interface SideNavigationProps extends UI5BaseProps {
collapsed?: boolean;
accessibleName?: string;
+ highlightedText?: string;
items?: React.ReactNode;
fixedItems?: React.ReactNode;
+ filterSection?: React.ReactNode;
header?: React.ReactNode;
eventDetails?: any;
onSelectionChange?: (event: UI5CustomEvent) => void;
@@ -2068,6 +2071,20 @@ interface SideNavigationGroupProps extends UI5BaseProps {
onClick?: (event: UI5CustomEvent) => void;
}
+/** SideNavigationSearchField component props */
+interface SideNavigationSearchFieldProps extends UI5BaseProps {
+ value?: string;
+ placeholder?: string;
+ showClearIcon?: boolean;
+ accessibleName?: string;
+ accessibleNameRef?: string;
+ accessibleDescriptionRef?: string;
+ ariaControls?: string;
+ eventDetails?: any;
+ onInput?: (event: UI5CustomEvent) => void;
+ onSearch?: (event: UI5CustomEvent) => void;
+}
+
/** NavigationLayout component props */
interface NavigationLayoutProps extends UI5BaseProps {
mode?: "Auto" | "Collapsed" | "Expanded";
@@ -4398,6 +4415,14 @@ declare module "@ui5/webcomponents-fiori/dist/SideNavigationGroup.js" {
export default SideNavigationGroup;
}
+declare module "@ui5/webcomponents-fiori/dist/SideNavigationSearchField.js" {
+ class SideNavigationSearchField {
+ static _jsxProps: SideNavigationSearchFieldProps;
+ _jsxProps: SideNavigationSearchFieldProps;
+ }
+ export default SideNavigationSearchField;
+}
+
declare module "@ui5/webcomponents-fiori/dist/NavigationLayout.js" {
class NavigationLayout {
static _jsxProps: NavigationLayoutProps;
@@ -5212,6 +5237,7 @@ declare function SideNavigation(props: SideNavigationProps): JSX.Element;
declare function SideNavigationItem(props: SideNavigationItemProps): JSX.Element;
declare function SideNavigationSubItem(props: SideNavigationSubItemProps): JSX.Element;
declare function SideNavigationGroup(props: SideNavigationGroupProps): JSX.Element;
+declare function SideNavigationSearchField(props: SideNavigationSearchFieldProps): JSX.Element;
declare function NavigationLayout(props: NavigationLayoutProps): JSX.Element;
declare function NotificationList(props: NotificationListProps): JSX.Element;
declare function NotificationListItem(props: NotificationListItemProps): JSX.Element;