diff --git a/packages/main/cypress/specs/Menu.cy.ts b/packages/main/cypress/specs/Menu.cy.ts
index 8694d8e1dace..4f5d04922490 100644
--- a/packages/main/cypress/specs/Menu.cy.ts
+++ b/packages/main/cypress/specs/Menu.cy.ts
@@ -495,5 +495,41 @@ describe("Menu interaction", () => {
.find("[ui5-responsive-popover]")
.should("have.attr", "accessible-name", "Select an option from the menu");
});
+
+ it("Menu items - navigation in endContent", () => {
+ cy.mount(html`Open Menu
+ `);
+
+ cy.get("[ui5-menu]")
+ .ui5MenuOpen();
+
+ cy.get("[ui5-menu] > [ui5-menu-item]").as("items");
+ cy.get("[ui5-menu] [ui5-button]").as("buttons");
+ cy.get("@items").first().should("be.focused");
+
+ cy.realPress("ArrowRight");
+ cy.get("@buttons").first().should("be.focused");
+
+ cy.realPress("ArrowRight");
+ cy.get("@buttons").last().should("be.focused");
+
+ cy.realPress("ArrowRight");
+ cy.get("@buttons").last().should("be.focused");
+
+ cy.realPress("ArrowLeft");
+ cy.get("@buttons").first().should("be.focused");
+
+ cy.realPress("ArrowLeft");
+ cy.get("@buttons").first().should("be.focused");
+
+ cy.realPress("ArrowDown");
+ cy.get("@items").last().should("be.focused");
+ });
});
});
diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts
index af035d4281f9..f812fb8be3e9 100644
--- a/packages/main/src/Menu.ts
+++ b/packages/main/src/Menu.ts
@@ -7,6 +7,10 @@ import {
isLeft,
isRight,
isEnter,
+ isTabNext,
+ isTabPrevious,
+ isDown,
+ isUp,
} from "@ui5/webcomponents-base/dist/Keys.js";
import {
isPhone,
@@ -84,6 +88,10 @@ type MenuBeforeCloseEventDetail = { escPressed: boolean };
* in the currently clicked menu item.
* - `Arrow Left` or `Escape` - Closes the currently opened sub-menu.
*
+ * when there is `endContent` :
+ * - `Arrow Left` or `ArrowRight` - Navigate between the menu item actions and the menu item itself
+ * - `Arrow Up` / `Arrow Down` - Navigates up and down the currently visible menu items
+ *
* Note: if the text ditrection is set to Right-to-left (RTL), `Arrow Right` and `Arrow Left` functionality is swapped.
*
* ### ES6 Module Import
@@ -341,27 +349,45 @@ class Menu extends UI5Element {
}
_itemKeyDown(e: KeyboardEvent) {
- if (!isLeft(e) && !isRight(e)) {
- return;
- }
-
- const shouldCloseMenu = this.isRtl ? isRight(e) : isLeft(e);
- const shouldOpenMenu = this.isRtl ? isLeft(e) : isRight(e);
+ const isTabNextPrevious = isTabNext(e) || isTabPrevious(e);
const item = e.target as MenuItem;
const parentElement = item.parentElement as MenuItem;
+ const shouldItemNavigation = isUp(e) || isDown(e);
+ const shouldOpenMenu = this.isRtl ? isLeft(e) : isRight(e);
+ const shouldCloseMenu = !shouldItemNavigation && !shouldOpenMenu && parentElement.hasAttribute("ui5-menu-item");
- if (isEnter(e)) {
- e.preventDefault();
- }
- if (shouldOpenMenu) {
- this._openItemSubMenu(item);
- } else if (shouldCloseMenu && parentElement.hasAttribute("ui5-menu-item") && parentElement._popover) {
- parentElement._popover.open = false;
- parentElement.selected = false;
- (parentElement._popover.opener as HTMLElement)?.focus();
+ if (item.hasAttribute("ui5-menu-item")) {
+ if (isEnter(e) || isTabNextPrevious) {
+ e.preventDefault();
+ }
+
+ if (isRight(e) || isLeft(e)) {
+ item._navigateToEndContent(isLeft(e));
+ }
+
+ if (shouldOpenMenu) {
+ this._openItemSubMenu(item);
+ } else if ((shouldCloseMenu || isTabNextPrevious) && parentElement._popover) {
+ parentElement._popover.open = false;
+ parentElement.selected = false;
+ parentElement._popover.focusOpener();
+ }
+ } else if (isUp(e)) {
+ this._navigateOutOfEndContent(parentElement);
+ } else if (isDown(e)) {
+ this._navigateOutOfEndContent(parentElement, true);
}
}
+ _navigateOutOfEndContent(menuItem: MenuItem, isDownwards?: boolean) {
+ const opener = menuItem?.parentElement as MenuItem | Menu;
+ const currentIndex = opener._menuItems.indexOf(menuItem);
+ const nextItem = isDownwards ? opener._menuItems[currentIndex + 1] : opener._menuItems[currentIndex - 1];
+ const itemToFocus = nextItem || opener._menuItems[currentIndex];
+
+ itemToFocus.focus();
+ }
+
_beforePopoverOpen(e: CustomEvent) {
const prevented = !this.fireEvent("before-open", {}, true, true);
diff --git a/packages/main/src/MenuItem.ts b/packages/main/src/MenuItem.ts
index dd2f218bff82..ae8a14529540 100644
--- a/packages/main/src/MenuItem.ts
+++ b/packages/main/src/MenuItem.ts
@@ -8,6 +8,9 @@ import type { AccessibilityAttributes } from "@ui5/webcomponents-base/dist/types
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import "@ui5/webcomponents-icons/dist/nav-back.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
+import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js";
+import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
+import ItemNavigationBehavior from "@ui5/webcomponents-base/dist/types/ItemNavigationBehavior.js";
import type { ListItemAccessibilityAttributes } from "./ListItem.js";
import ListItem from "./ListItem.js";
import ResponsivePopover from "./ResponsivePopover.js";
@@ -202,6 +205,37 @@ class MenuItem extends ListItem implements IMenuItem {
@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;
+ _itemNavigation: ItemNavigation;
+
+ constructor() {
+ super();
+
+ this._itemNavigation = new ItemNavigation(this, {
+ navigationMode: NavigationMode.Horizontal,
+ behavior: ItemNavigationBehavior.Static,
+ getItemsCallback: () => this._navigableItems,
+ });
+ }
+
+ get _navigableItems(): Array {
+ return [...this.endContent].filter(item => {
+ return item.hasAttribute("ui5-button")
+ || item.hasAttribute("ui5-link")
+ || (item.hasAttribute("ui5-icon") && item.getAttribute("mode") === "Interactive");
+ });
+ }
+
+ _navigateToEndContent(isLast?: boolean) {
+ const item = isLast
+ ? this._navigableItems[this._navigableItems.length - 1]
+ : this._navigableItems[0];
+
+ if (item) {
+ this._itemNavigation.setCurrentItem(item);
+ this._itemNavigation._focusCurrentItem();
+ }
+ }
+
get placement(): `${PopoverPlacement}` {
return this.isRtl ? "Start" : "End";
}
diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts
index 39d74d52d9bd..549a4d6e7391 100644
--- a/packages/main/src/Popover.ts
+++ b/packages/main/src/Popover.ts
@@ -507,6 +507,13 @@ class Popover extends Popup {
return this.shadowRoot!.querySelector(".ui5-popover-arrow")!;
}
+ /**
+ * @protected
+ */
+ focusOpener() {
+ this.getOpenerHTMLElement(this.opener)?.focus();
+ }
+
/**
* @private
*/