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 6 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
78 changes: 78 additions & 0 deletions packages/main/cypress/specs/Menu.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,5 +456,83 @@ 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="item1" text="Item 1" loading-delay="500" loading></ui5-menu-item>
<ui5-menu-item id="item2" text="Item 2">
<ui5-button id="newLock" slot="endContent" icon="locked" design="Transparent"></ui5-button>
<ui5-button id="newUnlock" slot="endContent" icon="unlocked" 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.realPress("{downarrow}");

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

cy.realPress("{rightarrow}");

cy.get("@items")
.eq(1)
.get("[ui5-button]")
.as("endContent");

cy.get("@endContent")
.eq(1)
.should("be.focused");

cy.realPress("{rightarrow}");

cy.get("@endContent")
.eq(2)
.should("be.focused");

cy.realPress("{rightarrow}");

cy.get("@endContent")
.eq(3)
.should("be.focused");

cy.realPress("{rightarrow}");

cy.get("@endContent")
.eq(3)
.should("be.focused");

cy.realPress("{leftarrow}");

cy.get("@endContent")
.eq(2)
.should("be.focused");

cy.realPress("{leftarrow}");

cy.get("@endContent")
.eq(1)
.should("be.focused");

cy.realPress("{leftarrow}");

cy.get("@endContent")
.eq(1)
.should("be.focused");

cy.realPress("{downarrow}");

cy.get("@items")
.eq(2)
.should("be.focused");
});
});
});
28 changes: 26 additions & 2 deletions packages/main/src/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
isLeft,
isRight,
isEnter,
isTabNext,
isDown,
isUp,
} from "@ui5/webcomponents-base/dist/Keys.js";
import {
isPhone,
Expand Down Expand Up @@ -356,15 +359,18 @@ class Menu extends UI5Element {
}

_itemKeyDown(e: KeyboardEvent) {
if (!isLeft(e) && !isRight(e)) {
return;
if (isTabNext(e)) {
e.preventDefault();
e.stopImmediatePropagation();
}

const shouldCloseMenu = this.isRtl ? isRight(e) : isLeft(e);
const shouldOpenMenu = this.isRtl ? isLeft(e) : isRight(e);
const item = e.target as MenuItem;
const parentElement = item.parentElement as MenuItem;

const menuItem = parentElement.hasAttribute("ui5-menu-item") ? parentElement : item;

if (isEnter(e)) {
e.preventDefault();
}
Expand All @@ -375,6 +381,24 @@ class Menu extends UI5Element {
parentElement.selected = false;
(parentElement._popover.opener as HTMLElement)?.focus();
}
if (!menuItem.hasAttribute("ui5-menu-item")) {
return;
}
if (isUp(e)) {
this._handleNextOrPreviousItem(menuItem);
unazko marked this conversation as resolved.
Show resolved Hide resolved
} else if (isDown(e)) {
this._handleNextOrPreviousItem(menuItem, true);
} else if (isLeft(e) || isRight(e)) {
menuItem._itemKeyDown(e);
}
}

_handleNextOrPreviousItem(menuItem: MenuItem, isNext?: boolean) {
const currentIndex = this._menuItems.indexOf(menuItem);
const nextItem = isNext ? this._menuItems[currentIndex + 1] : this._menuItems[currentIndex - 1];
const focusItem = nextItem || this._menuItems[currentIndex];

focusItem.focus();
}

_beforePopoverOpen(e: CustomEvent) {
Expand Down
46 changes: 46 additions & 0 deletions packages/main/src/MenuItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import AriaHasPopup from "@ui5/webcomponents-base/dist/types/AriaHasPopup.js";
import type { AccessibilityAttributes } from "@ui5/webcomponents-base/dist/types.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 {
isLeft,
isRight,
} from "@ui5/webcomponents-base/dist/Keys.js";
import type { ListItemAccessibilityAttributes } from "./ListItem.js";
import ListItem from "./ListItem.js";
import ResponsivePopover from "./ResponsivePopover.js";
Expand Down Expand Up @@ -201,6 +207,44 @@ class MenuItem extends ListItem implements IMenuItem {
@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;

_itemNavigation: ItemNavigation;
_lastFocusedItemIndex: number | null;

constructor() {
super();

this._itemNavigation = new ItemNavigation(this, {
getItemsCallback: () => this._navigableItems,
});
this._lastFocusedItemIndex = null;
}

get _navigableItems() {
return [...this.endContent] as Array<HTMLElement>;
}
Todor-ads marked this conversation as resolved.
Show resolved Hide resolved

_itemKeyDown(e: KeyboardEvent) {
if (isLeft(e)) {
this._handleNextOrPreviousItem(e);
} else if (isRight(e)) {
this._handleNextOrPreviousItem(e, true);
}
}

_handleNextOrPreviousItem(e: KeyboardEvent, isNext?: boolean) {
const target = e.target as MenuItem | HTMLElement;

const nextTargetIndex = isNext ? this._navigableItems.indexOf(target) + 1 : this._navigableItems.indexOf(target) - 1;
const nextTarget = this._navigableItems[nextTargetIndex];

if (nextTarget) {
e.preventDefault();

this._itemNavigation.setCurrentItem(nextTarget);
this._itemNavigation._focusCurrentItem();
}
}

get placement(): `${PopoverPlacement}` {
return this.isRtl ? "Start" : "End";
}
Expand Down Expand Up @@ -259,6 +303,8 @@ class MenuItem extends ListItem implements IMenuItem {
this._menuItems.forEach(item => {
item._siblingsWithIcon = siblingsWithIcon;
});

this._itemNavigation._navigationMode = NavigationMode.Horizontal;
}

get _focusable() {
Expand Down