From 187df923f7f9fe1d364568e44c43ab33fbb7cd68 Mon Sep 17 00:00:00 2001 From: Nikolay Hristov Date: Wed, 29 Jul 2026 14:30:41 +0300 Subject: [PATCH 1/6] feat(ui5-menu): page up/page down new behavior --- packages/main/cypress/specs/Menu.cy.tsx | 96 +++++++++++++++++++++++++ packages/main/src/Menu.ts | 24 +++++++ packages/main/test/pages/Menu.html | 35 +++++++++ 3 files changed, 155 insertions(+) diff --git a/packages/main/cypress/specs/Menu.cy.tsx b/packages/main/cypress/specs/Menu.cy.tsx index 436d0541b3210..b234bd6f4ed71 100644 --- a/packages/main/cypress/specs/Menu.cy.tsx +++ b/packages/main/cypress/specs/Menu.cy.tsx @@ -1511,4 +1511,100 @@ 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) => ( + + )); + + cy.mount( + <> + + + {items} + + + ); + + 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"); + cy.focused().realPress("End"); + cy.get("@items").last().should("be.focused"); + + cy.focused().realPress("PageUp"); + + // Verify we moved back more than one item, then PageDown should return exactly to last item + cy.get("@items").last().should("not.be.focused"); + cy.get("@items").eq(-2).should("not.be.focused"); + + cy.focused().realPress("PageDown"); + + cy.get("@items").last().should("be.focused"); + }); + + it("Page Down from last visible page focuses last item", () => { + mountLongMenu(); + + cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); + + cy.get("@items").first().should("be.focused"); + + // Press Page Down enough times to reach the end + cy.focused().realPress("PageDown"); + cy.focused().realPress("PageDown"); + cy.focused().realPress("PageDown"); + cy.focused().realPress("PageDown"); + 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"); + + // Go to last item first + cy.get("@items").first().should("be.focused"); + cy.focused().realPress("End"); + cy.get("@items").last().should("be.focused"); + + // Press Page Up enough times to reach the start + cy.focused().realPress("PageUp"); + cy.focused().realPress("PageUp"); + cy.focused().realPress("PageUp"); + cy.focused().realPress("PageUp"); + cy.focused().realPress("PageUp"); + + cy.get("@items").first().should("be.focused"); + }); }); \ No newline at end of file diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 32105ba1fc5e6..58bfb62565023 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -358,6 +358,29 @@ 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; + + console.warn(list._itemNavigation._skipItemsSize, visibleCount, popoverHeight, itemHeight); + } + _close() { this.open = false; } @@ -501,6 +524,7 @@ class Menu extends UI5Element { _afterPopoverOpen() { this._allMenuItems[0]?.focus(); + this._updatePageSize(); if (this.loading) { announce(Menu.i18nBundle.getText(MENU_ITEM_LOADING), InvisibleMessageMode.Polite); } diff --git a/packages/main/test/pages/Menu.html b/packages/main/test/pages/Menu.html index e4711a257915e..aec5da9167c9e 100644 --- a/packages/main/test/pages/Menu.html +++ b/packages/main/test/pages/Menu.html @@ -172,6 +172,36 @@ + Menu with long list of items + Open Menu

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
For more information: @@ -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; From 04cf2c5f66b0771da129fadb303df05464bcec21 Mon Sep 17 00:00:00 2001 From: Nikolay Hristov Date: Wed, 29 Jul 2026 16:28:33 +0300 Subject: [PATCH 2/6] feat(ui5-menu): fix lint errors --- packages/main/src/Menu.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 58bfb62565023..39b8fb5927e91 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -377,8 +377,6 @@ class Menu extends UI5Element { const popoverHeight = this._popover.getBoundingClientRect().height; const visibleCount = Math.round(popoverHeight / itemHeight); list._itemNavigation._skipItemsSize = visibleCount > 1 ? visibleCount - 1 : null; - - console.warn(list._itemNavigation._skipItemsSize, visibleCount, popoverHeight, itemHeight); } _close() { From 4aa2808f617f82cb9f78b3bc347e8e321a656024 Mon Sep 17 00:00:00 2001 From: Nikolay Hristov Date: Thu, 30 Jul 2026 11:29:35 +0300 Subject: [PATCH 3/6] feat(ui5-menu): fix tests --- packages/main/cypress/specs/Menu.cy.tsx | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/main/cypress/specs/Menu.cy.tsx b/packages/main/cypress/specs/Menu.cy.tsx index b234bd6f4ed71..1407beaec6f1d 100644 --- a/packages/main/cypress/specs/Menu.cy.tsx +++ b/packages/main/cypress/specs/Menu.cy.tsx @@ -1562,9 +1562,8 @@ describe("Menu - Page Up/Down navigation", () => { cy.focused().realPress("PageUp"); - // Verify we moved back more than one item, then PageDown should return exactly to last item + // Verify moved back, then PageDown must return exactly to last item (round-trip) cy.get("@items").last().should("not.be.focused"); - cy.get("@items").eq(-2).should("not.be.focused"); cy.focused().realPress("PageDown"); @@ -1576,13 +1575,13 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); + // Go to last item, step back one page, then Page Down must clamp to last item cy.get("@items").first().should("be.focused"); + cy.focused().realPress("End"); + cy.get("@items").last().should("be.focused"); + cy.focused().realPress("PageUp"); + cy.get("@items").last().should("not.be.focused"); - // Press Page Down enough times to reach the end - cy.focused().realPress("PageDown"); - cy.focused().realPress("PageDown"); - cy.focused().realPress("PageDown"); - cy.focused().realPress("PageDown"); cy.focused().realPress("PageDown"); cy.get("@items").last().should("be.focused"); @@ -1593,16 +1592,11 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); - // Go to last item first + // Step forward one page from start, then Page Up must clamp to first item cy.get("@items").first().should("be.focused"); - cy.focused().realPress("End"); - cy.get("@items").last().should("be.focused"); + cy.focused().realPress("PageDown"); + cy.get("@items").first().should("not.be.focused"); - // Press Page Up enough times to reach the start - cy.focused().realPress("PageUp"); - cy.focused().realPress("PageUp"); - cy.focused().realPress("PageUp"); - cy.focused().realPress("PageUp"); cy.focused().realPress("PageUp"); cy.get("@items").first().should("be.focused"); From 77b0b289d8ec7209c905d484e581bfa0edaa4386 Mon Sep 17 00:00:00 2001 From: Nikolay Hristov Date: Thu, 30 Jul 2026 13:51:24 +0300 Subject: [PATCH 4/6] feat(ui5-menu): fix tests --- packages/main/cypress/specs/Menu.cy.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/main/cypress/specs/Menu.cy.tsx b/packages/main/cypress/specs/Menu.cy.tsx index 1407beaec6f1d..677b7fc3fed18 100644 --- a/packages/main/cypress/specs/Menu.cy.tsx +++ b/packages/main/cypress/specs/Menu.cy.tsx @@ -1556,8 +1556,7 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); - cy.get("@items").first().should("be.focused"); - cy.focused().realPress("End"); + cy.get("@items").last().then(($item) => { $item[0].focus(); }); cy.get("@items").last().should("be.focused"); cy.focused().realPress("PageUp"); @@ -1575,9 +1574,8 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); - // Go to last item, step back one page, then Page Down must clamp to last item - cy.get("@items").first().should("be.focused"); - cy.focused().realPress("End"); + // Focus last item, step back one page, then Page Down must clamp to last item + cy.get("@items").last().then(($item) => { $item[0].focus(); }); cy.get("@items").last().should("be.focused"); cy.focused().realPress("PageUp"); cy.get("@items").last().should("not.be.focused"); From 4a9274cbfabcb17104728b69a05428fd372d9a80 Mon Sep 17 00:00:00 2001 From: Nikolay Hristov Date: Thu, 30 Jul 2026 14:13:29 +0300 Subject: [PATCH 5/6] feat(ui5-menu): fix tests --- packages/main/cypress/specs/Menu.cy.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/main/cypress/specs/Menu.cy.tsx b/packages/main/cypress/specs/Menu.cy.tsx index 677b7fc3fed18..98237ff42c03a 100644 --- a/packages/main/cypress/specs/Menu.cy.tsx +++ b/packages/main/cypress/specs/Menu.cy.tsx @@ -1556,7 +1556,14 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); - cy.get("@items").last().then(($item) => { $item[0].focus(); }); + cy.get("[ui5-menu]").then(($menu) => { + const menu = $menu[0] as unknown as Menu; + const items = menu._navigatableMenuItems; + const last = items[items.length - 1]; + menu._list!._itemNavigation.setCurrentItem(last); + last.scrollIntoView(); + last.focus(); + }); cy.get("@items").last().should("be.focused"); cy.focused().realPress("PageUp"); @@ -1574,8 +1581,14 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); - // Focus last item, step back one page, then Page Down must clamp to last item - cy.get("@items").last().then(($item) => { $item[0].focus(); }); + cy.get("[ui5-menu]").then(($menu) => { + const menu = $menu[0] as unknown as Menu; + const items = menu._navigatableMenuItems; + const last = items[items.length - 1]; + menu._list!._itemNavigation.setCurrentItem(last); + last.scrollIntoView(); + last.focus(); + }); cy.get("@items").last().should("be.focused"); cy.focused().realPress("PageUp"); cy.get("@items").last().should("not.be.focused"); From 24a4f950594afaca2fdd32b8c6b53ce255f4a1b7 Mon Sep 17 00:00:00 2001 From: Nikolay Hristov Date: Thu, 30 Jul 2026 14:59:05 +0300 Subject: [PATCH 6/6] feat(ui5-menu): fix tests --- packages/main/cypress/specs/Menu.cy.tsx | 52 +++++++++---------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/packages/main/cypress/specs/Menu.cy.tsx b/packages/main/cypress/specs/Menu.cy.tsx index 98237ff42c03a..2b6bba9afe762 100644 --- a/packages/main/cypress/specs/Menu.cy.tsx +++ b/packages/main/cypress/specs/Menu.cy.tsx @@ -1444,7 +1444,7 @@ describe("Menu - Submenu Focus Behavior", () => { .shadow() .find("[ui5-responsive-popover]") .as("submenuPopover"); - + cy.get("@submenuPopover") .should("have.attr", "open"); @@ -1453,7 +1453,7 @@ describe("Menu - Submenu Focus Behavior", () => { .last() .should("be.visible") .as("lastItem"); - + cy.get("@lastItem") .realHover(); @@ -1556,24 +1556,20 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); - cy.get("[ui5-menu]").then(($menu) => { - const menu = $menu[0] as unknown as Menu; - const items = menu._navigatableMenuItems; - const last = items[items.length - 1]; - menu._list!._itemNavigation.setCurrentItem(last); - last.scrollIntoView(); - last.focus(); - }); - cy.get("@items").last().should("be.focused"); - - cy.focused().realPress("PageUp"); + cy.get("@items").first().should("be.focused"); - // Verify moved back, then PageDown must return exactly to last item (round-trip) - cy.get("@items").last().should("not.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"); - cy.get("@items").last().should("be.focused"); + // 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", () => { @@ -1581,19 +1577,8 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); - cy.get("[ui5-menu]").then(($menu) => { - const menu = $menu[0] as unknown as Menu; - const items = menu._navigatableMenuItems; - const last = items[items.length - 1]; - menu._list!._itemNavigation.setCurrentItem(last); - last.scrollIntoView(); - last.focus(); - }); - cy.get("@items").last().should("be.focused"); - cy.focused().realPress("PageUp"); - cy.get("@items").last().should("not.be.focused"); - - cy.focused().realPress("PageDown"); + // 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"); }); @@ -1603,12 +1588,11 @@ describe("Menu - Page Up/Down navigation", () => { cy.get("[ui5-menu] > [ui5-menu-item]").as("items"); - // Step forward one page from start, then Page Up must clamp to first item - cy.get("@items").first().should("be.focused"); - cy.focused().realPress("PageDown"); - cy.get("@items").first().should("not.be.focused"); + // 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"); - cy.focused().realPress("PageUp"); + Cypress._.times(15, () => cy.focused().realPress("PageUp")); cy.get("@items").first().should("be.focused"); });