diff --git a/.gitignore b/.gitignore
index b608cf95c56a..3c6ff22bbcbd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,3 +99,5 @@ packages/playground/assets/*.gif
packages/playground/assets/*.css
packages/playground/assets/test/pages/*.css
packages/playground/assets/test/pages/*.js
+
+AGENTS.md
\ No newline at end of file
diff --git a/packages/fiori/cypress/specs/ShellBarV2.cy.tsx b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx
new file mode 100644
index 000000000000..ca1c52462202
--- /dev/null
+++ b/packages/fiori/cypress/specs/ShellBarV2.cy.tsx
@@ -0,0 +1,1668 @@
+import ShellBar from "../../src/ShellBarV2.js";
+import ShellBarItem from "../../src/ShellBarV2Item.js";
+import ShellBarSpacer from "../../src/ShellBarSpacer.js";
+import activities from "@ui5/webcomponents-icons/dist/activities.js";
+import navBack from "@ui5/webcomponents-icons/dist/nav-back.js";
+import sysHelp from "@ui5/webcomponents-icons/dist/sys-help.js";
+import da from "@ui5/webcomponents-icons/dist/da.js";
+import "@ui5/webcomponents-icons/dist/accept.js";
+import "@ui5/webcomponents-icons/dist/alert.js";
+import "@ui5/webcomponents-icons/dist/disconnected.js";
+import "@ui5/webcomponents-icons/dist/incoming-call.js";
+import Input from "@ui5/webcomponents/dist/Input.js";
+import Button from "@ui5/webcomponents/dist/Button.js";
+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 ShellBarBranding from "../../src/ShellBarBranding.js";
+import ShellBarSearch from "../../src/ShellBarSearch.js";
+
+const RESIZE_THROTTLE_RATE = 300; // ms
+
+describe("Responsiveness", () => {
+ function basicTemplate() {
+ return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Instructions
+
+
+
+ {/* PR2 */}
+ PR2
+ ;
+ }
+
+ function templateWithMenuItems() {
+ return
+
+
+
+ Application 3
+ Application 4
+ Application 5
+
+
+
+
+
+
+
+
+
+
+
+
+ Instructions
+
+ ;
+ }
+
+ function templateWithOnlyOneAction() {
+ return
+
+ ;
+ }
+ beforeEach(() => {
+ cy.mount(basicTemplate()).as("html");
+
+ // breakpoints are set on resize event
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("#shellbar")
+ .as("shellbar");
+ });
+ afterEach(() => {
+ cy.viewport(1920, 1080);
+ });
+
+ it("tests XL Breakpoint 1920px", () => {
+ cy.viewport(1920, 1080);
+
+ cy.get("@shellbar").should("have.prop", "breakpointSize", "XL");
+
+ cy.get("@shellbar").find("ui5-toggle-button[slot='assistant']").as("assistant");
+ cy.get("@shellbar").find("ui5-button[slot='startButton']").as("backButton");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-title").as("primaryTitle");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-secondary-title").as("secondaryTitle");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-custom-item").as("customActionIcon1");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-bell-button").as("notificationsIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-image-button").as("profileIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-button-product-switch").as("productSwitchIcon");
+
+ cy.get("@assistant").should("be.visible");
+ // V2: Overflow button uses conditional rendering - not rendered when nothing overflows
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-overflow-button").should("not.exist");
+ cy.get("@backButton").should("be.visible");
+ cy.get("@primaryTitle").should("be.visible");
+ cy.get("@secondaryTitle").should("be.visible");
+ cy.get("@customActionIcon1").should("be.visible");
+ cy.get("@notificationsIcon").should("be.visible");
+ cy.get("@profileIcon").should("be.visible");
+ cy.get("@productSwitchIcon").should("be.visible");
+ });
+
+ it("tests S Breakpoint and overflow 500px", () => {
+ cy.viewport(500, 1680);
+
+ cy.get("@shellbar").should("have.prop", "breakpointSize", "S");
+
+ cy.get("@shellbar").find("ui5-toggle-button[slot='assistant']").as("assistant");
+ cy.get("@shellbar").find("ui5-button[slot='startButton']").as("backButton");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-custom-item").as("customActionIcon1");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-bell-button").as("notificationsIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-image-button").as("profileIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-button-product-switch").as("productSwitchIcon");
+
+ cy.get("@assistant").should("be.visible");
+ // V2: Overflow button uses conditional rendering - not rendered when nothing overflows
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-overflow-button").should("not.exist");
+ cy.get("@backButton").should("be.visible");
+ cy.get("@customActionIcon1").should("be.visible");
+ cy.get("@notificationsIcon").should("be.visible");
+ cy.get("@profileIcon").should("be.visible");
+ cy.get("@productSwitchIcon").should("be.visible");
+ });
+
+ it("tests XL Breakpoint 1820px", () => {
+ cy.viewport(1820, 1680);
+
+ cy.get("@shellbar").should("have.prop", "breakpointSize", "L");
+ });
+
+ it("tests L Breakpoint 1400px", () => {
+ cy.viewport(1400, 1680);
+
+ cy.get("@shellbar").should("have.prop", "breakpointSize", "L");
+
+ cy.get("@shellbar").find("ui5-toggle-button[slot='assistant']").as("assistant");
+ cy.get("@shellbar").find("ui5-button[slot='startButton']").as("backButton");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-title").as("primaryTitle");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-secondary-title").as("secondaryTitle");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-search-button").as("searchIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-custom-item").as("customActionIcon1");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-bell-button").as("notificationsIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-image-button").as("profileIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-button-product-switch").as("productSwitchIcon");
+
+ cy.get("@assistant").should("be.visible");
+ // V2: Overflow button uses conditional rendering - not rendered when nothing overflows
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-overflow-button").should("not.exist");
+ cy.get("@backButton").should("be.visible");
+ cy.get("@primaryTitle").should("be.visible");
+ cy.get("@secondaryTitle").should("be.visible");
+ cy.get("@searchIcon").should("be.visible");
+ cy.get("@customActionIcon1").should("be.visible");
+ cy.get("@notificationsIcon").should("be.visible");
+ cy.get("@profileIcon").should("be.visible");
+ cy.get("@productSwitchIcon").should("be.visible");
+ });
+
+ it("tests M Breakpoint 870px", () => {
+ cy.viewport(870, 1680);
+
+ cy.get("@shellbar").should("have.prop", "breakpointSize", "M");
+
+ cy.get("@shellbar").find("ui5-toggle-button[slot='assistant']").as("assistant");
+ cy.get("@shellbar").find("ui5-button[slot='startButton']").as("backButton");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-title").as("primaryTitle");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-search-button").as("searchIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-bell-button").as("notificationsIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-image-button").as("profileIcon");
+ cy.get("@shellbar").shadow().find(".ui5-shellbar-button-product-switch").as("productSwitchIcon");
+
+ cy.get("@assistant").should("be.visible");
+ cy.get("@backButton").should("be.visible");
+ cy.get("@primaryTitle").should("be.visible");
+ cy.get("@searchIcon").should("be.visible");
+ cy.get("@notificationsIcon").should("be.visible");
+ cy.get("@profileIcon").should("be.visible");
+ cy.get("@productSwitchIcon").should("be.visible");
+ });
+
+ it("tests S Breakpoint 320px", () => {
+ cy.get("html").viewport("iphone-x");
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-overflow-button")
+ .should("exist");
+ cy.get("@shellbar")
+ .shadow()
+ .get("ui5-switch")
+ .should("be.hidden");
+ });
+
+ it("tests items visibility in Lean mode", () => {
+ cy.get("@shellbar")
+ .find("ui5-button[slot='startButton']")
+ .as("backButton");
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-search-button")
+ .as("searchButton");
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-bell-button")
+ .as("notificationsIcon");
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-image-button")
+ .as("profileIcon");
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-button-product-switch")
+ .as("productSwitchIcon");
+ });
+
+ it("tests logo and Primary title when no menuItems are presented", () => {
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-logo-area")
+ .as("logoLink");
+
+ cy.get("@logoLink").should("exist");
+ });
+
+ it("tests Primary title when menuItems are presented", () => {
+ cy.mount(templateWithMenuItems()).as("html1");
+
+ // V2: Menu button only renders at S breakpoint when menuItems exist
+ // This is by design - menu button is mobile-only feature
+ cy.viewport(510, 1680);
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-menu-button")
+ .as("menuButton");
+
+ cy.get("@menuButton").should("be.visible");
+ });
+
+ it("tests XXL Breakpoint Search bar", () => {
+ cy.get("@shellbar").invoke("attr", "show-open-search-field", "true");
+ cy.viewport(2560, 1080);
+ cy.get("[slot='searchField']")
+ .should("exist");
+ });
+
+ it("Test overflow button not showing, when only one action is presented", () => {
+ cy.mount(templateWithOnlyOneAction()).as("html1");
+
+ cy.get("html").viewport("iphone-6");
+ // V2: Overflow button uses conditional rendering - not rendered when nothing overflows
+ // This is more efficient than V1 which rendered but hid with CSS
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-overflow-button")
+ .should("not.exist");
+ });
+
+ it("Test accessibility attributes on custom action buttons", () => {
+ cy.mount(basicTemplate()).as("html");
+
+ // V2: ShellBarV2Item properly supports accessibilityAttributes property
+ // which are passed through to the ui5-button in its shadow root
+ cy.get("@shellbar")
+ .find(`[stable-dom-ref="call"]`)
+ .as("call-item")
+ .then($el => {
+ ($el.get(0) as any).accessibilityAttributes = { "hasPopup": "dialog", "expanded": "true" };
+ });
+ cy.get("@call-item")
+ .shadow()
+ .find("ui5-button")
+ .shadow()
+ .find("button")
+ .should("have.attr", "aria-expanded", "true")
+ .should("have.attr", "aria-haspopup", "dialog");
+ });
+});
+
+describe("Slots", () => {
+ describe("Profile slot", () => {
+ it("forwards click from shellbar profile button to slotted avatar (mount pattern)", () => {
+ const clickSpy = cy.spy().as("avatarClickSpy");
+ const profileClickSpy = cy.spy().as("profileClickSpy");
+
+ cy.mount(
+
+ );
+
+ cy.get("#test-avatar").then($el => {
+ $el[0].addEventListener("ui5-click", clickSpy);
+ });
+ cy.get("#test-shellbar").then($el => {
+ $el[0].addEventListener("ui5-profile-click", profileClickSpy);
+ });
+
+ cy.get("#test-shellbar").shadow().find(".ui5-shellbar-image-button").realClick();
+ cy.get("@profileClickSpy").should("have.been.calledOnce");
+ cy.get("@avatarClickSpy").should("have.been.calledOnce");
+ });
+ });
+
+ describe("Content slot", () => {
+ it("Test separators visibility", () => {
+ function assertStartSeparatorVisibility(expectedExist: boolean) {
+ cy.get("#shellbar")
+ .shadow()
+ .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-content-items > .ui5-shellbar-separator-end")
+ .should(expectedExist ? "exist" : "not.exist");
+ }
+
+ cy.mount(
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ // both separators should be visible
+ assertStartSeparatorVisibility(true);
+ assertEndSeparatorVisibility(true);
+
+ cy.viewport(420, 1080);
+ // only end separator should be hidden
+ assertStartSeparatorVisibility(true);
+ assertEndSeparatorVisibility(false);
+
+ cy.viewport(320, 1080);
+ // both separators should be hidden
+ assertStartSeparatorVisibility(false);
+ assertEndSeparatorVisibility(false);
+
+ // once items are hidden, both separators should be rendered with the last visible item
+ cy.get("#shellbar")
+ .shadow()
+ .find("div[id='content-2'] > .ui5-shellbar-separator-start")
+ .should("exist");
+
+ cy.get("#shellbar")
+ .shadow()
+ .find("div[id='content-6'] > .ui5-shellbar-separator-end")
+ .should("exist");
+
+ cy.viewport(1920, 1080);
+ // both separators should be visible
+ assertStartSeparatorVisibility(true);
+ assertEndSeparatorVisibility(true);
+
+ // once items are shown, both separators shouldn't be rendered with the last visible item
+ cy.get("#shellbar")
+ .shadow()
+ .find("div[id='content-2'] > .ui5-shellbar-separator-start")
+ .should("not.exist");
+
+ cy.get("#shellbar")
+ .shadow()
+ .find("div[id='content-6'] > .ui5-shellbar-separator-end")
+ .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 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);
+ });
+
+ it("Test search field is collapsed initially instead of being displayed in full width mode", () => {
+ cy.viewport(500, 1080);
+ cy.mount(
+ // needs some content to trigger the full width mode
+
+
+
+
+
+
+
+ );
+ cy.get("#shellbar").invoke("prop", "showSearchField").should("equal", false);
+ });
+
+ it("Test search field added after delay still works with events", () => {
+ cy.mount(
+
+ );
+
+ cy.get("#shellbar").as("shellbar");
+
+ // Add search field after a timeout (simulating real-world scenario)
+ cy.get("@shellbar").then(shellbar => {
+ setTimeout(() => {
+ const searchField = document.createElement("ui5-shellbar-search");
+ searchField.setAttribute("slot", "searchField");
+ searchField.setAttribute("id", "delayed-search");
+ shellbar.get(0).appendChild(searchField);
+ }, 100);
+ });
+
+ // Wait for the search field to be added
+ cy.get("#delayed-search", { timeout: 1000 }).should("exist");
+
+ // Search should now be visible and collapsed
+ cy.get("#shellbar [slot='searchField']")
+ .should("exist")
+ .should("have.prop", "collapsed", true);
+
+ // click the searchField to expand it
+ cy.get("#shellbar [slot='searchField']")
+ .click()
+ .should("have.prop", "collapsed", false);
+ // check shellbar's showSearchField property is also updated
+ cy.get("@shellbar").invoke("prop", "showSearchField").should("equal", true);
+ });
+
+ it.only("Test search toggle in overflow expands search when clicked", () => {
+ cy.mount(
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ // Use narrow viewport to force items into overflow
+ cy.viewport(320, 800);
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("#shellbar").as("shellbar");
+
+ // Verify overflow button exists (search should be in overflow when closed)
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-overflow-button")
+ .should("exist")
+ .click();
+
+ // Click search toggle in overflow popover
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-overflow-popover ui5-shellbar-v2-item[data-action-id='search']")
+ .should("exist")
+ .click();
+
+ // Verify search is expanded
+ cy.get("@shellbar")
+ .should("have.prop", "showSearchField", true);
+
+ // Verify search field is visible
+ cy.get("@shellbar")
+ .find("[slot='searchField']")
+ .should("be.visible");
+ });
+ });
+});
+
+describe("Events", () => {
+ it("Test click on the search button fires search-button-click event", () => {
+ cy.mount(
+
+
+
+ );
+ cy.get("[ui5-shellbar-v2]")
+ .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");
+ });
+
+ it("Test logo click fires logo-click event only once", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-logo-click", cy.stub().as("logoClick"));
+ });
+
+ // Test clicking on the logo area in large screens (combined logo layout)
+ cy.viewport(1920, 1080);
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-logo-area")
+ .as("logoArea")
+ .should("exist");
+
+ cy.get("@logoArea")
+ .realClick();
+
+ cy.get("@logoClick")
+ .should("have.been.calledOnce");
+
+ // Reset the stub for the next test
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-logo-click", cy.stub().as("logoClickSmall"));
+ });
+
+ // Test clicking on the logo in small screens (single logo layout)
+ cy.viewport(500, 1080);
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-logo")
+ .as("logo")
+ .should("exist");
+
+ cy.get("@logo")
+ .realClick();
+
+ cy.get("@logoClickSmall")
+ .should("have.been.calledOnce");
+ });
+
+ it("Test search field clear event default behavior", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ // Set up event listener without preventing default
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear"));
+ });
+
+ // Trigger full width search mode by reducing viewport
+ cy.viewport(400, 800);
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-cancel-button")
+ .click();
+
+ // Verify the event was fired
+ cy.get("@searchFieldClear")
+ .should("have.been.calledOnce");
+
+ // Verify search field value is cleared (default behavior)
+ cy.get("#search")
+ .should("have.value", "");
+
+ // Verify search is closed
+ cy.get("@shellbar")
+ .should("have.prop", "showSearchField", false);
+ });
+
+ it("Test search field clear event can be prevented", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ // Set up event listener that prevents default
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-search-field-clear", (event) => {
+ event.preventDefault();
+ });
+ shellbar.get(0).addEventListener("ui5-search-field-clear", cy.stub().as("searchFieldClear"));
+ });
+
+ // Trigger full width search mode by reducing viewport
+ cy.viewport(400, 800);
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-cancel-button")
+ .click();
+
+ // Verify the event was fired
+ cy.get("@searchFieldClear")
+ .should("have.been.calledOnce");
+
+ // Verify search field value is preserved (due to preventDefault)
+ cy.get("#search")
+ .should("have.value", "test search text");
+
+ // Verify search is closed
+ cy.get("@shellbar")
+ .should("have.prop", "showSearchField", false);
+ });
+
+ describe("Big screen", () => {
+ beforeEach(() => {
+ cy.viewport(1920, 1680);
+ });
+
+ it("tests opening of menu", () => {
+ cy.mount(
+
+ Menu Item 1
+ Menu Item 2
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-button")
+ .realClick();
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-popover")
+ .should("have.prop", "open", true);
+ });
+
+ it("tests notificationsClick event", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-notifications-click", cy.stub().as("notificationsClick"));
+ });
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-bell-button")
+ .click();
+
+ cy.get("@notificationsClick")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests profileClick event", () => {
+ cy.mount(
+
+
+
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-profile-click", cy.stub().as("profileClick"));
+ });
+
+ cy.get("@shellbar")
+ .shadow()
+ .find("[data-profile-btn]")
+ .click({ force: true });
+
+ cy.get("@profileClick")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests productSwitchClick event", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-product-switch-click", cy.stub().as("productSwitchClick"));
+ });
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-button-product-switch")
+ .click();
+
+ cy.get("@productSwitchClick")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests logoClick event", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-logo-click", cy.stub().as("logoClick"));
+ });
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-logo")
+ .realClick();
+
+ cy.get("@logoClick")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests search-button-click event", () => {
+ cy.viewport(870, 1680);
+
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-search-button-click", (e) => {
+ e.preventDefault();
+ cy.stub().as("searchButtonClick")();
+ });
+ });
+
+ cy.get("@shellbar").should("have.prop", "showSearchField", false);
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-search-button")
+ .click();
+
+ cy.get("@shellbar").should("have.prop", "showSearchField", false);
+
+ cy.get("@searchButtonClick")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests menuItemClick event", () => {
+ cy.mount(
+
+ Application 1
+ Application 2
+
+
+ );
+
+ cy.get("[ui5-li][slot='menuItems']").each(($item) => {
+ const item = $item[0];
+ item.addEventListener("click", cy.stub().as(`menuItemClick${item.getAttribute("data-key")}`));
+ });
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-button")
+ .click();
+
+ cy.get("[ui5-li][slot='menuItems']").first().click();
+
+ cy.get("@menuItemClickkey1")
+ .should("have.been.calledOnce");
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-button")
+ .click();
+
+ cy.get("[ui5-li][slot='menuItems']").eq(1).click();
+
+ cy.get("@menuItemClickkey2")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests if searchfield toggles when altering the showSearchField property", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-search-field")
+ .should("exist");
+
+ cy.get("[ui5-shellbar-v2]").invoke("prop", "showSearchField", false);
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-search-field")
+ .should("not.exist");
+
+ cy.get("[ui5-shellbar-v2]").invoke("prop", "showSearchField", true);
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-search-field")
+ .should("exist");
+ });
+ });
+
+ describe("Small screen", () => {
+ beforeEach(() => {
+ cy.viewport(510, 1680);
+ });
+
+ it("tests logoClick event", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-logo-click", cy.stub().as("logoClick"));
+ });
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-logo")
+ .realClick();
+
+ cy.get("@logoClick")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests opening of menu", () => {
+ cy.mount(
+
+ Menu Item 1
+ Menu Item 2
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-button")
+ .realClick();
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-popover")
+ .should("have.prop", "open", true);
+ });
+
+ it("tests profileClick event", () => {
+ cy.mount(
+
+
+
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-profile-click", cy.stub().as("profileClick"));
+ });
+
+ cy.get("@shellbar")
+ .shadow()
+ .find("[data-profile-btn]")
+ .click({ force: true });
+
+ cy.get("@profileClick")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests productSwitchClick event", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .as("shellbar");
+
+ cy.get("@shellbar")
+ .then(shellbar => {
+ shellbar.get(0).addEventListener("ui5-product-switch-click", cy.stub().as("productSwitchClick"));
+ });
+
+ cy.get("@shellbar")
+ .shadow()
+ .find(".ui5-shellbar-button-product-switch")
+ .click();
+
+ cy.get("@productSwitchClick")
+ .should("have.been.calledOnce");
+ });
+
+ it("tests preventDefault of click on a button with default behavior prevented", () => {
+ cy.viewport(320, 800);
+ cy.mount(
+
+
+
+
+
+
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]").then(($shellbar) => {
+ const shellbar = $shellbar[0] as HTMLElement;
+ shellbar.addEventListener("ui5-notifications-click", (e: Event) => {
+ e.preventDefault();
+ });
+ });
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-overflow-button")
+ .realClick();
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-overflow-popover")
+ .should("to.exist")
+ .invoke("prop", "open", true);
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-overflow-popover [ui5-list] [ui5-shellbar-v2-item]:nth-child(1)")
+ .realClick();
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-overflow-popover")
+ .should("to.exist")
+ .invoke("prop", "open", true);
+
+ });
+ });
+});
+
+describe("ButtonBadge in ShellBar", () => {
+ it("Test if ShellBarItem count appears in ButtonBadge", () => {
+ cy.mount(
+
+
+
+ );
+
+ // V2: Badge is inside ShellBarV2Item's shadow DOM, not directly in ShellBar's shadow
+ cy.get("#test-item")
+ .shadow()
+ .find("ui5-button-badge[slot='badge']")
+ .should("exist")
+ .should("have.attr", "text", "42");
+ });
+
+ it("Test count updates propagate to ButtonBadge", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("#test-invalidation-item").invoke("attr", "count", "3");
+
+ // V2: Badge is inside ShellBarV2Item's shadow DOM
+ cy.get("#test-invalidation-item")
+ .shadow()
+ .find("ui5-button-badge[slot='badge']")
+ .should("have.attr", "text", "3");
+ });
+
+ it("Test if overflow button shows appropriate badge when items are overflowed", () => {
+ cy.mount(
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ cy.viewport(320, 800);
+
+ // Wait for overflow calculation to complete
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("#shellbar-with-overflow")
+ .shadow()
+ .find(".ui5-shellbar-overflow-button")
+ .should("be.visible");
+
+ // V2: Overflow button badge - check if it's rendered
+ cy.get("#shellbar-with-overflow")
+ .shadow()
+ .find(".ui5-shellbar-overflow-button ui5-button-badge[slot='badge']")
+ .should("exist")
+ .should("have.attr", "design", "AttentionDot");
+
+ cy.mount(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ cy.viewport(320, 800);
+
+ // Wait for overflow calculation to complete
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("#shellbar-with-single-overflow")
+ .shadow()
+ .find(".ui5-shellbar-overflow-button")
+ .should("be.visible");
+
+ // V2: Overflow button badge - check if it's rendered
+ cy.get("#shellbar-with-single-overflow")
+ .shadow()
+ .find(".ui5-shellbar-overflow-button ui5-button-badge[slot='badge']")
+ .should("exist")
+ .should("have.attr", "text", "42");
+ });
+});
+
+describe("Search Controllers", () => {
+ it("Test search doesn't collapse in full-screen mode during resize", () => {
+ cy.mount(
+
+
+
+
+
+
+ );
+
+ // search not focused
+ cy.get("#search").should("not.be.focused");
+ // search field is empty
+ cy.get("#search").should("have.value", "");
+
+ cy.viewport(400, 800);
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("#shellbar").should("have.prop", "showSearchField", true);
+
+ cy.viewport(360, 800);
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("#shellbar").should("have.prop", "showSearchField", true);
+ });
+
+ it("Test legacy search doesn't collapse in full-screen mode during resize", () => {
+ cy.mount(
+
+
+
+
+
+
+ );
+
+ // search not focused
+ cy.get("#search").should("not.be.focused");
+ // search field is empty
+ cy.get("#search").should("have.value", "");
+
+ cy.viewport(400, 800);
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("#shellbar").should("have.prop", "showSearchField", true);
+
+ cy.viewport(360, 800);
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("#shellbar").should("have.prop", "showSearchField", true);
+ });
+});
+
+describe("Overflow", () => {
+ it("Test hidden actions stay hidden when search expands", () => {
+ cy.mount(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ cy.viewport(1000, 800);
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ // Verify actions are hidden in overflow
+ cy.get("#shellbar").shadow().find(".ui5-shellbar-overflow-button").should("exist");
+ cy.get("#item1").should("not.be.visible");
+
+ // Expand search
+ cy.get("#shellbar").invoke("prop", "showSearchField", true);
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ // Hidden actions should stay hidden (no flicker)
+ cy.get("#item1").should("not.be.visible");
+ });
+});
+
+describe("Keyboard Navigation", () => {
+ it("Test logo area elements are not rendered when no logo and primaryTitle are provided", () => {
+ cy.mount();
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-logo-area")
+ .should("not.exist");
+ });
+
+ it("Test arrow navigation within search input respects cursor position", () => {
+ cy.mount(
+
+
+
+
+
+ );
+ cy.wait(RESIZE_THROTTLE_RATE);
+
+ function placeAtStartOfInput() {
+ cy.get("[ui5-shellbar-v2] [slot='searchField']")
+ .shadow()
+ .find("input")
+ .then($input => {
+ $input[0].setSelectionRange(0, 0);
+ });
+ }
+ function placeAtEndOfInput() {
+ cy.get("[ui5-shellbar-v2] [slot='searchField']")
+ .shadow()
+ .find("input")
+ .then($input => {
+ const inputLength = $input.val().toString().length;
+ $input[0].setSelectionRange(inputLength, inputLength);
+ });
+ }
+ function placeInMiddleOfInput() {
+ cy.get("[ui5-shellbar-v2] [slot='searchField']")
+ .shadow()
+ .find("input")
+ .then($input => {
+ const inputLength = $input.val().toString().length;
+ const middlePosition = Math.floor(inputLength / 2);
+ $input[0].setSelectionRange(middlePosition, middlePosition);
+ });
+ }
+
+ // Focus the search input
+ cy.get("[ui5-shellbar-v2] [slot='searchField']")
+ .realClick()
+ .shadow()
+ .find("input")
+ .as("nativeInput");
+
+ placeAtStartOfInput();
+ // Press left arrow - should move focus away from input since cursor is at start
+ cy.get("@nativeInput").type("{leftArrow}");
+ // Verify focus is now on the button
+ cy.get("[ui5-shellbar-v2] [ui5-button]").should("be.focused");
+
+
+ placeAtEndOfInput();
+ // Press right arrow - should move focus away from input since cursor is at end
+ cy.get("@nativeInput").type("{rightArrow}");
+ // Verify focus is now on the ShellBarItem
+ cy.get("[ui5-shellbar-v2-item]")
+ .should("have.focus");
+
+ placeInMiddleOfInput();
+ // Press left arrow - should stay focused on input since cursor is in the middle
+ cy.get("@nativeInput").type("{leftArrow}");
+ cy.get("@nativeInput").should("be.focused");
+ // Press right arrow - should stay focused on input since cursor is in the middle
+ cy.get("@nativeInput").type("{rightArrow}");
+ cy.get("@nativeInput").should("be.focused");
+ });
+
+ it("Should focus the last ShellBar item on End key press", () => {
+ cy.mount(
+
+
+
+
+
+ );
+ cy.get("#button").shadow().find("button").focus().type('{end}');
+ cy.get("#sbSearch").should("be.focused");
+ });
+
+ it("Should focus the first ShellBar item on Home key press", () => {
+ cy.mount(
+
+
+
+
+
+ );
+ cy.get("#button2").shadow().find("button").focus().type('{home}');
+ cy.get("#button1").shadow().find("button").should("be.focused");
+ });
+});
+
+describe("Branding slot", () => {
+ it("Test branding slot priority over logo", () => {
+ cy.mount(
+
+
+
+
+ Branding Comp
+
+
+
+ )
+
+ cy.get("#shellbar")
+ .find("#mainLogo")
+ .should('exist')
+ .should('not.be.visible');
+
+ cy.get("#shellbar")
+ .find("#brandingLogo")
+ .should('exist')
+ .should('be.visible');
+
+ });
+});
+
+describe("Component Behavior", () => {
+ describe("Accessibility", () => {
+
+ it("tests accessibilityTexts property", () => {
+ const PROFILE_BTN_CUSTOM_TOOLTIP = "John Dow";
+ const LOGO_CUSTOM_TOOLTIP = "Custom logo title";
+
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]").then(($shellbar) => {
+ $shellbar[0].accessibilityAttributes = {
+ profile: {
+ name: PROFILE_BTN_CUSTOM_TOOLTIP,
+ },
+ logo: {
+ name: LOGO_CUSTOM_TOOLTIP
+ },
+ };
+ });
+
+ cy.get("[ui5-shellbar-v2]").then(($shellbar) => {
+ expect($shellbar[0].actionsAccessibilityInfo.profile.title).to.equal(PROFILE_BTN_CUSTOM_TOOLTIP);
+ expect($shellbar[0].legacyAdaptor.logoAriaLabel).to.equal(LOGO_CUSTOM_TOOLTIP);
+ });
+ });
+
+ it("tests accessibilityAttributes property", () => {
+ const NOTIFICATIONS_BTN_ARIA_HASPOPUP = "dialog";
+
+ cy.mount(
+
+
+ Product Title
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]").then(($shellbar) => {
+ $shellbar[0].accessibilityAttributes = {
+ notifications: {
+ hasPopup: NOTIFICATIONS_BTN_ARIA_HASPOPUP
+ },
+ };
+ });
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-bell-button")
+ .shadow()
+ .find("button")
+ .should("have.attr", "aria-haspopup", NOTIFICATIONS_BTN_ARIA_HASPOPUP);
+ });
+ });
+
+ describe("ui5-shellbar menu", () => {
+ it("tests prevents close on content click", () => {
+ cy.viewport(1920, 1680);
+
+ cy.mount(
+
+ Menu Item 1
+ Menu Item 2
+
+
+ );
+
+ cy.get("[ui5-li][slot='menuItems']").first().then($item => {
+ $item[0].addEventListener("click", cy.stub().as("menuItemClick"));
+ });
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-button")
+ .click();
+
+ cy.get("[ui5-li][slot='menuItems']").first().click();
+ cy.get("@menuItemClick")
+ .should("have.been.calledOnce");
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-popover")
+ .should("have.prop", "open", true);
+ });
+
+ it("tests close on content click", () => {
+ cy.mount(
+
+ Application 1
+ Application 2
+
+ );
+
+ cy.get("[ui5-shellbar-v2]").should("exist");
+
+ cy.get("[slot='menuItems']").should("have.length", 2);
+
+ cy.get("[ui5-shellbar-v2]").should(($shellbar) => {
+ const shellbar = $shellbar[0] as any;
+ expect(shellbar.menuItems).to.exist;
+ expect(shellbar.menuItems.length).to.be.greaterThan(0);
+ });
+
+ cy.get("[ui5-li][slot='menuItems']").first().then($item => {
+ $item[0].addEventListener("click", cy.stub().as("menuItemClick"));
+ });
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-button")
+ .should("exist")
+ .should("be.visible")
+ .realClick();
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-popover")
+ .should("have.prop", "open", true);
+
+ cy.get("[ui5-li][slot='menuItems']")
+ .first()
+ .should("be.visible")
+ .realClick();
+
+ cy.get("@menuItemClick")
+ .should("have.been.calledOnce");
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(".ui5-shellbar-menu-popover")
+ .should("have.prop", "open", false);
+ });
+ });
+
+ describe("ui5-shellbar-item", () => {
+ it("tests the stable-dom-ref attribute", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2]")
+ .shadow()
+ .find(`[data-ui5-stable="schedule"]`)
+ .should("exist");
+ });
+
+ it("tests 'click' on custom action", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("[ui5-shellbar-v2-item]").each(($item) => {
+ const item = $item[0];
+ const icon = item.getAttribute("icon");
+ const stubAlias = icon === "accept" ? "acceptClick" : "alertClick";
+ item.addEventListener("click", cy.stub().as(stubAlias));
+ });
+
+ cy.get("[ui5-shellbar-v2-item][icon='accept']")
+ .click();
+
+ cy.get("@acceptClick")
+ .should("have.been.calledOnce");
+
+ cy.get("[ui5-shellbar-v2-item][icon='alert']")
+ .click();
+
+ cy.get("@alertClick")
+ .should("have.been.calledOnce");
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/fiori/src/ShellBarV2.ts b/packages/fiori/src/ShellBarV2.ts
new file mode 100644
index 000000000000..1145ff16da87
--- /dev/null
+++ b/packages/fiori/src/ShellBarV2.ts
@@ -0,0 +1,1227 @@
+import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
+import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
+import property from "@ui5/webcomponents-base/dist/decorators/property.js";
+import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
+import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
+import query from "@ui5/webcomponents-base/dist/decorators/query.js";
+import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
+import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
+import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
+import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
+import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScopeUtils.js";
+import arraysAreEqual from "@ui5/webcomponents-base/dist/util/arraysAreEqual.js";
+import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
+import { renderFinished } from "@ui5/webcomponents-base";
+import throttle from "@ui5/webcomponents-base/dist/util/throttle.js";
+
+import type { IButton } from "@ui5/webcomponents/dist/Button.js";
+import Button from "@ui5/webcomponents/dist/Button.js";
+import ButtonBadge from "@ui5/webcomponents/dist/ButtonBadge.js";
+import Icon from "@ui5/webcomponents/dist/Icon.js";
+import Popover from "@ui5/webcomponents/dist/Popover.js";
+import Menu from "@ui5/webcomponents/dist/Menu.js";
+import List from "@ui5/webcomponents/dist/List.js";
+import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js";
+import searchIcon from "@ui5/webcomponents-icons/dist/search.js";
+import bellIcon from "@ui5/webcomponents-icons/dist/bell.js";
+import gridIcon from "@ui5/webcomponents-icons/dist/grid.js";
+import daIcon from "@ui5/webcomponents-icons/dist/da.js";
+import overflowIcon from "@ui5/webcomponents-icons/dist/overflow.js";
+
+import ShellBarV2Template from "./ShellBarV2Template.js";
+import shellBarV2Styles from "./generated/themes/ShellBarV2.css.js";
+import ShellBarPopoverCss from "./generated/themes/ShellBarPopover.css.js";
+import shellBarV2LegacyStyles from "./generated/themes/ShellBarV2Legacy.css.js";
+
+import type { IShellBarSearchController } from "./shellbarv2/IShellBarSearchController.js";
+
+import ShellBarV2Legacy from "./shellbarv2/ShellBarLegacy.js";
+import ShellBarV2Search from "./shellbarv2/ShellBarSearch.js";
+import ShellBarV2SearchLegacy from "./shellbarv2/ShellBarSearchLegacy.js";
+import ShellBarV2Overflow from "./shellbarv2/ShellBarOverflow.js";
+import ShellBarV2Accessibility from "./shellbarv2/ShellBarAccessibility.js";
+import ShellBarV2ItemNavigation from "./shellbarv2/ShellBarItemNavigation.js";
+
+import ShellBarV2Item from "./ShellBarV2Item.js";
+import ShellBarSpacer from "./ShellBarSpacer.js";
+import type ShellBarBranding from "./ShellBarBranding.js";
+import type { ShellBarV2OverflowResult } from "./shellbarv2/ShellBarOverflow.js";
+
+import type {
+ ShellBarV2AccessibilityInfo,
+ ShellBarV2AccessibilityAttributes,
+ ShellBarV2AreaAccessibilityAttributes,
+ ShellBarV2LogoAccessibilityAttributes,
+ ShellBarV2ProfileAccessibilityAttributes,
+} from "./shellbarv2/ShellBarAccessibility.js";
+
+import {
+ SHELLBAR_LABEL,
+ SHELLBAR_NOTIFICATIONS,
+ SHELLBAR_NOTIFICATIONS_NO_COUNT,
+ SHELLBAR_PROFILE,
+ SHELLBAR_PRODUCTS,
+ SHELLBAR_SEARCH,
+ SHELLBAR_ASSISTANT,
+ SHELLBAR_OVERFLOW,
+ SHELLBAR_ADDITIONAL_CONTEXT,
+} from "./generated/i18n/i18n-defaults.js";
+import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js";
+
+type ShellBarV2Breakpoint = "S" | "M" | "L" | "XL" | "XXL";
+
+// actions always visible in lean mode, order is important
+const PREDEFINED_PLACE_ITEMS = ["feedback", "sys-help"];
+
+const ShellBarV2Actions = {
+ Search: "search",
+ Profile: "profile",
+ Overflow: "overflow",
+ Assistant: "assistant",
+ ProductSwitch: "products",
+ Notifications: "notifications",
+};
+
+const ShellBarV2ActionsSelectors = {
+ Search: ".ui5-shellbar-search-toggle",
+ Profile: ".ui5-shellbar-image-button",
+ Overflow: ".ui5-shellbar-overflow-button",
+ Assistant: ".ui5-shellbar-assistant-button",
+ ProductSwitch: ".ui5-shellbar-button-product-switch",
+ Notifications: ".ui5-shellbar-bell-button",
+};
+
+type ShellBarV2ActionId = typeof ShellBarV2Actions[keyof typeof ShellBarV2Actions];
+
+type ShellBarV2ActionItem = {
+ id: ShellBarV2ActionId;
+ icon?: string;
+ count?: string;
+ enabled: boolean; // Whether the action is enabled and should be displayed
+ selector: string; // The selector by which we can target the action
+ isProtected: boolean // Whether the action can go into the overflow
+ stableDomRef?: string;
+};
+
+interface IShellBarSearchField extends HTMLElement {
+ focused: boolean;
+ value: string;
+ collapsed?: boolean;
+ open?: boolean;
+}
+
+// Event Types
+
+type ShellBarV2NotificationsClickEventDetail = {
+ targetRef: HTMLElement;
+};
+
+type ShellBarV2ProfileClickEventDetail = {
+ targetRef: HTMLElement;
+};
+
+type ShellBarV2ProductSwitchClickEventDetail = {
+ targetRef: HTMLElement;
+};
+
+type ShellBarV2LogoClickEventDetail = {
+ targetRef: HTMLElement;
+};
+
+type ShellBarV2MenuItemClickEventDetail = {
+ item: HTMLElement;
+};
+
+type ShellBarV2ContentItemVisibilityChangeEventDetail = {
+ items: Array
+};
+
+type ShellBarV2SearchButtonEventDetail = {
+ targetRef: HTMLElement;
+ searchFieldVisible: boolean;
+};
+
+type ShellBarV2SearchFieldToggleEventDetail = {
+ expanded: boolean;
+};
+
+type ShellBarV2SearchFieldClearEventDetail = {
+ targetRef: HTMLElement;
+};
+
+/**
+ * @class
+ * ### Overview
+ *
+ * The `ui5-shellbar` is meant to serve as an application header
+ * and includes numerous built-in features, such as: logo, profile image/icon, title, search field, notifications and so on.
+ *
+ * ### Stable DOM Refs
+ *
+ * You can use the following stable DOM refs for the `ui5-shellbar`:
+ *
+ * - logo
+ * - notifications
+ * - overflow
+ * - profile
+ * - product-switch
+ *
+ * ### Keyboard Handling
+ *
+ * #### Fast Navigation
+ * This component provides a build in fast navigation group which can be used via [F6] / [Shift] + [F6] / [Ctrl] + [Alt/Option] / [Down] or [Ctrl] + [Alt/Option] + [Up].
+ * In order to use this functionality, you need to import the following module:
+ * `import "@ui5/webcomponents-base/dist/features/F6Navigation.js"`
+ *
+ * ### ES6 Module Import
+ * `import "@ui5/webcomponents-fiori/dist/ShellBar.js";`
+ * @csspart root - Used to style the outermost wrapper of the `ui5-shellbar`
+ * @constructor
+ * @extends UI5Element
+ * @public
+ * @since 0.8.0
+ */
+
+@customElement({
+ tag: "ui5-shellbar-v2",
+ styles: [shellBarV2Styles, shellBarV2LegacyStyles, ShellBarPopoverCss],
+ renderer: jsxRenderer,
+ template: ShellBarV2Template,
+ fastNavigation: true,
+ languageAware: true,
+ dependencies: [
+ Icon,
+ List,
+ Button,
+ ButtonBadge,
+ Popover,
+ ShellBarSpacer,
+ ShellBarV2Item,
+ ListItemStandard,
+ // legacy dependencies
+ Menu,
+ ],
+})
+/**
+ *
+ * Fired, when the notification icon is activated.
+ * @param {HTMLElement} targetRef dom ref of the activated element
+ * @public
+ */
+@event("notifications-click", {
+ cancelable: true,
+ bubbles: true,
+})
+
+/**
+ * Fired, when the profile slot is present.
+ * @param {HTMLElement} targetRef dom ref of the activated element
+ * @public
+ */
+@event("profile-click", {
+ bubbles: true,
+})
+
+/**
+ * Fired, when the product switch icon is activated.
+ *
+ * **Note:** You can prevent closing of overflow popover by calling `event.preventDefault()`.
+ * @param {HTMLElement} targetRef dom ref of the activated element
+ * @public
+ */
+@event("product-switch-click", {
+ cancelable: true,
+ bubbles: true,
+})
+
+/**
+ * Fired, when the logo is activated.
+ * @param {HTMLElement} targetRef dom ref of the activated element
+ * @since 0.10
+ * @public
+ */
+@event("logo-click", {
+ bubbles: true,
+})
+
+/**
+ * Fired, when a menu item is activated
+ *
+ * **Note:** You can prevent closing of overflow popover by calling `event.preventDefault()`.
+ * @param {HTMLElement} item DOM ref of the activated list item
+ * @since 0.10
+ * @public
+ */
+@event("menu-item-click", {
+ bubbles: true,
+ cancelable: true,
+})
+
+/**
+ * Fired, when the search button is activated.
+ *
+ * **Note:** You can prevent expanding/collapsing of the search field by calling `event.preventDefault()`.
+ * @param {HTMLElement} targetRef dom ref of the activated element
+ * @param {Boolean} searchFieldVisible whether the search field is visible
+ * @public
+ */
+
+@event("search-button-click", {
+ cancelable: true,
+ 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 the search cancel button is activated.
+ *
+ * **Note:** You can prevent the default behavior (clearing the search field value) by calling `event.preventDefault()`. The search will still be closed.
+ * **Note:** The `search-field-clear` event is in an experimental state and is a subject to change.
+ * @param {HTMLElement} targetRef dom ref of the cancel button element
+ * @since 2.14.0
+ * @public
+ */
+@event("search-field-clear", {
+ cancelable: true,
+ 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.
+ *
+ * @param {Array} array of all the items that are hidden
+ * @public
+ * @since 2.7.0
+ */
+@event("content-item-visibility-change", {
+ bubbles: true,
+})
+
+class ShellBarV2 extends UI5Element {
+ eventDetails!: {
+ "notifications-click": ShellBarV2NotificationsClickEventDetail,
+ "profile-click": ShellBarV2ProfileClickEventDetail,
+ "product-switch-click": ShellBarV2ProductSwitchClickEventDetail,
+ "logo-click": ShellBarV2LogoClickEventDetail,
+ "menu-item-click": ShellBarV2MenuItemClickEventDetail,
+ "search-button-click": ShellBarV2SearchButtonEventDetail,
+ "search-field-toggle": ShellBarV2SearchFieldToggleEventDetail,
+ "search-field-clear": ShellBarV2SearchFieldClearEventDetail,
+ "content-item-visibility-change": ShellBarV2ContentItemVisibilityChangeEventDetail
+ }
+
+ /**
+ * Defines a `ui5-button` in the bar that will be placed in the beginning.
+ * We encourage this slot to be used for a menu button.
+ * It gets overstyled to match ShellBar's styling.
+ * @public
+ */
+ @slot()
+ startButton!: Array;
+
+ /**
+ * Defines the branding slot.
+ * The `ui5-shellbar-branding` component is intended to be placed inside this slot.
+ * Content placed here takes precedence over the `primaryTitle` property and the `logo` content slot.
+ *
+ * **Note:** The `branding` slot is in an experimental state and is a subject to change.
+ *
+ * @since 2.12.0
+ * @public
+ */
+ @slot()
+ branding!: Array;
+
+ /**
+ * Define the items displayed in the content area.
+ *
+ * Use the `data-hide-order` attribute with numeric value to specify the order of the items to be hidden when the space is not enough.
+ * Lower values will be hidden first.
+ *
+ * **Note:** The `content` slot is in an experimental state and is a subject to change.
+ *
+ * @public
+ * @since 2.7.0
+ */
+ @slot({ type: HTMLElement, individualSlots: true })
+ content!: Array;
+
+ /**
+ * Defines the `ui5-input`, that will be used as a search field.
+ * @public
+ */
+ @slot({ type: HTMLElement })
+ searchField!: Array;
+
+ /**
+ * Defines the assistant slot.
+ *
+ * @since 2.0.0
+ * @public
+ */
+ @slot()
+ assistant!: Array;
+
+ /**
+ * Defines the `ui5-shellbar` additional items.
+ *
+ * **Note:**
+ * You can use the ``.
+ * @public
+ */
+ @slot({ type: HTMLElement, "default": true, individualSlots: true })
+ items!: Array;
+
+ /**
+ * You can pass `ui5-avatar` to set the profile image/icon.
+ * If no profile slot is set - profile will be excluded from actions.
+ *
+ * **Note:** We recommend not using the `size` attribute of `ui5-avatar` because
+ * it should have specific size by design in the context of `ui5-shellbar` profile.
+ * @since 1.0.0-rc.6
+ * @public
+ */
+ @slot()
+ profile!: Array;
+
+ /**
+ * 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 `disableSearchCollapse` property is in an experimental state and is a subject to change.
+ * @default false
+ * @public
+ */
+ @property({ type: Boolean })
+ disableSearchCollapse = false;
+
+ /**
+ * Defines the `primaryTitle`.
+ *
+ * **Note:** The `primaryTitle` would be hidden on S screen size (less than approx. 700px).
+ * @default undefined
+ * @public
+ */
+ @property()
+ primaryTitle?: string;
+
+ /**
+ * Defines the `secondaryTitle`.
+ *
+ * **Note:** The `secondaryTitle` would be hidden on S and M screen sizes (less than approx. 1300px).
+ * @default undefined
+ * @public
+ */
+ @property()
+ secondaryTitle?: string;
+
+ /**
+ * Defines the `notificationsCount`,
+ * displayed in the notification icon top-right corner.
+ * @default undefined
+ * @public
+ */
+ @property()
+ notificationsCount?: string;
+
+ /**
+ * Defines, if the notification icon would be displayed.
+ * @default false
+ * @public
+ */
+ @property({ type: Boolean })
+ showNotifications = false;
+
+ /**
+ * Defines, if the product switch icon would be displayed.
+ * @default false
+ * @public
+ */
+ @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.
+ *
+ * The accessibilityAttributes object has the following fields,
+ * where each field is an object supporting one or more accessibility attributes:
+ *
+ * - **logo** - `logo.role` and `logo.name`.
+ * - **notifications** - `notifications.expanded` and `notifications.hasPopup`.
+ * - **profile** - `profile.expanded`, `profile.hasPopup` and `profile.name`.
+ * - **product** - `product.expanded` and `product.hasPopup`.
+ * - **search** - `search.hasPopup`.
+ * - **overflow** - `overflow.expanded` and `overflow.hasPopup`.
+ * - **branding** - `branding.name`.
+ *
+ * The accessibility attributes support the following values:
+ *
+ * - **role**: Defines the accessible ARIA role of the logo area.
+ * Accepts the following string values: `button` or `link`.
+ *
+ * - **expanded**: Indicates whether the button, or another grouping element it controls,
+ * is currently expanded or collapsed.
+ * Accepts the following string values: `true` or `false`.
+ *
+ * - **hasPopup**: Indicates the availability and type of interactive popup element,
+ * such as menu or dialog, that can be triggered by the button.
+ *
+ * Accepts the following string values: `dialog`, `grid`, `listbox`, `menu` or `tree`.
+ * - **name**: Defines the accessible ARIA name of the area.
+ * Accepts any string.
+ *
+ * @default {}
+ * @public
+ * @since 1.10.0
+ */
+ @property({ type: Object })
+ accessibilityAttributes: ShellBarV2AccessibilityAttributes = {};
+
+ /**
+ * @private
+ */
+ @property()
+ breakpointSize = "S";
+
+ /**
+ * Actions computed from controllers.
+ * @private
+ */
+ @property({ type: Object })
+ actions: ShellBarV2ActionItem[] = [];
+
+ /**
+ * Show overflow button when items are hidden.
+ * @private
+ */
+ @property({ type: Boolean })
+ showOverflowButton = false;
+
+ /**
+ * Open state of the overflow popover.
+ * @private
+ */
+ @property({ type: Boolean })
+ overflowPopoverOpen = false;
+
+ /**
+ * IDs of items currently hidden due to overflow.
+ * Used to trigger rerender for conditional rendering.
+ * @private
+ */
+ @property({ type: Object })
+ hiddenItemsIds: string[] = [];
+
+ /**
+ * Show full-screen search overlay.
+ * @private
+ */
+ @property({ type: Boolean })
+ showFullWidthSearch = false;
+
+ /**
+ * Spacer element.
+ * @private
+ */
+ @query(".ui5-shellbar-spacer")
+ spacer?: HTMLElement;
+
+ /**
+ * Outer container of the overflow container.
+ * @private
+ */
+ @query(".ui5-shellbar-overflow-container")
+ overflowOuter?: HTMLElement;
+
+ /**
+ * Inner container of the overflow container.
+ * @private
+ */
+ @query(".ui5-shellbar-overflow-container-inner")
+ overflowInner?: HTMLElement;
+
+ @i18n("@ui5/webcomponents-fiori")
+ static i18nBundle: I18nBundle;
+
+ private readonly RESIZE_THROTTLE_RATE = 100; // ms
+ private handleResizeBound: ResizeObserverCallback = throttle(this.handleResize.bind(this), this.RESIZE_THROTTLE_RATE);
+
+ private readonly breakpoints = [599, 1023, 1439, 1919, 10000];
+ private readonly breakpointMap: Record = {
+ 599: "S",
+ 1023: "M",
+ 1439: "L",
+ 1919: "XL",
+ 10000: "XXL",
+ };
+
+ itemNavigation = new ShellBarV2ItemNavigation({
+ getDomRef: () => this.getDomRef() || null,
+ });
+
+ overflow = new ShellBarV2Overflow();
+ accessibility: ShellBarV2Accessibility = new ShellBarV2Accessibility();
+
+ private _searchAdaptor = new ShellBarV2Search(this.getSearchDeps());
+ private _searchAdaptorLegacy = new ShellBarV2SearchLegacy({
+ ...this.getSearchDeps(),
+ getDisableSearchCollapse: () => this.disableSearchCollapse,
+ });
+
+ /* =================== Legacy Members =================== */
+
+ /**
+ * Defines the logo of the `ui5-shellbar`.
+ * For example, you can use `ui5-avatar` or `img` elements as logo.
+ * @since 1.0.0-rc.8
+ * @public
+ */
+ @slot()
+ logo!: Array;
+
+ /**
+ * Defines the items displayed in menu after a click on a start button.
+ *
+ * **Note:** You can use the `` and its ancestors.
+ * @since 0.10
+ * @public
+ */
+ @slot()
+ menuItems!: Array;
+
+ /**
+ * Open state of the menu popover (legacy).
+ * @private
+ */
+ @property({ type: Boolean })
+ menuPopoverOpen = false;
+
+ /**
+ * The container is positioned in the center of the `ui5-shellbar` and occupies one-third of the total length of the `ui5-shellbar`.
+ *
+ * **Note:** If set, the `searchField` slot is not rendered.
+ * @private
+ */
+ @slot()
+ midContent!: Array;
+
+ legacyAdaptor?: ShellBarV2Legacy;
+
+ /* =================== Lifecycle Methods =================== */
+
+ onEnterDOM() {
+ ResizeHandler.register(this, this.handleResizeBound);
+ this.searchAdaptor?.subscribe();
+ }
+
+ onExitDOM() {
+ ResizeHandler.deregister(this, this.handleResizeBound);
+ this.searchAdaptor?.unsubscribe();
+ }
+
+ onBeforeRendering() {
+ if (!this.legacyAdaptor) {
+ this.initLegacyController();
+ }
+ // Sync branding breakpoint state
+ this.branding.forEach(brandingEl => {
+ brandingEl._isSBreakPoint = this.isSBreakPoint;
+ });
+
+ this.buildActions();
+
+ this.searchAdaptor?.syncShowSearchFieldState();
+ // subscribe to search adaptor for cases when search is added dynamically
+ this.searchAdaptor?.unsubscribe();
+ this.searchAdaptor?.subscribe();
+ }
+
+ onAfterRendering() {
+ this.updateBreakpoint();
+ this.updateOverflow();
+ }
+
+ /* =================== Actions Management =================== */
+
+ private buildActions() {
+ this.actions = [
+ {
+ id: ShellBarV2Actions.Search,
+ icon: searchIcon,
+ enabled: this.enabledFeatures.search,
+ selector: ShellBarV2ActionsSelectors.Search,
+ isProtected: false,
+ stableDomRef: "toggle-search",
+ },
+ {
+ id: ShellBarV2Actions.Assistant,
+ icon: daIcon,
+ enabled: this.enabledFeatures.assistant,
+ selector: ShellBarV2ActionsSelectors.Assistant,
+ isProtected: false,
+ },
+ {
+ id: ShellBarV2Actions.Notifications,
+ icon: bellIcon,
+ count: this.notificationsCount,
+ enabled: this.enabledFeatures.notifications,
+ selector: ShellBarV2ActionsSelectors.Notifications,
+ isProtected: false,
+ stableDomRef: "notifications",
+ },
+ {
+ id: ShellBarV2Actions.Overflow,
+ icon: overflowIcon,
+ enabled: this.enabledFeatures.overflow,
+ selector: ShellBarV2ActionsSelectors.Overflow,
+ isProtected: true,
+ stableDomRef: "overflow",
+ },
+ {
+ id: ShellBarV2Actions.Profile,
+ enabled: this.enabledFeatures.profile,
+ selector: ShellBarV2ActionsSelectors.Profile,
+ isProtected: true,
+ stableDomRef: "profile",
+ },
+ {
+ id: ShellBarV2Actions.ProductSwitch,
+ icon: gridIcon,
+ enabled: this.enabledFeatures.productSwitch,
+ selector: ShellBarV2ActionsSelectors.ProductSwitch,
+ isProtected: true,
+ stableDomRef: "product-switch",
+ },
+ ].filter(action => action.enabled);
+ }
+
+ getAction(actionId: ShellBarV2ActionId) {
+ return this.actions.find(action => action.id === actionId);
+ }
+
+ getActionOverflowText(actionId: ShellBarV2ActionId): string {
+ const texts: Record = {
+ [ShellBarV2Actions.Search]: this.texts.search,
+ [ShellBarV2Actions.Profile]: this.texts.profile,
+ [ShellBarV2Actions.Overflow]: this.texts.overflow,
+ [ShellBarV2Actions.Assistant]: this.texts.assistant,
+ [ShellBarV2Actions.ProductSwitch]: this.texts.products,
+ [ShellBarV2Actions.Notifications]: this.texts.notificationsNoCount,
+ };
+ return texts[actionId] || actionId;
+ }
+
+ /* =================== Breakpoint Management =================== */
+
+ get isSBreakPoint() {
+ return this.breakpointSize === "S";
+ }
+
+ private updateBreakpoint() {
+ const width = this.getBoundingClientRect().width;
+ const bp = this.breakpoints.find(b => width <= b) || 10000;
+ const breakpoint = this.breakpointMap[bp];
+
+ if (this.breakpointSize !== breakpoint) {
+ this.breakpointSize = breakpoint;
+ }
+ }
+
+ /* =================== Overflow Management =================== */
+
+ private updateOverflow() {
+ if (!this.overflow) {
+ return;
+ }
+
+ const result = this.overflow.updateOverflow({
+ actions: this.actions,
+ content: this.sortContent(this.content),
+ customItems: this.sortItems(this.items),
+ hiddenItemsIds: this.hiddenItemsIds,
+ showSearchField: this.enabledFeatures.search && this.showSearchField,
+ overflowOuter: this.overflowOuter!,
+ overflowInner: this.overflowInner!,
+ setVisible: (selector: string, visible: boolean) => {
+ const element = this.shadowRoot!.querySelector(selector);
+ if (element) {
+ element.classList[visible ? "remove" : "add"]("ui5-shellbar-hidden");
+ }
+ },
+ });
+
+ this.handleUpdateOverflowResult(result);
+
+ return result.hiddenItemsIds;
+ }
+
+ private handleUpdateOverflowResult(result: ShellBarV2OverflowResult) {
+ const { hiddenItemsIds, showOverflowButton } = result;
+
+ // Update items overflow state
+ this.items.forEach(item => {
+ item.inOverflow = hiddenItemsIds.includes(item._id);
+ if (item.inOverflow) {
+ // clear the hidden class to ensure the item is visible in the overflow popover
+ item.classList.remove("ui5-shellbar-hidden");
+ }
+ });
+
+ if (!arraysAreEqual(this.hiddenItemsIds, hiddenItemsIds)) {
+ this.handleContentVisibilityChanged(this.hiddenItemsIds, hiddenItemsIds);
+ this.hiddenItemsIds = hiddenItemsIds;
+ this.showOverflowButton = showOverflowButton;
+ }
+ this.showFullWidthSearch = this.searchAdaptor?.shouldShowFullScreen() || false;
+ }
+
+ private handleContentVisibilityChanged(oldHiddenItemsIds: string[], newHiddenItemsIds: string[]) {
+ const filterContentIds = (ids: string[]) => ids.filter(id => this.content.some(item => (item as any)._individualSlot as string === id));
+ const oldHiddenContentIds = filterContentIds(oldHiddenItemsIds);
+ const newHiddenContentIds = filterContentIds(newHiddenItemsIds);
+
+ if (!arraysAreEqual(oldHiddenContentIds, newHiddenContentIds)) {
+ this.fireDecoratorEvent("content-item-visibility-change", {
+ items: newHiddenContentIds.map(id => this.content.find(item => (item as any)._individualSlot as string === id)!),
+ });
+ }
+ }
+
+ private handleResize() {
+ this.overflowPopoverOpen = false;
+ this.updateBreakpoint();
+ const hiddenItemsIds = this.updateOverflow() ?? [];
+ const spacerWidth = this.spacer?.getBoundingClientRect().width || 0;
+ this.searchAdaptor?.autoManageSearchState(hiddenItemsIds.length, spacerWidth);
+ }
+
+ isHidden(itemId: string) {
+ return this.hiddenItemsIds.includes(itemId);
+ }
+
+ handleOverflowClick() {
+ this.overflowPopoverOpen = !this.overflowPopoverOpen;
+ }
+
+ onPopoverClose() {
+ this.overflowPopoverOpen = false;
+ }
+
+ /**
+ * Closes the overflow popover.
+ * @public
+ */
+ closeOverflow(): void {
+ this.overflowPopoverOpen = false;
+ }
+
+ handleOverflowItemClick(e: MouseEvent) {
+ const target = e.target as HTMLElement;
+ const actionId = target.getAttribute("data-action-id");
+
+ let prevented = e.defaultPrevented; // for custom actions
+
+ if (actionId === ShellBarV2Actions.Notifications) {
+ prevented = this.handleNotificationsClick();
+ } else if (actionId === ShellBarV2Actions.Search) {
+ prevented = this.handleSearchButtonClick();
+ }
+
+ if (!prevented) {
+ this.overflowPopoverOpen = false;
+ }
+ }
+
+ get overflowItems() {
+ return this.overflow.getOverflowItems({
+ actions: this.actions,
+ customItems: this.sortItems(this.items),
+ hiddenItemsIds: this.hiddenItemsIds,
+ });
+ }
+
+ /**
+ * Returns badge text for overflow button.
+ * Shows count if only one item with count is overflowed, otherwise shows attention dot.
+ */
+ get overflowBadge(): string | undefined {
+ const itemsWithCount = this.overflowItems.filter(item => item.data.count);
+ if (itemsWithCount.length === 1) {
+ return itemsWithCount[0].data.count;
+ }
+ if (itemsWithCount.length > 1) {
+ return " "; // Attention dot
+ }
+ return undefined;
+ }
+
+ /* =================== Search Management =================== */
+
+ get search() {
+ return this.searchField.length ? this.searchField[0] : null;
+ }
+
+ get isSelfCollapsibleSearch(): boolean {
+ const searchField = this.search;
+ if (searchField) {
+ return "collapsed" in searchField && "open" in searchField;
+ }
+ return false;
+ }
+
+ private getSearchDeps() {
+ return {
+ getSearchField: () => this.search,
+ getSearchState: () => this.enabledFeatures.search && this.showSearchField,
+ getCSSVariable: (cssVar: string) => this.getCSSVariable(cssVar),
+ setSearchState: (expanded: boolean) => this.setSearchState(expanded),
+ getOverflowed: () => this.overflow.isOverflowing(this.overflowOuter!, this.overflowInner!),
+ };
+ }
+
+ get searchAdaptor(): IShellBarSearchController {
+ if (this.isSelfCollapsibleSearch) {
+ return this._searchAdaptor;
+ }
+ return this._searchAdaptorLegacy;
+ }
+
+ handleSearchButtonClick() {
+ const searchButton = this.shadowRoot!.querySelector