Skip to content

Commit 7a4f054

Browse files
authored
chore(ui5-side-navigation): make click event cancelable (#11271)
- Click event for <ui5-side-navigation-item> is cancelable - Enriched with event parameters about key modifiers: -- altKey -- ctrlKey -- metaKey -- shiftKey JIRA: BGSOFUIRODOPI-3433 Fixes: #10603
1 parent 8702780 commit 7a4f054

File tree

8 files changed

+360
-40
lines changed

8 files changed

+360
-40
lines changed

packages/fiori/cypress/specs/SideNavigation.cy.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,136 @@ describe("Side Navigation interaction", () => {
473473
cy.url().should("not.include", "#test");
474474
});
475475

476+
it("Tests preventDefault of 'click' event", () => {
477+
const handleClick = (event: Event) => {
478+
event.preventDefault();
479+
};
480+
481+
const handleSelectionChange = cy.stub().as("selectionChangeHandler");
482+
483+
cy.mount(
484+
<SideNavigation id="sideNav" onClick={handleClick} onSelectionChange={handleSelectionChange}>
485+
<SideNavigationItem id="linkItem" text="external link" unselectable={true} href="#preventDefault" />
486+
<SideNavigationItem id="item" text="item"/>
487+
</SideNavigation>
488+
);
489+
490+
cy.url()
491+
.should("not.include", "#preventDefault");
492+
493+
// Act
494+
cy.get("#linkItem").realClick();
495+
496+
// Assert
497+
cy.get("@selectionChangeHandler").should("not.have.been.called");
498+
cy.url()
499+
.should("not.include", "#preventDefault");
500+
501+
cy.get("#item").realClick();
502+
503+
// Assert
504+
cy.get("@selectionChangeHandler").should("not.have.been.called");
505+
cy.url()
506+
.should("not.include", "#preventDefault");
507+
});
508+
509+
it("Tests preventDefault of items in overflow menu", () => {
510+
const handleClick = (event: Event) => {
511+
event.preventDefault();
512+
};
513+
514+
const handleSelectionChange = cy.stub().as("selectionChangeHandler");
515+
516+
cy.mount(
517+
<SideNavigation id="sideNav" collapsed={true} onClick={handleClick} onSelectionChange={handleSelectionChange}>
518+
<SideNavigationItem unselectable={true} href="#test" text="link"></SideNavigationItem>
519+
<SideNavigationItem text="item"></SideNavigationItem>
520+
</SideNavigation>
521+
);
522+
523+
cy.get("#sideNav")
524+
.invoke("attr", "style", "height: 50px");
525+
526+
cy.get("#sideNav")
527+
.shadow()
528+
.find(".ui5-sn-item-overflow")
529+
.realClick();
530+
531+
cy.get("#sideNav")
532+
.shadow()
533+
.find(".ui5-side-navigation-overflow-menu [ui5-navigation-menu-item][text='link']")
534+
.realClick();
535+
536+
cy.url()
537+
.should("not.include", "#test");
538+
539+
cy.get("#sideNav")
540+
.shadow()
541+
.find(".ui5-side-navigation-overflow-menu [ui5-navigation-menu-item][text='item']")
542+
.realClick();
543+
544+
cy.get("@selectionChangeHandler").should("not.have.been.called");
545+
});
546+
547+
it("Tests preventDefault on child items in collapsed side navigation", () => {
548+
const handleClick = (event: Event) => {
549+
event.preventDefault();
550+
};
551+
552+
const handleSelectionChange = cy.stub().as("selectionChangeHandler");
553+
554+
cy.mount(
555+
<SideNavigation onClick={handleClick} onSelectionChange={handleSelectionChange} id="sideNav" collapsed={true}>
556+
<SideNavigationItem id="parentItem" text="2">
557+
<SideNavigationItem text="child" />
558+
<SideNavigationItem href="#test" text="link"></SideNavigationItem>
559+
</SideNavigationItem>
560+
</SideNavigation>
561+
);
562+
563+
cy.get("#parentItem")
564+
.realClick();
565+
566+
cy.get("#sideNav")
567+
.shadow()
568+
.find("[ui5-responsive-popover] [ui5-side-navigation-sub-item][text='child']")
569+
.realClick();
570+
571+
// Assert
572+
cy.get("@selectionChangeHandler").should("not.have.been.called");
573+
574+
cy.get("#sideNav")
575+
.shadow()
576+
.find("[ui5-responsive-popover] [ui5-side-navigation-sub-item][text='link']")
577+
.realClick();
578+
579+
cy.get("@selectionChangeHandler").should("not.have.been.called");
580+
cy.url()
581+
.should("not.include", "#test");
582+
});
583+
584+
it("Tests key modifiers when item is clicked", () => {
585+
const handleClick = cy.stub().as("clickHandler");
586+
587+
cy.mount(
588+
<SideNavigation id="sideNav" onClick={e => {e.preventDefault(); handleClick(e);}}>
589+
<SideNavigationItem id="linkItem" text="external link"/>
590+
</SideNavigation>
591+
);
592+
593+
const keyModifiers = [
594+
{ key: "ctrlKey", options: { ctrlKey: true } },
595+
{ key: "metaKey", options: { metaKey: true } },
596+
{ key: "altKey", options: { altKey: true } },
597+
{ key: "shiftKey", options: { shiftKey: true } },
598+
];
599+
600+
keyModifiers.forEach(({ key, options }) => {
601+
cy.get("#sideNav").realClick(options);
602+
cy.get("@clickHandler").should("be.calledWithMatch", { detail: { [key]: true } });
603+
});
604+
});
605+
476606
it("Tests 'selection-change' event", () => {
477607
cy.mount(
478608
<SideNavigation id="sideNav">

packages/fiori/src/NavigationMenuItem.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import property from "@ui5/webcomponents-base/dist/decorators/property.js";
55
import MenuItem from "@ui5/webcomponents/dist/MenuItem.js";
66
import type SideNavigationItemDesign from "./types/SideNavigationItemDesign.js";
77
import NavigationMenu from "./NavigationMenu.js";
8+
import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js";
9+
import type SideNavigationSelectableItemBase from "./SideNavigationSelectableItemBase.js";
810

911
// Templates
1012
import NavigationMenuItemTemplate from "./NavigationMenuItemTemplate.js";
@@ -15,6 +17,7 @@ import navigationMenuItemCss from "./generated/themes/NavigationMenuItem.css.js"
1517
import {
1618
NAVIGATION_MENU_POPOVER_HIDDEN_TEXT,
1719
} from "./generated/i18n/i18n-defaults.js";
20+
import type SideNavigationItem from "./SideNavigationItem.js";
1821

1922
/**
2023
* @class
@@ -79,6 +82,8 @@ class NavigationMenuItem extends MenuItem {
7982
@property()
8083
design: `${SideNavigationItemDesign}` = "Default";
8184

85+
associatedItem?: SideNavigationSelectableItemBase;
86+
8287
get isExternalLink() {
8388
return this.href && this.target === "_blank";
8489
}
@@ -107,6 +112,101 @@ class NavigationMenuItem extends MenuItem {
107112
return result;
108113
}
109114

115+
_onclick(e: MouseEvent) {
116+
this._activate(e);
117+
}
118+
119+
_activate(e: MouseEvent | KeyboardEvent) {
120+
e.stopPropagation();
121+
122+
const item = this.associatedItem;
123+
124+
if (this.disabled || !item) {
125+
return;
126+
}
127+
128+
const sideNav = item.sideNavigation;
129+
const overflowMenu = sideNav?.getOverflowPopover();
130+
const isSelectable = item.isSelectable;
131+
132+
const executeEvent = item.fireDecoratorEvent("click", {
133+
altKey: e.altKey,
134+
ctrlKey: e.ctrlKey,
135+
metaKey: e.metaKey,
136+
shiftKey: e.shiftKey,
137+
});
138+
139+
if (!executeEvent) {
140+
e.preventDefault();
141+
142+
if (this.hasSubmenu) {
143+
overflowMenu?._openItemSubMenu(this);
144+
} else {
145+
sideNav?.closeMenu();
146+
}
147+
148+
return;
149+
}
150+
151+
const shouldSelect = !this.hasSubmenu && isSelectable;
152+
153+
if (this.hasSubmenu) {
154+
overflowMenu?._openItemSubMenu(this);
155+
}
156+
157+
if (shouldSelect) {
158+
sideNav?._selectItem(item);
159+
}
160+
161+
if (!this.hasSubmenu) {
162+
sideNav?.closeMenu();
163+
this._handleFocus(item);
164+
}
165+
}
166+
167+
_handleFocus(associatedItem: SideNavigationSelectableItemBase) {
168+
const sideNavigation = associatedItem.sideNavigation;
169+
170+
if (associatedItem.nodeName.toLowerCase() === "ui5-side-navigation-sub-item") {
171+
const parent = associatedItem.parentElement as SideNavigationItem;
172+
sideNavigation?.focusItem(parent);
173+
parent?.focus();
174+
} else {
175+
sideNavigation?.focusItem(associatedItem);
176+
associatedItem?.focus();
177+
}
178+
}
179+
180+
async _onkeydown(e: KeyboardEvent): Promise<void> {
181+
if (isSpace(e)) {
182+
e.preventDefault();
183+
}
184+
185+
if (isEnter(e)) {
186+
this._activate(e);
187+
}
188+
189+
return Promise.resolve();
190+
}
191+
192+
_onkeyup(e: KeyboardEvent) {
193+
if (isSpace(e)) {
194+
this._activate(e);
195+
196+
if (this.href && !e.defaultPrevented) {
197+
const customEvent = new MouseEvent("click");
198+
199+
customEvent.stopImmediatePropagation();
200+
if (this.getDomRef()!.querySelector("a")) {
201+
this.getDomRef()!.querySelector("a")!.dispatchEvent(customEvent);
202+
} else {
203+
// when Side Navigation is collapsed and it is first level item we have directly <a> element
204+
this.getDomRef()!.dispatchEvent(customEvent);
205+
}
206+
}
207+
}
208+
}
209+
110210
get acessibleNameText() {
111211
return NavigationMenu.i18nBundle.getText(NAVIGATION_MENU_POPOVER_HIDDEN_TEXT);
112212
}

packages/fiori/src/NavigationMenuItemTemplate.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function iconEnd(this: NavigationMenuItem) {
5555
/>;
5656
}
5757

58-
if (this.href) {
58+
if (this.isExternalLink) {
5959
return <Icon
6060
class="ui5-sn-item-external-link-icon"
6161
name={arrowRightIcon}

0 commit comments

Comments
 (0)