Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
79 changes: 35 additions & 44 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 Down Expand Up @@ -285,18 +286,6 @@ 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()) {
// respect mouseover only on desktop
Expand Down Expand Up @@ -326,10 +315,9 @@ class Menu extends UI5Element {
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);
const menuItems = this._menuItems;
if (menuItems.indexOf(item) > -1) {
menuItems.forEach(menuItem => { menuItem !== item && menuItem._close(); });
}

this._openItemSubMenu(item);
Expand All @@ -355,42 +343,45 @@ 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 MenuItem; // Type assignment here is misleading, as item can also be EndContent
const menuItemInMenu = this._menuItems.indexOf(item) > -1;
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 (!this._isInstanceOfMenuItem(item)) {
return;
}

if (isRight(e) || isLeft(e)) {
item._navigateToEndContent(isLeft(e));
}
if (isEnter(e) || isTabNextPrevious) {
e.preventDefault();
}

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 (isEndContentNavigation) {
item._navigateToEndContent(isLeft(e));
}

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 isLast = e.detail.isLast;
const itemIndex = this._menuItems.indexOf(item);

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

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

_beforePopoverOpen(e: CustomEvent) {
Expand Down
85 changes: 83 additions & 2 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 = { isLast: 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("navigate-out", {
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,
"navigate-out": MenuNavigateOutOfEndContentEventDetail,
}
/**
* Defines the text of the tree item.
Expand Down Expand Up @@ -382,6 +403,61 @@ class MenuItem extends ListItem implements IMenuItem {
return this.items.filter((item): item is MenuItem => !item.isSeparator);
}

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

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

const menuItems = this._menuItems;
if (menuItems.indexOf(item) > -1) {
menuItems.forEach(menuItem => { menuItem !== item && menuItem._close(); });
}
}
}
}

_itemKeyDown(e: KeyboardEvent) {
const item = e.target as MenuItem;
const itemInMenuItems = this._menuItems.indexOf(item) > -1;
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("navigate-out", { isLast: isDown(e) });
}
}

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

if (itemIndex > -1) {
const nextItem = isLast ? 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 +469,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 @@ -431,6 +508,10 @@ class MenuItem extends ListItem implements IMenuItem {
this.fireDecoratorEvent("close");
}

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

get isMenuItem(): boolean {
return true;
}
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-navigate-out={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 @@ -54,6 +54,7 @@ export default function MenuTemplate(this: Menu) {
onKeyDown={this._itemKeyDown}
// handles event from slotted children
onui5-close-menu={this._close}
onui5-navigate-out={this._navigateOutOfEndContent}
>
<slot></slot>
</List>)
Expand Down
Loading