diff --git a/packages/fiori/cypress/specs/UserMenu.cy.tsx b/packages/fiori/cypress/specs/UserMenu.cy.tsx
index f90a185634b2..afd555a232ad 100644
--- a/packages/fiori/cypress/specs/UserMenu.cy.tsx
+++ b/packages/fiori/cypress/specs/UserMenu.cy.tsx
@@ -916,6 +916,123 @@ describe("Responsiveness", () => {
});
});
+describe("Submenu hover behavior", () => {
+ it("should open submenu on hover over item with subitems", () => {
+ cy.mount(
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+
+ cy.get("[ui5-user-menu]").as("userMenu");
+ cy.get("@userMenu")
+ .find("> [ui5-user-menu-item]")
+ .as("items");
+
+ cy.get("@items")
+ .eq(1)
+ .should("be.visible")
+ .as("parentItem");
+
+ cy.get("@parentItem").realHover();
+
+ cy.get("@parentItem")
+ .shadow()
+ .find("[ui5-responsive-popover]")
+ .should("have.attr", "open");
+ });
+
+ it("should close submenu when hover moves to another item", () => {
+ cy.mount(
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+
+ cy.get("[ui5-user-menu]").as("userMenu");
+ cy.get("@userMenu")
+ .find("> [ui5-user-menu-item]")
+ .as("items");
+
+ cy.get("@items")
+ .eq(1)
+ .should("be.visible")
+ .as("parentItem");
+
+ cy.get("@parentItem").realHover();
+
+ cy.get("@parentItem")
+ .shadow()
+ .find("[ui5-responsive-popover]")
+ .as("submenuPopover");
+
+ cy.get("@submenuPopover")
+ .should("have.attr", "open");
+
+ cy.get("@items")
+ .eq(0)
+ .should("be.visible")
+ .as("otherItem");
+
+ cy.get("@otherItem").realHover();
+
+ cy.get("@submenuPopover")
+ .should("not.have.attr", "open");
+ });
+
+ it("should not move focus to submenu when opened via hover", () => {
+ cy.mount(
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+
+ cy.get("[ui5-user-menu]").as("userMenu");
+ cy.get("@userMenu")
+ .find("> [ui5-user-menu-item]")
+ .first()
+ .should("be.visible")
+ .as("parentItem");
+
+ cy.get("@parentItem").realHover();
+
+ cy.get("@parentItem")
+ .shadow()
+ .find("[ui5-responsive-popover]")
+ .should("have.attr", "open");
+
+ cy.get("@parentItem")
+ .should("be.focused");
+
+ cy.get("[ui5-user-menu-item] > [ui5-user-menu-item]")
+ .first()
+ .should("not.be.focused");
+ });
+});
+
describe("Footer configuration", () => {
it("tests default footer with Sign Out button", () => {
cy.mount(
diff --git a/packages/fiori/src/UserMenu.ts b/packages/fiori/src/UserMenu.ts
index 40077b566f26..ee52fd126151 100644
--- a/packages/fiori/src/UserMenu.ts
+++ b/packages/fiori/src/UserMenu.ts
@@ -15,7 +15,8 @@ import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import type { PopupScrollEventDetail } from "@ui5/webcomponents/dist/Popup.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import { isInstanceOfMenuItem } from "@ui5/webcomponents/dist/MenuItem.js";
-import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
+import { isPhone, isDesktop } from "@ui5/webcomponents-base/dist/Device.js";
+import type { Timeout } from "@ui5/webcomponents-base/dist/types.js";
import type UserMenuAccount from "./UserMenuAccount.js";
import type UserMenuItem from "./UserMenuItem.js";
import UserMenuTemplate from "./UserMenuTemplate.js";
@@ -35,6 +36,8 @@ import {
USER_MENU_ACTIONS_TXT,
} from "./generated/i18n/i18n-defaults.js";
+const MENU_OPEN_DELAY = 300;
+
type UserMenuItemClickEventDetail = {
item: UserMenuItem;
}
@@ -260,6 +263,11 @@ class UserMenu extends UI5Element {
*/
_observer?: IntersectionObserver;
+ /**
+ * @private
+ */
+ _timeout?: Timeout;
+
/**
* @private
*/
@@ -372,7 +380,7 @@ class UserMenu extends UI5Element {
}
_handleMenuItemClick(e: CustomEvent) {
- const item = e.detail.item as UserMenuItem; // imrove: improve this ideally without "as" cating
+ const item = e.detail.item as UserMenuItem;
item._updateCheckedState();
@@ -385,6 +393,7 @@ class UserMenu extends UI5Element {
item.fireEvent("close-menu");
}
} else {
+ this._closeOtherSubMenus(item);
this._openItemSubMenu(item);
}
}
@@ -409,7 +418,44 @@ class UserMenu extends UI5Element {
this.fireDecoratorEvent("close");
}
- _openItemSubMenu(item: UserMenuItem) {
+ _itemMouseOver(e: MouseEvent) {
+ if (!isDesktop()) {
+ return;
+ }
+
+ const item = e.target as UserMenuItem;
+ if (!isInstanceOfMenuItem(item)) {
+ return;
+ }
+
+ item.getFocusDomRef()?.focus();
+ this._startOpenTimeout(item);
+ }
+
+ _startOpenTimeout(item: UserMenuItem) {
+ clearTimeout(this._timeout);
+
+ this._timeout = setTimeout(() => {
+ this._closeOtherSubMenus(item);
+ this._openItemSubMenu(item, true);
+ }, MENU_OPEN_DELAY);
+ }
+
+ _closeOtherSubMenus(item: UserMenuItem) {
+ if (!this._menuItems.includes(item)) {
+ return;
+ }
+
+ this._menuItems.forEach(menuItem => {
+ if (menuItem !== item) {
+ menuItem._close();
+ }
+ });
+ }
+
+ _openItemSubMenu(item: UserMenuItem, openedByMouse = false) {
+ clearTimeout(this._timeout);
+
if (!item._popover || item._popover.open) {
return;
}
@@ -417,6 +463,7 @@ class UserMenu extends UI5Element {
item._popover.opener = item;
item._popover.open = true;
item.selected = true;
+ item._openedByMouse = openedByMouse;
}
_closeUserMenu() {
diff --git a/packages/fiori/src/UserMenuTemplate.tsx b/packages/fiori/src/UserMenuTemplate.tsx
index 3fe60944720e..9d641f8cf96b 100644
--- a/packages/fiori/src/UserMenuTemplate.tsx
+++ b/packages/fiori/src/UserMenuTemplate.tsx
@@ -79,6 +79,7 @@ export default function UserMenuTemplate(this: UserMenu) {
accessibleRole="Menu"
accessibleName={this._ariaLabelledByActions}
onItemClick={this._handleMenuItemClick}
+ onMouseOver={this._itemMouseOver}
onui5-close-menu={this._handleMenuItemClose}
>