Skip to content

chore(ui5-side-navigation): make click event cancelable #11271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
130 changes: 130 additions & 0 deletions packages/fiori/cypress/specs/SideNavigation.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,136 @@ describe("Side Navigation interaction", () => {
cy.url().should("not.include", "#test");
});

it("Tests preventDefault of 'click' event", () => {
const handleClick = (event: Event) => {
event.preventDefault();
};

const handleSelectionChange = cy.stub().as("selectionChangeHandler");

cy.mount(
<SideNavigation id="sideNav" onClick={handleClick} onSelectionChange={handleSelectionChange}>
<SideNavigationItem id="linkItem" text="external link" unselectable={true} href="#preventDefault" />
<SideNavigationItem id="item" text="item"/>
</SideNavigation>
);

cy.url()
.should("not.include", "#preventDefault");

// Act
cy.get("#linkItem").realClick();

// Assert
cy.get("@selectionChangeHandler").should("not.have.been.called");
cy.url()
.should("not.include", "#preventDefault");

cy.get("#item").realClick();

// Assert
cy.get("@selectionChangeHandler").should("not.have.been.called");
cy.url()
.should("not.include", "#preventDefault");
});

it("Tests preventDefault of items in overflow menu", () => {
const handleClick = (event: Event) => {
event.preventDefault();
};

const handleSelectionChange = cy.stub().as("selectionChangeHandler");

cy.mount(
<SideNavigation id="sideNav" collapsed={true} onClick={handleClick} onSelectionChange={handleSelectionChange}>
<SideNavigationItem unselectable={true} href="#test" text="link"></SideNavigationItem>
<SideNavigationItem text="item"></SideNavigationItem>
</SideNavigation>
);

cy.get("#sideNav")
.invoke("attr", "style", "height: 50px");

cy.get("#sideNav")
.shadow()
.find(".ui5-sn-item-overflow")
.realClick();

cy.get("#sideNav")
.shadow()
.find(".ui5-side-navigation-overflow-menu [ui5-navigation-menu-item][text='link']")
.realClick();

cy.url()
.should("not.include", "#test");

cy.get("#sideNav")
.shadow()
.find(".ui5-side-navigation-overflow-menu [ui5-navigation-menu-item][text='item']")
.realClick();

cy.get("@selectionChangeHandler").should("not.have.been.called");
});

it("Tests preventDefault on child items in collapsed side navigation", () => {
const handleClick = (event: Event) => {
event.preventDefault();
};

const handleSelectionChange = cy.stub().as("selectionChangeHandler");

cy.mount(
<SideNavigation onClick={handleClick} onSelectionChange={handleSelectionChange} id="sideNav" collapsed={true}>
<SideNavigationItem id="parentItem" text="2">
<SideNavigationItem text="child" />
<SideNavigationItem href="#test" text="link"></SideNavigationItem>
</SideNavigationItem>
</SideNavigation>
);

cy.get("#parentItem")
.realClick();

cy.get("#sideNav")
.shadow()
.find("[ui5-responsive-popover] [ui5-side-navigation-sub-item][text='child']")
.realClick();

// Assert
cy.get("@selectionChangeHandler").should("not.have.been.called");

cy.get("#sideNav")
.shadow()
.find("[ui5-responsive-popover] [ui5-side-navigation-sub-item][text='link']")
.realClick();

cy.get("@selectionChangeHandler").should("not.have.been.called");
cy.url()
.should("not.include", "#test");
});

it("Tests key modifiers when item is clicked", () => {
const handleClick = cy.stub().as("clickHandler");

cy.mount(
<SideNavigation id="sideNav" onClick={e => {e.preventDefault(); handleClick(e);}}>
<SideNavigationItem id="linkItem" text="external link"/>
</SideNavigation>
);

const keyModifiers = [
{ key: "ctrlKey", options: { ctrlKey: true } },
{ key: "metaKey", options: { metaKey: true } },
{ key: "altKey", options: { altKey: true } },
{ key: "shiftKey", options: { shiftKey: true } },
];

keyModifiers.forEach(({ key, options }) => {
cy.get("#sideNav").realClick(options);
cy.get("@clickHandler").should("be.calledWithMatch", { detail: { [key]: true } });
});
});

it("Tests 'selection-change' event", () => {
cy.mount(
<SideNavigation id="sideNav">
Expand Down
100 changes: 100 additions & 0 deletions packages/fiori/src/NavigationMenuItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import MenuItem from "@ui5/webcomponents/dist/MenuItem.js";
import type SideNavigationItemDesign from "./types/SideNavigationItemDesign.js";
import NavigationMenu from "./NavigationMenu.js";
import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js";
import type SideNavigationSelectableItemBase from "./SideNavigationSelectableItemBase.js";

// Templates
import NavigationMenuItemTemplate from "./NavigationMenuItemTemplate.js";
Expand All @@ -15,6 +17,7 @@ import navigationMenuItemCss from "./generated/themes/NavigationMenuItem.css.js"
import {
NAVIGATION_MENU_POPOVER_HIDDEN_TEXT,
} from "./generated/i18n/i18n-defaults.js";
import type SideNavigationItem from "./SideNavigationItem.js";

/**
* @class
Expand Down Expand Up @@ -79,6 +82,8 @@ class NavigationMenuItem extends MenuItem {
@property()
design: `${SideNavigationItemDesign}` = "Default";

associatedItem?: SideNavigationSelectableItemBase;

get isExternalLink() {
return this.href && this.target === "_blank";
}
Expand Down Expand Up @@ -107,6 +112,101 @@ class NavigationMenuItem extends MenuItem {
return result;
}

_onclick(e: MouseEvent) {
this._activate(e);
}

_activate(e: MouseEvent | KeyboardEvent) {
e.stopPropagation();

const item = this.associatedItem;

if (this.disabled || !item) {
return;
}

const sideNav = item.sideNavigation;
const overflowMenu = sideNav?.getOverflowPopover();
const isSelectable = item.isSelectable;

const executeEvent = item.fireDecoratorEvent("click", {
altKey: e.altKey,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
shiftKey: e.shiftKey,
});

if (!executeEvent) {
e.preventDefault();

if (this.hasSubmenu) {
overflowMenu?._openItemSubMenu(this);
} else {
sideNav?.closeMenu();
}

return;
}

const shouldSelect = !this.hasSubmenu && isSelectable;

if (this.hasSubmenu) {
overflowMenu?._openItemSubMenu(this);
}

if (shouldSelect) {
sideNav?._selectItem(item);
}

if (!this.hasSubmenu) {
sideNav?.closeMenu();
this._handleFocus(item);
}
}

_handleFocus(associatedItem: SideNavigationSelectableItemBase) {
const sideNavigation = associatedItem.sideNavigation;

if (associatedItem.nodeName.toLowerCase() === "ui5-side-navigation-sub-item") {
const parent = associatedItem.parentElement as SideNavigationItem;
sideNavigation?.focusItem(parent);
parent?.focus();
} else {
sideNavigation?.focusItem(associatedItem);
associatedItem?.focus();
}
}

async _onkeydown(e: KeyboardEvent): Promise<void> {
if (isSpace(e)) {
e.preventDefault();
}

if (isEnter(e)) {
this._activate(e);
}

return Promise.resolve();
}

_onkeyup(e: KeyboardEvent) {
if (isSpace(e)) {
this._activate(e);

if (this.href && !e.defaultPrevented) {
const customEvent = new MouseEvent("click");

customEvent.stopImmediatePropagation();
if (this.getDomRef()!.querySelector("a")) {
this.getDomRef()!.querySelector("a")!.dispatchEvent(customEvent);
} else {
// when Side Navigation is collapsed and it is first level item we have directly <a> element
this.getDomRef()!.dispatchEvent(customEvent);
}
}
}
}

get acessibleNameText() {
return NavigationMenu.i18nBundle.getText(NAVIGATION_MENU_POPOVER_HIDDEN_TEXT);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/fiori/src/NavigationMenuItemTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function iconEnd(this: NavigationMenuItem) {
/>;
}

if (this.href) {
if (this.isExternalLink) {
return <Icon
class="ui5-sn-item-external-link-icon"
name={arrowRightIcon}
Expand Down
Loading
Loading