Skip to content
89 changes: 87 additions & 2 deletions packages/main/cypress/specs/Menu.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1444,7 +1444,7 @@ describe("Menu - Submenu Focus Behavior", () => {
.shadow()
.find("[ui5-responsive-popover]")
.as("submenuPopover");

cy.get("@submenuPopover")
.should("have.attr", "open");

Expand All @@ -1453,7 +1453,7 @@ describe("Menu - Submenu Focus Behavior", () => {
.last()
.should("be.visible")
.as("lastItem");

cy.get("@lastItem")
.realHover();

Expand Down Expand Up @@ -1511,4 +1511,89 @@ describe("Menu - Submenu Focus Behavior", () => {
cy.get("@childItem")
.should("be.focused");
});
});

describe("Menu - Page Up/Down navigation", () => {
function mountLongMenu() {
cy.viewport(800, 300);

const items = Array.from({ length: 25 }, (_, i) => (
<MenuItem key={i} text={`Item ${i + 1}`}></MenuItem>
));

cy.mount(
<>
<Button id="btnOpen">Open Menu</Button>
<Menu id="menu" opener="btnOpen">
{items}
</Menu>
</>
);

cy.get("[ui5-menu]").ui5MenuOpen({ opener: "btnOpen" });
}

it("Page Down moves focus forward by page size", () => {
mountLongMenu();

cy.get("[ui5-menu] > [ui5-menu-item]").as("items");

cy.get("@items").first().should("be.focused");

cy.focused().realPress("PageDown");

// Record which item was landed on, then PageUp should return exactly to item 0
cy.get("@items").first().should("not.be.focused");
cy.get("@items").eq(1).should("not.be.focused");

cy.focused().realPress("PageUp");

cy.get("@items").first().should("be.focused");
});

it("Page Up moves focus backward by page size", () => {
mountLongMenu();

cy.get("[ui5-menu] > [ui5-menu-item]").as("items");

cy.get("@items").first().should("be.focused");

// Press PageDown twice to land somewhere in the middle
cy.focused().realPress("PageDown");
cy.focused().realPress("PageDown");

// PageUp must go back exactly one page — not to item 1, not to where PageDown×2 landed
cy.focused().realPress("PageUp");
cy.focused().realPress("PageDown");

// Two PageDowns and one PageUp then one PageDown must equal two PageDowns net
// — verify we are not at item 1 (moved forward) and not at item 2 (moved more than 1)
cy.get("@items").first().should("not.be.focused");
cy.get("@items").eq(1).should("not.be.focused");
});

it("Page Down from last visible page focuses last item", () => {
mountLongMenu();

cy.get("[ui5-menu] > [ui5-menu-item]").as("items");

// Press PageDown 15 times — clamps to last item regardless of page size
Cypress._.times(15, () => cy.focused().realPress("PageDown"));

cy.get("@items").last().should("be.focused");
});

it("Page Up from first visible page focuses first item", () => {
mountLongMenu();

cy.get("[ui5-menu] > [ui5-menu-item]").as("items");

// Reach the last item via keyboard, then press PageUp 15 times — clamps to first item
Cypress._.times(15, () => cy.focused().realPress("PageDown"));
cy.get("@items").last().should("be.focused");

Cypress._.times(15, () => cy.focused().realPress("PageUp"));

cy.get("@items").first().should("be.focused");
});
});
22 changes: 22 additions & 0 deletions packages/main/src/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,27 @@ class Menu extends UI5Element {
}
}

_updatePageSize() {
const list = this._list;
if (!list) {
return;
}

const firstItem = this._navigatableMenuItems[0];
if (!firstItem) {
return;
}

const itemHeight = firstItem.offsetHeight;
if (itemHeight === 0) {
return;
}

const popoverHeight = this._popover.getBoundingClientRect().height;
const visibleCount = Math.round(popoverHeight / itemHeight);
list._itemNavigation._skipItemsSize = visibleCount > 1 ? visibleCount - 1 : null;
}

_close() {
this.open = false;
}
Expand Down Expand Up @@ -501,6 +522,7 @@ class Menu extends UI5Element {

_afterPopoverOpen() {
this._allMenuItems[0]?.focus();
this._updatePageSize();
if (this.loading) {
announce(Menu.i18nBundle.getText(MENU_ITEM_LOADING), InvisibleMessageMode.Polite);
}
Expand Down
35 changes: 35 additions & 0 deletions packages/main/test/pages/Menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,36 @@
<ui5-menu-item text="Exit"></ui5-menu-item>
</ui5-menu>

<ui5-title level="H5" class="header-title">Menu with long list of items</ui5-title>
<ui5-button id="btnOpenLongList">Open Menu</ui5-button> <br/><br/>
<ui5-menu id="menuLongList" header-text="Long List">
<ui5-menu-item text="Item 1"></ui5-menu-item>
<ui5-menu-item text="Item 2"></ui5-menu-item>
<ui5-menu-item text="Item 3"></ui5-menu-item>
<ui5-menu-item text="Item 4"></ui5-menu-item>
<ui5-menu-item text="Item 5"></ui5-menu-item>
<ui5-menu-item text="Item 6"></ui5-menu-item>
<ui5-menu-item text="Item 7"></ui5-menu-item>
<ui5-menu-item text="Item 8"></ui5-menu-item>
<ui5-menu-item text="Item 9"></ui5-menu-item>
<ui5-menu-item text="Item 10"></ui5-menu-item>
<ui5-menu-item text="Item 11"></ui5-menu-item>
<ui5-menu-item text="Item 12"></ui5-menu-item>
<ui5-menu-item text="Item 13"></ui5-menu-item>
<ui5-menu-item text="Item 14"></ui5-menu-item>
<ui5-menu-item text="Item 15"></ui5-menu-item>
<ui5-menu-item text="Item 16"></ui5-menu-item>
<ui5-menu-item text="Item 17"></ui5-menu-item>
<ui5-menu-item text="Item 18"></ui5-menu-item>
<ui5-menu-item text="Item 19"></ui5-menu-item>
<ui5-menu-item text="Item 20"></ui5-menu-item>
<ui5-menu-item text="Item 21"></ui5-menu-item>
<ui5-menu-item text="Item 22"></ui5-menu-item>
<ui5-menu-item text="Item 23"></ui5-menu-item>
<ui5-menu-item text="Item 24"></ui5-menu-item>
<ui5-menu-item text="Item 25"></ui5-menu-item>
</ui5-menu>

<ui5-popover id="detailsPopover" initialFocus="detailsLink">
<div>
<ui5-label for="detailsLink">For more information:</ui5-label>
Expand Down Expand Up @@ -212,6 +242,11 @@
menuGroups.open = true;
});

btnOpenLongList.addEventListener("click", function() {
menuLongList.opener = "btnOpenLongList";
menuLongList.open = true;
});

btnOpenEndContent.addEventListener("click", function() {
menuEndContent.opener = "btnOpenEndContent";
menuEndContent.open = !menu.open;
Expand Down
Loading