Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui5-menu): Enhance keyboard navigation #10243

Merged
merged 23 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
67fc519
feat(ui5-menu): endContent is now accessible via keyboard
Todor-ads Nov 25, 2024
20be657
feat(ui5-menu-item): fix code indentation
Todor-ads Nov 25, 2024
b41c57f
feat(ui5-menu-item): fix lint error
Todor-ads Nov 25, 2024
0bf70af
feat(ui5-menu): fix comment
Todor-ads Nov 25, 2024
fba0794
Delete package-lock.json
unazko Nov 25, 2024
b92cd15
feat(ui5-menu): fix lint error
Todor-ads Nov 25, 2024
b21c4ba
feat(ui5-menu): fix menu item navigation for sub-menu items
Todor-ads Nov 25, 2024
0a542e5
feat(ui5-menu): fix forgotten debugger
Todor-ads Nov 25, 2024
8289f51
fix: adjust sub-menu keyboard interactions
unazko Nov 26, 2024
541439b
fix: clear lint errors
unazko Nov 26, 2024
847767e
fix: adjust endContent navigation
unazko Nov 26, 2024
ab99e11
fix: adjust internal method names
unazko Nov 26, 2024
75bd6c6
fix: provide early return on menu close
unazko Dec 10, 2024
ca25fb1
fix: remove console log
unazko Dec 10, 2024
129b2b4
Merge branch 'main' into menu-content-navigation
unazko Dec 10, 2024
fb294fe
fix: adjust on tab behavior
unazko Dec 17, 2024
568a04e
fix: remove redundant variable
unazko Dec 17, 2024
fc3a83a
fix: correction based on code review suggestions
Todor-ads Jan 21, 2025
a45057e
fix: correction based on code review suggestions
Todor-ads Jan 22, 2025
53209b4
fix: correction based on code review suggestions
Todor-ads Jan 24, 2025
31495d9
fix: correction based on code review suggestions
Todor-ads Jan 24, 2025
27161f7
fix: removal of unnecessarily created files
Todor-ads Jan 24, 2025
cee2991
Merge remote-tracking branch 'origin/main' into menu-content-navigation
vladitasev Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/main/cypress/specs/Menu.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`<ui5-button id="btnOpen">Open Menu</ui5-button>
<ui5-menu id="menu" opener="btnOpen">
<ui5-menu-item id="item2" text="Item 2">
<ui5-button id="newLock" slot="endContent" icon="locked" design="Transparent"></ui5-button>
<ui5-button id="newFavorite" slot="endContent" icon="favorite" design="Transparent"></ui5-button>
</ui5-menu-item>
<ui5-menu-item text="Item3" additional-text="Ctrl+F" icon="add-folder" ></ui5-menu-item>
</ui5-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");
});
});
});
56 changes: 41 additions & 15 deletions packages/main/src/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
isLeft,
isRight,
isEnter,
isTabNext,
isTabPrevious,
isDown,
isUp,
} from "@ui5/webcomponents-base/dist/Keys.js";
import {
isPhone,
Expand Down Expand Up @@ -81,6 +85,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
Expand Down Expand Up @@ -346,27 +354,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);
Todor-ads marked this conversation as resolved.
Show resolved Hide resolved
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;
Todor-ads marked this conversation as resolved.
Show resolved Hide resolved
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;
unazko marked this conversation as resolved.
Show resolved Hide resolved
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.fireDecoratorEvent("before-open", {});

Expand Down
34 changes: 34 additions & 0 deletions packages/main/src/MenuItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
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 type ResponsivePopover from "./ResponsivePopover.js";
Expand Down Expand Up @@ -249,6 +252,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<HTMLElement> {
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";
}
Expand Down
7 changes: 7 additions & 0 deletions packages/main/src/Popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,13 @@ class Popover extends Popup {
return this.shadowRoot!.querySelector(".ui5-popover-arrow")!;
}

/**
* @protected
*/
focusOpener() {
this.getOpenerHTMLElement(this.opener)?.focus();
}

/**
* @private
*/
Expand Down
Loading