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} >