Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
116 changes: 58 additions & 58 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,8 @@ 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 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 Down Expand Up @@ -294,30 +294,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 MenuItem;
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,15 +321,24 @@ class Menu extends UI5Element {
return super.focus(focusOptions);
}

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

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

_startOpenTimeout(item: MenuItem) {
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);
Expand All @@ -365,41 +364,46 @@ 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);

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 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();

itemToFocus.focus();
e.stopPropagation();
}
}

_beforePopoverOpen(e: CustomEvent) {
Expand Down Expand Up @@ -429,10 +433,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
105 changes: 99 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 @@ -382,6 +404,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 (!MenuItem._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 @@ -393,6 +481,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,6 +523,10 @@ class MenuItem extends ListItem implements IMenuItem {
get isMenuItem(): boolean {
return true;
}

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

MenuItem.define();
Expand Down
5 changes: 4 additions & 1 deletion packages/main/src/MenuItemTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function rightContent(this: MenuItem) {
</div>
);
case this.hasEndContent:
return <slot name="endContent"></slot>;
return <slot name="endContent" onKeyDown={this._endContentKeyDown}></slot>;
case !!this.additionalText:
return (
<span
Expand Down Expand Up @@ -123,8 +123,11 @@ function listItemPostContent(this: MenuItem) {
accessibleRole="Menu"
loading={this.loading}
loadingDelay={this.loadingDelay}
onMouseOver={this._itemMouseOver}
onKeyDown={this._itemKeyDown}
// handles event from slotted children
onui5-close-menu={this._close}
onui5-exit-end-content={this._navigateOutOfEndContent}
>
<slot></slot>
</List>
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/MenuTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default function MenuTemplate(this: Menu) {
onKeyDown={this._itemKeyDown}
// handles event from slotted children
onui5-close-menu={this._close}
onui5-exit-end-content={this._navigateOutOfEndContent}
>
<slot></slot>
</List>)
Expand Down
6 changes: 1 addition & 5 deletions packages/main/test/pages/Menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
<ui5-menu-separator></ui5-menu-separator>
<ui5-menu-item text="Open" icon="open-folder" accessible-name="Choose platform">
<ui5-menu-item text="Open Locally" icon="open-folder" additional-text="Ctrl+K">
<ui5-menu-item text="Open from C"></ui5-menu-item>
<ui5-menu-item text="Open from C" id="menuItemFromC"></ui5-menu-item>
<ui5-menu-item text="Open from D"></ui5-menu-item>
<ui5-menu-separator></ui5-menu-separator>
<ui5-menu-item text="Open from E" disabled></ui5-menu-item>
Expand Down Expand Up @@ -298,10 +298,6 @@
role: "menuitemcheckbox",
};

menuItemFromD.accessibilityAttributes = {
role: "menuitemcheckbox",
};

</script>
</body>
</html>
Loading