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 */