From faea27311640190eeeea36d20fbb4e6c8e815a6a Mon Sep 17 00:00:00 2001 From: Siyana Todorova Date: Tue, 21 Apr 2026 15:46:41 +0300 Subject: [PATCH 1/3] feat(ui5-side-navigation): add indication tag slot JIRA: BGSOFUIRODOPI-3651 --- packages/fiori/src/SideNavigationItem.ts | 27 +- .../fiori/src/SideNavigationItemTemplate.tsx | 3 + .../src/SideNavigationPopoverTemplate.tsx | 65 ++++- .../src/SideNavigationSelectableItemBase.ts | 4 + packages/fiori/src/SideNavigationSubItem.ts | 21 ++ .../src/SideNavigationSubItemTemplate.tsx | 4 + .../src/themes/SideNavigationItemBase.css | 35 +++ .../fiori/test/pages/SideNavigationTags.html | 248 ++++++++++++++++++ 8 files changed, 398 insertions(+), 9 deletions(-) create mode 100644 packages/fiori/test/pages/SideNavigationTags.html diff --git a/packages/fiori/src/SideNavigationItem.ts b/packages/fiori/src/SideNavigationItem.ts index bf2699ad4583..01faae9255e5 100644 --- a/packages/fiori/src/SideNavigationItem.ts +++ b/packages/fiori/src/SideNavigationItem.ts @@ -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"; @@ -81,6 +82,16 @@ class SideNavigationItem extends SideNavigationSelectableItemBase { @slot({ type: HTMLElement, invalidateOnChildChange: true, "default": true }) items!: DefaultSlot; + /** + * Defines the tag to be displayed. + * Only `ui5-tag` component should be used. + * + * @public + * @since 2.7.0 + */ + @slot({ type: HTMLElement }) + tag!: Slot; + @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; @@ -183,9 +194,21 @@ class SideNavigationItem extends SideNavigationSelectableItemBase { } get _describedBy() { + const parts: string[] = []; + + if (this.hasTag) { + parts.push(this._tagId); + } + if (!this.effectiveDisabled && this.items.length && !this.unselectable) { - return SideNavigationItem.i18nBundle.getText(SIDE_NAVIGATION_PARENT_ITEM_SELECTABLE_DESCRIPTION, this.text ?? ""); + parts.push(SideNavigationItem.i18nBundle.getText(SIDE_NAVIGATION_PARENT_ITEM_SELECTABLE_DESCRIPTION, this.text ?? "")); } + + return parts.length > 0 ? parts.join(" ") : undefined; + } + + get hasTag() { + return !!this.tag.length; } get classesArray() { diff --git a/packages/fiori/src/SideNavigationItemTemplate.tsx b/packages/fiori/src/SideNavigationItemTemplate.tsx index 30ef50c8c6aa..d348bfb54af6 100644 --- a/packages/fiori/src/SideNavigationItemTemplate.tsx +++ b/packages/fiori/src/SideNavigationItemTemplate.tsx @@ -52,6 +52,9 @@ function ItemTemplate(this: SideNavigationItem) { this.icon && }
{this.text}
+ {this.hasTag && + + } {this.sideNavCollapsed ? !!this.items.length && { + if (el && item.tag.length > 0) { + if (!el.hasAttribute('data-tags-appended')) { + item.tag.forEach((tagEl) => { + const clonedTag = tagEl.cloneNode(true) as HTMLElement; + clonedTag.slot = 'endContent'; + el.appendChild(clonedTag); + }); + el.setAttribute('data-tags-appended', 'true'); + } + } + this.captureRef.bind(item)(el as any); + }} > - {item.children.length > 0 && !item.unselectable && () + ref={(el: HTMLElement | null) => { + if (el && item.tag.length > 0) { + if (!el.hasAttribute('data-tags-appended')) { + item.tag.forEach((tagEl) => { + const clonedTag = tagEl.cloneNode(true) as HTMLElement; + clonedTag.slot = 'endContent'; + el.appendChild(clonedTag); + }); + el.setAttribute('data-tags-appended', 'true'); + } + } + this.captureRef.bind(item)(el as any); + }} + > + ) } {(item as any).items?.map(renderMenuItem)} @@ -79,7 +103,20 @@ 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) { + // Only append if the element doesn't have our marker + if (!el.hasAttribute('data-tags-appended')) { + this._popoverContents.item.tag.forEach((tagEl) => { + const clonedTag = tagEl.cloneNode(true) as HTMLElement; + clonedTag.slot = 'tag'; + el.appendChild(clonedTag); + }); + el.setAttribute('data-tags-appended', 'true'); + } + } + this.captureRef.bind(this._popoverContents.item)(el as SideNavigationItem | null); + }} > {this._popoverContents.subItems.map(item => + ref={(el: HTMLElement | null) => { + if (el && item.tag.length > 0) { + // Only append if the element doesn't have our marker + if (!el.hasAttribute('data-tags-appended')) { + item.tag.forEach((tagEl) => { + const clonedTag = tagEl.cloneNode(true) as HTMLElement; + clonedTag.slot = 'tag'; + el.appendChild(clonedTag); + }); + el.setAttribute('data-tags-appended', 'true'); + } + } + this.captureRef.bind(item)(el as SideNavigationSubItem | null); + }} + > + )} diff --git a/packages/fiori/src/SideNavigationSelectableItemBase.ts b/packages/fiori/src/SideNavigationSelectableItemBase.ts index 318af80f4ba6..68138d81cbbe 100644 --- a/packages/fiori/src/SideNavigationSelectableItemBase.ts +++ b/packages/fiori/src/SideNavigationSelectableItemBase.ts @@ -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"; diff --git a/packages/fiori/src/SideNavigationSubItem.ts b/packages/fiori/src/SideNavigationSubItem.ts index 6fabffe14dbf..32931d34e01a 100644 --- a/packages/fiori/src/SideNavigationSubItem.ts +++ b/packages/fiori/src/SideNavigationSubItem.ts @@ -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"; @@ -30,6 +33,24 @@ import SideNavigationSubItemCss from "./generated/themes/SideNavigationSubItem.c styles: SideNavigationSubItemCss, }) class SideNavigationSubItem extends SideNavigationSelectableItemBase { + /** + * Defines the tag to be displayed. + * Only `ui5-tag` component should be used. + * + * @public + * @since 2.7.0 + */ + @slot({ type: HTMLElement }) + tag!: Slot; + + get hasTag() { + return !!this.tag.length; + } + + get _describedBy() { + return this.hasTag ? this._tagId : undefined; + } + _onkeydown(e: KeyboardEvent) { super._onkeydown(e); } diff --git a/packages/fiori/src/SideNavigationSubItemTemplate.tsx b/packages/fiori/src/SideNavigationSubItemTemplate.tsx index 0d7727ef5a34..72646ca41b78 100644 --- a/packages/fiori/src/SideNavigationSubItemTemplate.tsx +++ b/packages/fiori/src/SideNavigationSubItemTemplate.tsx @@ -23,11 +23,15 @@ export default function SideNavigationSubItemTemplate(this: SideNavigationSubIte href={this._href} target={this._target} aria-haspopup={this._ariaHasPopup} + aria-describedby={this._describedBy} > {this.icon && }
{this.text}
+ {this.hasTag && + + } {this.isExternalLink && + + + + Side Navigation - Indication Tags PoC + + + + + + + + + +
+
+
+
+ + + + + New + + + + + + New + + + Alpha + + + + + + + + RC + + + Preview + + + + + Deprecated + + + + + + New + + +
+
+ Toggle Collapsed +
+
+
+ +
+
+
+ + + + + Beta + + + + New + + + + Canary + + + Alpha + + + Beta + + + + + + + Dev + + + + Beta + + + + Alpha + + + + New + + + + Experimental + + + + Preview + + + + Sunset + + + + + + New + + +
+
+ Toggle Expanded +
+
+
+ +
+
+
+ + + + + Beta Version + + + + Experimental Phase + + + + Alpha Release + + New Feature + + + Preview Build + + + + + Beta Testing + + + + + + + New Content + + +
+
+ Toggle Collapsed +
+
+
+
+ + + + From 836ecbc6505f5ae4c7925fdf5d57b0bd57673512 Mon Sep 17 00:00:00 2001 From: Siyana Todorova Date: Fri, 24 Apr 2026 15:45:05 +0300 Subject: [PATCH 2/3] fix: popover items cloning --- .../src/SideNavigationPopoverTemplate.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/fiori/src/SideNavigationPopoverTemplate.tsx b/packages/fiori/src/SideNavigationPopoverTemplate.tsx index 4392667a2494..47b8825a4e96 100644 --- a/packages/fiori/src/SideNavigationPopoverTemplate.tsx +++ b/packages/fiori/src/SideNavigationPopoverTemplate.tsx @@ -19,13 +19,13 @@ export default function SideNavigationTemplate(this: SideNavigation) { tooltip={item._tooltip} ref={(el: HTMLElement | null) => { if (el && item.tag.length > 0) { - if (!el.hasAttribute('data-tags-appended')) { + 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); }); - el.setAttribute('data-tags-appended', 'true'); } } this.captureRef.bind(item)(el as any); @@ -44,13 +44,13 @@ export default function SideNavigationTemplate(this: SideNavigation) { tooltip={item._tooltip} ref={(el: HTMLElement | null) => { if (el && item.tag.length > 0) { - if (!el.hasAttribute('data-tags-appended')) { + const existingTags = el.querySelectorAll('[slot="endContent"]'); + if (existingTags.length === 0) { item.tag.forEach((tagEl) => { const clonedTag = tagEl.cloneNode(true) as HTMLElement; clonedTag.slot = 'endContent'; el.appendChild(clonedTag); }); - el.setAttribute('data-tags-appended', 'true'); } } this.captureRef.bind(item)(el as any); @@ -105,14 +105,13 @@ export default function SideNavigationTemplate(this: SideNavigation) { onui5-click={this.handlePopupItemClick} ref={(el: HTMLElement | null) => { if (el && this._popoverContents.item.tag.length > 0) { - // Only append if the element doesn't have our marker - if (!el.hasAttribute('data-tags-appended')) { + 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); }); - el.setAttribute('data-tags-appended', 'true'); } } this.captureRef.bind(this._popoverContents.item)(el as SideNavigationItem | null); @@ -132,14 +131,13 @@ export default function SideNavigationTemplate(this: SideNavigation) { onui5-click={this.handlePopupItemClick} ref={(el: HTMLElement | null) => { if (el && item.tag.length > 0) { - // Only append if the element doesn't have our marker - if (!el.hasAttribute('data-tags-appended')) { + 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); }); - el.setAttribute('data-tags-appended', 'true'); } } this.captureRef.bind(item)(el as SideNavigationSubItem | null); From a77a668645f871ffbb432f668f299d3cac395f26 Mon Sep 17 00:00:00 2001 From: Siyana Todorova Date: Mon, 27 Apr 2026 10:43:37 +0300 Subject: [PATCH 3/3] fix: lint errors --- .../src/SideNavigationPopoverTemplate.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/fiori/src/SideNavigationPopoverTemplate.tsx b/packages/fiori/src/SideNavigationPopoverTemplate.tsx index 47b8825a4e96..6400ff222c5b 100644 --- a/packages/fiori/src/SideNavigationPopoverTemplate.tsx +++ b/packages/fiori/src/SideNavigationPopoverTemplate.tsx @@ -19,16 +19,16 @@ export default function SideNavigationTemplate(this: SideNavigation) { tooltip={item._tooltip} ref={(el: HTMLElement | null) => { if (el && item.tag.length > 0) { - const existingTags = Array.from(el.children).filter(child => child.getAttribute('slot') === 'endContent'); + const existingTags = Array.from(el.children).filter(child => child.getAttribute("slot") === "endContent"); if (existingTags.length === 0) { - item.tag.forEach((tagEl) => { + item.tag.forEach(tagEl => { const clonedTag = tagEl.cloneNode(true) as HTMLElement; - clonedTag.slot = 'endContent'; + clonedTag.slot = "endContent"; el.appendChild(clonedTag); }); } } - this.captureRef.bind(item)(el as any); + this.captureRef.bind(item)(el); }} > {item.children.length > 0 && !item.unselectable && @@ -44,16 +44,16 @@ export default function SideNavigationTemplate(this: SideNavigation) { tooltip={item._tooltip} ref={(el: HTMLElement | null) => { if (el && item.tag.length > 0) { - const existingTags = el.querySelectorAll('[slot="endContent"]'); + const existingTags = Array.from(el.children).filter(child => child.getAttribute("slot") === "endContent"); if (existingTags.length === 0) { - item.tag.forEach((tagEl) => { + item.tag.forEach(tagEl => { const clonedTag = tagEl.cloneNode(true) as HTMLElement; - clonedTag.slot = 'endContent'; + clonedTag.slot = "endContent"; el.appendChild(clonedTag); }); } } - this.captureRef.bind(item)(el as any); + this.captureRef.bind(item)(el); }} > ) @@ -105,11 +105,11 @@ export default function SideNavigationTemplate(this: SideNavigation) { onui5-click={this.handlePopupItemClick} ref={(el: HTMLElement | null) => { if (el && this._popoverContents.item.tag.length > 0) { - const existingTags = Array.from(el.children).filter(child => child.getAttribute('slot') === 'tag'); + const existingTags = Array.from(el.children).filter(child => child.getAttribute("slot") === "tag"); if (existingTags.length === 0) { - this._popoverContents.item.tag.forEach((tagEl) => { + this._popoverContents.item.tag.forEach(tagEl => { const clonedTag = tagEl.cloneNode(true) as HTMLElement; - clonedTag.slot = 'tag'; + clonedTag.slot = "tag"; el.appendChild(clonedTag); }); } @@ -131,11 +131,11 @@ export default function SideNavigationTemplate(this: SideNavigation) { onui5-click={this.handlePopupItemClick} ref={(el: HTMLElement | null) => { if (el && item.tag.length > 0) { - const existingTags = Array.from(el.children).filter(child => child.getAttribute('slot') === 'tag'); + const existingTags = Array.from(el.children).filter(child => child.getAttribute("slot") === "tag"); if (existingTags.length === 0) { - item.tag.forEach((tagEl) => { + item.tag.forEach(tagEl => { const clonedTag = tagEl.cloneNode(true) as HTMLElement; - clonedTag.slot = 'tag'; + clonedTag.slot = "tag"; el.appendChild(clonedTag); }); }