Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
faea273
feat(ui5-side-navigation): add indication tag slot
s-todorova Apr 21, 2026
836ecbc
fix: popover items cloning
s-todorova Apr 24, 2026
a77a668
fix: lint errors
s-todorova Apr 27, 2026
095bdb8
Merge remote-tracking branch 'origin/main' into sidenavigation_tags_poc
s-todorova Apr 27, 2026
27391ed
Merge branch 'main' into sidenavigation_tags_poc
s-todorova Apr 27, 2026
33bfb4c
Merge branch 'main' into sidenavigation_tags_poc
s-todorova May 19, 2026
044cb74
chore: add public samples and documentation
s-todorova May 19, 2026
1d0cf16
Merge remote-tracking branch 'origin/main' into sidenavigation_tags_poc
s-todorova May 19, 2026
c22163d
fix: address code review comments
s-todorova May 21, 2026
06c356d
Merge branch 'main' into sidenavigation_tags_poc
s-todorova May 21, 2026
bdba289
chore: address code review comments
s-todorova May 22, 2026
3f6423c
Merge branch 'main' into sidenavigation_tags_poc
s-todorova May 22, 2026
f0b24de
Merge branch 'main' into sidenavigation_tags_poc
s-todorova May 26, 2026
4737658
chore: add tests and update documentation
s-todorova May 26, 2026
6a3c531
Merge branch 'main' into sidenavigation_tags_poc
s-todorova May 26, 2026
7838929
chore: remove accidentally committed .claude files
s-todorova May 26, 2026
6f0ae98
chore: address review comments
s-todorova May 26, 2026
9a0f5b5
chore: delete commented out code
s-todorova May 26, 2026
8a91f92
Merge branch 'main' into sidenavigation_tags_poc
s-todorova May 26, 2026
65b89ac
chore: update since tag version
s-todorova May 26, 2026
f28affc
Merge branch 'main' into sidenavigation_tags_poc
s-todorova May 26, 2026
d4ce746
Merge branch 'main' into sidenavigation_tags_poc
s-todorova May 26, 2026
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
17 changes: 16 additions & 1 deletion packages/fiori/src/NavigationMenuItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,28 @@ class NavigationMenuItem extends MenuItem {
return (!this.disabled && this.href) ? this.href : undefined;
}
Comment thread
s-todorova marked this conversation as resolved.

get _tagContainerId() {
return `${this._id}-tag-container`;
}

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

if (this.hasEndContent) {
ids.push(this._tagContainerId);
}

return ids.filter(Boolean).join(" ");
}

get _accInfo() {
const accInfo = super._accInfo;

accInfo.role = "none";

if (this.hasSubmenu && this.associatedItem?.isSelectable) {
// For the menu item on first level (parent item)
accInfo.ariaSelectedText = NavigationMenuItem.i18nBundleFiori.getText(NAVIGATION_MENU_SELECTABLE_ITEM_HIDDEN_TEXT);
}

Expand Down
34 changes: 20 additions & 14 deletions packages/fiori/src/NavigationMenuItemTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,24 @@ function iconBegin(this: NavigationMenuItem) {
}

function iconEnd(this: NavigationMenuItem) {
if (this.hasSubmenu) {
return <Icon
part="icon"
name={slimArrowRightIcon}
class="ui5-menu-item-icon-end"
/>;
}

if (this.isExternalLink) {
return <Icon
class="ui5-sn-item-external-link-icon"
name={arrowRightIcon}
/>;
}
return (<>
{this.hasEndContent &&
Comment thread
s-todorova marked this conversation as resolved.
Outdated
<span id={this._tagContainerId} class="ui5-navmenu-item-tag-container">
<slot name="endContent"></slot>
</span>
}
{this.hasSubmenu &&
<Icon
part="icon"
name={slimArrowRightIcon}
class="ui5-menu-item-icon-end"
/>
}
{!this.hasSubmenu && this.isExternalLink &&
<Icon
class="ui5-sn-item-external-link-icon"
name={arrowRightIcon}
/>
}
</>);
}
52 changes: 51 additions & 1 deletion packages/fiori/src/SideNavigationItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
SIDE_NAVIGATION_OVERFLOW_ITEM_LABEL,
SIDE_NAVIGATION_PARENT_ITEM_SELECTABLE_DESCRIPTION,
} from "./generated/i18n/i18n-defaults.js";
import type { DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js";
import type { DefaultSlot, Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";
import "@ui5/webcomponents/dist/Tag.js";

// Templates
import SideNavigationItemTemplate from "./SideNavigationItemTemplate.js";
Expand Down Expand Up @@ -81,6 +82,21 @@ class SideNavigationItem extends SideNavigationSelectableItemBase {
@slot({ type: HTMLElement, invalidateOnChildChange: true, "default": true })
items!: DefaultSlot<SideNavigationSubItem>;

/**
* Defines the tag to be displayed.
*
* **Note:** Only one `ui5-tag` is allowed. The tag should use `design="Set2"`, `hide-state-icon`,
* and `colorScheme` values 5-10 to avoid confusion with semantic colors (1-4).
*
* **Note:** It is recommended to limit tag width to 64px (4rem). If tag text exceeds this,
* use shortened forms or abbreviations (e.g., "Experimental" → "Exp").
*
* @public
* @since 2.7.0
*/
@slot({ type: HTMLElement })
tag!: Slot<HTMLElement>;

@i18n("@ui5/webcomponents-fiori")
static i18nBundle: I18nBundle;

Expand Down Expand Up @@ -183,9 +199,43 @@ class SideNavigationItem extends SideNavigationSelectableItemBase {
}

get _describedBy() {
const parts: string[] = [];

if (this.hasTag) {
parts.push(this._tagId);
}

if (!this.effectiveDisabled && this.items.length && !this.unselectable) {
parts.push(this._selectableItemDescriptionId);
}

return parts.length > 0 ? parts.join(" ") : undefined;
}

get _selectableItemDescriptionId() {
return `${this._id}-selectable-description`;
}

get _selectableItemDescriptionText() {
if (!this.effectiveDisabled && this.items.length && !this.unselectable) {
return SideNavigationItem.i18nBundle.getText(SIDE_NAVIGATION_PARENT_ITEM_SELECTABLE_DESCRIPTION, this.text ?? "");
}
return undefined;
}

get hasTag() {
return !!this.tag.length;
}

Comment thread
s-todorova marked this conversation as resolved.
get _textId() {
return `${this._id}-text`;
}

get _textAriaLabelledBy() {
Comment thread
s-todorova marked this conversation as resolved.
Outdated
if (this.hasTag) {
return `${this._textId} ${this._tagId}`;
}
return undefined;
}

get classesArray() {
Expand Down
10 changes: 9 additions & 1 deletion packages/fiori/src/SideNavigationItemTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ function ItemTemplate(this: SideNavigationItem) {
:
this.icon && <Icon class="ui5-sn-item-icon" name={this.icon}/>
}
<div class="ui5-sn-item-text">{this.text}</div>
<div class="ui5-sn-item-text" id={this._textId} aria-labelledby={this._textAriaLabelledBy}>{this.text}</div>
Comment thread
s-todorova marked this conversation as resolved.
Outdated
{this.hasTag &&
<div id={this._tagId} class="ui5-sn-item-tag-slot">
<slot name="tag"></slot>
</div>
}
{this.sideNavCollapsed ?
!!this.items.length &&
<Icon class="ui5-sn-item-toggle-icon"
Expand Down Expand Up @@ -81,6 +86,9 @@ function ItemTemplate(this: SideNavigationItem) {
<slot></slot>
</ul>
}
{this._selectableItemDescriptionText &&
<span id={this._selectableItemDescriptionId} class="ui5-hidden-text">{this._selectableItemDescriptionText}</span>
}
</>
);
}
65 changes: 57 additions & 8 deletions packages/fiori/src/SideNavigationPopoverTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,21 @@ export default function SideNavigationTemplate(this: SideNavigation) {
target={item.target}
title={item.title}
tooltip={item._tooltip}
ref={this.captureRef.bind(item)}
ref={(el: HTMLElement | null) => {
Comment thread
s-todorova marked this conversation as resolved.
Outdated
if (el && item.tag.length > 0) {
const existingTags = Array.from(el.children).filter(child => child.getAttribute("slot") === "endContent");
if (existingTags.length === 0) {
item.tag.forEach(tagEl => {
const clonedTag = tagEl.cloneNode(true) as HTMLElement;
clonedTag.slot = "endContent";
el.appendChild(clonedTag);
});
}
}
this.captureRef.bind(item)(el);
}}
>

{item.children.length > 0 && !item.unselectable &&
{(item as SideNavigationItem).items?.length > 0 && !item.unselectable &&
(<NavigationMenuItem
class="ui5-navigation-menu-item-root-parent"
accessibilityAttributes={item.accessibilityAttributes}
Expand All @@ -31,8 +42,21 @@ export default function SideNavigationTemplate(this: SideNavigation) {
target={item.target}
title={item.title}
tooltip={item._tooltip}
ref={this.captureRef.bind(item)}
></NavigationMenuItem>)
ref={(el: HTMLElement | null) => {
if (el && item.tag.length > 0) {
const existingTags = Array.from(el.children).filter(child => child.getAttribute("slot") === "endContent");
if (existingTags.length === 0) {
item.tag.forEach(tagEl => {
const clonedTag = tagEl.cloneNode(true) as HTMLElement;
clonedTag.slot = "endContent";
el.appendChild(clonedTag);
});
}
}
this.captureRef.bind(item)(el);
}}
>
</NavigationMenuItem>)
}

{(item as any).items?.map(renderMenuItem)}
Expand Down Expand Up @@ -79,7 +103,19 @@ export default function SideNavigationTemplate(this: SideNavigation) {
selected={this._popoverContents.item.selected}
unselectable={this._popoverContents.item.unselectable}
onui5-click={this.handlePopupItemClick}
ref={this.captureRef.bind(this._popoverContents.item)}
ref={(el: HTMLElement | null) => {
if (el && this._popoverContents.item.tag.length > 0) {
const existingTags = Array.from(el.children).filter(child => child.getAttribute("slot") === "tag");
if (existingTags.length === 0) {
this._popoverContents.item.tag.forEach(tagEl => {
const clonedTag = tagEl.cloneNode(true) as HTMLElement;
clonedTag.slot = "tag";
el.appendChild(clonedTag);
});
}
}
this.captureRef.bind(this._popoverContents.item)(el as SideNavigationItem | null);
}}
>
{this._popoverContents.subItems.map(item =>
<SideNavigationSubItem
Expand All @@ -93,8 +129,21 @@ export default function SideNavigationTemplate(this: SideNavigation) {
selected={item.selected}
unselectable={item.unselectable}
onui5-click={this.handlePopupItemClick}
ref={this.captureRef.bind(item)}
/>
ref={(el: HTMLElement | null) => {
if (el && item.tag.length > 0) {
const existingTags = Array.from(el.children).filter(child => child.getAttribute("slot") === "tag");
if (existingTags.length === 0) {
item.tag.forEach(tagEl => {
const clonedTag = tagEl.cloneNode(true) as HTMLElement;
clonedTag.slot = "tag";
el.appendChild(clonedTag);
});
}
}
this.captureRef.bind(item)(el as SideNavigationSubItem | null);
}}
>
</SideNavigationSubItem>
)}
</SideNavigationItem>
</SideNavigation>
Expand Down
4 changes: 4 additions & 0 deletions packages/fiori/src/SideNavigationSelectableItemBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ class SideNavigationSelectableItemBase extends SideNavigationItemBase {
return this.selected;
}

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

_onkeydown(e: KeyboardEvent) {
const isRTL = this.effectiveDir === "rtl";

Expand Down
37 changes: 37 additions & 0 deletions packages/fiori/src/SideNavigationSubItem.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import jsxRender from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";
import SideNavigationSelectableItemBase from "./SideNavigationSelectableItemBase.js";
import SideNavigationSubItemTemplate from "./SideNavigationSubItemTemplate.js";
import "@ui5/webcomponents/dist/Tag.js";

// Styles
import SideNavigationSubItemCss from "./generated/themes/SideNavigationSubItem.css.js";
Expand Down Expand Up @@ -30,6 +33,40 @@ import SideNavigationSubItemCss from "./generated/themes/SideNavigationSubItem.c
styles: SideNavigationSubItemCss,
})
class SideNavigationSubItem extends SideNavigationSelectableItemBase {
/**
* Defines the tag to be displayed.
*
* **Note:** Only one `ui5-tag` is allowed. The tag should use `design="Set2"`, `hide-state-icon`,
* and `colorScheme` values 5-10 to avoid confusion with semantic colors (1-4).
*
* **Note:** It is recommended to limit tag width to 64px (4rem). If tag text exceeds this,
* use shortened forms or abbreviations (e.g., "Experimental" → "Exp").
*
* @public
* @since 2.7.0
*/
@slot({ type: HTMLElement })
tag!: Slot<HTMLElement>;

get hasTag() {
return !!this.tag.length;
}

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

get _textAriaLabelledBy() {
Comment thread
s-todorova marked this conversation as resolved.
Outdated
if (this.hasTag) {
return `${this._textId} ${this._tagId}`;
}
return undefined;
}

get _describedBy() {
return this.hasTag ? this._tagId : undefined;
}

_onkeydown(e: KeyboardEvent) {
super._onkeydown(e);
}
Expand Down
8 changes: 7 additions & 1 deletion packages/fiori/src/SideNavigationSubItemTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ export default function SideNavigationSubItemTemplate(this: SideNavigationSubIte
href={this._href}
target={this._target}
aria-haspopup={this._ariaHasPopup}
aria-describedby={this._describedBy}
>
{this.icon &&
<Icon class="ui5-sn-item-icon" name={this.icon}/>
}
<div class="ui5-sn-item-text">{this.text}</div>
<div class="ui5-sn-item-text" id={this._textId} aria-labelledby={this._textAriaLabelledBy}>{this.text}</div>
Comment thread
s-todorova marked this conversation as resolved.
Outdated
{this.hasTag &&
<div id={this._tagId} class="ui5-sn-item-tag-slot">
<slot name="tag"></slot>
</div>
}
{this.isExternalLink &&
<Icon class="ui5-sn-item-external-link-icon"
name={arrowRight}
Expand Down
10 changes: 9 additions & 1 deletion packages/fiori/src/themes/NavigationMenuItem.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,12 @@

::slotted([ui5-navigation-menu-item]:not(:last-of-type)) {
margin-block-end: var(--_ui5_side_navigation_item_bottom_margin);
}
}

.ui5-navmenu-item-tag-container {
display: inline-flex;
align-items: center;
margin-inline-start: 0.5rem;
height: 1.375rem;
min-width: 1.375rem;
}
1 change: 1 addition & 0 deletions packages/fiori/src/themes/SideNavigationItem.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "./SideNavigationItemBase.css";
@import "./InvisibleTextStyles.css";

:host {
color: var(--sapList_TextColor);
Expand Down
Loading
Loading