From e998cc131c9c0314b20a0b2b21429bf7aa596c50 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Wed, 7 Feb 2024 17:16:29 +0200 Subject: [PATCH 01/29] feat(ui5-menu): use custom list items as base for menu items --- packages/main/src/List.ts | 8 +- packages/main/src/ListItem.ts | 2 +- packages/main/src/ListItemBase.ts | 8 +- packages/main/src/Menu.hbs | 54 +------- packages/main/src/Menu.ts | 139 +++++++------------- packages/main/src/MenuItem.hbs | 39 ++++++ packages/main/src/MenuItem.ts | 42 +++--- packages/main/src/NavigationMenu.hbs | 91 +------------ packages/main/src/NavigationMenu.ts | 49 ++----- packages/main/src/NavigationMenuItem.hbs | 45 +++++++ packages/main/src/NavigationMenuItem.ts | 8 ++ packages/main/src/themes/Menu.css | 84 ------------ packages/main/src/themes/MenuItem.css | 88 +++++++++++++ packages/main/src/themes/NavigationMenu.css | 77 +++++------ 14 files changed, 323 insertions(+), 411 deletions(-) create mode 100644 packages/main/src/MenuItem.hbs create mode 100644 packages/main/src/NavigationMenuItem.hbs create mode 100644 packages/main/src/themes/MenuItem.css diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index e1a67b61f30d..08946f10dcfe 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -752,7 +752,7 @@ class List extends UI5Element { } getEnabledItems(): Array { - return this.getItems().filter(item => !item.disabled); + return this.getItems().filter(item => item._focusable); } getItems(): Array { @@ -848,7 +848,7 @@ class List extends UI5Element { } if (lastTabbableEl === target) { - if (this.getFirstItem(x => x.selected && !x.disabled)) { + if (this.getFirstItem(x => x.selected && x._focusable)) { this.focusFirstSelectedItem(); } else if (this.getPreviouslyFocusedItem()) { this.focusPreviouslyFocusedItem(); @@ -1023,7 +1023,7 @@ class List extends UI5Element { */ focusFirstItem() { // only enabled items are focusable - const firstItem = this.getFirstItem(x => !x.disabled); + const firstItem = this.getFirstItem(x => x._focusable); if (firstItem) { firstItem.focus(); @@ -1040,7 +1040,7 @@ class List extends UI5Element { focusFirstSelectedItem() { // only enabled items are focusable - const firstSelectedItem = this.getFirstItem(x => x.selected && !x.disabled); + const firstSelectedItem = this.getFirstItem(x => x.selected && !x._focusable); if (firstSelectedItem) { firstSelectedItem.focus(); diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts index c1f59f9138c1..92c77cf0e468 100644 --- a/packages/main/src/ListItem.ts +++ b/packages/main/src/ListItem.ts @@ -374,7 +374,7 @@ abstract class ListItem extends ListItemBase { } fireItemPress(e: Event) { - if (this.isInactive) { + if (this.isInactive || this.disabled) { return; } if (isEnter(e as KeyboardEvent)) { diff --git a/packages/main/src/ListItemBase.ts b/packages/main/src/ListItemBase.ts index 21b4695591af..72e8d6c44f7e 100644 --- a/packages/main/src/ListItemBase.ts +++ b/packages/main/src/ListItemBase.ts @@ -130,7 +130,7 @@ class ListItemBase extends UI5Element implements ITabbable { return { main: { "ui5-li-root": true, - "ui5-li--focusable": !this.disabled, + "ui5-li--focusable": this._focusable, }, }; } @@ -139,12 +139,16 @@ class ListItemBase extends UI5Element implements ITabbable { return this.disabled ? true : undefined; } + get _focusable() { + return !this.disabled; + } + get hasConfigurableMode() { return false; } get _effectiveTabIndex() { - if (this.disabled) { + if (!this._focusable) { return -1; } if (this.selected) { diff --git a/packages/main/src/Menu.hbs b/packages/main/src/Menu.hbs index 09401c25385f..55821d29cade 100644 --- a/packages/main/src/Menu.hbs +++ b/packages/main/src/Menu.hbs @@ -1,5 +1,4 @@ - {{#if _currentItems.length}} + {{#if items.length}} - {{#each _currentItems}} - -
- {{#if this.item.hasDummyIcon}} -
-
- {{/if}} - {{this.item.text}} - {{#if this.item.hasSubmenu}} -
- - -
- {{else if this.item._siblingsWithChildren}} -
-
- {{/if}} -
-
- {{/each}} +
{{else if busy}} ; - /** * Stores a list of parent menu items for each sub-menu (on phone). * @private @@ -305,12 +287,8 @@ class Menu extends UI5Element { Menu.i18nBundle = await getI18nBundle("@ui5/webcomponents"); } - get itemsWithChildren() { - return !!this._currentItems.filter(item => item.item.items.length).length; - } - get itemsWithIcon() { - return !!this._currentItems.filter(item => item.item.icon !== "").length; + return !!this.items.filter(item => item.icon !== "").length; } get isRtl() { @@ -347,25 +325,20 @@ class Menu extends UI5Element { } onBeforeRendering() { - !isPhone() && this._prepareCurrentItems(this.items); - - const itemsWithChildren = this.itemsWithChildren; const itemsWithIcon = this.itemsWithIcon; - this._currentItems.forEach(item => { - item.item._siblingsWithChildren = itemsWithChildren; - item.item._siblingsWithIcon = itemsWithIcon; - const subMenu = item.item._subMenu; - const menuItem = item.item; + this.items.forEach(item => { + item._siblingsWithIcon = itemsWithIcon; + const subMenu = item._subMenu; if (subMenu && subMenu.busy) { subMenu.innerHTML = ""; - const fragment = this._clonedItemsFragment(menuItem); + const fragment = this._clonedItemsFragment(item); subMenu.appendChild(fragment); } if (subMenu) { - subMenu.busy = item.item.busy; - subMenu.busyDelay = item.item.busyDelay; + subMenu.busy = item.busy; + subMenu.busyDelay = item.busyDelay; } }); } @@ -391,18 +364,17 @@ class Menu extends UI5Element { * @param opener the element that the popover is shown at * @public */ - async showAt(opener: HTMLElement): Promise { + showAt(opener: HTMLElement): void { if (isPhone()) { - this._prepareCurrentItems(this.items); this._parentItemsStack = []; } if (!this._isSubMenu) { this._parentMenuItem = undefined; } - const popover = await this._createPopover(); + const popover = this._createPopover(); popover.initialFocus = ""; - for (let index = 0; index < this._currentItems.length; index++) { - if (!this._currentItems[index].item.disabled) { + for (let index = 0; index < this.items.length; index++) { + if (!this.items[index].disabled) { popover.initialFocus = `${this._id}-menu-item-${index}`; break; } @@ -425,9 +397,8 @@ class Menu extends UI5Element { } } - async _createPopover() { - const staticAreaItemDomRef = await this.getStaticAreaItemDomRef(); - this._popover = staticAreaItemDomRef!.querySelector("[ui5-responsive-popover]")!; + _createPopover() { + this._popover = this.shadowRoot!.querySelector("[ui5-responsive-popover]")!; return this._popover; } @@ -441,35 +412,23 @@ class Menu extends UI5Element { this.focus(); if (parentMenuItem) { - const parentMenuItemParent = parentMenuItem.parentElement as MenuItem; - this._prepareCurrentItems(parentMenuItemParent.items); this._parentMenuItem = this._parentItemsStack.length ? this._parentItemsStack[this._parentItemsStack.length - 1] : undefined; } } - _prepareCurrentItems(items: Array) { - this._currentItems = items.map((item, index) => { - return { - item, - position: index + 1, - ariaHasPopup: item.hasSubmenu ? "menu" : undefined, - }; - }); - } - - _createSubMenu(item: MenuItem, openerId: string) { + async _createSubMenu(item: MenuItem) { 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.busy = item.busy; subMenu.busyDelay = item.busyDelay; + item._subMenu = subMenu; const fragment = this._clonedItemsFragment(item); subMenu.appendChild(fragment); - this.staticAreaItem!.shadowRoot!.querySelector(".ui5-menu-submenus")!.appendChild(subMenu); - item._subMenu = subMenu; + this.shadowRoot!.querySelector(".ui5-menu-submenus")!.appendChild(subMenu); + await renderFinished(); } _clonedItemsFragment(item: MenuItem) { @@ -483,15 +442,14 @@ class Menu extends UI5Element { return fragment; } - _openItemSubMenu(item: MenuItem, opener: HTMLElement, actionId: string) { + _openItemSubMenu(item: MenuItem) { const mainMenu = this._findMainMenu(item); mainMenu.fireEvent("before-open", { item, }, false, false); - item._subMenu!.showAt(opener); + item._subMenu!.showAt(item); item._preventSubMenuClose = true; this._openedSubMenuItem = item; - this._subMenuOpenerId = actionId; } _closeItemSubMenu(item: MenuItem, forceClose = false) { @@ -519,15 +477,13 @@ class Menu extends UI5Element { } } - _prepareSubMenuDesktopTablet(item: MenuItem, opener: HTMLElement, actionId: string) { - if (actionId !== this._subMenuOpenerId || (item && item.hasSubmenu)) { + async _prepareSubMenuDesktopTablet(item: MenuItem) { + if (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, actionId); - this._openItemSubMenu(item, opener, actionId); + await this._createSubMenu(item); + this._openItemSubMenu(item); } if (this._parentMenuItem) { this._parentMenuItem._preventSubMenuClose = true; @@ -535,18 +491,17 @@ class Menu extends UI5Element { } _prepareSubMenuPhone(item: MenuItem) { - this._prepareCurrentItems(item.items); this._parentMenuItem = item; this._parentItemsStack.push(item); } - _startOpenTimeout(item: MenuItem, opener: OpenerStandardListItem, hoverId: string) { + _startOpenTimeout(item: MenuItem) { // If theres already a timeout, clears it this._clearTimeout(); // Sets the new timeout this._timeout = setTimeout(() => { - this._prepareSubMenuDesktopTablet(item, opener, hoverId); + this._prepareSubMenuDesktopTablet(item); }, MENU_OPEN_DELAY); } @@ -567,19 +522,19 @@ class Menu extends UI5Element { } _itemMouseOver(e: MouseEvent) { + this._busyMouseOver(); + if (isDesktop()) { // respect mouseover only on desktop - const opener = e.target as OpenerStandardListItem; - const item = opener.associatedItem; - const hoverId = opener.getAttribute("id")!; + const item = e.target as MenuItem; - opener.focus(); + item.focus(); // If there is a pending close operation, cancel it this._clearTimeout(); // Opens submenu with 300ms delay - this._startOpenTimeout(item, opener, hoverId); + this._startOpenTimeout(item); } } @@ -591,8 +546,7 @@ class Menu extends UI5Element { _itemMouseOut(e: MouseEvent) { if (isDesktop()) { - const opener = e.target as OpenerStandardListItem; - const item = opener.associatedItem; + const item = e.target as MenuItem; // If there is a pending open operation, cancel it this._clearTimeout(); @@ -606,7 +560,7 @@ class Menu extends UI5Element { } } - _itemKeyDown(e: KeyboardEvent) { + async _itemKeyDown(e: KeyboardEvent) { const isMenuClose = this.isRtl ? isRight(e) : isLeft(e); const isMenuOpen = this.isRtl ? isLeft(e) : isRight(e); @@ -614,21 +568,16 @@ class Menu extends UI5Element { e.preventDefault(); } if (isMenuOpen) { - const opener = e.target as OpenerStandardListItem; - const item = opener.associatedItem; - const hoverId = opener.getAttribute("id")!; - - item.hasSubmenu && this._prepareSubMenuDesktopTablet(item, opener, hoverId); + const item = e.target as MenuItem; + item.hasSubmenu && await this._prepareSubMenuDesktopTablet(item); } else if (isMenuClose && this._isSubMenu && this._parentMenuItem) { const parentMenuItemParent = this._parentMenuItem.parentElement as Menu; parentMenuItemParent._closeItemSubMenu(this._parentMenuItem, true); } } - _itemClick(e: CustomEvent) { - const opener = e.detail.item as OpenerStandardListItem; - const item = opener.associatedItem; - const actionId = opener.getAttribute("id")!; + async _itemClick(e: CustomEvent) { + const item = e.detail.item as MenuItem; if (!item.hasSubmenu) { // click on an item that doesn't have sub-items fires an "item-click" event @@ -670,7 +619,7 @@ class Menu extends UI5Element { this._prepareSubMenuPhone(item); } else if (isTablet()) { // prepares and opens sub-menu on tablet - this._prepareSubMenuDesktopTablet(item, opener, actionId); + await this._prepareSubMenuDesktopTablet(item); } } diff --git a/packages/main/src/MenuItem.hbs b/packages/main/src/MenuItem.hbs new file mode 100644 index 000000000000..c612766ba96b --- /dev/null +++ b/packages/main/src/MenuItem.hbs @@ -0,0 +1,39 @@ +{{>include "./ListItem.hbs"}} + +{{#*inline "listItemContent"}} + {{#if text}} +
+ +
{{text}}
+
+
+ {{/if}} + {{#if _additionalText}} + {{_additionalText}} + {{/if}} +{{/inline}} + +{{#*inline "iconBegin"}} + {{#if _siblingsWithIcon}} + {{#if hasIcon}} + + {{else}} +
+ {{/if}} + {{/if}} +{{/inline}} + +{{#*inline "iconEnd"}} + {{#if hasSubmenu}} +
+ + +
+ {{/if}} +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/MenuItem.ts b/packages/main/src/MenuItem.ts index b556c4499082..302bebca3da0 100644 --- a/packages/main/src/MenuItem.ts +++ b/packages/main/src/MenuItem.ts @@ -1,9 +1,14 @@ -import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; +import CustomListItem from "./CustomListItem.js"; +import MenuItemTemplate from "./generated/templates/MenuItemTemplate.lit.js"; import type Menu from "./Menu.js"; +import HasPopup from "./types/HasPopup.js"; + +// Styles +import menuItemCss from "./generated/themes/MenuItem.css.js"; /** * @class @@ -24,13 +29,16 @@ import type Menu from "./Menu.js"; * import "@ui5/webcomponents/dist/MenuItem.js"; * * @constructor - * @extends UI5Element - * @abstract + * @extends CustomListItem * @since 1.3.0 * @public */ -@customElement("ui5-menu-item") -class MenuItem extends UI5Element { +@customElement({ + tag: "ui5-menu-item", + template: MenuItemTemplate, + styles: [CustomListItem.styles, menuItemCss], +}) +class MenuItem extends CustomListItem { /** * Defines the text of the tree item. * @@ -117,12 +125,6 @@ class MenuItem extends UI5Element { @property() accessibleName!: string; - /** - * Indicates whether any of the element siblings have children items. - */ - @property({ type: Boolean, noAttribute: true }) - _siblingsWithChildren!: boolean; - /** * Indicates whether any of the element siblings have icon. */ @@ -153,8 +155,8 @@ class MenuItem extends UI5Element { return !!(this.items.length || this.busy); } - get hasDummyIcon() { - return this._siblingsWithIcon && !this.icon; + get hasIcon() { + return !!this.icon; } get subMenuOpened() { @@ -165,8 +167,18 @@ class MenuItem extends UI5Element { return this.hasSubmenu ? "" : this.additionalText; } - get ariaLabelledByText() { - return `${this.text} ${this.accessibleName}`.trim(); + get _focusable() { + return true; + } + + get _accInfo() { + const accInfoSettings = { + role: "menuitem", + listItemAriaLabel: this.text, + ariaHaspopup: this.hasSubmenu ? HasPopup.Menu.toLowerCase() as Lowercase : undefined, + }; + + return { ...super._accInfo, ...accInfoSettings }; } } diff --git a/packages/main/src/NavigationMenu.hbs b/packages/main/src/NavigationMenu.hbs index 07723ed1c0e3..d9d5d6ce6c26 100644 --- a/packages/main/src/NavigationMenu.hbs +++ b/packages/main/src/NavigationMenu.hbs @@ -45,7 +45,7 @@ id="{{_id}}-menu-main" class="ui5-navigation-menu-main" > - {{#if _currentItems.length}} + {{#if items.length}} - {{#each _currentItems}} - {{#if this.item.href}} - - - {{#if this.item.hasDummyIcon}} -
-
- {{/if}} - {{this.item.text}} - {{#if this.item.hasSubmenu}} - - - {{else if this.item._siblingsWithChildren}} -
-
- {{/if}} -
-
- {{else}} - - {{#if this.item.hasDummyIcon}} -
-
- {{/if}} - {{this.item.text}} - {{#if this.item.hasSubmenu}} - - - {{else if this.item._siblingsWithChildren}} -
-
- {{/if}} -
- {{/if}} - {{/each}} +
{{else if busy}} - const test = opener.parentElement as any; - if (opener.parentElement) { - item = test.associatedItem; - } + item = item.parentElement as MenuItem; } // If there is a pending close operation, cancel it this._clearTimeout(); // Opens submenu with 300ms delay - this._startOpenTimeout(item, opener, hoverId); + this._startOpenTimeout(item); } } - _clonedItemsFragment(item: MenuItem) { - const fragment = document.createDocumentFragment(); - - for (let i = 0; i < item.items.length; ++i) { - const subItem = item.items[i] as any; - - const clonedItem = item.items[i].cloneNode(true) as any; - if (subItem.associatedItem) { - clonedItem.associatedItem = subItem.associatedItem; - } - fragment.appendChild(clonedItem); - } - - return fragment; - } - - _itemClick(e: CustomEvent) { - const opener = e.detail.item as OpenerStandardListItem; - const item = opener.associatedItem; - const actionId = opener.getAttribute("id")!; + async _itemClick(e: CustomEvent) { + const item = e.detail.item as MenuItem; const mainMenu = this._findMainMenu(item); const prevented = !mainMenu.fireEvent("item-click", { "item": item, @@ -134,7 +109,7 @@ class NavigationMenu extends Menu { this._prepareSubMenuPhone(item); } else if (isTablet()) { // prepares and opens sub-menu on tablet - this._prepareSubMenuDesktopTablet(item, opener, actionId); + await this._prepareSubMenuDesktopTablet(item); } } get accSideNavigationPopoverHiddenText() { diff --git a/packages/main/src/NavigationMenuItem.hbs b/packages/main/src/NavigationMenuItem.hbs new file mode 100644 index 000000000000..8677dbf1c00f --- /dev/null +++ b/packages/main/src/NavigationMenuItem.hbs @@ -0,0 +1,45 @@ +{{#if href}} + + + {{>include "./ListItem.hbs"}} + + {{#*inline "listItemContent"}} + {{#if text}} +
+ +
{{text}}
+
+
+ {{/if}} + {{/inline}} + + {{#*inline "iconBegin"}} + {{#if _siblingsWithIcon}} + {{#if hasIcon}} + + {{else}} +
+ {{/if}} + {{/if}} + {{/inline}} + + {{#*inline "iconEnd"}} + {{#if hasSubmenu}} +
+ + +
+ {{/if}} + {{/inline}} +
+{{else}} + {{>include "./MenuItem.hbs"}} +{{/if}} diff --git a/packages/main/src/NavigationMenuItem.ts b/packages/main/src/NavigationMenuItem.ts index 63138f6dd86f..76e210c5f21a 100644 --- a/packages/main/src/NavigationMenuItem.ts +++ b/packages/main/src/NavigationMenuItem.ts @@ -62,6 +62,14 @@ class NavigationMenuItem extends MenuItem { */ @property() target!: string; + + get _accInfo() { + const accInfoSettings = { + role: this.href ? "none" : "treeitem", + }; + + return { ...super._accInfo, ...accInfoSettings }; + } } NavigationMenuItem.define(); diff --git a/packages/main/src/themes/Menu.css b/packages/main/src/themes/Menu.css index c5a1eddfa0bc..75b5abb4bb90 100644 --- a/packages/main/src/themes/Menu.css +++ b/packages/main/src/themes/Menu.css @@ -1,4 +1,3 @@ - .ui5-menu-rp[ui5-responsive-popover]::part(header), .ui5-menu-rp[ui5-responsive-popover]::part(content), .ui5-menu-rp[ui5-responsive-popover]::part(footer) { @@ -11,46 +10,6 @@ max-width: 20rem; } -.ui5-menu-item-icon-end { - display: inline-block; - vertical-align: middle; - padding-inline-start: 0.5rem; - pointer-events: none; - position: absolute; - inset-inline-end: var(--_ui5_menu_item_submenu_icon_right); -} - -.ui5-menu-item-no-icon-end { - min-width: var(--_ui5_list_item_icon_size); - min-height: var(--_ui5_list_item_icon_size); - display: inline-block; - vertical-align: middle; - padding-inline-start: 0.5rem; - pointer-events: none; - inset-inline-end: var(--_ui5_menu_item_submenu_icon_right); -} - -.ui5-menu-item[additional-text] .ui5-menu-item-no-icon-end { - display: none; -} - -.ui5-menu-item-dummy-icon { - min-width: var(--_ui5_list_item_icon_size); - min-height: var(--_ui5_list_item_icon_size); - display: inline-block; - vertical-align: middle; - padding-inline-end: 0.5rem; - pointer-events: none; -} - -.ui5-menu-item-submenu-icon { - min-width: var(--_ui5_list_item_icon_size); - min-height: var(--_ui5_list_item_icon_size); - display: inline-block; - vertical-align: middle; - pointer-events: none; -} - .ui5-menu-busy-indicator { width: 100%; } @@ -86,32 +45,6 @@ margin-right: 1rem; } -.ui5-menu-item::part(title) { - font-size: var(--sapFontSize); - padding-top: 0.125rem; -} - -.ui5-menu-item[icon]:not([is-phone])::part(title), -.ui5-menu-item[is-phone]:not([icon=""])::part(title) { - padding-top: 0; -} - -.ui5-menu-item:not([is-phone])::part(native-li) { - padding: var(--_ui5_menu_item_padding); -} - -.ui5-menu-item[starts-section] { - border-top: 1px solid var(--sapGroup_ContentBorderColor); -} - -.ui5-menu-item[active] .ui5-menu-item-icon-end { - color: var(--sapList_Active_TextColor); -} - -.ui5-menu-item[focused]:not([active]) { - background-color: var(--sapList_Hover_Background); -} - .ui5-menu-rp[sub-menu] { margin-top: 0.25rem; margin-inline: var(--_ui5_menu_submenu_margin_offset); @@ -121,20 +54,3 @@ margin-top: 0.25rem; margin-inline: var(--_ui5_menu_submenu_placement_type_left_margin_offset); } - -.ui5-menu-item::part(additional-text) { - margin-inline-start: var(--_ui5_menu_item_additional_text_start_margin); - color: var(--sapContent_LabelColor); - min-width: max-content; -} - -.ui5-menu-item-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; -} - -.ui5-menu-item-text:has(.ui5-menu-item-submenu-icon) { - padding-inline-end: 1rem; -} \ No newline at end of file diff --git a/packages/main/src/themes/MenuItem.css b/packages/main/src/themes/MenuItem.css new file mode 100644 index 000000000000..344552296340 --- /dev/null +++ b/packages/main/src/themes/MenuItem.css @@ -0,0 +1,88 @@ +:host(ui5-menu-item[disabled]) { + pointer-events: initial; +} + +/* hovered and active */ +:host(ui5-menu-item[disabled][actionable]:not([active]):not([selected]):hover), +:host(ui5-menu-item[disabled][active][actionable]) { + background: var(--ui5-listitem-background-color); +} + +/* active */ +:host(ui5-menu-item[disabled][active][actionable]) .ui5-li-root .ui5-li-icon { + color: var(--sapContent_NonInteractiveIconColor); +} + +:host(ui5-menu-item[focused]:not([active]):not([disabled])) { + background-color: var(--sapList_Hover_Background); +} + +:host(ui5-menu-item:not([is-phone]))::part(native-li) { + padding: var(--_ui5_menu_item_padding); +} + + +:host(ui5-menu-item)::part(additional-text) { + margin-inline-start: var(--_ui5_menu_item_additional_text_start_margin); + color: var(--sapContent_LabelColor); + min-width: max-content; +} + +.ui5-menu-item-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; +} + +.ui5-menu-item-dummy-icon { + visibility: hidden; +} + +:host(ui5-menu-item)::part(title) { + font-size: var(--sapFontSize); + padding-top: 0.125rem; +} + +:host(ui5-menu-item[icon]:not([is-phone]))::part(title), +:host(ui5-menu-item[is-phone]:not([icon=""]))::part(title) { + padding-top: 0; +} + +:host(ui5-menu-item:not([is-phone]))::part(native-li) { + padding: var(--_ui5_menu_item_padding); +} + +:host(ui5-menu-item[starts-section]) { + border-top: 1px solid var(--sapGroup_ContentBorderColor); +} + +:host(ui5-menu-item)::part(content) { + padding-inline-end: 0.5rem; +} + +.ui5-menu-item-submenu-icon { + min-width: var(--_ui5_list_item_icon_size); + min-height: var(--_ui5_list_item_icon_size); + display: inline-block; + vertical-align: middle; + pointer-events: none; +} + +.ui5-menu-item-icon-end { + display: inline-block; + vertical-align: middle; + padding-inline-start: 0.5rem; + pointer-events: none; + position: absolute; + inset-inline-end: var(--_ui5_menu_item_submenu_icon_right); +} + +.ui5-menu-item-dummy-icon { + min-width: var(--_ui5_list_item_icon_size); + min-height: var(--_ui5_list_item_icon_size); + display: inline-block; + vertical-align: middle; + padding-inline-end: 0.5rem; + pointer-events: none; +} diff --git a/packages/main/src/themes/NavigationMenu.css b/packages/main/src/themes/NavigationMenu.css index 069a803c49d7..a5d86cfa6dd5 100644 --- a/packages/main/src/themes/NavigationMenu.css +++ b/packages/main/src/themes/NavigationMenu.css @@ -1,20 +1,21 @@ + @import "./InvisibleTextStyles.css"; -.ui5-navigation-menu .ui5-navigation-menu-main { +ui5-navigation-menu .ui5-navigation-menu-main { padding: var(--_ui5_side_navigation_parent_popup_padding); } -.ui5-navigation-menu .ui5-menu-item.ui5-navigation-menu-item::part(native-li) { - padding-left: 0.5rem; +ui5-navigation-menu ui5-navigation-menu-item::part(native-li) { + padding-left: 0.5rem; width: auto; } -.ui5-navigation-menu .ui5-menu-item.ui5-navigation-menu-item::part(native-li)::after { - border-radius: 0.375rem; - inset: 0; +ui5-navigation-menu ui5-navigation-menu-item::part(native-li)::after { + border-radius: 0.375rem; + inset: 0; } -.ui5-navigation-menu .ui5-navigation-menu-item { - display: flex; +ui5-navigation-menu ui5-navigation-menu-item { + display: flex; align-items: center; width: 100%; box-sizing: border-box; @@ -24,46 +25,46 @@ border-radius: 0.375rem; transition: var(--_ui5_side_navigation_item_transition); color: var(--sapList_TextColor); - font-weight: bold; - font-size: var(--sapFontSize); - border-bottom: none; + font-weight: bold; + font-size: var(--sapFontSize); + border-bottom: none; } -.ui5-navigation-menu .ui5-navigation-menu-item a { - display: flex; - align-items: center; - text-decoration: none; - color: var(--sapList_TextColor); +ui5-navigation-menu ui5-navigation-menu-item a { + display: flex; + align-items: center; + text-decoration: none; + color: var(--sapList_TextColor); } -.ui5-navigation-menu[sub-menu] .ui5-navigation-menu-item { - font-weight: normal; +ui5-navigation-menu[sub-menu] ui5-navigation-menu-item { + font-weight: normal; } -.ui5-menu-rp.ui5-navigation-menu { - box-shadow: var(--_ui5_side_navigation_popup_box_shadow); - width: auto; - min-width: 10rem; - height: auto; - background: var(--sapGroup_ContentBackground); - border: none; - border-radius: 0.75rem; +ui5-menu-rp.ui5-navigation-menu { + box-shadow: var(--_ui5_side_navigation_popup_box_shadow); + width: auto; + min-width: 10rem; + height: auto; + background: var(--sapGroup_ContentBackground); + border: none; + border-radius: 0.75rem; } -.ui5-navigation-menu .ui5-navigation-menu-item .ui5-menu-item-icon-end { - position: relative; - inset-inline-end: auto; - width: 0.75rem; - height: 0.75rem; - color: var(--sapList_TextColor); +ui5-navigation-menu ui5-navigation-menu-item .ui5-menu-item-icon-end { + position: relative; + inset-inline-end: auto; + width: 0.75rem; + height: 0.75rem; + color: var(--sapList_TextColor); } -.ui5-navigation-menu .ui5-navigation-menu-item::part(icon){ - color: var(--sapList_TextColor); - min-width: 1rem; - min-height: 1rem; +ui5-navigation-menu ui5-navigation-menu-item::part(icon){ + color: var(--sapList_TextColor); + min-width: 1rem; + min-height: 1rem; } -.ui5-navigation-menu .ui5-menu-item[focused]:not([active]){ - background: none; +ui5-navigation-menu ui5-menu-item[focused]:not([active]){ + background: none; } \ No newline at end of file From e0d0b994bd6ff55620b63bd879fcc624064e2e52 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Wed, 7 Feb 2024 18:59:10 +0200 Subject: [PATCH 02/29] feat(ui5-menu): use custom list items as base of the menu items --- packages/main/src/MenuItem.ts | 4 ++-- packages/main/src/NavigationMenuItem.ts | 5 ++-- packages/main/src/themes/MenuItem.css | 26 ++++++++++----------- packages/main/src/themes/NavigationMenu.css | 21 ++++++++--------- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/main/src/MenuItem.ts b/packages/main/src/MenuItem.ts index 302bebca3da0..44d3e81933f0 100644 --- a/packages/main/src/MenuItem.ts +++ b/packages/main/src/MenuItem.ts @@ -20,8 +20,8 @@ import menuItemCss from "./generated/themes/MenuItem.css.js"; * *

Usage

* - * ui5-menu-item is an abstract element, representing a node in a ui5-menu. The menu itself is rendered as a list, - * and each ui5-menu-item is represented by a list item (ui5-li) in that list. Therefore, you should only use + * ui5-menu-item is representing a node in a ui5-menu. The menu itself is rendered as a list, + * and each ui5-menu-item is represented by a list item in that list. Therefore, you should only use * ui5-menu-item directly in your apps. The ui5-li list item is internal for the list, and not intended for public use. * *

ES6 Module Import

diff --git a/packages/main/src/NavigationMenuItem.ts b/packages/main/src/NavigationMenuItem.ts index 76e210c5f21a..17db24afab5e 100644 --- a/packages/main/src/NavigationMenuItem.ts +++ b/packages/main/src/NavigationMenuItem.ts @@ -12,8 +12,8 @@ import MenuItem from "./MenuItem.js"; * *

Usage

* - * ui5-navigation-menu-item is an abstract element, representing a node in a ui5-navigation-menu. The navigation menu itself is rendered as a list, - * and each ui5-navigation-menu-item is represented by a list item (ui5-li) in that list. Therefore, you should only use + * ui5-navigation-menu-item is representing a node in a ui5-navigation-menu. The navigation menu itself is rendered as a list, + * and each ui5-navigation-menu-item is represented by a list item in that list. Therefore, you should only use * ui5-navigation-menu-item directly in your apps. The ui5-li list item is internal for the list, and not intended for public use. * *

ES6 Module Import

@@ -22,7 +22,6 @@ import MenuItem from "./MenuItem.js"; * * @constructor * @extends MenuItem - * @abstract * @since 1.22.0 * @private */ diff --git a/packages/main/src/themes/MenuItem.css b/packages/main/src/themes/MenuItem.css index 344552296340..1f5faa03c869 100644 --- a/packages/main/src/themes/MenuItem.css +++ b/packages/main/src/themes/MenuItem.css @@ -1,28 +1,28 @@ -:host(ui5-menu-item[disabled]) { +:host([ui5-menu-item][disabled]) { pointer-events: initial; } /* hovered and active */ -:host(ui5-menu-item[disabled][actionable]:not([active]):not([selected]):hover), -:host(ui5-menu-item[disabled][active][actionable]) { +:host([ui5-menu-item][disabled][actionable]:not([active]):not([selected]):hover), +:host([ui5-menu-item][disabled][active][actionable]) { background: var(--ui5-listitem-background-color); } /* active */ -:host(ui5-menu-item[disabled][active][actionable]) .ui5-li-root .ui5-li-icon { +:host([ui5-menu-item][disabled][active][actionable]) .ui5-li-root .ui5-li-icon { color: var(--sapContent_NonInteractiveIconColor); } -:host(ui5-menu-item[focused]:not([active]):not([disabled])) { +:host([ui5-menu-item][focused]:not([active]):not([disabled])) { background-color: var(--sapList_Hover_Background); } -:host(ui5-menu-item:not([is-phone]))::part(native-li) { +:host([ui5-menu-item]:not([is-phone]))::part(native-li) { padding: var(--_ui5_menu_item_padding); } -:host(ui5-menu-item)::part(additional-text) { +:host([ui5-menu-item])::part(additional-text) { margin-inline-start: var(--_ui5_menu_item_additional_text_start_margin); color: var(--sapContent_LabelColor); min-width: max-content; @@ -39,25 +39,25 @@ visibility: hidden; } -:host(ui5-menu-item)::part(title) { +:host([ui5-menu-item])::part(title) { font-size: var(--sapFontSize); padding-top: 0.125rem; } -:host(ui5-menu-item[icon]:not([is-phone]))::part(title), -:host(ui5-menu-item[is-phone]:not([icon=""]))::part(title) { +:host([ui5-menu-item][icon]:not([is-phone]))::part(title), +:host([ui5-menu-item][is-phone]:not([icon=""]))::part(title) { padding-top: 0; } -:host(ui5-menu-item:not([is-phone]))::part(native-li) { +:host([ui5-menu-item]:not([is-phone]))::part(native-li) { padding: var(--_ui5_menu_item_padding); } -:host(ui5-menu-item[starts-section]) { +:host([ui5-menu-item][starts-section]) { border-top: 1px solid var(--sapGroup_ContentBorderColor); } -:host(ui5-menu-item)::part(content) { +:host([ui5-menu-item])::part(content) { padding-inline-end: 0.5rem; } diff --git a/packages/main/src/themes/NavigationMenu.css b/packages/main/src/themes/NavigationMenu.css index a5d86cfa6dd5..5d0dbbcfc12f 100644 --- a/packages/main/src/themes/NavigationMenu.css +++ b/packages/main/src/themes/NavigationMenu.css @@ -1,20 +1,19 @@ - @import "./InvisibleTextStyles.css"; -ui5-navigation-menu .ui5-navigation-menu-main { +.ui5-navigation-menu .ui5-navigation-menu-main { padding: var(--_ui5_side_navigation_parent_popup_padding); } -ui5-navigation-menu ui5-navigation-menu-item::part(native-li) { +.ui5-navigation-menu ::slotted([ui5-menu-item]).ui5-navigation-menu-item::part(native-li) { padding-left: 0.5rem; width: auto; } -ui5-navigation-menu ui5-navigation-menu-item::part(native-li)::after { +.ui5-navigation-menu ::slotted([ui5-menu-item]).ui5-navigation-menu-item::part(native-li)::after { border-radius: 0.375rem; inset: 0; } -ui5-navigation-menu ui5-navigation-menu-item { +.ui5-navigation-menu .ui5-navigation-menu-item { display: flex; align-items: center; width: 100%; @@ -30,18 +29,18 @@ ui5-navigation-menu ui5-navigation-menu-item { border-bottom: none; } -ui5-navigation-menu ui5-navigation-menu-item a { +.ui5-navigation-menu .ui5-navigation-menu-item a { display: flex; align-items: center; text-decoration: none; color: var(--sapList_TextColor); } -ui5-navigation-menu[sub-menu] ui5-navigation-menu-item { +.ui5-navigation-menu[sub-menu] .ui5-navigation-menu-item { font-weight: normal; } -ui5-menu-rp.ui5-navigation-menu { +.ui5-menu-rp.ui5-navigation-menu { box-shadow: var(--_ui5_side_navigation_popup_box_shadow); width: auto; min-width: 10rem; @@ -51,7 +50,7 @@ ui5-menu-rp.ui5-navigation-menu { border-radius: 0.75rem; } -ui5-navigation-menu ui5-navigation-menu-item .ui5-menu-item-icon-end { +.ui5-navigation-menu .ui5-navigation-menu-item .ui5-menu-item-icon-end { position: relative; inset-inline-end: auto; width: 0.75rem; @@ -59,12 +58,12 @@ ui5-navigation-menu ui5-navigation-menu-item .ui5-menu-item-icon-end { color: var(--sapList_TextColor); } -ui5-navigation-menu ui5-navigation-menu-item::part(icon){ +.ui5-navigation-menu .ui5-navigation-menu-item::part(icon){ color: var(--sapList_TextColor); min-width: 1rem; min-height: 1rem; } -ui5-navigation-menu ui5-menu-item[focused]:not([active]){ +.ui5-navigation-menu ::slotted([ui5-menu-item][focused]):not([active]){ background: none; } \ No newline at end of file From b4bc6e5e0fd1babb99444975063a1962882c4e76 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 03:11:38 +0200 Subject: [PATCH 03/29] feat(ui5-menu): refactor menu items display --- packages/main/src/Menu.ts | 16 ++-------------- packages/main/src/NavigationMenu.ts | 3 --- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index f3c518fd3a88..454e6efc6985 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -496,9 +496,6 @@ class Menu extends UI5Element { } _startOpenTimeout(item: MenuItem) { - // If theres already a timeout, clears it - this._clearTimeout(); - // Sets the new timeout this._timeout = setTimeout(() => { this._prepareSubMenuDesktopTablet(item); @@ -507,7 +504,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(() => { @@ -515,12 +512,6 @@ class Menu extends UI5Element { }, MENU_CLOSE_DELAY); } - _clearTimeout() { - if (this._timeout) { - clearTimeout(this._timeout); - } - } - _itemMouseOver(e: MouseEvent) { this._busyMouseOver(); @@ -530,9 +521,6 @@ class Menu extends UI5Element { item.focus(); - // If there is a pending close operation, cancel it - this._clearTimeout(); - // Opens submenu with 300ms delay this._startOpenTimeout(item); } @@ -549,7 +537,7 @@ class Menu extends UI5Element { const item = e.target as MenuItem; // 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) { diff --git a/packages/main/src/NavigationMenu.ts b/packages/main/src/NavigationMenu.ts index 9dc2bd245b91..d4be78140434 100644 --- a/packages/main/src/NavigationMenu.ts +++ b/packages/main/src/NavigationMenu.ts @@ -75,9 +75,6 @@ class NavigationMenu extends Menu { item = item.parentElement as MenuItem; } - // If there is a pending close operation, cancel it - this._clearTimeout(); - // Opens submenu with 300ms delay this._startOpenTimeout(item); } From 90eff730bda8d1380cb624e3580a83cfa4a3cc97 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 04:03:06 +0200 Subject: [PATCH 04/29] feat(ui5-menu): refactor menu items display --- packages/main/src/NavigationMenu.ts | 6 +-- packages/main/test/specs/Menu.spec.js | 68 ++++++++++----------------- 2 files changed, 28 insertions(+), 46 deletions(-) diff --git a/packages/main/src/NavigationMenu.ts b/packages/main/src/NavigationMenu.ts index d4be78140434..6ec8469a7716 100644 --- a/packages/main/src/NavigationMenu.ts +++ b/packages/main/src/NavigationMenu.ts @@ -13,8 +13,8 @@ import type NavigationMenuItem from "./NavigationMenuItem.js"; import AreaMenuTemplate from "./generated/templates/NavigationMenuTemplate.lit.js"; // Styles -import AreaNavigationMenuCss from "./generated/themes/NavigationMenu.css.js"; -import AreaMenuCss from "./generated/themes/Menu.css.js"; +import areaNavigationMenuCss from "./generated/themes/NavigationMenu.css.js"; +import areaMenuCss from "./generated/themes/Menu.css.js"; import { NAVIGATION_MENU_POPOVER_HIDDEN_TEXT, @@ -50,7 +50,7 @@ type MenuItemClickEventDetail = { @customElement({ tag: "ui5-navigation-menu", renderer: litRender, - styles: [AreaMenuCss, AreaNavigationMenuCss], + styles: [areaMenuCss, areaNavigationMenuCss], template: AreaMenuTemplate, }) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 69d255036728..3ba5f0dd9fc0 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -6,10 +6,9 @@ describe("Menu interaction", () => { const openButton = await browser.$("#btnOpen"); openButton.click(); - const staticAreaItemClassName = await browser.getStaticAreaItemClassName("#menu"); - const popover = await browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); - assert.strictEqual(await popover.getAttribute("id"), `${staticAreaItemClassName}-menu-rp`, "There is popover for the menu created in the static area"); + assert.ok(popover, "There is popover created in the shadow DOM of the menu"); }); it("Menu opens after setting of opener and open", async () => { @@ -19,10 +18,9 @@ describe("Menu interaction", () => { openerButton.click(); openButton.click(); - const staticAreaItemClassName = await browser.getStaticAreaItemClassName("#menu"); - const popover = await browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); - assert.strictEqual(await popover.getAttribute("id"), `${staticAreaItemClassName}-menu-rp`, "There is popover for the menu created in the static area"); + assert.ok(popover, "There is popover created in the shadow DOM of the menu"); }); it("Top level menu items appearance", async () => { @@ -32,17 +30,12 @@ describe("Menu interaction", () => { openButton.click(); - const staticAreaItemClassName = await browser.getStaticAreaItemClassName("#menu"); - const popover = await browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); - const listItems = await popover.$("ui5-list").$$("ui5-li"); - assert.strictEqual(await menuItems.length, 7, "There are proper count of menu items in the top level menu"); - assert.strictEqual(await listItems.length, 7, "There are proper count of list items in the top level menu popover list"); - assert.strictEqual(await listItems[0].getAttribute("additional-text"), await menuItems[0].getAttribute("additional-text"), "The first list item has proper additional text set"); - assert.strictEqual(await listItems[1].getAttribute("disabled"), "true", "The second list item is disabled"); - assert.strictEqual(await listItems[2].getAttribute("starts-section"), "", "The third list item has separator addded"); - assert.ok(await listItems[3].$(".ui5-menu-item-icon-end"), "The third list item has sub-items and must have arrow right icon after the text"); - assert.ok(await listItems[4].$(".ui5-menu-item-dummy-icon"), "The fourth list item has no icon and has dummy div instead of icon"); + assert.strictEqual(await menuItems[0].getAttribute("additional-text"), await menuItems[0].getAttribute("additional-text"), "The first list item has proper additional text set"); + assert.strictEqual(await menuItems[1].getAttribute("disabled"), "true", "The second list item is disabled"); + assert.strictEqual(await menuItems[2].getAttribute("starts-section"), "", "The third list item has separator addded"); + assert.ok(await menuItems[3].$(".ui5-menu-item-icon-end"), "The third list item has sub-items and must have arrow right icon after the text"); + assert.ok(await menuItems[4].$(".ui5-menu-item-dummy-icon"), "The fourth list item has no icon and has dummy div instead of icon"); }); it("Sub-menu creation, opening, closing and destroying", async () => { @@ -51,13 +44,11 @@ describe("Menu interaction", () => { openButton.click(); - const staticAreaItemClassName = await browser.getStaticAreaItemClassName("#menu"); - const staticAreaItem = await browser.$(`.${staticAreaItemClassName}`); - const popover = staticAreaItem.shadow$("ui5-responsive-popover"); - const listItems = await popover.$("ui5-list").$$("ui5-li"); - const submenuList = await staticAreaItem.shadow$(".ui5-menu-submenus"); + const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); + const menuItems = await popover.$("ui5-list").$$("ui5-menu-item"); + const submenuList = await browser.$("#menu").shadow$(".ui5-menu-submenus"); - listItems[3].click(); // open sub-menu + menuItems[3].click(); // open sub-menu await submenuList.$("ui5-menu").waitForExist({ timeout: 1000, @@ -85,12 +76,11 @@ describe("Menu interaction", () => { openButton.click(); - const staticAreaItemClassName = await browser.getStaticAreaItemClassName("#menu"); - const popover = await browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); - const listItems = await popover.$("ui5-list").$$("ui5-li"); + const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); + const menuItems = await popover.$("ui5-list").$$("ui5-menu-item"); const selectionInput = await browser.$("#selectionInput"); - await listItems[0].click({x: 1, y: 1}); + await menuItems[0].click({x: 1, y: 1}); assert.strictEqual(await selectionInput.getAttribute("value"), "New File", "Click on first item fires an event"); }); @@ -101,9 +91,6 @@ describe("Menu interaction", () => { openButton.click(); - const staticAreaItemClassName = await browser.getStaticAreaItemClassName("#menu"); - const popover = await browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); - const listItems = await popover.$("ui5-list").$$("ui5-li"); const selectionInput = await browser.$("#selectionInput"); await browser.keys("Space"); @@ -116,10 +103,7 @@ describe("Menu interaction", () => { const openButton = await browser.$("#btnOpen"); openButton.click(); - - const staticAreaItemClassName = await browser.getStaticAreaItemClassName("#menu"); - const popover = await browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); - const listItems = await popover.$("ui5-list").$$("ui5-li"); + const selectionInput = await browser.$("#selectionInput"); await browser.keys("Enter"); @@ -179,10 +163,9 @@ describe("Menu interaction", () => { await browser.url(`test/pages/Menu.html`); const openButton = await browser.$("#btnOpen"); openButton.click(); - await browser.pause(100); - const menuPopover = await browser.$("ui5-static-area-item:last-of-type").shadow$("ui5-responsive-popover"); - const newFileItem = await menuPopover.$("ui5-li[accessible-name='New File']"); + const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); + const newFileItem = await popover.$("ui5-menu-item[text='New File']"); newFileItem.click(); await browser.pause(100); @@ -194,20 +177,19 @@ describe("Menu Accessibility", () => { it("Menu and Menu items accessibility attributes", async () => { await browser.url(`test/pages/Menu.html`); const openButton = await browser.$("#btnOpen"); - const menuItems = await browser.$$("ui5-menu>ui5-menu-item"); openButton.click(); - const staticAreaItemClassName = await browser.getStaticAreaItemClassName("#menu"); - const popover = await browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); const list = await popover.$("ui5-list"); - const listItems = await popover.$("ui5-list").$$("ui5-li"); + const menuItems = await popover.$("ui5-list").$$("ui5-li"); assert.strictEqual(await list.getAttribute("accessible-role"), "menu", "There is proper 'menu' role for the menu list"); - assert.strictEqual(await listItems[0].getAttribute("accessible-role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); + assert.strictEqual(await menuItems[0].getAttribute("accessible-role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); assert.strictEqual( - await listItems[0].getAttribute("accessible-name"), - "New File Opens a file explorer", + await menuItems[0].getAttribute("accessible-name"), + "Opens a file explorer", "There is additional description added"); + assert.strictEqual(await menuItems[2].$shadow("li").getAttribute("aria-haspopup"), "menu", "Popup attribute is properly set"); }); }); From c14bcacb37f10d5db1b849b89dd25dab86cf01e1 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 11:08:20 +0200 Subject: [PATCH 05/29] feat(ui5-menu): refactor menu items display --- packages/main/test/specs/Menu.spec.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 3ba5f0dd9fc0..1ef24d696f18 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -44,8 +44,7 @@ describe("Menu interaction", () => { openButton.click(); - const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); - const menuItems = await popover.$("ui5-list").$$("ui5-menu-item"); + const menuItems = await browser.$$("#menu > ui5-menu-item"); const submenuList = await browser.$("#menu").shadow$(".ui5-menu-submenus"); menuItems[3].click(); // open sub-menu @@ -76,8 +75,7 @@ describe("Menu interaction", () => { openButton.click(); - const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); - const menuItems = await popover.$("ui5-list").$$("ui5-menu-item"); + const menuItems = await browser.$$("#menu > ui5-menu-item"); const selectionInput = await browser.$("#selectionInput"); await menuItems[0].click({x: 1, y: 1}); @@ -170,6 +168,21 @@ describe("Menu interaction", () => { await browser.pause(100); assert.ok(await menuPopover.getProperty("open"), "Menu is still opened."); + + await browser.keys("Escape"); + }); + + it("Enable navigaion over disabled items", async () => { + await browser.url(`test/pages/Menu.html`); + const openButton = await browser.$("#btnOpen"); + openButton.click(); + + const menuItem = await browser.$("#menu > ui5-menu-item[text='New Folder with very long title for a menu item']"); + + await browser.keys("ArrowDown"); + + assert.ok(await menuItem.getAttribute("disabled"), "The menu item is disabled"); + assert.ok(await menuItem.getAttribute("focused"), "The menu item is focused"); }); }); @@ -182,7 +195,7 @@ describe("Menu Accessibility", () => { const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); const list = await popover.$("ui5-list"); - const menuItems = await popover.$("ui5-list").$$("ui5-li"); + const menuItems = await browser.$$("#menu > ui5-menu-item"); assert.strictEqual(await list.getAttribute("accessible-role"), "menu", "There is proper 'menu' role for the menu list"); assert.strictEqual(await menuItems[0].getAttribute("accessible-role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); From 47cabb2003c468c81c4ff48b8c768a55c3d7c135 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 11:31:55 +0200 Subject: [PATCH 06/29] feat(ui5-menu): refactor menu items display --- packages/main/test/specs/Menu.spec.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 1ef24d696f18..8b954a4e9c7c 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -163,11 +163,10 @@ describe("Menu interaction", () => { openButton.click(); const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); - const newFileItem = await popover.$("ui5-menu-item[text='New File']"); - newFileItem.click(); - await browser.pause(100); + const menuItem = await browser.$("#menu > ui5-menu-item[text='New File']"); + menuItem.click(); - assert.ok(await menuPopover.getProperty("open"), "Menu is still opened."); + assert.ok(await popover.getProperty("open"), "Menu is still opened."); await browser.keys("Escape"); }); @@ -178,8 +177,7 @@ describe("Menu interaction", () => { openButton.click(); const menuItem = await browser.$("#menu > ui5-menu-item[text='New Folder with very long title for a menu item']"); - - await browser.keys("ArrowDown"); + menuItem.click(); assert.ok(await menuItem.getAttribute("disabled"), "The menu item is disabled"); assert.ok(await menuItem.getAttribute("focused"), "The menu item is focused"); From df44060035cb033e7b22fc428ffa9e24d892c8a7 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 11:53:30 +0200 Subject: [PATCH 07/29] feat(ui5-menu): refactor menu items display - This PR affects the ui5-menu and ui5-side-navigation components rendering. - The ui5-menu-item now extends ui5-li-custom class and it will be represented directly as a list item in the DOM. As a result the ui5-menu-item is no longer an abstract class, but a physical component and it will ease the possibility in the future to support custom content supplied by the application developers. - Additionally the ui5-reponsive-popover is no longer rendered into the static area. Thus the ui5-menu structure defined by the application developers is represented more accurately into the DOM and the browser popover API could be used once available for all supported browsers. - There is also a new behavior implemented, which enables the end users to navigate via mouse and keyboard to a disabled ui5-menu-item element. This behavior is aimed to improve the accessibility of the ui5-menu component by providing screen reader announcements for disabled menu items as well. Fixes: #7096 --- packages/main/test/specs/Menu.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 8b954a4e9c7c..c4cd2b2d2cc7 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -179,8 +179,8 @@ describe("Menu interaction", () => { const menuItem = await browser.$("#menu > ui5-menu-item[text='New Folder with very long title for a menu item']"); menuItem.click(); - assert.ok(await menuItem.getAttribute("disabled"), "The menu item is disabled"); - assert.ok(await menuItem.getAttribute("focused"), "The menu item is focused"); + assert.ok(await menuItem.getProperty("disabled"), "The menu item is disabled"); + assert.ok(await menuItem.getProperty("focused"), "The menu item is focused"); }); }); From 9b3435be237b4c41dc57a30bb6a5f0b9152b3034 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 12:33:56 +0200 Subject: [PATCH 08/29] feat(ui5-menu): refactor menu items display --- packages/main/test/specs/Menu.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index c4cd2b2d2cc7..6760b5414e20 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -196,9 +196,9 @@ describe("Menu Accessibility", () => { const menuItems = await browser.$$("#menu > ui5-menu-item"); assert.strictEqual(await list.getAttribute("accessible-role"), "menu", "There is proper 'menu' role for the menu list"); - assert.strictEqual(await menuItems[0].getAttribute("accessible-role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); + assert.strictEqual(await menuItems[0].$shadow("li").getAttribute("role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); assert.strictEqual( - await menuItems[0].getAttribute("accessible-name"), + await menuItems[0].getProperty("accessible-name"), "Opens a file explorer", "There is additional description added"); assert.strictEqual(await menuItems[2].$shadow("li").getAttribute("aria-haspopup"), "menu", "Popup attribute is properly set"); From 494076e6dfb46f929631f6d841464820993c6e97 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 13:02:12 +0200 Subject: [PATCH 09/29] feat(ui5-menu): refactor menu items display --- packages/main/test/pages/Menu.html | 4 ++-- packages/main/test/specs/Menu.spec.js | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/main/test/pages/Menu.html b/packages/main/test/pages/Menu.html index 007e7cb66ac8..604bf112c71a 100644 --- a/packages/main/test/pages/Menu.html +++ b/packages/main/test/pages/Menu.html @@ -16,9 +16,9 @@ Open Menu
- + - + diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 6760b5414e20..f956747db336 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -79,6 +79,7 @@ describe("Menu interaction", () => { const selectionInput = await browser.$("#selectionInput"); await menuItems[0].click({x: 1, y: 1}); + await browser.pause(100); assert.strictEqual(await selectionInput.getAttribute("value"), "New File", "Click on first item fires an event"); }); @@ -92,6 +93,7 @@ describe("Menu interaction", () => { const selectionInput = await browser.$("#selectionInput"); await browser.keys("Space"); + await browser.pause(100); assert.strictEqual(await selectionInput.getAttribute("value"), "New File", "Pressing [Space] on first item fires an event"); }); @@ -193,14 +195,11 @@ describe("Menu Accessibility", () => { const popover = await browser.$("#menu").shadow$("ui5-responsive-popover"); const list = await popover.$("ui5-list"); - const menuItems = await browser.$$("#menu > ui5-menu-item"); + const menuItem = await browser.$("#menu > ui5-menu-item[text='Open']"); assert.strictEqual(await list.getAttribute("accessible-role"), "menu", "There is proper 'menu' role for the menu list"); - assert.strictEqual(await menuItems[0].$shadow("li").getAttribute("role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); - assert.strictEqual( - await menuItems[0].getProperty("accessible-name"), - "Opens a file explorer", - "There is additional description added"); - assert.strictEqual(await menuItems[2].$shadow("li").getAttribute("aria-haspopup"), "menu", "Popup attribute is properly set"); + assert.strictEqual(await menuItem.$shadow("li").getAttribute("role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); + assert.strictEqual(await menuItem.$shadow("li").getAttribute("aria-haspopup"), "menu", "Popup attribute is properly set"); + assert.strictEqual(await menuItem.getProperty("accessible-name"), "Choose platform", "Additional description is added"); }); }); From 58c49764001981565a508de9cf9b54a29c954cfe Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 16:14:57 +0200 Subject: [PATCH 10/29] feat(ui5-menu): refactor menu items display --- packages/fiori/src/SideNavigationPopover.hbs | 5 +++-- packages/main/src/ListItem.hbs | 2 +- packages/main/src/ListItem.ts | 2 ++ packages/main/src/Menu.ts | 2 +- packages/main/src/NavigationMenu.ts | 16 ---------------- packages/main/test/specs/Menu.spec.js | 4 ++-- 6 files changed, 9 insertions(+), 22 deletions(-) diff --git a/packages/fiori/src/SideNavigationPopover.hbs b/packages/fiori/src/SideNavigationPopover.hbs index 304b5e580a6e..61a6f8b59a37 100644 --- a/packages/fiori/src/SideNavigationPopover.hbs +++ b/packages/fiori/src/SideNavigationPopover.hbs @@ -6,7 +6,8 @@ @ui5-before-close="{{_onBeforeMenuClose}}" @ui5-item-click="{{handleOverflowItemClick}}" class="ui5-side-navigation-popover ui5-side-navigation-overflow-menu"> - {{#each _menuPopoverItems}} + + {{!-- {{#each _menuPopoverItems}} {{/each}} - {{/each}} + {{/each}} --}} {{else}} || undefined, setsize: this.accessibilityAttributes.ariaSetsize, posinset: this.accessibilityAttributes.ariaPosinset, + title: this.title, }; } diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 454e6efc6985..f5257c277384 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -375,7 +375,7 @@ class Menu extends UI5Element { popover.initialFocus = ""; for (let index = 0; index < this.items.length; index++) { if (!this.items[index].disabled) { - popover.initialFocus = `${this._id}-menu-item-${index}`; + popover.initialFocus = `${this._id}-menu-item-${index}`; // ???? break; } } diff --git a/packages/main/src/NavigationMenu.ts b/packages/main/src/NavigationMenu.ts index 6ec8469a7716..ba15d7a3989a 100644 --- a/packages/main/src/NavigationMenu.ts +++ b/packages/main/src/NavigationMenu.ts @@ -2,7 +2,6 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; import { - isDesktop, isPhone, isTablet, } from "@ui5/webcomponents-base/dist/Device.js"; @@ -65,21 +64,6 @@ class NavigationMenu extends Menu { @slot({ "default": true, type: HTMLElement, invalidateOnChildChange: true }) declare items: Array; - _itemMouseOver(e: MouseEvent) { - if (isDesktop()) { - // respect mouseover only on desktop - let item = e.target as MenuItem; - - if (item.tagName !== "ui5-menu-item") { - // for nested - item = item.parentElement as MenuItem; - } - - // Opens submenu with 300ms delay - this._startOpenTimeout(item); - } - } - async _itemClick(e: CustomEvent) { const item = e.detail.item as MenuItem; const mainMenu = this._findMainMenu(item); diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index f956747db336..19f141ffb104 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -198,8 +198,8 @@ describe("Menu Accessibility", () => { const menuItem = await browser.$("#menu > ui5-menu-item[text='Open']"); assert.strictEqual(await list.getAttribute("accessible-role"), "menu", "There is proper 'menu' role for the menu list"); - assert.strictEqual(await menuItem.$shadow("li").getAttribute("role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); - assert.strictEqual(await menuItem.$shadow("li").getAttribute("aria-haspopup"), "menu", "Popup attribute is properly set"); + assert.strictEqual(await menuItem.shadow$("li").getAttribute("role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); + assert.strictEqual(await menuItem.shadow$("li").getAttribute("aria-haspopup"), "menu", "Popup attribute is properly set"); assert.strictEqual(await menuItem.getProperty("accessible-name"), "Choose platform", "Additional description is added"); }); }); From b2e74dab299b221b56c506a66957e1aa7913ed05 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 8 Feb 2024 17:02:19 +0200 Subject: [PATCH 11/29] feat(ui5-menu): refactor menu items display --- packages/main/test/specs/Menu.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 19f141ffb104..ae99f5143dc6 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -200,6 +200,6 @@ describe("Menu Accessibility", () => { assert.strictEqual(await list.getAttribute("accessible-role"), "menu", "There is proper 'menu' role for the menu list"); assert.strictEqual(await menuItem.shadow$("li").getAttribute("role"), "menuitem", "There is proper 'menuitem' role for the menu list items"); assert.strictEqual(await menuItem.shadow$("li").getAttribute("aria-haspopup"), "menu", "Popup attribute is properly set"); - assert.strictEqual(await menuItem.getProperty("accessible-name"), "Choose platform", "Additional description is added"); + assert.strictEqual(await menuItem.getAttribute("accessible-name"), "Choose platform", "Additional description is added"); }); }); From 6dbdb43fd439ba66c1429c522e0c391527e6ac9a Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Fri, 9 Feb 2024 11:28:37 +0200 Subject: [PATCH 12/29] feat(ui5-menu): refactor menu items display --- packages/fiori/src/SideNavigationPopover.hbs | 9 ++--- packages/main/src/Menu.ts | 42 +------------------- packages/main/src/NavigationMenu.ts | 13 +----- 3 files changed, 6 insertions(+), 58 deletions(-) diff --git a/packages/fiori/src/SideNavigationPopover.hbs b/packages/fiori/src/SideNavigationPopover.hbs index 61a6f8b59a37..f289ee6f2ed8 100644 --- a/packages/fiori/src/SideNavigationPopover.hbs +++ b/packages/fiori/src/SideNavigationPopover.hbs @@ -6,8 +6,7 @@ @ui5-before-close="{{_onBeforeMenuClose}}" @ui5-item-click="{{handleOverflowItemClick}}" class="ui5-side-navigation-popover ui5-side-navigation-overflow-menu"> - - {{!-- {{#each _menuPopoverItems}} + {{#each _menuPopoverItems}} + title="{{title}}"> {{#each this.items}} {{/each}} - {{/each}} --}} + {{/each}} {{else}} ; - /** * Stores the ResponsivePopover instance */ @@ -365,20 +357,10 @@ class Menu extends UI5Element { * @public */ showAt(opener: HTMLElement): void { - if (isPhone()) { - this._parentItemsStack = []; - } if (!this._isSubMenu) { this._parentMenuItem = undefined; } const popover = this._createPopover(); - popover.initialFocus = ""; - for (let index = 0; index < this.items.length; index++) { - if (!this.items[index].disabled) { - popover.initialFocus = `${this._id}-menu-item-${index}`; // ???? - break; - } - } popover.showAt(opener); } @@ -389,9 +371,6 @@ class Menu extends UI5Element { */ close(): void { if (this._popover) { - if (isPhone()) { - this._parentItemsStack = []; - } this._popover.close(); this._popover.resetFocus(); } @@ -408,12 +387,7 @@ class Menu extends UI5Element { } _navigateBack() { - const parentMenuItem = this._parentItemsStack.pop(); - - this.focus(); - if (parentMenuItem) { - this._parentMenuItem = this._parentItemsStack.length ? this._parentItemsStack[this._parentItemsStack.length - 1] : undefined; - } + this._closeItemSubMenu(this._parentMenuItem as MenuItem, true); } async _createSubMenu(item: MenuItem) { @@ -490,11 +464,6 @@ class Menu extends UI5Element { } } - _prepareSubMenuPhone(item: MenuItem) { - this._parentMenuItem = item; - this._parentItemsStack.push(item); - } - _startOpenTimeout(item: MenuItem) { // Sets the new timeout this._timeout = setTimeout(() => { @@ -570,9 +539,6 @@ class Menu extends UI5Element { if (!item.hasSubmenu) { // click on an item that doesn't have sub-items fires an "item-click" event if (!this._isSubMenu) { - if (isPhone()) { - this._parentMenuItem = undefined; - } // fire event if the click is on top-level menu item const prevented = !this.fireEvent("item-click", { "item": item, @@ -602,11 +568,7 @@ class Menu extends UI5Element { mainMenu._popover!.close(); } } - } else if (isPhone()) { - // prepares and opens sub-menu on phone - this._prepareSubMenuPhone(item); - } else if (isTablet()) { - // prepares and opens sub-menu on tablet + } else { await this._prepareSubMenuDesktopTablet(item); } } diff --git a/packages/main/src/NavigationMenu.ts b/packages/main/src/NavigationMenu.ts index ba15d7a3989a..6e7ded4729c1 100644 --- a/packages/main/src/NavigationMenu.ts +++ b/packages/main/src/NavigationMenu.ts @@ -1,10 +1,6 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; -import { - isPhone, - isTablet, -} from "@ui5/webcomponents-base/dist/Device.js"; import type { ListItemClickEventDetail } from "./List.js"; import Menu from "./Menu.js"; import MenuItem from "./MenuItem.js"; @@ -84,14 +80,7 @@ class NavigationMenu extends Menu { mainMenu._popover!.close(); } - - if (isPhone()) { - // prepares and opens sub-menu on phone - this._prepareSubMenuPhone(item); - } else if (isTablet()) { - // prepares and opens sub-menu on tablet - await this._prepareSubMenuDesktopTablet(item); - } + await this._prepareSubMenuDesktopTablet(item); } get accSideNavigationPopoverHiddenText() { return NavigationMenu.i18nBundle.getText(NAVIGATION_MENU_POPOVER_HIDDEN_TEXT); From e23deb96a322fbfdcaca8442bd5317a1666b6b27 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Fri, 9 Feb 2024 12:29:59 +0200 Subject: [PATCH 13/29] feat(ui5-menu): refactor menu items display --- packages/fiori/src/SideNavigation.ts | 42 ++++++++++------------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/packages/fiori/src/SideNavigation.ts b/packages/fiori/src/SideNavigation.ts index fcbb9540ee99..be986843bffa 100644 --- a/packages/fiori/src/SideNavigation.ts +++ b/packages/fiori/src/SideNavigation.ts @@ -54,20 +54,6 @@ type SideNavigationSelectionChangeEventDetail = { item: SideNavigationItemBase; }; -// used for the inner side navigation used in the SideNavigationPopoverTemplate -type PopupClickEventDetail = { - target: { - associatedItem: SideNavigationItemBase - } -}; - -// used for the inner side navigation used in the SideNavigationPopoverTemplate -type NavigationMenuClickEventDetail = { - item: { - associatedItem: SideNavigationItemBase - } -}; - /** * @class * @@ -243,7 +229,7 @@ class SideNavigation extends UI5Element { const selectedItem = this._findSelectedItem(this.items)!; await renderFinished(); - selectedItem.getDomRef().focus(); + selectedItem?.getDomRef().focus(); } async _onBeforePopoverOpen() { @@ -283,39 +269,39 @@ class SideNavigation extends UI5Element { return SideNavigation.i18nBundle.getText(SIDE_NAVIGATION_OVERFLOW_ACCESSIBLE_NAME); } - async handlePopupItemClick(e: PopupClickEventDetail) { - const associatedItem = e.target.associatedItem; + async handlePopupItemClick(e: SideNavigationSelectionChangeEventDetail) { + const item = e.item; - associatedItem.fireEvent("click"); - if (associatedItem.selected) { + item.fireEvent("click"); + if (item.selected) { this.closePicker(); return; } - this._selectItem(associatedItem); + this._selectItem(item); this.closePicker(); await renderFinished(); this._popoverContents.item.getDomRef().classList.add("ui5-sn-item-no-hover-effect"); } - handleOverflowItemClick(e: CustomEvent) { - const associatedItem = e.detail?.item.associatedItem; + handleOverflowItemClick(e: CustomEvent) { + const item = e.detail?.item; - associatedItem.fireEvent("click"); - if (associatedItem.selected) { + item.fireEvent("click"); + if (item.selected) { this.closeMenu(); return; } - this._selectItem(associatedItem); + this._selectItem(item); // When subitem is selected in collapsed mode parent element should be focused - if (associatedItem.nodeName.toLowerCase() === "ui5-side-navigation-sub-item") { - const parent = associatedItem.parentElement as SideNavigationItem; + if (item.nodeName.toLowerCase() === "ui5-side-navigation-sub-item") { + const parent = item.parentElement as SideNavigationItem; this._flexibleItemNavigation.setCurrentItem(parent); } else { - this._flexibleItemNavigation.setCurrentItem(associatedItem); + this._flexibleItemNavigation.setCurrentItem(item); } this.closeMenu(); From 46b8f8fd5c1a684bdc55668399c2bc4d431fbaf2 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Fri, 9 Feb 2024 16:02:30 +0200 Subject: [PATCH 14/29] feat(ui5-menu): refactor menu items display --- packages/main/src/ListItem.hbs | 2 +- packages/main/src/ListItem.ts | 2 -- packages/main/src/Menu.ts | 14 ++++---------- packages/main/test/specs/Menu.spec.js | 9 +++++---- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/main/src/ListItem.hbs b/packages/main/src/ListItem.hbs index 8d0b5544ddb2..a68cfa30000c 100644 --- a/packages/main/src/ListItem.hbs +++ b/packages/main/src/ListItem.hbs @@ -14,7 +14,7 @@ @click="{{_onclick}}" role="{{_accInfo.role}}" aria-expanded="{{_accInfo.ariaExpanded}}" - title="{{_accInfo.title}}" + title="{{title}}" aria-level="{{_accInfo.ariaLevel}}" aria-haspopup="{{_accInfo.ariaHaspopup}}" aria-posinset="{{_accInfo.posinset}}" diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts index 8d7d2994859d..92c77cf0e468 100644 --- a/packages/main/src/ListItem.ts +++ b/packages/main/src/ListItem.ts @@ -53,7 +53,6 @@ type AccInfo = { role: string; ariaExpanded?: boolean; ariaLevel?: number; - title?: string; ariaLabel: string; ariaLabelRadioButton: string; ariaSelectedText?: string; @@ -488,7 +487,6 @@ abstract class ListItem extends ListItemBase { ariaHaspopup: this.ariaHaspopup?.toLowerCase() as Lowercase || undefined, setsize: this.accessibilityAttributes.ariaSetsize, posinset: this.accessibilityAttributes.ariaPosinset, - title: this.title, }; } diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 70dc4f48a01f..ab053857e0b7 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -237,13 +237,6 @@ class Menu extends UI5Element { @property({ type: Boolean, noAttribute: true }) _isSubMenu!: boolean; - /** - * Stores id of a list item that opened sub-menu. - * @private - */ - @property() - _subMenuOpenerId!: string; - /** * Stores the ResponsivePopover instance */ @@ -391,6 +384,10 @@ class Menu extends UI5Element { } async _createSubMenu(item: MenuItem) { + if (item._subMenu) { + return; + } + const ctor = this.constructor as typeof Menu; const subMenu = document.createElement(ctor.getMetadata().getTag()) as Menu; @@ -443,10 +440,7 @@ class Menu extends UI5Element { if (forceClose || !parentItem._preventSubMenuClose) { subMenu.close(); - subMenu.remove(); - parentItem._subMenu = undefined; this._openedSubMenuItem = undefined; - this._subMenuOpenerId = ""; } } } diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index ae99f5143dc6..1309e86be6b9 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -57,16 +57,17 @@ describe("Menu interaction", () => { assert.ok(await submenuList.$("ui5-menu"), "The second level sub-menu is being created"); // new ui5-menu element is created for the sub-menu await browser.keys("ArrowLeft"); // back to main menu - await browser.keys("ArrowDown"); // go to the next menu item (close sub-menu) + await browser.keys("ArrowUp"); // go to the next menu item (close sub-menu) + await browser.keys("Space"); // open sub-menu await submenuList.$("ui5-menu").waitForExist({ reverse: true, timeout: 1000, - timeoutMsg: "The second level sub-menu is should be destroyed" + timeoutMsg: "Second sub-menu is created" }) - assert.strictEqual(await submenuList.$$("ui5-menu").length, 0, - "The second level sub-menu is being destroyed"); // sub-menu ui5-menu element is destroyed + assert.strictEqual(await submenuList.$$("ui5-menu").length, 2, + "Two sub-menus are present"); }); it("Event firing after 'click' on menu item", async () => { From 0c282f63a3d353c66e1ad6bf2772a038431b480e Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Sat, 10 Feb 2024 15:05:22 +0200 Subject: [PATCH 15/29] feat(ui5-menu): refactor menu items display --- packages/main/src/Menu.hbs | 4 +-- packages/main/src/Menu.ts | 52 +++++++++++++++++----------- packages/main/src/NavigationMenu.hbs | 2 +- packages/main/test/pages/Menu.html | 6 +++- 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/packages/main/src/Menu.hbs b/packages/main/src/Menu.hbs index 55821d29cade..28494cb807d5 100644 --- a/packages/main/src/Menu.hbs +++ b/packages/main/src/Menu.hbs @@ -16,7 +16,7 @@ slot="header" class="ui5-menu-dialog-header" > - {{#if isSubMenuOpened}} + {{#if isSubMenuOpen}} diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index ab053857e0b7..092a36b9ac08 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -301,8 +301,8 @@ class Menu extends UI5Element { return isPhone(); } - get isSubMenuOpened() { - return !!this._parentMenuItem; + get isSubMenuOpen() { + return this._popover && this._popover.isOpen(); } get menuHeaderTextPhone() { @@ -335,7 +335,7 @@ class Menu extends UI5Element { if (this.open) { const opener = this.getOpener(); - if (opener) { + if (opener && !this.isSubMenuOpen) { this.showAt(opener); } } else { @@ -354,7 +354,11 @@ class Menu extends UI5Element { this._parentMenuItem = undefined; } const popover = this._createPopover(); - popover.showAt(opener); + popover.showAt(opener, true); + + if (this.items.length) { + this.items[0].focus(); + } } /** @@ -363,10 +367,7 @@ class Menu extends UI5Element { * @public */ close(): void { - if (this._popover) { - this._popover.close(); - this._popover.resetFocus(); - } + this._popover?.close(false, false, true); } _createPopover() { @@ -383,6 +384,11 @@ class Menu extends UI5Element { this._closeItemSubMenu(this._parentMenuItem as MenuItem, true); } + _closeAll() { + const mainMenu = this._findMainMenu(this); + mainMenu.close(); + } + async _createSubMenu(item: MenuItem) { if (item._subMenu) { return; @@ -440,15 +446,17 @@ class Menu extends UI5Element { if (forceClose || !parentItem._preventSubMenuClose) { subMenu.close(); + this._openedSubMenuItem?.focus(); this._openedSubMenuItem = undefined; } } } async _prepareSubMenuDesktopTablet(item: MenuItem) { + // close opened sub-menu if there is any opened + this._closeItemSubMenu(this._openedSubMenuItem!, true); + if (item && item.hasSubmenu) { - // close opened sub-menu if there is any opened - this._closeItemSubMenu(this._openedSubMenuItem!, true); // create new sub-menu await this._createSubMenu(item); this._openItemSubMenu(item); @@ -459,6 +467,9 @@ class Menu extends UI5Element { } _startOpenTimeout(item: MenuItem) { + // If theres already a timeout, clears it + clearTimeout(this._timeout); + // Sets the new timeout this._timeout = setTimeout(() => { this._prepareSubMenuDesktopTablet(item); @@ -499,9 +510,6 @@ class Menu extends UI5Element { if (isDesktop()) { const item = e.target as MenuItem; - // If there is a pending open operation, cancel it - clearTimeout(this._timeout); - // Close submenu with 400ms delay if (item && item.hasSubmenu && item._subMenu) { // try to close the sub-menu @@ -512,18 +520,18 @@ class Menu extends UI5Element { } async _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 item = e.target as MenuItem; item.hasSubmenu && await this._prepareSubMenuDesktopTablet(item); - } else if (isMenuClose && this._isSubMenu && this._parentMenuItem) { - const parentMenuItemParent = this._parentMenuItem.parentElement as Menu; - parentMenuItemParent._closeItemSubMenu(this._parentMenuItem, true); + } else if (shouldCloseMenu && this._isSubMenu && this._parentMenuItem) { + const parentItemMenu = this._parentMenuItem.parentElement as Menu; + parentItemMenu._closeItemSubMenu(this._parentMenuItem, true); } } @@ -567,8 +575,10 @@ class Menu extends UI5Element { } } - _findMainMenu(item: MenuItem) { - let parentMenu = item.parentElement as Menu; + _findMainMenu(element: MenuItem | Menu) { + let parentMenu = element.tagName.toLocaleLowerCase() === "ui5-menu" + ? element as Menu + : element.parentElement as Menu; while (parentMenu._parentMenuItem) { parentMenu = parentMenu._parentMenuItem.parentElement as Menu; } diff --git a/packages/main/src/NavigationMenu.hbs b/packages/main/src/NavigationMenu.hbs index d9d5d6ce6c26..6c0f5ea51a65 100644 --- a/packages/main/src/NavigationMenu.hbs +++ b/packages/main/src/NavigationMenu.hbs @@ -15,7 +15,7 @@ slot="header" class="ui5-menu-dialog-header" > - {{#if isSubMenuOpened}} + {{#if isSubMenuOpen}} Date: Sat, 10 Feb 2024 17:40:40 +0200 Subject: [PATCH 16/29] feat(ui5-menu): refactor menu items display --- packages/main/src/Menu.ts | 12 +++++++----- packages/main/test/pages/Menu.html | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 092a36b9ac08..c04b81a094bc 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -429,24 +429,26 @@ class Menu extends UI5Element { this._openedSubMenuItem = item; } - _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(); - this._openedSubMenuItem?.focus(); + if (keyboard) { + this._openedSubMenuItem?.focus(); + } this._openedSubMenuItem = undefined; } } @@ -531,7 +533,7 @@ class Menu extends UI5Element { item.hasSubmenu && await this._prepareSubMenuDesktopTablet(item); } else if (shouldCloseMenu && this._isSubMenu && this._parentMenuItem) { const parentItemMenu = this._parentMenuItem.parentElement as Menu; - parentItemMenu._closeItemSubMenu(this._parentMenuItem, true); + parentItemMenu._closeItemSubMenu(this._parentMenuItem, true, true); } } diff --git a/packages/main/test/pages/Menu.html b/packages/main/test/pages/Menu.html index 809fc8b5931a..1a48d2500175 100644 --- a/packages/main/test/pages/Menu.html +++ b/packages/main/test/pages/Menu.html @@ -60,6 +60,11 @@ Event logger + Open + + + + \ No newline at end of file From c77f865e158b65ac07f1dfcc94c96f2beb6a563c Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 04:04:45 +0200 Subject: [PATCH 17/29] feat(ui5-menu): refactor menu items display --- packages/fiori/src/SideNavigation.ts | 10 +++++--- packages/main/test/pages/Menu.html | 34 +++++++++++++++++++++++++++ packages/main/test/specs/Menu.spec.js | 2 +- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/fiori/src/SideNavigation.ts b/packages/fiori/src/SideNavigation.ts index be986843bffa..27f0a4dc0a72 100644 --- a/packages/fiori/src/SideNavigation.ts +++ b/packages/fiori/src/SideNavigation.ts @@ -54,6 +54,11 @@ type SideNavigationSelectionChangeEventDetail = { item: SideNavigationItemBase; }; +// used for the inner side navigation used in the SideNavigationPopoverTemplate +type PopupClickEventDetail = { + target: SideNavigationItemBase +}; + /** * @class * @@ -269,10 +274,9 @@ class SideNavigation extends UI5Element { return SideNavigation.i18nBundle.getText(SIDE_NAVIGATION_OVERFLOW_ACCESSIBLE_NAME); } - async handlePopupItemClick(e: SideNavigationSelectionChangeEventDetail) { - const item = e.item; + async handlePopupItemClick(e: PopupClickEventDetail) { + const item = e.target; - item.fireEvent("click"); if (item.selected) { this.closePicker(); return; diff --git a/packages/main/test/pages/Menu.html b/packages/main/test/pages/Menu.html index 1a48d2500175..6422cdf0eb02 100644 --- a/packages/main/test/pages/Menu.html +++ b/packages/main/test/pages/Menu.html @@ -64,6 +64,16 @@ + + + + + Load menu + + + + + diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 1309e86be6b9..8d07e9cf8752 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -26,7 +26,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 > ui5-menu-item"); openButton.click(); From b218ffb25247d2df0567efcfcab6c2198d777282 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 04:08:07 +0200 Subject: [PATCH 18/29] feat(ui5-menu): refactor menu items display --- packages/main/test/pages/Menu.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/main/test/pages/Menu.html b/packages/main/test/pages/Menu.html index 6422cdf0eb02..3619b68f0680 100644 --- a/packages/main/test/pages/Menu.html +++ b/packages/main/test/pages/Menu.html @@ -61,9 +61,6 @@ Open - - - From 2ad0e5b8355473bc8866feb54134f867530fc02e Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 11:29:28 +0200 Subject: [PATCH 19/29] feat(ui5-menu): refactor menu items display --- packages/fiori/test/specs/SideNavigation.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fiori/test/specs/SideNavigation.spec.js b/packages/fiori/test/specs/SideNavigation.spec.js index 9de7d35a88de..361cd9bad9ad 100644 --- a/packages/fiori/test/specs/SideNavigation.spec.js +++ b/packages/fiori/test/specs/SideNavigation.spec.js @@ -62,15 +62,15 @@ describe("Component Behavior", () => { await items[0].click(); - assert.strictEqual(await input.getProperty("value"), "6", "Event is fired"); + assert.strictEqual(await input.getProperty("value"), "5", "Event is fired"); await items[3].click(); - assert.strictEqual(await input.getProperty("value"), "6", "Event is not fired"); + assert.strictEqual(await input.getProperty("value"), "5", "Event is not fired"); assert.strictEqual(await items[3].getAttribute("aria-expanded"), "true" ,"Expanded is toggled"); await items[1].click(); - assert.strictEqual(await input.getProperty("value"), "7", "Event is fired"); + assert.strictEqual(await input.getProperty("value"), "6", "Event is fired"); }); it("Tests header visibility", async () => { From feff9c5d8c7638d1ebc2952394b226b3939fab3c Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 11:52:59 +0200 Subject: [PATCH 20/29] feat(ui5-menu): refactor menu items display --- packages/main/test/specs/Menu.spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 8d07e9cf8752..643d462072e4 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -26,7 +26,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.$$("#menu > ui5-menu-item"); openButton.click(); @@ -61,7 +61,6 @@ describe("Menu interaction", () => { await browser.keys("Space"); // open sub-menu await submenuList.$("ui5-menu").waitForExist({ - reverse: true, timeout: 1000, timeoutMsg: "Second sub-menu is created" }) From 7f4a930f3bccf68dfc0a5dae9fbdf9d34a553e95 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 12:48:07 +0200 Subject: [PATCH 21/29] feat(ui5-menu): refactor menu items display --- packages/main/test/specs/Menu.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 643d462072e4..9100e5d4c682 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -49,7 +49,7 @@ describe("Menu interaction", () => { menuItems[3].click(); // open sub-menu - await submenuList.$("ui5-menu").waitForExist({ + await submenuList.$("ui5-menu:nth-last-child(1)").waitForExist({ timeout: 1000, timeoutMsg: "The second level sub-menu is should be created" }) @@ -60,7 +60,7 @@ describe("Menu interaction", () => { await browser.keys("ArrowUp"); // go to the next menu item (close sub-menu) await browser.keys("Space"); // open sub-menu - await submenuList.$("ui5-menu").waitForExist({ + await submenuList.$("ui5-menu:nth-last-child(2)").waitForExist({ timeout: 1000, timeoutMsg: "Second sub-menu is created" }) From 475e0ef5289c4a291a915f5f85faa8238e6c7b75 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 12:59:05 +0200 Subject: [PATCH 22/29] feat(ui5-menu): refactor menu items display --- packages/main/test/pages/Menu.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/test/pages/Menu.html b/packages/main/test/pages/Menu.html index 3619b68f0680..895ec713da8a 100644 --- a/packages/main/test/pages/Menu.html +++ b/packages/main/test/pages/Menu.html @@ -111,7 +111,7 @@ twoNode.setAttribute("text", "Open from Google Cloud"); item.append(oneNode, twoNode); fetched = true; - }, 1000); + }, 500); } }); From 822f1639e0ad801ab97ffe094882e30bbe1e6d04 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 13:55:10 +0200 Subject: [PATCH 23/29] feat(ui5-menu): refactor menu items display --- packages/main/test/specs/Menu.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 9100e5d4c682..00f18663ffa7 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -49,18 +49,18 @@ describe("Menu interaction", () => { menuItems[3].click(); // open sub-menu - await submenuList.$("ui5-menu:nth-last-child(1)").waitForExist({ + await submenuList.$("ui5-menu:nth-of-type(1)").waitForExist({ timeout: 1000, - timeoutMsg: "The second level sub-menu is should be created" + timeoutMsg: "First sub-menu is created" }) assert.ok(await submenuList.$("ui5-menu"), "The second level sub-menu is being created"); // new ui5-menu element is created for the sub-menu await browser.keys("ArrowLeft"); // back to main menu - await browser.keys("ArrowUp"); // go to the next menu item (close sub-menu) + await browser.keys("ArrowDown"); // go to the next menu item (close sub-menu) await browser.keys("Space"); // open sub-menu - await submenuList.$("ui5-menu:nth-last-child(2)").waitForExist({ + await submenuList.$("ui5-menu:nth-of-type(2)").waitForExist({ timeout: 1000, timeoutMsg: "Second sub-menu is created" }) From 90105b100e59f0294036656f92ef2288a54da7db Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 14:12:47 +0200 Subject: [PATCH 24/29] feat(ui5-menu): refactor menu items display --- packages/main/test/specs/Menu.spec.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/main/test/specs/Menu.spec.js b/packages/main/test/specs/Menu.spec.js index 00f18663ffa7..623e7276a60b 100644 --- a/packages/main/test/specs/Menu.spec.js +++ b/packages/main/test/specs/Menu.spec.js @@ -56,9 +56,7 @@ describe("Menu interaction", () => { assert.ok(await submenuList.$("ui5-menu"), "The second level sub-menu is being created"); // new ui5-menu element is created for the sub-menu - await browser.keys("ArrowLeft"); // back to main menu - await browser.keys("ArrowDown"); // go to the next menu item (close sub-menu) - await browser.keys("Space"); // open sub-menu + menuItems[4].click(); // open sub-menu await submenuList.$("ui5-menu:nth-of-type(2)").waitForExist({ timeout: 1000, From 898150bf424616580dace17519bf4b7c07516466 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 16:16:42 +0200 Subject: [PATCH 25/29] feat(ui5-menu): refactor menu items display --- packages/fiori/src/SideNavigation.ts | 36 ++++++++++++------- packages/fiori/src/SideNavigationPopover.hbs | 4 ++- .../fiori/test/specs/SideNavigation.spec.js | 6 ++-- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/fiori/src/SideNavigation.ts b/packages/fiori/src/SideNavigation.ts index 27f0a4dc0a72..fcbb9540ee99 100644 --- a/packages/fiori/src/SideNavigation.ts +++ b/packages/fiori/src/SideNavigation.ts @@ -56,7 +56,16 @@ type SideNavigationSelectionChangeEventDetail = { // used for the inner side navigation used in the SideNavigationPopoverTemplate type PopupClickEventDetail = { - target: SideNavigationItemBase + target: { + associatedItem: SideNavigationItemBase + } +}; + +// used for the inner side navigation used in the SideNavigationPopoverTemplate +type NavigationMenuClickEventDetail = { + item: { + associatedItem: SideNavigationItemBase + } }; /** @@ -234,7 +243,7 @@ class SideNavigation extends UI5Element { const selectedItem = this._findSelectedItem(this.items)!; await renderFinished(); - selectedItem?.getDomRef().focus(); + selectedItem.getDomRef().focus(); } async _onBeforePopoverOpen() { @@ -275,37 +284,38 @@ class SideNavigation extends UI5Element { } async handlePopupItemClick(e: PopupClickEventDetail) { - const item = e.target; + const associatedItem = e.target.associatedItem; - if (item.selected) { + associatedItem.fireEvent("click"); + if (associatedItem.selected) { this.closePicker(); return; } - this._selectItem(item); + this._selectItem(associatedItem); this.closePicker(); await renderFinished(); this._popoverContents.item.getDomRef().classList.add("ui5-sn-item-no-hover-effect"); } - handleOverflowItemClick(e: CustomEvent) { - const item = e.detail?.item; + handleOverflowItemClick(e: CustomEvent) { + const associatedItem = e.detail?.item.associatedItem; - item.fireEvent("click"); - if (item.selected) { + associatedItem.fireEvent("click"); + if (associatedItem.selected) { this.closeMenu(); return; } - this._selectItem(item); + this._selectItem(associatedItem); // When subitem is selected in collapsed mode parent element should be focused - if (item.nodeName.toLowerCase() === "ui5-side-navigation-sub-item") { - const parent = item.parentElement as SideNavigationItem; + if (associatedItem.nodeName.toLowerCase() === "ui5-side-navigation-sub-item") { + const parent = associatedItem.parentElement as SideNavigationItem; this._flexibleItemNavigation.setCurrentItem(parent); } else { - this._flexibleItemNavigation.setCurrentItem(item); + this._flexibleItemNavigation.setCurrentItem(associatedItem); } this.closeMenu(); diff --git a/packages/fiori/src/SideNavigationPopover.hbs b/packages/fiori/src/SideNavigationPopover.hbs index f289ee6f2ed8..304b5e580a6e 100644 --- a/packages/fiori/src/SideNavigationPopover.hbs +++ b/packages/fiori/src/SideNavigationPopover.hbs @@ -14,12 +14,14 @@ expanded="true" href="{{href}}" target="{{target}}" - title="{{title}}"> + title="{{title}}" + .associatedItem="{{this}}"> {{#each this.items}} { await items[0].click(); - assert.strictEqual(await input.getProperty("value"), "5", "Event is fired"); + assert.strictEqual(await input.getProperty("value"), "6", "Event is fired"); await items[3].click(); - assert.strictEqual(await input.getProperty("value"), "5", "Event is not fired"); + assert.strictEqual(await input.getProperty("value"), "6", "Event is not fired"); assert.strictEqual(await items[3].getAttribute("aria-expanded"), "true" ,"Expanded is toggled"); await items[1].click(); - assert.strictEqual(await input.getProperty("value"), "6", "Event is fired"); + assert.strictEqual(await input.getProperty("value"), "7", "Event is fired"); }); it("Tests header visibility", async () => { From 4941aa410f198767e3b3bcc4d6edbe96db33e931 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Mon, 12 Feb 2024 16:47:17 +0200 Subject: [PATCH 26/29] feat(ui5-menu): refactor menu items display --- packages/main/src/NavigationMenu.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/main/src/NavigationMenu.ts b/packages/main/src/NavigationMenu.ts index 6e7ded4729c1..4ddfaadc133b 100644 --- a/packages/main/src/NavigationMenu.ts +++ b/packages/main/src/NavigationMenu.ts @@ -60,6 +60,22 @@ class NavigationMenu extends Menu { @slot({ "default": true, type: HTMLElement, invalidateOnChildChange: true }) declare items: Array; + _clonedItemsFragment(item: MenuItem) { + const fragment = document.createDocumentFragment(); + + for (let i = 0; i < item.items.length; ++i) { + const subItem = item.items[i] as any; + + const clonedItem = item.items[i].cloneNode(true) as any; + if (subItem.associatedItem) { + clonedItem.associatedItem = subItem.associatedItem; + } + fragment.appendChild(clonedItem); + } + + return fragment; + } + async _itemClick(e: CustomEvent) { const item = e.detail.item as MenuItem; const mainMenu = this._findMainMenu(item); From 8001cbd0cd71660233bac6f097ce5a1fea05abfc Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Wed, 14 Feb 2024 08:20:22 +0200 Subject: [PATCH 27/29] feat(ui5-menu): refactor menu items display --- packages/main/src/Menu.ts | 4 +- packages/main/src/MenuItem.hbs | 6 +- packages/main/src/NavigationMenu.ts | 15 ++++ packages/main/src/NavigationMenuItem.hbs | 81 +++++++++---------- packages/main/src/NavigationMenuItem.ts | 23 +++++- packages/main/src/themes/MenuItem.css | 26 +++--- packages/main/src/themes/NavigationMenu.css | 60 +------------- .../main/src/themes/NavigationMenuItem.css | 54 +++++++++++++ 8 files changed, 149 insertions(+), 120 deletions(-) create mode 100644 packages/main/src/themes/NavigationMenuItem.css diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 22ab09c2015b..00853d379e9c 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -387,7 +387,7 @@ class Menu extends UI5Element { mainMenu.close(); } - async _createSubMenu(item: MenuItem) { + async _createSubMenu(item: MenuItem): Promise { if (item._subMenu) { return; } @@ -423,6 +423,7 @@ class Menu extends UI5Element { item, }, false, false); item._subMenu!.showAt(item); + item.selected = true; item._preventSubMenuClose = true; this._openedSubMenuItem = item; } @@ -444,6 +445,7 @@ class Menu extends UI5Element { if (forceClose || !parentItem._preventSubMenuClose) { subMenu.close(); + parentItem.selected = false; if (keyboard) { this._openedSubMenuItem?.focus(); } diff --git a/packages/main/src/MenuItem.hbs b/packages/main/src/MenuItem.hbs index c612766ba96b..23be170136cb 100644 --- a/packages/main/src/MenuItem.hbs +++ b/packages/main/src/MenuItem.hbs @@ -2,11 +2,7 @@ {{#*inline "listItemContent"}} {{#if text}} -
- -
{{text}}
-
-
+
{{text}}
{{/if}} {{#if _additionalText}} {{_additionalText}} diff --git a/packages/main/src/NavigationMenu.ts b/packages/main/src/NavigationMenu.ts index 4ddfaadc133b..b4649aeaee2e 100644 --- a/packages/main/src/NavigationMenu.ts +++ b/packages/main/src/NavigationMenu.ts @@ -1,6 +1,9 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; +import { + isDesktop, +} from "@ui5/webcomponents-base/dist/Device.js"; import type { ListItemClickEventDetail } from "./List.js"; import Menu from "./Menu.js"; import MenuItem from "./MenuItem.js"; @@ -60,6 +63,18 @@ class NavigationMenu extends Menu { @slot({ "default": true, type: HTMLElement, invalidateOnChildChange: true }) declare items: Array; + _itemMouseOver(e: MouseEvent) { + this._busyMouseOver(); + + if (isDesktop()) { + // respect mouseover only on desktop + const item = e.target as MenuItem; + + // Opens submenu with 300ms delay + this._startOpenTimeout(item); + } + } + _clonedItemsFragment(item: MenuItem) { const fragment = document.createDocumentFragment(); diff --git a/packages/main/src/NavigationMenuItem.hbs b/packages/main/src/NavigationMenuItem.hbs index a60d5fd8d9b4..2d9154af23ee 100644 --- a/packages/main/src/NavigationMenuItem.hbs +++ b/packages/main/src/NavigationMenuItem.hbs @@ -1,50 +1,47 @@ -{{#if href}} +{{#if _href}}
- - {{>include "./ListItem.hbs"}} + {{>include "./ListItem.hbs"}} + +{{else}} + {{>include "./ListItem.hbs"}} +{{/if}} - {{#*inline "listItemContent"}} - {{#if text}} -
- -
{{text}}
-
-
- {{/if}} - {{/inline}} - - {{#*inline "iconBegin"}} - {{#if _siblingsWithIcon}} - {{#if hasIcon}} - - {{else}} -
- {{/if}} - {{/if}} - {{#if target}} + +{{#*inline "listItemContent"}} + {{text}} +{{/inline}} + +{{#*inline "iconBegin"}} + {{#if _siblingsWithIcon}} + {{#if hasIcon}} + + + {{else}} +
+ {{/if}} + {{/if}} +{{/inline}} + +{{#*inline "iconEnd"}} + {{#if hasSubmenu}} + + + {{/if}} + {{#if href}} + {{#if target}} {{/if}} - {{/inline}} - - {{#*inline "iconEnd"}} - {{#if hasSubmenu}} -
- - -
- {{/if}} - {{/inline}} - -{{else}} - {{>include "./MenuItem.hbs"}} -{{/if}} + {{/if}} +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/NavigationMenuItem.ts b/packages/main/src/NavigationMenuItem.ts index 101eead4a680..b242199f3976 100644 --- a/packages/main/src/NavigationMenuItem.ts +++ b/packages/main/src/NavigationMenuItem.ts @@ -1,6 +1,11 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import type { ClassMap } from "@ui5/webcomponents-base/dist/types.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import MenuItem from "./MenuItem.js"; +import NavigationMenuItemTemplate from "./generated/templates/NavigationMenuItemTemplate.lit.js"; + +// Styles +import navigationMenuItemCss from "./generated/themes/NavigationMenuItem.css.js"; /** * @class @@ -25,7 +30,11 @@ import MenuItem from "./MenuItem.js"; * @since 1.22.0 * @private */ -@customElement("ui5-navigation-menu-item") +@customElement({ + tag: "ui5-navigation-menu-item", + template: NavigationMenuItemTemplate, + styles: [MenuItem.styles, navigationMenuItemCss], +}) class NavigationMenuItem extends MenuItem { /** * Defines the link target URI. Supports standard hyperlink behavior. @@ -62,6 +71,10 @@ class NavigationMenuItem extends MenuItem { @property() target!: string; + get _href() { + return (!this.disabled && this.href) ? this.href : undefined; + } + get _accInfo() { const accInfoSettings = { role: this.href ? "none" : "treeitem", @@ -69,6 +82,14 @@ class NavigationMenuItem extends MenuItem { return { ...super._accInfo, ...accInfoSettings }; } + + get classes(): ClassMap { + const result = super.classes; + + result.main["ui5-navigation-menu-item-root"] = true; + + return result; + } } NavigationMenuItem.define(); diff --git a/packages/main/src/themes/MenuItem.css b/packages/main/src/themes/MenuItem.css index 1f5faa03c869..66d7012d3034 100644 --- a/packages/main/src/themes/MenuItem.css +++ b/packages/main/src/themes/MenuItem.css @@ -1,28 +1,28 @@ -:host([ui5-menu-item][disabled]) { +:host([disabled]) { pointer-events: initial; } /* hovered and active */ -:host([ui5-menu-item][disabled][actionable]:not([active]):not([selected]):hover), -:host([ui5-menu-item][disabled][active][actionable]) { +:host([disabled][actionable]:not([active]):not([selected])):hover, +:host([disabled][active][actionable]) { background: var(--ui5-listitem-background-color); } /* active */ -:host([ui5-menu-item][disabled][active][actionable]) .ui5-li-root .ui5-li-icon { +:host([disabled][active][actionable]) .ui5-li-root .ui5-li-icon { color: var(--sapContent_NonInteractiveIconColor); } -:host([ui5-menu-item][focused]:not([active]):not([disabled])) { +:host([focused]:not([active]):not([disabled])) { background-color: var(--sapList_Hover_Background); } -:host([ui5-menu-item]:not([is-phone]))::part(native-li) { +:host(:not([is-phone]))::part(native-li) { padding: var(--_ui5_menu_item_padding); } -:host([ui5-menu-item])::part(additional-text) { +:host::part(additional-text) { margin-inline-start: var(--_ui5_menu_item_additional_text_start_margin); color: var(--sapContent_LabelColor); min-width: max-content; @@ -39,25 +39,25 @@ visibility: hidden; } -:host([ui5-menu-item])::part(title) { +:host::part(title) { font-size: var(--sapFontSize); padding-top: 0.125rem; } -:host([ui5-menu-item][icon]:not([is-phone]))::part(title), -:host([ui5-menu-item][is-phone]:not([icon=""]))::part(title) { +:host([icon]:not([is-phone]))::part(title), +:host([is-phone]:not([icon=""]))::part(title) { padding-top: 0; } -:host([ui5-menu-item]:not([is-phone]))::part(native-li) { +:host(:not([is-phone]))::part(native-li) { padding: var(--_ui5_menu_item_padding); } -:host([ui5-menu-item][starts-section]) { +:host([starts-section]) { border-top: 1px solid var(--sapGroup_ContentBorderColor); } -:host([ui5-menu-item])::part(content) { +:host::part(content) { padding-inline-end: 0.5rem; } diff --git a/packages/main/src/themes/NavigationMenu.css b/packages/main/src/themes/NavigationMenu.css index b6fb7397dfd1..0c1db5e730fd 100644 --- a/packages/main/src/themes/NavigationMenu.css +++ b/packages/main/src/themes/NavigationMenu.css @@ -4,42 +4,6 @@ padding: var(--_ui5_side_navigation_parent_popup_padding); } -.ui5-navigation-menu ::slotted([ui5-menu-item]).ui5-navigation-menu-item::part(native-li) { - padding-left: 0.5rem; - width: auto; -} -.ui5-navigation-menu ::slotted([ui5-menu-item]).ui5-navigation-menu-item::part(native-li)::after { - border-radius: 0.375rem; - inset: 0; - } - -.ui5-navigation-menu .ui5-navigation-menu-item { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - position: relative; - height: var(--_ui5_side_navigation_item_height); - min-height: var(--_ui5_side_navigation_item_height); - border-radius: 0.375rem; - transition: var(--_ui5_side_navigation_item_transition); - color: var(--sapList_TextColor); - font-weight: bold; - font-size: var(--sapFontSize); - border-bottom: none; -} - -.ui5-navigation-menu .ui5-navigation-menu-item a { - display: flex; - align-items: center; - text-decoration: none; - color: var(--sapList_TextColor); -} - -.ui5-navigation-menu[sub-menu] .ui5-navigation-menu-item { - font-weight: normal; -} - .ui5-menu-rp.ui5-navigation-menu { box-shadow: var(--_ui5_side_navigation_popup_box_shadow); width: auto; @@ -50,26 +14,6 @@ border-radius: 0.75rem; } -.ui5-navigation-menu .ui5-navigation-menu-item .ui5-menu-item-icon-end { - position: relative; - inset-inline-end: auto; - width: 0.75rem; - height: 0.75rem; - color: var(--sapList_TextColor); -} - -.ui5-navigation-menu .ui5-navigation-menu-item::part(icon){ - color: var(--sapList_TextColor); - min-width: 1rem; - min-height: 1rem; -} - -.ui5-navigation-menu ::slotted([ui5-menu-item][focused]):not([active]){ - background: none; -} - -.ui5-sn-item-external-link-icon { - color: var(--_ui5_side_navigation_external_link_icon_color); - min-width: 2rem; - height: 0.875rem; +.ui5-navigation-menu[sub-menu] ::slotted([ui5-navigation-menu-item]) { + font-weight: normal; } \ No newline at end of file diff --git a/packages/main/src/themes/NavigationMenuItem.css b/packages/main/src/themes/NavigationMenuItem.css new file mode 100644 index 000000000000..7b325df154a7 --- /dev/null +++ b/packages/main/src/themes/NavigationMenuItem.css @@ -0,0 +1,54 @@ + +:host::part(native-li) { + padding-left: 0.5rem; + width: auto; +} +:host::part(native-li)::after { + border-radius: 0.375rem; + inset: 0; + } + + :host { + display: flex; + align-items: center; + width: 100%; + box-sizing: border-box; + position: relative; + height: var(--_ui5_side_navigation_item_height); + min-height: var(--_ui5_side_navigation_item_height); + border-radius: 0.375rem; + transition: var(--_ui5_side_navigation_item_transition); + color: var(--sapList_TextColor); + font-weight: bold; + font-size: var(--sapFontSize); + border-bottom: none; +} + +:host a { + text-decoration: none; + color: var(--sapList_TextColor); +} + +.ui5-navigation-menu-item-root .ui5-menu-item-icon-end { + position: relative; + inset-inline-end: auto; + width: 0.75rem; + height: 0.75rem; + color: var(--sapList_TextColor); +} + +.ui5-navigation-menu-item-root .ui5-sn-item-external-link-icon { + color: var(--_ui5_side_navigation_external_link_icon_color); + min-width: 2rem; + height: 0.875rem; +} + +:host::part(icon) { + color: var(--sapList_TextColor); + min-width: 1rem; + min-height: 1rem; +} + +:host([focused]):not([active]) { + background: none; +} \ No newline at end of file From f2617d4575c315cf861436a48862ed561df393d2 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Wed, 14 Feb 2024 15:24:13 +0200 Subject: [PATCH 28/29] feat(ui5-menu): refactor menu items display --- packages/main/src/List.ts | 2 +- packages/main/src/Menu.ts | 13 +++++++------ packages/main/src/NavigationMenu.ts | 2 +- packages/main/src/NavigationMenuItem.hbs | 1 + packages/main/src/themes/NavigationMenuItem.css | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index 08946f10dcfe..0a396ed68bcd 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -1040,7 +1040,7 @@ class List extends UI5Element { focusFirstSelectedItem() { // only enabled items are focusable - const firstSelectedItem = this.getFirstItem(x => x.selected && !x._focusable); + const firstSelectedItem = this.getFirstItem(x => x.selected && x._focusable); if (firstSelectedItem) { firstSelectedItem.focus(); diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 00853d379e9c..655e144be249 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -369,7 +369,9 @@ class Menu extends UI5Element { } _createPopover() { - this._popover = this.shadowRoot!.querySelector("[ui5-responsive-popover]")!; + if (!this._popover) { + this._popover = this.shadowRoot!.querySelector("[ui5-responsive-popover]")!; + } return this._popover; } @@ -447,15 +449,14 @@ class Menu extends UI5Element { subMenu.close(); parentItem.selected = false; if (keyboard) { - this._openedSubMenuItem?.focus(); + parentItem.focus(); } this._openedSubMenuItem = undefined; } } } - async _prepareSubMenuDesktopTablet(item: MenuItem) { - // close opened sub-menu if there is any opened + async _prepareSubMenuDesktopTablet(item: MenuItem): Promise { this._closeItemSubMenu(this._openedSubMenuItem!, true); if (item && item.hasSubmenu) { @@ -521,7 +522,7 @@ class Menu extends UI5Element { } } - async _itemKeyDown(e: KeyboardEvent) { + async _itemKeyDown(e: KeyboardEvent): Promise { const shouldCloseMenu = this.isRtl ? isRight(e) : isLeft(e); const shouldOpenMenu = this.isRtl ? isLeft(e) : isRight(e); @@ -537,7 +538,7 @@ class Menu extends UI5Element { } } - async _itemClick(e: CustomEvent) { + async _itemClick(e: CustomEvent): Promise { const item = e.detail.item as MenuItem; if (!item.hasSubmenu) { diff --git a/packages/main/src/NavigationMenu.ts b/packages/main/src/NavigationMenu.ts index b4649aeaee2e..ed3b3c473424 100644 --- a/packages/main/src/NavigationMenu.ts +++ b/packages/main/src/NavigationMenu.ts @@ -91,7 +91,7 @@ class NavigationMenu extends Menu { return fragment; } - async _itemClick(e: CustomEvent) { + async _itemClick(e: CustomEvent): Promise { const item = e.detail.item as MenuItem; const mainMenu = this._findMainMenu(item); const prevented = !mainMenu.fireEvent("item-click", { diff --git a/packages/main/src/NavigationMenuItem.hbs b/packages/main/src/NavigationMenuItem.hbs index 2d9154af23ee..84ea45039c08 100644 --- a/packages/main/src/NavigationMenuItem.hbs +++ b/packages/main/src/NavigationMenuItem.hbs @@ -1,5 +1,6 @@ {{#if _href}} {{>include "./ListItem.hbs"}} diff --git a/packages/main/src/themes/NavigationMenuItem.css b/packages/main/src/themes/NavigationMenuItem.css index 7b325df154a7..f66e899f6830 100644 --- a/packages/main/src/themes/NavigationMenuItem.css +++ b/packages/main/src/themes/NavigationMenuItem.css @@ -24,7 +24,7 @@ border-bottom: none; } -:host a { +:host .ui5-navmenu-item-link { text-decoration: none; color: var(--sapList_TextColor); } From d638c111c60c32be6747ce84aa47497c920c8960 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Wed, 14 Feb 2024 16:21:15 +0200 Subject: [PATCH 29/29] feat(ui5-menu): refactor menu items display --- packages/main/src/Menu.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/main/src/Menu.ts b/packages/main/src/Menu.ts index 655e144be249..82a76045cd65 100644 --- a/packages/main/src/Menu.ts +++ b/packages/main/src/Menu.ts @@ -579,9 +579,7 @@ class Menu extends UI5Element { } _findMainMenu(element: MenuItem | Menu) { - let parentMenu = element.tagName.toLocaleLowerCase() === "ui5-menu" - ? element as Menu - : element.parentElement as Menu; + let parentMenu = element instanceof Menu ? element : element.parentElement as Menu; while (parentMenu._parentMenuItem) { parentMenu = parentMenu._parentMenuItem.parentElement as Menu; }