Skip to content
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
99 changes: 99 additions & 0 deletions packages/fiori/cypress/specs/SideNavigation.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2051,6 +2051,78 @@ describe("Focusable items", () => {
.should("exist")
.should("have.text", "New");
});

it("shows the full tag in the collapsed popover (not clamped to 6rem)", () => {
cy.mount(
<SideNavigation id="sideNav" collapsed>
<SideNavigationItem id="item1" text="Leads" icon="home">
<Tag slot="tag" design="Set2" colorScheme="6" hideStateIcon>
Beta Long Text Beta Long Text Beta Long Text
</Tag>
<SideNavigationSubItem text="All Leads" />
</SideNavigationItem>
</SideNavigation>
);

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

cy.get("#sideNav")
.shadow()
.find("[ui5-responsive-popover] [ui5-side-navigation-item] > [ui5-tag]")
.then($tag => {
// the popover tag is not truncated: it renders wider than the 6rem rail clamp
expect($tag[0].getBoundingClientRect().width).to.be.greaterThan(96);
});
});

it("renders the overflow menu item tag inline right after the text", () => {
cy.mount(
<SideNavigation id="sideNav" collapsed>
<SideNavigationItem text="dummy" />
<SideNavigationItem text="Contacts">
<Tag slot="tag" design="Set2" colorScheme="6" hideStateIcon>Beta</Tag>
</SideNavigationItem>
</SideNavigation>
);

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

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

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

// the tag renders inline right after the text (tag.left close to text.right)
cy.get("@menuItem").shadow().find(".ui5-navmenu-item-text").then($text => {
const textRight = $text[0].getBoundingClientRect().right;
cy.get("@menuItem").find("[ui5-tag]").should("have.text", "Beta").then($tag => {
const tagLeft = $tag[0].getBoundingClientRect().left;
expect(tagLeft - textRight).to.be.lessThan(16);
});
});
});

it("forces the rail item tag onto a single line when collapsed", () => {
cy.mount(
<SideNavigation id="sideNav" collapsed>
<SideNavigationItem id="item1" text="Favorites Long Text" icon="home">
<Tag slot="tag" design="Set2" colorScheme="6" hideStateIcon>Beta Long Text Beta Long Text</Tag>
</SideNavigationItem>
</SideNavigation>
);

// collapsed: the tag is rendered non-wrapping so max-width truncates it
cy.get("#item1").find("[ui5-tag]").should("have.attr", "wrapping-type", "None");

// expanded: the author's wrapping is restored (no forced None)
cy.get("#sideNav").invoke("prop", "collapsed", false);
cy.get("#item1").find("[ui5-tag]").should("not.have.attr", "wrapping-type");
});
});

describe("Side Navigation no match", () => {
Expand Down Expand Up @@ -2399,6 +2471,33 @@ describe("Side Navigation highlightedText", () => {

cy.window().its("__xss").should("not.exist");
});

it("does not highlight the tag text when the term matches a tag", () => {
cy.mount(
<SideNavigation id="sideNav" highlightedText="Beta">
<SideNavigationItem id="item1" text="Leads">
<Tag slot="tag" design="Set2" hideStateIcon>Beta</Tag>
</SideNavigationItem>
</SideNavigation>
);

// the item stays visible
cy.get("#item1").should("be.visible");

// no highlight span is rendered inside the item (text does not match)
cy.get("#item1")
.shadow()
.find(".ui5-sn-item-highlight")
.should("not.exist");

// the slotted tag text is not wrapped in a highlight span
cy.get("#item1")
.find("[ui5-tag] .ui5-sn-item-highlight")
.should("not.exist");
cy.get("#item1")
.find("[ui5-tag]")
.should("have.text", "Beta");
});
});

describe("Side Navigation search match announcement", () => {
Expand Down
110 changes: 110 additions & 0 deletions packages/fiori/cypress/specs/SideNavigationItem.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import SideNavigation from "../../src/SideNavigation.js";
import SideNavigationItem from "../../src/SideNavigationItem.js";
import SideNavigationSubItem from "../../src/SideNavigationSubItem.js";
import Tag from "@ui5/webcomponents/dist/Tag.js";

describe("SideNavigationItem - indication tag positioning", () => {
it("renders the tag inline right after the text, not right-aligned", () => {
cy.mount(
<SideNavigation style={{ width: "320px" }}>
<SideNavigationItem id="item" text="Nav Item">
<Tag slot="tag" design="Set2" hideStateIcon>Beta</Tag>
<SideNavigationSubItem text="Child" />
</SideNavigationItem>
</SideNavigation>
);

// text and tag share the text-tag container
cy.get("#item")
.shadow()
.find(".ui5-sn-item-text-tag-container")
.as("container");

cy.get("@container").find(".ui5-sn-item-text").should("exist");
cy.get("@container").find(".ui5-sn-item-tag-slot").should("exist");

// the tag sits next to the text, not pushed to the item's trailing edge
cy.get("#item").shadow().find(".ui5-sn-item-text").then($text => {
const textRight = $text[0].getBoundingClientRect().right;

cy.get("#item").shadow().find(".ui5-sn-item-tag-slot").then($tag => {
const tagLeft = $tag[0].getBoundingClientRect().left;
const itemWidth = $text[0].closest(".ui5-sn-item")!.getBoundingClientRect().width;

// tag starts close to where the text ends (within ~1rem gap)
expect(tagLeft - textRight).to.be.lessThan(16);
// and well before the item's trailing edge (not right-aligned)
expect(tagLeft).to.be.lessThan(itemWidth / 2);
});
});
});

it("wraps the tag to a second line when text and tag do not fit inline", () => {
cy.mount(
<SideNavigation style={{ width: "220px" }}>
<SideNavigationItem id="item" text="Customer Relationship Management System" icon="home">
<Tag slot="tag" design="Set2" hideStateIcon>Beta Version</Tag>
</SideNavigationItem>
</SideNavigation>
);

cy.get("#item").shadow().find(".ui5-sn-item-text").then($text => {
const textRect = $text[0].getBoundingClientRect();

cy.get("#item").shadow().find(".ui5-sn-item-tag-slot").then($tag => {
const tagRect = $tag[0].getBoundingClientRect();

// tag dropped to a second line (below the text)
expect(tagRect.top).to.be.greaterThan(textRect.top);
// vertical gap between text and tag is ~0.5rem (8px)
expect(tagRect.top - textRect.bottom).to.be.closeTo(8, 2);
});
});
});

it("describes the item via an invisible 'Tag {text}' string, not the visible tag", () => {
cy.mount(
<SideNavigation style={{ width: "320px" }}>
<SideNavigationItem id="item" text="Leads">
<Tag slot="tag" design="Set2" hideStateIcon>Beta</Tag>
</SideNavigationItem>
</SideNavigation>
);

cy.get("#item").shadow().find(".ui5-sn-item")
.invoke("attr", "aria-describedby")
.then(describedBy => {
const tagId = describedBy!.split(" ").find(id => id.endsWith("-tag"))!;
cy.get("#item").shadow().find(`#${CSS.escape(tagId)}`)
.should("have.class", "ui5-hidden-text")
.and("have.text", "Tag Beta");
});

cy.get("#item").shadow().find(".ui5-sn-item-tag-slot")
.should("have.attr", "aria-hidden", "true");
});

it("describes a sub-item via the invisible 'Tag {text}' string", () => {
cy.mount(
<SideNavigation style={{ width: "320px" }}>
<SideNavigationItem text="Leads" expanded>
<SideNavigationSubItem id="sub" text="Lead Sources">
<Tag slot="tag" design="Set2" hideStateIcon>Beta</Tag>
</SideNavigationSubItem>
</SideNavigationItem>
</SideNavigation>
);

cy.get("#sub").shadow().find(".ui5-sn-item")
.invoke("attr", "aria-describedby")
.then(describedBy => {
const tagId = describedBy!.split(" ").find(id => id.endsWith("-tag"))!;
cy.get("#sub").shadow().find(`#${CSS.escape(tagId)}`)
.should("have.class", "ui5-hidden-text")
.and("have.text", "Tag Beta");
});

cy.get("#sub").shadow().find(".ui5-sn-item-tag-slot")
.should("have.attr", "aria-hidden", "true");
});
});
44 changes: 44 additions & 0 deletions packages/fiori/cypress/specs/visuals/SideNavigationItem.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,50 @@ describe("SideNavigationItem visual", () => {
cy.screenshot();
});

it("with tag and trailing toggle icon", () => {
cy.mount(
<SideNavigation style={{ height: "300px" }}>
<SideNavigationItem text="Leads" icon={home} expanded>
<Tag slot="tag" design="Set2" colorScheme="6" hideStateIcon>New</Tag>
<SideNavigationSubItem text="All Leads" />
</SideNavigationItem>
</SideNavigation>
);
cy.screenshot();
});

it("tag wraps to the second line on long text", () => {
cy.mount(
<SideNavigation style={{ width: "220px", height: "300px" }}>
<SideNavigationItem text="Customer Relationship Management System" icon={home}>
<Tag slot="tag" design="Set2" colorScheme="8" hideStateIcon>Beta Version</Tag>
</SideNavigationItem>
</SideNavigation>
);
cy.screenshot();
});

it("collapsed popover — wrapped tag keeps vertical spacing", () => {
cy.viewport(650, 400);
cy.mount(
<SideNavigation id="sideNav" collapsed style={{ height: "300px" }}>
<SideNavigationItem id="parentItem" text="Favorites Long Text Favorites Long Text" icon={home} expanded unselectable>
<Tag slot="tag" design="Set2" colorScheme="5" hideStateIcon>Beta Long Text Beta Long Text Beta Long Text</Tag>
<SideNavigationSubItem text="My Accounts Long Text My Accounts Long Text">
<Tag slot="tag" design="Set2" colorScheme="8" hideStateIcon>Active Long Text Active Long Text Active Long Text Active Long Text</Tag>
</SideNavigationSubItem>
<SideNavigationSubItem text="My Orders">
<Tag slot="tag" design="Set2" colorScheme="7" hideStateIcon>Pending</Tag>
</SideNavigationSubItem>
<SideNavigationSubItem text="No tag" />
</SideNavigationItem>
</SideNavigation>
);
cy.get("#parentItem").realClick();
cy.get("#sideNav").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened();
cy.screenshot();
});

it("collapsed side navigation", () => {
cy.mount(
<SideNavigation collapsed style={{ height: "300px" }}>
Expand Down
15 changes: 14 additions & 1 deletion packages/fiori/src/NavigationMenuItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import navigationMenuItemCss from "./generated/themes/NavigationMenuItem.css.js"

import {
NAVIGATION_MENU_SELECTABLE_ITEM_HIDDEN_TEXT,
SIDE_NAVIGATION_LIST_ITEM_TAG_TEXT,
} from "./generated/i18n/i18n-defaults.js";

/**
Expand Down Expand Up @@ -114,13 +115,25 @@ class NavigationMenuItem extends MenuItem {
return `${this._id}-tag-container`;
}

get _tagTextId() {
return `${this._id}-tag-text`;
}

get _tagText() {
return this.tag[0]?.textContent?.trim() ?? "";
}

get _tagDescriptionText() {
return this.hasTag ? NavigationMenuItem.i18nBundleFiori.getText(SIDE_NAVIGATION_LIST_ITEM_TAG_TEXT, this._tagText) : undefined;
}

get _ariaDescribedByIds() {
const ids = [
`${this._id}-invisibleText-describedby`,
];

if (this.hasTag) {
ids.push(this._tagContainerId);
ids.push(this._tagTextId);
}

return ids.filter(Boolean).join(" ");
Expand Down
19 changes: 13 additions & 6 deletions packages/fiori/src/NavigationMenuItemTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,19 @@ export default function NavigationMenuItemTemplate(this: NavigationMenuItem, hoo
}

function listItemContent(this: NavigationMenuItem) {
return <>{this.text}</>;
return (
<div class="ui5-navmenu-item-text-tag-container">
<span class="ui5-navmenu-item-text">{this.text}</span>
{this.hasTag &&
<span id={this._tagContainerId} class="ui5-navmenu-item-tag-container" aria-hidden="true">
<slot name="tag"></slot>
</span>
}
{this.hasTag &&
<span id={this._tagTextId} class="ui5-hidden-text">{this._tagDescriptionText}</span>
}
</div>
);
}

function iconBegin(this: NavigationMenuItem) {
Expand All @@ -48,11 +60,6 @@ function iconBegin(this: NavigationMenuItem) {

function iconEnd(this: NavigationMenuItem) {
return (<>
{this.hasTag &&
<span id={this._tagContainerId} class="ui5-navmenu-item-tag-container">
<slot name="tag"></slot>
</span>
}
{this.hasSubmenu &&
<Icon
part="icon"
Expand Down
10 changes: 10 additions & 0 deletions packages/fiori/src/SideNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,16 @@ class SideNavigation extends UI5Element {
item.tag.forEach((tagEl: HTMLElement) => {
const clonedTag = tagEl.cloneNode(true) as HTMLElement;
clonedTag.setAttribute("slot", "tag");
// the popover shows the full tag inline (like expanded mode), so it must
// not inherit the collapsed single-line wrapping forced on the rail item;
// restore the author's original wrapping-type (if any)
const authorWrapping = clonedTag.getAttribute("data-ui5-tag-wrapping-synced");
clonedTag.removeAttribute("data-ui5-tag-wrapping-synced");
if (authorWrapping) {
clonedTag.setAttribute("wrapping-type", authorWrapping);
} else {
clonedTag.removeAttribute("wrapping-type");
}
ref.appendChild(clonedTag);
});
}
Expand Down
11 changes: 11 additions & 0 deletions packages/fiori/src/SideNavigationItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
SIDE_NAVIGATION_ICON_EXPAND,
SIDE_NAVIGATION_OVERFLOW_ITEM_LABEL,
SIDE_NAVIGATION_PARENT_ITEM_SELECTABLE_DESCRIPTION,
SIDE_NAVIGATION_LIST_ITEM_TAG_TEXT,
} from "./generated/i18n/i18n-defaults.js";
import type { DefaultSlot, Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";
import "@ui5/webcomponents/dist/Tag.js";
Expand Down Expand Up @@ -110,6 +111,8 @@ class SideNavigationItem extends SideNavigationSelectableItemBase {
this.items.forEach(item => {
item._parentDisabled = this.effectiveDisabled;
});

this._syncTagWrapping();
}

get overflowItems() : Array<SideNavigationItem> {
Expand Down Expand Up @@ -233,6 +236,14 @@ class SideNavigationItem extends SideNavigationSelectableItemBase {
return !!this.tag.length;
}

get _tagText() {
return this.tag[0]?.textContent?.trim() ?? "";
}

get _tagDescriptionText() {
return this.hasTag ? SideNavigationItem.i18nBundle.getText(SIDE_NAVIGATION_LIST_ITEM_TAG_TEXT, this._tagText) : undefined;
}

get _textId() {
return `${this._id}-text`;
}
Expand Down
Loading
Loading