Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 66 additions & 65 deletions packages/main/src/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isLeft,
isRight,
isEnter,
isSpace,
isTabNext,
isTabPrevious,
isDown,
Expand All @@ -24,9 +25,9 @@ import type { Timeout } from "@ui5/webcomponents-base/dist/types.js";
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import DOMReferenceConverter from "@ui5/webcomponents-base/dist/converters/DOMReference.js";
import type ResponsivePopover from "./ResponsivePopover.js";
import type MenuItemT from "./MenuItem.js";
import * as MenuItem from "./MenuItem.js";
import type PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js";
import type MenuItem from "./MenuItem.js";
import "./MenuItem.js";
import "./MenuSeparator.js";
import type {
ListItemClickEventDetail,
Expand All @@ -53,11 +54,11 @@ interface IMenuItem extends UI5Element {
}

type MenuItemClickEventDetail = {
item: MenuItem,
item: MenuItemT,
text: string,
}

type MenuBeforeOpenEventDetail = { item?: MenuItem };
type MenuBeforeOpenEventDetail = { item?: MenuItemT };
type MenuBeforeCloseEventDetail = { escPressed: boolean };

/**
Expand Down Expand Up @@ -260,7 +261,7 @@ class Menu extends UI5Element {
}

get _menuItems() {
return this.items.filter((item): item is MenuItem => !item.isSeparator);
return this.items.filter((item): item is MenuItemT => !item.isSeparator);
}

get acessibleNameText() {
Expand All @@ -279,7 +280,7 @@ class Menu extends UI5Element {
this.open = false;
}

_openItemSubMenu(item: MenuItem) {
_openItemSubMenu(item: MenuItemT) {
clearTimeout(this._timeout);

if (!item._popover || item._popover.open) {
Expand All @@ -294,30 +295,20 @@ class Menu extends UI5Element {
item.selected = true;
}

_closeItemSubMenu(item: MenuItem) {
if (item && item._popover) {
const openedSibling = item._menuItems.find(menuItem => menuItem._popover && menuItem._popover.open);
if (openedSibling) {
this._closeItemSubMenu(openedSibling);
}

item._popover.open = false;
item.selected = false;
_itemMouseOver(e: MouseEvent) {
if (!isDesktop()) {
return;
}
}

_itemMouseOver(e: MouseEvent) {
if (isDesktop()) {
// respect mouseover only on desktop
const item = e.target as MenuItem;
const item = e.target as MenuItemT;
if (!MenuItem.isInstanceOfMenuItem(item)) {
return;
}

if (this._isInstanceOfMenuItem(item)) {
item.focus();
item.focus();

// Opens submenu with 300ms delay
this._startOpenTimeout(item);
}
}
// Opens submenu with 300ms delay
this._startOpenTimeout(item);
}

async focus(focusOptions?: FocusOptions): Promise<void> {
Expand All @@ -331,22 +322,31 @@ class Menu extends UI5Element {
return super.focus(focusOptions);
}

_startOpenTimeout(item: MenuItem) {
_closeOtherSubMenus(item: MenuItemT) {
const menuItems = this._menuItems;
if (!menuItems.includes(item)) {
return;
}

menuItems.forEach(menuItem => {
if (menuItem !== item) {
menuItem._close();
}
});
}

_startOpenTimeout(item: MenuItemT) {
clearTimeout(this._timeout);

this._timeout = setTimeout(() => {
const opener = item.parentElement as MenuItem | Menu;
const openedSibling = opener && opener._menuItems.find(menuItem => menuItem._popover && menuItem._popover.open);
if (openedSibling) {
this._closeItemSubMenu(openedSibling);
}
this._closeOtherSubMenus(item);

this._openItemSubMenu(item);
}, MENU_OPEN_DELAY);
}

_itemClick(e: CustomEvent<ListItemClickEventDetail>) {
const item = e.detail.item as MenuItem;
const item = e.detail.item as MenuItemT;

if (!item._popover) {
const prevented = !this.fireDecoratorEvent("item-click", {
Expand All @@ -364,42 +364,47 @@ class Menu extends UI5Element {

_itemKeyDown(e: KeyboardEvent) {
const isTabNextPrevious = isTabNext(e) || isTabPrevious(e);
const item = e.target as MenuItem;
const parentElement = item.parentElement as MenuItem;
const shouldItemNavigation = isUp(e) || isDown(e);
const item = e.target as MenuItemT;

if (!MenuItem.isInstanceOfMenuItem(item)) {
return;
}

const menuItemInMenu = this._menuItems.includes(item);
const isItemNavigation = isUp(e) || isDown(e);
const isItemSelection = isEnter(e) || isSpace(e);
const isEndContentNavigation = isRight(e) || isLeft(e);
const shouldOpenMenu = this.isRtl ? isLeft(e) : isRight(e);
const shouldCloseMenu = !shouldItemNavigation && !shouldOpenMenu && this._isInstanceOfMenuItem(parentElement);
const shouldCloseMenu = menuItemInMenu && !(isItemNavigation || isItemSelection || isEndContentNavigation);

if (this._isInstanceOfMenuItem(item)) {
if (isEnter(e) || isTabNextPrevious) {
e.preventDefault();
}
if (isEnter(e) || isTabNextPrevious) {
e.preventDefault();
}

if (isRight(e) || isLeft(e)) {
item._navigateToEndContent(isLeft(e));
}
if (isEndContentNavigation) {
item._navigateToEndContent(isLeft(e));
}

if (shouldOpenMenu) {
this._openItemSubMenu(item);
} else if ((shouldCloseMenu || isTabNextPrevious) && parentElement._popover) {
parentElement._popover.open = false;
parentElement.selected = false;
parentElement._popover.focusOpener();
}
} else if (isUp(e)) {
this._navigateOutOfEndContent(parentElement);
} else if (isDown(e)) {
this._navigateOutOfEndContent(parentElement, true);
if (shouldOpenMenu) {
this._openItemSubMenu(item);
} else if ((shouldCloseMenu || isTabNextPrevious)) {
this._close();
}
}

_navigateOutOfEndContent(menuItem: MenuItem, isDownwards?: boolean) {
const opener = menuItem?.parentElement as MenuItem | Menu;
const currentIndex = opener._menuItems.indexOf(menuItem);
const nextItem = isDownwards ? opener._menuItems[currentIndex + 1] : opener._menuItems[currentIndex - 1];
const itemToFocus = nextItem || opener._menuItems[currentIndex];
_navigateOutOfEndContent(e: CustomEvent) {
const item = e.target as MenuItemT;
const shouldNavigateToNextItem = e.detail.shouldNavigateToNextItem;
const menuItems = this._menuItems;
const itemIndex = menuItems.indexOf(item);

itemToFocus.focus();
if (itemIndex > -1) {
const nextItem = shouldNavigateToNextItem ? menuItems[itemIndex + 1] : menuItems[itemIndex - 1];
const itemToFocus = nextItem || menuItems[itemIndex];
itemToFocus?.focus();

e.stopPropagation();
}
}

_beforePopoverOpen(e: CustomEvent) {
Expand Down Expand Up @@ -429,10 +434,6 @@ class Menu extends UI5Element {
this.open = false;
this.fireDecoratorEvent("close");
}

_isInstanceOfMenuItem(object: any): object is MenuItem {
return "isMenuItem" in object;
}
}

Menu.define();
Expand Down
109 changes: 103 additions & 6 deletions packages/main/src/MenuItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ import type { AccessibilityAttributes, AriaHasPopup, AriaRole } from "@ui5/webco
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
import {
isLeft,
isRight,
isEnter,
isSpace,
isTabNext,
isTabPrevious,
isDown,
isUp,
} from "@ui5/webcomponents-base/dist/Keys.js";
import { isDesktop, isPhone } from "@ui5/webcomponents-base/dist/Device.js";
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import "@ui5/webcomponents-icons/dist/nav-back.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
Expand All @@ -30,6 +40,8 @@ import menuItemCss from "./generated/themes/MenuItem.css.js";
type MenuBeforeOpenEventDetail = { item?: MenuItem };
type MenuBeforeCloseEventDetail = { escPressed: boolean };

type MenuNavigateOutOfEndContentEventDetail = { shouldNavigateToNextItem: boolean };

type MenuItemAccessibilityAttributes = Pick<AccessibilityAttributes, "ariaKeyShortcuts" | "role"> & ListItemAccessibilityAttributes;

/**
Expand Down Expand Up @@ -88,6 +100,14 @@ type MenuItemAccessibilityAttributes = Pick<AccessibilityAttributes, "ariaKeySho
bubbles: true,
})

/**
* Fired when navigating out of end-content.
* @private
*/
@event("exit-end-content", {
bubbles: true,
})

/**
* Fired before the menu is closed. This event can be cancelled, which will prevent the menu from closing.
* @public
Expand All @@ -110,7 +130,8 @@ class MenuItem extends ListItem implements IMenuItem {
"open": void
"before-close": MenuBeforeCloseEventDetail
"close": void
"close-menu": void
"close-menu": void,
"exit-end-content": MenuNavigateOutOfEndContentEventDetail,
}
/**
* Defines the text of the tree item.
Expand Down Expand Up @@ -272,10 +293,11 @@ class MenuItem extends ListItem implements IMenuItem {
});
}

_navigateToEndContent(isLast?: boolean) {
const item = isLast
? this._navigableItems[this._navigableItems.length - 1]
: this._navigableItems[0];
_navigateToEndContent(shouldNavigateToPreviousItem: boolean) {
const navigatableItems = this._navigableItems;
const item = shouldNavigateToPreviousItem
? navigatableItems[navigatableItems.length - 1]
: navigatableItems[0];

if (item) {
this._itemNavigation.setCurrentItem(item);
Expand Down Expand Up @@ -378,6 +400,72 @@ class MenuItem extends ListItem implements IMenuItem {
return this.items.filter((item): item is MenuItem => !item.isSeparator);
}

_closeOtherSubMenus(item: MenuItem) {
const menuItems = this._menuItems;
if (!menuItems.includes(item)) {
return;
}

menuItems.forEach(menuItem => {
if (menuItem !== item) {
menuItem._close();
}
});
}

_itemMouseOver(e: MouseEvent) {
if (!isDesktop()) {
return;
}
const item = e.target as MenuItem;

if (!isInstanceOfMenuItem(item)) {
return;
}
item.focus();

this._closeOtherSubMenus(item);
}

_itemKeyDown(e: KeyboardEvent) {
const item = e.target as MenuItem;
const itemInMenuItems = this._menuItems.includes(item);
const isTabNextPrevious = isTabNext(e) || isTabPrevious(e);
const isItemNavigation = isUp(e) || isDown(e);
const isItemSelection = isSpace(e) || isEnter(e);
const shouldOpenMenu = this.isRtl ? isLeft(e) : isRight(e);
const shouldCloseMenu = !(isItemNavigation || isItemSelection || shouldOpenMenu) || isTabNextPrevious;

if (itemInMenuItems && shouldCloseMenu) {
this._close();
this.focus();
e.stopPropagation();
}
}

_endContentKeyDown(e: KeyboardEvent) {
const shouldNavigateOutOfEndContent = isUp(e) || isDown(e);

if (shouldNavigateOutOfEndContent) {
this.fireDecoratorEvent("exit-end-content", { shouldNavigateToNextItem: isDown(e) });
}
}

_navigateOutOfEndContent(e: CustomEvent) {
const item = e.target as MenuItem;
const shouldNavigateToNextItem = e.detail.shouldNavigateToNextItem;
const menuItems = this._menuItems;
const itemIndex = menuItems.indexOf(item);

if (itemIndex > -1) {
const nextItem = shouldNavigateToNextItem ? menuItems[itemIndex + 1] : menuItems[itemIndex - 1];
const itemToFocus = nextItem || menuItems[itemIndex];
itemToFocus?.focus();

e.stopPropagation();
}
}

_closeAll() {
if (this._popover) {
this._popover.open = false;
Expand All @@ -389,6 +477,7 @@ class MenuItem extends ListItem implements IMenuItem {
_close() {
if (this._popover) {
this._popover.open = false;
this._menuItems.forEach(item => item._close());
}
this.selected = false;
}
Expand Down Expand Up @@ -434,10 +523,18 @@ class MenuItem extends ListItem implements IMenuItem {

MenuItem.define();

const isInstanceOfMenuItem = (object: any): object is MenuItem => {
return "isMenuItem" in object;
};

export default MenuItem;

export type {
MenuBeforeCloseEventDetail,
MenuBeforeOpenEventDetail,
MenuItemAccessibilityAttributes,
};

export {
isInstanceOfMenuItem,
};
Loading
Loading