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

fix(ui5-menu): improve focus handling #8348

Merged
merged 12 commits into from
Feb 28, 2024
1 change: 1 addition & 0 deletions packages/main/src/Menu.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
@mouseover={{../_itemMouseOver}}
@mouseout={{../_itemMouseOut}}
@keydown={{../_itemKeyDown}}
@_focused={{../_onfocusin}}
>
<div class="ui5-menu-item-text">
{{#if this.item.hasDummyIcon}}
Expand Down
70 changes: 39 additions & 31 deletions packages/main/src/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ class Menu extends UI5Element {
@property({ type: Object, defaultValue: undefined })
_parentMenuItem?: MenuItem;

/**
* Stores parent menu item DOM representation (if there is such).
*/
@property({ type: Object, defaultValue: undefined })
_opener?: HTMLElement;

/**
* Stores menu item that have sub-menu opened.
*/
Expand Down Expand Up @@ -400,15 +406,10 @@ class Menu extends UI5Element {
}
if (!this._isSubMenu) {
this._parentMenuItem = undefined;
this._opener = undefined;
unazko marked this conversation as resolved.
Show resolved Hide resolved
}
const popover = await this._createPopover();
popover.initialFocus = "";
for (let index = 0; index < this._currentItems.length; index++) {
if (!this._currentItems[index].item.disabled) {
popover.initialFocus = `${this._id}-menu-item-${index}`;
break;
}
}
popover.initialFocus = `${this._id}-menu-item-0`;
unazko marked this conversation as resolved.
Show resolved Hide resolved
popover.showAt(opener);
}

Expand All @@ -422,8 +423,7 @@ class Menu extends UI5Element {
if (isPhone()) {
this._parentItemsStack = [];
}
this._popover.close();
this._popover.resetFocus();
this._popover.close(false, false, true);
}
}

Expand Down Expand Up @@ -459,13 +459,14 @@ class Menu extends UI5Element {
});
}

_createSubMenu(item: MenuItem, openerId: string) {
_createSubMenu(item: MenuItem, opener: HTMLElement, openerId: string) {
unazko marked this conversation as resolved.
Show resolved Hide resolved
const ctor = this.constructor as typeof Menu;
const subMenu = document.createElement(ctor.getMetadata().getTag()) as Menu;

subMenu._isSubMenu = true;
subMenu.setAttribute("id", `submenu-${openerId}`);
subMenu._parentMenuItem = item;
subMenu._opener = opener;
subMenu.busy = item.busy;
subMenu.busyDelay = item.busyDelay;
const fragment = this._clonedItemsFragment(item);
Expand Down Expand Up @@ -496,25 +497,28 @@ class Menu extends UI5Element {
this._subMenuOpenerId = actionId;
}

_closeItemSubMenu(item: MenuItem, forceClose = false) {
_closeItemSubMenu(item: MenuItem, forceClose = false, keyboard = false) {
if (item) {
if (forceClose) {
item._preventSubMenuClose = false;
this._closeSubMenuPopover(item._subMenu!, true);
this._closeSubMenuPopover(item._subMenu!, forceClose, keyboard);
} else {
setTimeout(() => this._closeSubMenuPopover(item._subMenu!), 0);
}
}
}

_closeSubMenuPopover(subMenu: Menu, forceClose = false) {
_closeSubMenuPopover(subMenu: Menu, forceClose = false, keyboard = false) {
if (subMenu) {
const parentItem = subMenu._parentMenuItem!;

if (forceClose || !parentItem._preventSubMenuClose) {
subMenu.close();
subMenu.remove();
parentItem._subMenu = undefined;
if (keyboard) {
subMenu._opener?.focus();
}
this._openedSubMenuItem = undefined;
this._subMenuOpenerId = "";
}
Expand All @@ -528,7 +532,7 @@ class Menu extends UI5Element {
}
if (item && item.hasSubmenu) {
// create new sub-menu
this._createSubMenu(item, actionId);
this._createSubMenu(item, opener, actionId);
this._openItemSubMenu(item, opener, actionId);
}
if (this._parentMenuItem) {
Expand All @@ -542,9 +546,18 @@ class Menu extends UI5Element {
this._parentItemsStack.push(item);
}

_onfocusin(e: FocusEvent): void {
const target = e.target as HTMLElement;
const menuListItem = target.hasAttribute("ui5-menu-li")
? target as MenuListItem
: (target.getRootNode() as ShadowRoot).host as MenuListItem;
const item = menuListItem.associatedItem as MenuItem;
const mainMenu = this._findMainMenu(item);
mainMenu?.fireEvent("ui5-item-focus", { ref: menuListItem, item });
unazko marked this conversation as resolved.
Show resolved Hide resolved
}

_startOpenTimeout(item: MenuItem, opener: OpenerStandardListItem, hoverId: string) {
// If theres already a timeout, clears it
this._clearTimeout();
clearTimeout(this._timeout);
unazko marked this conversation as resolved.
Show resolved Hide resolved

// Sets the new timeout
this._timeout = setTimeout(() => {
Expand All @@ -553,8 +566,7 @@ class Menu extends UI5Element {
}

_startCloseTimeout(item: MenuItem) {
// If theres already a timeout, clears it
this._clearTimeout();
clearTimeout(this._timeout);
unazko marked this conversation as resolved.
Show resolved Hide resolved

// Sets the new timeout
this._timeout = setTimeout(() => {
Expand All @@ -577,9 +589,6 @@ class Menu extends UI5Element {

opener.focus();

// If there is a pending close operation, cancel it
this._clearTimeout();

// Opens submenu with 300ms delay
this._startOpenTimeout(item, opener, hoverId);
}
Expand All @@ -596,8 +605,7 @@ class Menu extends UI5Element {
const opener = e.target as OpenerStandardListItem;
const item = opener.associatedItem;

// If there is a pending open operation, cancel it
this._clearTimeout();
clearTimeout(this._timeout);
unazko marked this conversation as resolved.
Show resolved Hide resolved

// Close submenu with 400ms delay
if (item && item.hasSubmenu && item._subMenu) {
Expand All @@ -609,21 +617,21 @@ class Menu extends UI5Element {
}

_itemKeyDown(e: KeyboardEvent) {
const isMenuClose = this.isRtl ? isRight(e) : isLeft(e);
const isMenuOpen = this.isRtl ? isLeft(e) : isRight(e);
const shouldCloseMenu = this.isRtl ? isRight(e) : isLeft(e);
const shouldOpenMenu = this.isRtl ? isLeft(e) : isRight(e);

if (isEnter(e)) {
e.preventDefault();
}
if (isMenuOpen) {
if (shouldOpenMenu) {
const opener = e.target as OpenerStandardListItem;
const item = opener.associatedItem;
const hoverId = opener.getAttribute("id")!;

item.hasSubmenu && this._prepareSubMenuDesktopTablet(item, opener, hoverId);
} else if (isMenuClose && this._isSubMenu && this._parentMenuItem) {
} else if (shouldCloseMenu && this._isSubMenu && this._parentMenuItem) {
const parentMenuItemParent = this._parentMenuItem.parentElement as Menu;
parentMenuItemParent._closeItemSubMenu(this._parentMenuItem, true);
parentMenuItemParent._closeItemSubMenu(this._parentMenuItem, true, true);
}
}

Expand Down Expand Up @@ -676,9 +684,9 @@ class Menu extends UI5Element {
}
}

_findMainMenu(item: MenuItem) {
let parentMenu = item.parentElement as Menu;
while (parentMenu._parentMenuItem) {
_findMainMenu(element: MenuItem | Menu) {
let parentMenu = element.hasAttribute("ui5-menu") ? element as Menu : element.parentElement as Menu;
while (parentMenu && parentMenu._parentMenuItem) {
parentMenu = parentMenu._parentMenuItem.parentElement as Menu;
}

Expand Down
3 changes: 0 additions & 3 deletions packages/main/src/NavigationMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,6 @@ class NavigationMenu extends Menu {
}
}

// If there is a pending close operation, cancel it
this._clearTimeout();

// Opens submenu with 300ms delay
this._startOpenTimeout(item, opener, hoverId);
}
Expand Down
65 changes: 63 additions & 2 deletions packages/main/test/pages/Menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
<body class="bg">
<ui5-button id="btnOpen">Open Menu</ui5-button> <br/>
<ui5-menu id="menu" header-text="My ui5-menu">

<ui5-menu-item text="New File" accessible-name="Opens a file explorer" additional-text="Ctrl+Alt+Shift+N" tooltip="Select a file" icon="add-document"></ui5-menu-item>
<ui5-menu-item text="New Folder with very long title for a menu item" additional-text="Ctrl+F" icon="add-folder" disabled></ui5-menu-item>
<ui5-menu-item text="Open" icon="open-folder" starts-section busy-delay="100" busy>
<ui5-menu-item text="Open Locally" icon="open-folder" additional-text="Ctrl+K">
<ui5-menu-item text="Open from C"></ui5-menu-item>
<ui5-menu-item text="Open from D"></ui5-menu-item>
<ui5-menu-item text="Open from E"></ui5-menu-item>
<ui5-menu-item text="Open from E" disabled></ui5-menu-item>
</ui5-menu-item>
<ui5-menu-item text="Open from SAP Cloud" additional-text="Ctrl+L"></ui5-menu-item>
</ui5-menu-item>
Expand Down Expand Up @@ -60,6 +59,24 @@
<ui5-title level="H5" class="header-title">Event logger</ui5-title>
<ui5-input id="eventLogger" style="width: 100%;"></ui5-input>

<ui5-button id="btnOpen2">Open Menu</ui5-button> <br/>
<ui5-menu id="menu2" header-text="My ui5-menu">
<ui5-menu-item text="New Folder" additional-text="Ctrl+F" icon="add-folder" disabled></ui5-menu-item>
<ui5-menu-item text="Open" icon="open-folder" starts-section>
<ui5-menu-item text="Open Locally" icon="open-folder" additional-text="Ctrl+K"></ui5-menu-item>
<ui5-menu-item text="Open from SAP Cloud" additional-text="Ctrl+L" disabled></ui5-menu-item>
</ui5-menu-item>
<ui5-menu-item text="Preferences" icon="action-settings" starts-section disabled></ui5-menu-item>
<ui5-menu-item text="Exit" icon="journey-arrive"></ui5-menu-item>
</ui5-menu>

<ui5-popover id="detailsPopover">
<div>
<ui5-label for="detailsLink">For more information:</ui5-label>
</div>
<ui5-link id="detailsLink" href="https://en.wikipedia.org/wiki/Google_logo" target="_blank"></ui5-link>
</ui5-popover>

<script>
btnOpen.accessibilityAttributes = {
hasPopup: "Menu",
Expand Down Expand Up @@ -114,6 +131,7 @@
});

menu.addEventListener("ui5-after-open", function() {
shouldOpenDetailsPopover = true;
var opener = menu.opener.id ? menu.opener.id : menu.opener;
openerInput.value = opener;
eventLogger.value += "* [after-open] ";
Expand All @@ -125,6 +143,7 @@
});

menu.addEventListener("ui5-after-close", function() {
shouldOpenDetailsPopover = false;
openerInput.value = "";
btnToggleOpen.pressed = false;
eventLogger.value += "* [after-close]";
Expand All @@ -135,6 +154,48 @@
window["sap-ui-webcomponents-bundle"].applyDirection();
});

btnOpen2.accessibilityAttributes = {
hasPopup: "Menu",
};

btnOpen2.addEventListener("click", function() {
menu2.showAt(btnOpen2);
});

let shouldOpenDetailsPopover = false;

detailsPopover.addEventListener("ui5-before-close", function() {
shouldOpenDetailsPopover = false;
});

detailsPopover.addEventListener("ui5-after-close", function() {
shouldOpenDetailsPopover = true;
});

menu2.addEventListener("ui5-after-open", function() {
shouldOpenDetailsPopover = true;
});

menu2.addEventListener("ui5-after-close", function() {
shouldOpenDetailsPopover = false;
});

menu2.addEventListener("ui5-item-focus", function(event) {
const item = event.detail.item;
const opener = event.detail.ref;

if (detailsPopover.isOpen()) {
detailsPopover.close(false, false, true);
}

if (item.disabled && shouldOpenDetailsPopover) {
const link = detailsPopover.querySelector("#detailsLink");
link.textContent = item.text;
detailsPopover.showAt(opener, true)
link.focus();
}
});

</script>
</body>
</html>
2 changes: 1 addition & 1 deletion packages/main/test/specs/Menu.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("Menu interaction", () => {
it("Top level menu items appearance", async () => {
await browser.url(`test/pages/Menu.html`);
const openButton = await browser.$("#btnOpen");
const menuItems = await browser.$$("ui5-menu>ui5-menu-item");
const menuItems = await browser.$$("ui5-menu[id='menu']>ui5-menu-item");

openButton.click();

Expand Down