Skip to content

Commit 2701d79

Browse files
fix(ui5-user-menu): open submenu on hover instead of click
Align UserMenu submenu behavior with Menu by opening submenus on mouse hover with a 300ms delay, matching the standard Menu component behavior.
1 parent 38fe941 commit 2701d79

3 files changed

Lines changed: 168 additions & 3 deletions

File tree

packages/fiori/cypress/specs/UserMenu.cy.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,123 @@ describe("Responsiveness", () => {
916916
});
917917
});
918918

919+
describe("Submenu hover behavior", () => {
920+
it("should open submenu on hover over item with subitems", () => {
921+
cy.mount(
922+
<>
923+
<Button id="openUserMenuBtn">Open User Menu</Button>
924+
<UserMenu open={true} opener="openUserMenuBtn">
925+
<UserMenuAccount slot="accounts" titleText="Alain Chevalier 1"></UserMenuAccount>
926+
<UserMenuItem text="Setting" data-id="setting"></UserMenuItem>
927+
<UserMenuItem text="Legal Information">
928+
<UserMenuItem text="Privacy Policy" data-id="privacy-policy"></UserMenuItem>
929+
<UserMenuItem text="Terms of Use" data-id="terms-of-use"></UserMenuItem>
930+
</UserMenuItem>
931+
</UserMenu>
932+
</>
933+
);
934+
935+
cy.get("[ui5-user-menu]").as("userMenu");
936+
cy.get("@userMenu")
937+
.find("> [ui5-user-menu-item]")
938+
.as("items");
939+
940+
cy.get("@items")
941+
.eq(1)
942+
.should("be.visible")
943+
.as("parentItem");
944+
945+
cy.get("@parentItem").realHover();
946+
947+
cy.get("@parentItem")
948+
.shadow()
949+
.find("[ui5-responsive-popover]")
950+
.should("have.attr", "open");
951+
});
952+
953+
it("should close submenu when hover moves to another item", () => {
954+
cy.mount(
955+
<>
956+
<Button id="openUserMenuBtn">Open User Menu</Button>
957+
<UserMenu open={true} opener="openUserMenuBtn">
958+
<UserMenuAccount slot="accounts" titleText="Alain Chevalier 1"></UserMenuAccount>
959+
<UserMenuItem text="Setting" data-id="setting"></UserMenuItem>
960+
<UserMenuItem text="Legal Information">
961+
<UserMenuItem text="Privacy Policy" data-id="privacy-policy"></UserMenuItem>
962+
<UserMenuItem text="Terms of Use" data-id="terms-of-use"></UserMenuItem>
963+
</UserMenuItem>
964+
</UserMenu>
965+
</>
966+
);
967+
968+
cy.get("[ui5-user-menu]").as("userMenu");
969+
cy.get("@userMenu")
970+
.find("> [ui5-user-menu-item]")
971+
.as("items");
972+
973+
cy.get("@items")
974+
.eq(1)
975+
.should("be.visible")
976+
.as("parentItem");
977+
978+
cy.get("@parentItem").realHover();
979+
980+
cy.get("@parentItem")
981+
.shadow()
982+
.find("[ui5-responsive-popover]")
983+
.as("submenuPopover");
984+
985+
cy.get("@submenuPopover")
986+
.should("have.attr", "open");
987+
988+
cy.get("@items")
989+
.eq(0)
990+
.should("be.visible")
991+
.as("otherItem");
992+
993+
cy.get("@otherItem").realHover();
994+
995+
cy.get("@submenuPopover")
996+
.should("not.have.attr", "open");
997+
});
998+
999+
it("should not move focus to submenu when opened via hover", () => {
1000+
cy.mount(
1001+
<>
1002+
<Button id="openUserMenuBtn">Open User Menu</Button>
1003+
<UserMenu open={true} opener="openUserMenuBtn">
1004+
<UserMenuAccount slot="accounts" titleText="Alain Chevalier 1"></UserMenuAccount>
1005+
<UserMenuItem text="Legal Information">
1006+
<UserMenuItem text="Privacy Policy" data-id="privacy-policy"></UserMenuItem>
1007+
<UserMenuItem text="Terms of Use" data-id="terms-of-use"></UserMenuItem>
1008+
</UserMenuItem>
1009+
</UserMenu>
1010+
</>
1011+
);
1012+
1013+
cy.get("[ui5-user-menu]").as("userMenu");
1014+
cy.get("@userMenu")
1015+
.find("> [ui5-user-menu-item]")
1016+
.first()
1017+
.should("be.visible")
1018+
.as("parentItem");
1019+
1020+
cy.get("@parentItem").realHover();
1021+
1022+
cy.get("@parentItem")
1023+
.shadow()
1024+
.find("[ui5-responsive-popover]")
1025+
.should("have.attr", "open");
1026+
1027+
cy.get("@parentItem")
1028+
.should("be.focused");
1029+
1030+
cy.get("[ui5-user-menu-item] > [ui5-user-menu-item]")
1031+
.first()
1032+
.should("not.be.focused");
1033+
});
1034+
});
1035+
9191036
describe("Footer configuration", () => {
9201037
it("tests default footer with Sign Out button", () => {
9211038
cy.mount(

packages/fiori/src/UserMenu.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
1515
import type { PopupScrollEventDetail } from "@ui5/webcomponents/dist/Popup.js";
1616
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
1717
import { isInstanceOfMenuItem } from "@ui5/webcomponents/dist/MenuItem.js";
18-
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
18+
import { isPhone, isDesktop } from "@ui5/webcomponents-base/dist/Device.js";
19+
import type { Timeout } from "@ui5/webcomponents-base/dist/types.js";
1920
import type UserMenuAccount from "./UserMenuAccount.js";
2021
import type UserMenuItem from "./UserMenuItem.js";
2122
import UserMenuTemplate from "./UserMenuTemplate.js";
@@ -35,6 +36,8 @@ import {
3536
USER_MENU_ACTIONS_TXT,
3637
} from "./generated/i18n/i18n-defaults.js";
3738

39+
const MENU_OPEN_DELAY = 300;
40+
3841
type UserMenuItemClickEventDetail = {
3942
item: UserMenuItem;
4043
}
@@ -260,6 +263,11 @@ class UserMenu extends UI5Element {
260263
*/
261264
_observer?: IntersectionObserver;
262265

266+
/**
267+
* @private
268+
*/
269+
_timeout?: Timeout;
270+
263271
/**
264272
* @private
265273
*/
@@ -372,7 +380,7 @@ class UserMenu extends UI5Element {
372380
}
373381

374382
_handleMenuItemClick(e: CustomEvent<ListItemClickEventDetail>) {
375-
const item = e.detail.item as UserMenuItem; // imrove: improve this ideally without "as" cating
383+
const item = e.detail.item as UserMenuItem;
376384

377385
item._updateCheckedState();
378386

@@ -385,6 +393,7 @@ class UserMenu extends UI5Element {
385393
item.fireEvent("close-menu");
386394
}
387395
} else {
396+
this._closeOtherSubMenus(item);
388397
this._openItemSubMenu(item);
389398
}
390399
}
@@ -409,14 +418,52 @@ class UserMenu extends UI5Element {
409418
this.fireDecoratorEvent("close");
410419
}
411420

412-
_openItemSubMenu(item: UserMenuItem) {
421+
_itemMouseOver(e: MouseEvent) {
422+
if (!isDesktop()) {
423+
return;
424+
}
425+
426+
const item = e.target as UserMenuItem;
427+
if (!isInstanceOfMenuItem(item)) {
428+
return;
429+
}
430+
431+
item.getFocusDomRef()?.focus();
432+
this._startOpenTimeout(item);
433+
}
434+
435+
_startOpenTimeout(item: UserMenuItem) {
436+
clearTimeout(this._timeout);
437+
438+
this._timeout = setTimeout(() => {
439+
this._closeOtherSubMenus(item);
440+
this._openItemSubMenu(item, true);
441+
}, MENU_OPEN_DELAY);
442+
}
443+
444+
_closeOtherSubMenus(item: UserMenuItem) {
445+
if (!this._menuItems.includes(item)) {
446+
return;
447+
}
448+
449+
this._menuItems.forEach(menuItem => {
450+
if (menuItem !== item) {
451+
menuItem._close();
452+
}
453+
});
454+
}
455+
456+
_openItemSubMenu(item: UserMenuItem, openedByMouse = false) {
457+
clearTimeout(this._timeout);
458+
413459
if (!item._popover || item._popover.open) {
414460
return;
415461
}
416462

417463
item._popover.opener = item;
418464
item._popover.open = true;
419465
item.selected = true;
466+
item._openedByMouse = openedByMouse;
420467
}
421468

422469
_closeUserMenu() {

packages/fiori/src/UserMenuTemplate.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export default function UserMenuTemplate(this: UserMenu) {
7979
accessibleRole="Menu"
8080
accessibleName={this._ariaLabelledByActions}
8181
onItemClick={this._handleMenuItemClick}
82+
onMouseOver={this._itemMouseOver}
8283
onui5-close-menu={this._handleMenuItemClose}
8384
>
8485
<slot></slot>

0 commit comments

Comments
 (0)