From 407181cc6c34874dd128512b02b7acd1d6f42526 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Sun, 25 Feb 2024 21:57:50 +0200 Subject: [PATCH 01/12] fix(ui5-menu): dispatch internal focus in event --- packages/main/src/Menu.hbs | 2 + packages/main/src/Menu.ts | 71 +++++++++++++++------------ packages/main/src/MenuItem.ts | 2 +- packages/main/test/pages/Menu.html | 20 +++++++- packages/main/test/specs/Menu.spec.js | 16 +++--- 5 files changed, 69 insertions(+), 42 deletions(-) diff --git a/packages/main/src/Menu.hbs b/packages/main/src/Menu.hbs index 168ba8b97619..63b32e5cb19f 100644 --- a/packages/main/src/Menu.hbs +++ b/packages/main/src/Menu.hbs @@ -67,6 +67,7 @@ accessible-role="menuitem" .additionalText="{{this.item._additionalText}}" tooltip="{{this.item.tooltip}}" + selected="{{this.item.subMenuOpened}}" ?disabled={{this.item.disabled}} ?starts-section={{this.item.startsSection}} ?selected={{this.item.subMenuOpened}} @@ -74,6 +75,7 @@ @mouseover={{../_itemMouseOver}} @mouseout={{../_itemMouseOut}} @keydown={{../_itemKeyDown}} + @_focused={{../_onfocusin}} >
{{#if this.item.hasDummyIcon}} diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 3f1382cb1768..eca1ec86904a 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -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. */ @@ -341,7 +347,7 @@ class Menu extends UI5Element { } get isSubMenuOpened() { - return !!this._parentMenuItem; + return !!this._popover?.isOpen(); } get menuHeaderTextPhone() { @@ -400,15 +406,10 @@ class Menu extends UI5Element { } if (!this._isSubMenu) { this._parentMenuItem = undefined; + this._opener = undefined; } 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`; popover.showAt(opener); } @@ -422,14 +423,15 @@ class Menu extends UI5Element { if (isPhone()) { this._parentItemsStack = []; } - this._popover.close(); - this._popover.resetFocus(); + this._popover.close(false, false, true); } } async _createPopover() { - const staticAreaItemDomRef = await this.getStaticAreaItemDomRef(); - this._popover = staticAreaItemDomRef!.querySelector("[ui5-responsive-popover]")!; + if (!this._popover) { + const staticAreaItemDomRef = await this.getStaticAreaItemDomRef(); + this._popover = staticAreaItemDomRef!.querySelector("[ui5-responsive-popover]")!; + } return this._popover; } @@ -459,13 +461,17 @@ class Menu extends UI5Element { }); } - _createSubMenu(item: MenuItem, openerId: string) { + _createSubMenu(item: MenuItem, opener: HTMLElement, openerId: string) { + if (item._subMenu) { + return; + } 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); @@ -487,7 +493,7 @@ class Menu extends UI5Element { _openItemSubMenu(item: MenuItem, opener: HTMLElement, actionId: string) { const mainMenu = this._findMainMenu(item); - mainMenu.fireEvent("before-open", { + mainMenu?.fireEvent("before-open", { item, }, false, false); item._subMenu!.showAt(opener); @@ -496,25 +502,26 @@ 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 = ""; } @@ -528,7 +535,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) { @@ -536,6 +543,14 @@ class Menu extends UI5Element { } } + _onfocusin(e: CustomEvent): void { + const target = e.target as HTMLElement; + const opener = target instanceof MenuListItem ? target : (target.getRootNode() as any).host; + const menuItem = opener.associatedItem as MenuItem; + const mainMenu = this._findMainMenu(menuItem); + mainMenu?.fireEvent("ui5-item-focusin", { opener, menuItem }); + } + _prepareSubMenuPhone(item: MenuItem) { this._prepareCurrentItems(item.items); this._parentMenuItem = item; @@ -543,8 +558,7 @@ class Menu extends UI5Element { } _startOpenTimeout(item: MenuItem, opener: OpenerStandardListItem, hoverId: string) { - // If theres already a timeout, clears it - this._clearTimeout(); + clearTimeout(this._timeout); // Sets the new timeout this._timeout = setTimeout(() => { @@ -553,8 +567,7 @@ class Menu extends UI5Element { } _startCloseTimeout(item: MenuItem) { - // If theres already a timeout, clears it - this._clearTimeout(); + clearTimeout(this._timeout); // Sets the new timeout this._timeout = setTimeout(() => { @@ -577,9 +590,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); } @@ -596,8 +606,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); // Close submenu with 400ms delay if (item && item.hasSubmenu && item._subMenu) { @@ -623,7 +632,7 @@ class Menu extends UI5Element { item.hasSubmenu && this._prepareSubMenuDesktopTablet(item, opener, hoverId); } else if (isMenuClose && this._isSubMenu && this._parentMenuItem) { const parentMenuItemParent = this._parentMenuItem.parentElement as Menu; - parentMenuItemParent._closeItemSubMenu(this._parentMenuItem, true); + parentMenuItemParent._closeItemSubMenu(this._parentMenuItem, true, true); } } @@ -678,7 +687,7 @@ class Menu extends UI5Element { _findMainMenu(item: MenuItem) { let parentMenu = item.parentElement as Menu; - while (parentMenu._parentMenuItem) { + while (parentMenu && parentMenu._parentMenuItem) { parentMenu = parentMenu._parentMenuItem.parentElement as Menu; } diff --git a/packages/main/src/MenuItem.ts b/packages/main/src/MenuItem.ts index 5a34b2f96397..585860e0dd99 100644 --- a/packages/main/src/MenuItem.ts +++ b/packages/main/src/MenuItem.ts @@ -168,7 +168,7 @@ class MenuItem extends UI5Element { } get subMenuOpened() { - return !!this._subMenu; + return !!this._subMenu?._popover?.isOpen(); } get _additionalText() { diff --git a/packages/main/test/pages/Menu.html b/packages/main/test/pages/Menu.html index b7ad6f69e39a..8bb52d595d99 100644 --- a/packages/main/test/pages/Menu.html +++ b/packages/main/test/pages/Menu.html @@ -22,7 +22,7 @@ - + @@ -60,6 +60,10 @@ Event logger + + + + \ No newline at end of file diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 397770824fb1..b9d6eebacdd6 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -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(); From 51949232ae42514828350e16719bd8e1eeec1e3d Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Wed, 28 Feb 2024 15:54:19 +0200 Subject: [PATCH 12/12] fix(ui5-menu): improve focus handling --- packages/main/src/Menu.ts | 43 ++++++++++++----------------- packages/main/src/NavigationMenu.ts | 6 ++-- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index c29a75f5fc48..da010d3ec7eb 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -459,12 +459,12 @@ class Menu extends UI5Element { }); } - _createSubMenu(item: MenuItem, opener: HTMLElement, openerId: string) { + _createSubMenu(item: MenuItem, opener: HTMLElement) { 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.setAttribute("id", `submenu-${opener.id}`); subMenu._parentMenuItem = item; subMenu._opener = opener; subMenu.busy = item.busy; @@ -486,7 +486,7 @@ class Menu extends UI5Element { return fragment; } - _openItemSubMenu(item: MenuItem, opener: HTMLElement, actionId: string) { + _openItemSubMenu(item: MenuItem, opener: HTMLElement) { const mainMenu = this._findMainMenu(item); mainMenu.fireEvent("before-open", { item, @@ -494,7 +494,7 @@ class Menu extends UI5Element { item._subMenu!.showAt(opener); item._preventSubMenuClose = true; this._openedSubMenuItem = item; - this._subMenuOpenerId = actionId; + this._subMenuOpenerId = opener.id; } _closeItemSubMenu(item: MenuItem, forceClose = false, keyboard = false) { @@ -525,15 +525,15 @@ class Menu extends UI5Element { } } - _prepareSubMenuDesktopTablet(item: MenuItem, opener: HTMLElement, actionId: string) { - if (actionId !== this._subMenuOpenerId || (item && item.hasSubmenu)) { + _prepareSubMenuDesktopTablet(item: MenuItem, opener: HTMLElement) { + if (opener.id !== this._subMenuOpenerId || (item && item.hasSubmenu)) { // close opened sub-menu if there is any opened this._closeItemSubMenu(this._openedSubMenuItem!, true); } if (item && item.hasSubmenu) { // create new sub-menu - this._createSubMenu(item, opener, actionId); - this._openItemSubMenu(item, opener, actionId); + this._createSubMenu(item, opener); + this._openItemSubMenu(item, opener); } if (this._parentMenuItem) { this._parentMenuItem._preventSubMenuClose = true; @@ -553,15 +553,15 @@ class Menu extends UI5Element { : (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 }); + mainMenu?.fireEvent("item-focus", { ref: menuListItem, item }); } - _startOpenTimeout(item: MenuItem, opener: OpenerStandardListItem, hoverId: string) { + _startOpenTimeout(item: MenuItem, opener: OpenerStandardListItem) { clearTimeout(this._timeout); // Sets the new timeout this._timeout = setTimeout(() => { - this._prepareSubMenuDesktopTablet(item, opener, hoverId); + this._prepareSubMenuDesktopTablet(item, opener); }, MENU_OPEN_DELAY); } @@ -574,23 +574,16 @@ class Menu extends UI5Element { }, MENU_CLOSE_DELAY); } - _clearTimeout() { - if (this._timeout) { - clearTimeout(this._timeout); - } - } - _itemMouseOver(e: MouseEvent) { if (isDesktop()) { // respect mouseover only on desktop const opener = e.target as OpenerStandardListItem; const item = opener.associatedItem; - const hoverId = opener.getAttribute("id")!; opener.focus(); // Opens submenu with 300ms delay - this._startOpenTimeout(item, opener, hoverId); + this._startOpenTimeout(item, opener); } } @@ -626,9 +619,8 @@ class Menu extends UI5Element { if (shouldOpenMenu) { const opener = e.target as OpenerStandardListItem; const item = opener.associatedItem; - const hoverId = opener.getAttribute("id")!; - item.hasSubmenu && this._prepareSubMenuDesktopTablet(item, opener, hoverId); + item.hasSubmenu && this._prepareSubMenuDesktopTablet(item, opener); } else if (shouldCloseMenu && this._isSubMenu && this._parentMenuItem) { const parentMenuItemParent = this._parentMenuItem.parentElement as Menu; parentMenuItemParent._closeItemSubMenu(this._parentMenuItem, true, true); @@ -638,7 +630,6 @@ class Menu extends UI5Element { _itemClick(e: CustomEvent) { const opener = e.detail.item as OpenerStandardListItem; const item = opener.associatedItem; - const actionId = opener.getAttribute("id")!; if (!item.hasSubmenu) { // click on an item that doesn't have sub-items fires an "item-click" event @@ -680,13 +671,13 @@ class Menu extends UI5Element { this._prepareSubMenuPhone(item); } else if (isTablet()) { // prepares and opens sub-menu on tablet - this._prepareSubMenuDesktopTablet(item, opener, actionId); + this._prepareSubMenuDesktopTablet(item, opener); } } - _findMainMenu(element: MenuItem | Menu) { - let parentMenu = element.hasAttribute("ui5-menu") ? element as Menu : element.parentElement as Menu; - while (parentMenu && parentMenu._parentMenuItem) { + _findMainMenu(item: MenuItem) { + let parentMenu = item.parentElement as Menu; + while (parentMenu._parentMenuItem) { parentMenu = parentMenu._parentMenuItem.parentElement as Menu; } diff --git a/packages/main/src/NavigationMenu.ts b/packages/main/src/NavigationMenu.ts index 0151d3042f79..704810a689a2 100644 --- a/packages/main/src/NavigationMenu.ts +++ b/packages/main/src/NavigationMenu.ts @@ -72,7 +72,6 @@ class NavigationMenu extends Menu { // respect mouseover only on desktop const opener = e.target as OpenerStandardListItem; let item = opener.associatedItem; - const hoverId = opener.getAttribute("id")!; if (!item) { // for nested @@ -83,7 +82,7 @@ class NavigationMenu extends Menu { } // Opens submenu with 300ms delay - this._startOpenTimeout(item, opener, hoverId); + this._startOpenTimeout(item, opener); } } @@ -106,7 +105,6 @@ class NavigationMenu extends Menu { _itemClick(e: CustomEvent) { const opener = e.detail.item as OpenerStandardListItem; const item = opener.associatedItem; - const actionId = opener.getAttribute("id")!; const mainMenu = this._findMainMenu(item); const prevented = !mainMenu.fireEvent("item-click", { "item": item, @@ -131,7 +129,7 @@ class NavigationMenu extends Menu { this._prepareSubMenuPhone(item); } else if (isTablet()) { // prepares and opens sub-menu on tablet - this._prepareSubMenuDesktopTablet(item, opener, actionId); + this._prepareSubMenuDesktopTablet(item, opener); } } get accSideNavigationPopoverHiddenText() {