Skip to content

Commit 26b51ec

Browse files
authored
feat(ui5-menu): keep focus over hovered item (#12921)
Now when hovering over a `ui5-menu-item` with a mouse, the focus remains on the current item instead of focusing on the first item in the sub-menu.
1 parent e2178ae commit 26b51ec

File tree

3 files changed

+189
-20
lines changed

3 files changed

+189
-20
lines changed

packages/main/cypress/specs/Menu.cy.tsx

Lines changed: 177 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,8 +1055,6 @@ describe("Menu interaction", () => {
10551055
.should("have.attr", "accessible-name", "Select an option from the menu");
10561056
});
10571057

1058-
/* The test is valid, but currently it is not stable. It will be reviewed further and stabilized afterwards. */
1059-
10601058
it("Menu items - navigation in endContent", () => {
10611059
cy.mount(
10621060
<>
@@ -1071,6 +1069,10 @@ describe("Menu interaction", () => {
10711069
</>
10721070
);
10731071

1072+
// Move mouse to opener button to avoid interference with menu item hover behavior
1073+
cy.get("[ui5-button]")
1074+
.realHover();
1075+
10741076
cy.get("[ui5-menu]")
10751077
.ui5MenuOpen();
10761078

@@ -1079,24 +1081,34 @@ describe("Menu interaction", () => {
10791081

10801082
cy.get("@items")
10811083
.first()
1082-
.should("be.focused");
1084+
.should("be.focused")
1085+
.realPress("ArrowRight");
10831086

1084-
cy.realPress("ArrowRight");
1085-
cy.get("@buttons").first().should("be.focused");
1087+
cy.get("@buttons")
1088+
.first()
1089+
.should("be.focused")
1090+
.realPress("ArrowRight");
10861091

1087-
cy.realPress("ArrowRight");
1088-
cy.get("@buttons").last().should("be.focused");
1092+
cy.get("@buttons")
1093+
.last()
1094+
.should("be.focused")
1095+
.realPress("ArrowRight");
10891096

1090-
cy.realPress("ArrowRight");
1091-
cy.get("@buttons").last().should("be.focused");
1097+
cy.get("@buttons")
1098+
.last()
1099+
.should("be.focused")
1100+
.realPress("ArrowLeft");
10921101

1093-
cy.realPress("ArrowLeft");
1094-
cy.get("@buttons").first().should("be.focused");
1102+
cy.get("@buttons")
1103+
.first()
1104+
.should("be.focused")
1105+
.realPress("ArrowLeft");
10951106

1096-
cy.realPress("ArrowLeft");
1097-
cy.get("@buttons").first().should("be.focused");
1107+
cy.get("@buttons")
1108+
.first()
1109+
.should("be.focused")
1110+
.realPress("ArrowDown");
10981111

1099-
cy.realPress("ArrowDown");
11001112
cy.get("@items").last().should("be.focused");
11011113
});
11021114
});
@@ -1162,4 +1174,155 @@ describe("Menu - getFocusDomRef", () => {
11621174
expect(menu.getFocusDomRef()).equal(clickedItem.getFocusDomRef());
11631175
});
11641176
});
1177+
});
1178+
1179+
describe("Menu - Submenu Focus Behavior", () => {
1180+
it("should not move focus when submenu opens via mouse hover", () => {
1181+
cy.mount(
1182+
<>
1183+
<Button id="btnOpen">Open Menu</Button>
1184+
<Menu opener="btnOpen">
1185+
<MenuItem text="Parent Item">
1186+
<MenuItem text="Child Item 1"></MenuItem>
1187+
<MenuItem text="Child Item 2"></MenuItem>
1188+
</MenuItem>
1189+
<MenuItem text="Another Item"></MenuItem>
1190+
</Menu>
1191+
</>
1192+
);
1193+
1194+
cy.get("[ui5-menu]")
1195+
.ui5MenuOpen();
1196+
1197+
cy.get("[ui5-menu] > [ui5-menu-item]")
1198+
.as("items");
1199+
1200+
cy.get("@items")
1201+
.first()
1202+
.should("be.visible")
1203+
.as("parentItem");
1204+
1205+
// Hover item to open submenu
1206+
cy.get("@parentItem").realHover();
1207+
1208+
cy.get("@parentItem")
1209+
.shadow()
1210+
.find("[ui5-responsive-popover]")
1211+
.should("have.attr", "open");
1212+
1213+
// Verify focus not moved to submenu
1214+
cy.get("@parentItem")
1215+
.should("be.focused");
1216+
1217+
cy.get("[ui5-menu-item] > [ui5-menu-item]")
1218+
.as("submenuitems");
1219+
1220+
cy.get("@submenuitems")
1221+
.first()
1222+
.should("be.visible")
1223+
.as("childItem");
1224+
1225+
cy.get("@childItem")
1226+
.should("not.be.focused");
1227+
});
1228+
1229+
it("should close submenu when hover moves to another item", () => {
1230+
cy.mount(
1231+
<>
1232+
<Button id="btnOpen">Open Menu</Button>
1233+
<Menu opener="btnOpen">
1234+
<MenuItem text="Parent Item">
1235+
<MenuItem text="Child Item 1"></MenuItem>
1236+
<MenuItem text="Child Item 2"></MenuItem>
1237+
</MenuItem>
1238+
<MenuItem text="Another Item"></MenuItem>
1239+
</Menu>
1240+
</>
1241+
);
1242+
1243+
cy.get("[ui5-menu]")
1244+
.ui5MenuOpen();
1245+
1246+
cy.get("[ui5-menu] > [ui5-menu-item]")
1247+
.as("items");
1248+
1249+
cy.get("@items")
1250+
.first()
1251+
.should("be.visible")
1252+
.as("parentItem");
1253+
1254+
// Hover item to open submenu
1255+
cy.get("@parentItem").realHover();
1256+
1257+
cy.get("@parentItem")
1258+
.shadow()
1259+
.find("[ui5-responsive-popover]")
1260+
.as("submenuPopover");
1261+
1262+
cy.get("@submenuPopover")
1263+
.should("have.attr", "open");
1264+
1265+
// Hover over another top-level item
1266+
cy.get("@items")
1267+
.last()
1268+
.should("be.visible")
1269+
.as("lastItem");
1270+
1271+
cy.get("@lastItem")
1272+
.realHover();
1273+
1274+
// The original submenu should be closed
1275+
cy.get("@submenuPopover")
1276+
.should("not.have.attr", "open");
1277+
});
1278+
1279+
it("should move focus when submenu opens via keyboard", () => {
1280+
cy.mount(
1281+
<>
1282+
<Button id="btnOpen">Open Menu</Button>
1283+
<Menu opener="btnOpen">
1284+
<MenuItem text="Parent Item">
1285+
<MenuItem text="Child Item 1"></MenuItem>
1286+
<MenuItem text="Child Item 2"></MenuItem>
1287+
</MenuItem>
1288+
<MenuItem text="Another Item"></MenuItem>
1289+
</Menu>
1290+
</>
1291+
);
1292+
1293+
cy.get("[ui5-menu]")
1294+
.ui5MenuOpen();
1295+
1296+
cy.get("[ui5-menu] > [ui5-menu-item]")
1297+
.as("items");
1298+
1299+
cy.get("@items")
1300+
.first()
1301+
.should("be.visible")
1302+
.as("parentItem");
1303+
1304+
// Open submenu with keyboard
1305+
cy.get("@parentItem")
1306+
.should("be.focused")
1307+
.realPress("ArrowRight");
1308+
cy.get("@parentItem")
1309+
.shadow()
1310+
.find("[ui5-responsive-popover]")
1311+
.should("have.attr", "open");
1312+
1313+
// Verify focus is moved to submenu
1314+
cy.get("@parentItem")
1315+
.should("not.be.focused");
1316+
1317+
cy.get("[ui5-menu-item] > [ui5-menu-item]")
1318+
.as("submenuitems");
1319+
1320+
cy.get("@submenuitems")
1321+
.first()
1322+
.should("be.visible")
1323+
.as("childItem");
1324+
1325+
cy.get("@childItem")
1326+
.should("be.focused");
1327+
});
11651328
});

packages/main/src/Menu.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ class Menu extends UI5Element {
351351
this.open = false;
352352
}
353353

354-
_openItemSubMenu(item: MenuItem) {
354+
_openItemSubMenu(item: MenuItem, openedByMouse = false) {
355355
clearTimeout(this._timeout);
356356

357357
if (!item._popover || item._popover.open) {
@@ -365,6 +365,7 @@ class Menu extends UI5Element {
365365
item._popover.opener = item;
366366
item._popover.open = true;
367367
item.selected = true;
368+
item._openedByMouse = openedByMouse;
368369
}
369370

370371
_itemMouseOver(e: MouseEvent) {
@@ -377,7 +378,7 @@ class Menu extends UI5Element {
377378
return;
378379
}
379380

380-
item.focus();
381+
item.getFocusDomRef()?.focus();
381382

382383
// Opens submenu with 300ms delay
383384
this._startOpenTimeout(item);
@@ -413,7 +414,7 @@ class Menu extends UI5Element {
413414
this._timeout = setTimeout(() => {
414415
this._closeOtherSubMenus(item);
415416

416-
this._openItemSubMenu(item);
417+
this._openItemSubMenu(item, true);
417418
}, MENU_OPEN_DELAY);
418419
}
419420

@@ -455,7 +456,7 @@ class Menu extends UI5Element {
455456
}
456457

457458
if (shouldOpenMenu) {
458-
this._openItemSubMenu(item);
459+
this._openItemSubMenu(item, false);
459460
} else if (isTabNextPrevious) {
460461
this._close();
461462
}

packages/main/src/MenuItem.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ class MenuItem extends ListItem implements IMenuItem {
319319

320320
_itemNavigation: ItemNavigation;
321321
_shiftPressed: boolean = false;
322+
_openedByMouse = false;
322323

323324
constructor() {
324325
super();
@@ -535,7 +536,8 @@ class MenuItem extends ListItem implements IMenuItem {
535536
if (!isInstanceOfMenuItem(item)) {
536537
return;
537538
}
538-
item.focus();
539+
540+
item.getFocusDomRef()?.focus();
539541

540542
this._closeOtherSubMenus(item);
541543
}
@@ -622,7 +624,9 @@ class MenuItem extends ListItem implements IMenuItem {
622624
}
623625

624626
_afterPopoverOpen() {
625-
this._allMenuItems[0]?.focus();
627+
if (!this._openedByMouse) {
628+
this._allMenuItems[0]?.focus();
629+
}
626630
this.fireDecoratorEvent("open");
627631
}
628632

@@ -644,6 +648,7 @@ class MenuItem extends ListItem implements IMenuItem {
644648
}
645649

646650
_afterPopoverClose() {
651+
this._openedByMouse = false;
647652
this.fireDecoratorEvent("close");
648653
}
649654

0 commit comments

Comments
 (0)