From c2b600ececec6dcc5e7a01267b4bb73ba9a12fde Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Thu, 21 May 2026 11:34:17 +0300 Subject: [PATCH 1/5] feat(ui5-icon): add symbol slot for font-based icon libraries --- packages/main/cypress/specs/Icon.cy.tsx | 146 +++++++++- packages/main/src/Icon.ts | 35 +++ packages/main/src/IconTemplate.tsx | 18 ++ packages/main/src/themes/Icon.css | 6 + packages/main/test/pages/Icon_symbol.html | 308 ++++++++++++++++++++++ 5 files changed, 510 insertions(+), 3 deletions(-) create mode 100644 packages/main/test/pages/Icon_symbol.html diff --git a/packages/main/cypress/specs/Icon.cy.tsx b/packages/main/cypress/specs/Icon.cy.tsx index 3256add32a05..ddedb1d5c888 100644 --- a/packages/main/cypress/specs/Icon.cy.tsx +++ b/packages/main/cypress/specs/Icon.cy.tsx @@ -297,7 +297,7 @@ describe("Icon general interaction", () => { cy.get("[ui5-icon][mode='Interactive']").then($icon => { const icon = $icon[0] as any; const accessibilityInfo = icon.accessibilityInfo; - + // For Interactive mode, accessibilityInfo should have role, type and description expect(accessibilityInfo).to.not.be.undefined; expect(accessibilityInfo.role).to.equal("button"); @@ -317,7 +317,7 @@ describe("Icon general interaction", () => { cy.get("[ui5-icon][mode='Decorative']").then($icon => { const icon = $icon[0] as any; const accessibilityInfo = icon.accessibilityInfo; - + // For Decorative mode, accessibilityInfo should return an empty object expect(accessibilityInfo).to.deep.equal({}); }); @@ -334,7 +334,7 @@ describe("Icon general interaction", () => { cy.get("[ui5-icon][mode='Image']").then($icon => { const icon = $icon[0] as any; const accessibilityInfo = icon.accessibilityInfo; - + // For Image mode, accessibilityInfo should have role, type and description expect(accessibilityInfo).to.not.be.undefined; expect(accessibilityInfo.role).to.equal("img"); @@ -343,3 +343,143 @@ describe("Icon general interaction", () => { }); }); }); + +describe("Icon symbol slot", () => { + it("renders a span root instead of svg when symbol slot is used", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root").should("exist"); + cy.get("[ui5-icon]").shadow().find("svg").should("not.exist"); + }); + + it("Decorative mode: span root has role=presentation and aria-hidden=true", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root") + .should("have.attr", "role", "presentation") + .should("have.attr", "aria-hidden", "true"); + }); + + it("Image mode: span root has role=img and aria-label", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root") + .should("have.attr", "role", "img") + .should("have.attr", "aria-label", "Star") + .should("not.have.attr", "aria-hidden"); + }); + + it("Interactive mode: span root has role=button and tabindex=0", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root") + .should("have.attr", "role", "button") + .should("have.attr", "tabindex", "0") + .should("have.attr", "aria-label", "Add"); + }); + + it("Interactive mode: fires ui5-click on mouse click", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").then($icon => { + $icon[0].addEventListener("ui5-click", cy.stub().as("ui5Click")); + }); + + cy.get("[ui5-icon]").realClick(); + cy.get("@ui5Click").should("have.been.calledOnce"); + }); + + it("Interactive mode: fires ui5-click on Enter key", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").then($icon => { + $icon[0].addEventListener("ui5-click", cy.stub().as("ui5Click")); + }); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root").focus(); + cy.realPress("Enter"); + cy.get("@ui5Click").should("have.been.calledOnce"); + }); + + it("Interactive mode: fires ui5-click on Space key", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").then($icon => { + $icon[0].addEventListener("ui5-click", cy.stub().as("ui5Click")); + }); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root").focus(); + cy.realPress("Space"); + cy.get("@ui5Click").should("have.been.calledOnce"); + }); + + it("Decorative mode: does not fire ui5-click on click", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").then($icon => { + $icon[0].addEventListener("ui5-click", cy.stub().as("ui5Click")); + }); + + cy.get("[ui5-icon]").realClick(); + cy.get("@ui5Click").should("not.have.been.called"); + }); + + it("no accessible-name: aria-label is not set", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root") + .should("not.have.attr", "aria-label"); + }); + + it("accessible-name takes effect when set", () => { + cy.mount( + + + + ); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root") + .should("have.attr", "aria-label", "Initial"); + + cy.get("[ui5-icon]").invoke("prop", "accessibleName", "Updated"); + + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root") + .should("have.attr", "aria-label", "Updated"); + }); +}); diff --git a/packages/main/src/Icon.ts b/packages/main/src/Icon.ts index d18405141e52..83f25330ec3a 100644 --- a/packages/main/src/Icon.ts +++ b/packages/main/src/Icon.ts @@ -3,7 +3,9 @@ import jsxRender from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; import type { AriaRole } from "@ui5/webcomponents-base/dist/types.js"; +import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js"; import type { IconData, UnsafeIconData } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js"; import { getIconData, getIconDataSync } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js"; import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; @@ -194,6 +196,25 @@ class Icon extends UI5Element implements IIcon { @property() mode: `${IconMode}` = "Decorative"; + /** + * Defines the symbol to be used as an icon. + * Intended for font-based icon libraries (e.g. Font Awesome, Material Icons) where + * the application loads the font and provides a slotted element with the unicode character. + * When this slot is used, the component renders a `` instead of an ``. + * Accessibility is fully delegated to the application — set `accessible-name` and `mode` explicitly. + * + * **Example:** + * ```html + * + * + * + * ``` + * @public + * @since 2.12.0 + */ + @slot({ type: HTMLElement }) + symbol!: Slot; + /** * @private */ @@ -288,6 +309,16 @@ class Icon extends UI5Element implements IIcon { } async onBeforeRendering() { + if (this.symbol.length) { + // Font-based icon via slot — skip registry, accessibility is app's responsibility + if (!this.accessibleName) { + this.effectiveAccessibleName = undefined; + } else { + this.effectiveAccessibleName = this.accessibleName; + } + return; + } + const name = this.name; if (!name) { return; @@ -344,6 +375,10 @@ class Icon extends UI5Element implements IIcon { } } + get hasSymbol() { + return this.symbol.length > 0; + } + get hasIconTooltip() { return this.showTooltip && this.effectiveAccessibleName; } diff --git a/packages/main/src/IconTemplate.tsx b/packages/main/src/IconTemplate.tsx index 380161620a01..6705e67edbf9 100644 --- a/packages/main/src/IconTemplate.tsx +++ b/packages/main/src/IconTemplate.tsx @@ -1,6 +1,24 @@ import type Icon from "./Icon.js"; export default function IconTemplate(this: Icon) { + if (this.hasSymbol) { + return ( + + + + ); + } + return ( + + + + + + + Icon - Symbol Slot (Font-based Icons) + + + + + + + + + + + + + + + + + + +

Icon - symbol slot

+

Font-based icons via the symbol slot. The application loads the font; ui5-icon wraps it for sizing, design tokens, and accessibility.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ScenarioSAP Icons (SVG)Material SymbolsFont AwesomeBootstrap IconsPhosphor Icons
Decorative (default) + + check_circle + + + + + + + + + + + + + +
Image mode + accessible-name + + home + + + + + + + + + + + + + +
Interactive mode + + add_circle + + + + + + + + + + + + + +
design="Positive" + + check_circle + + + + + + + + + + + + + +
design="Negative" + + cancel + + + + + + + + + + + + + +
design="Critical" + + warning + + + + + + + + + + + + + +
3rem size + + settings + + + + + + + + + + + + + +
5rem×5rem, different font-size per icon
(tests font-size vs width/height independence)
+ + settings + + + + + + + + + + + + + +
Custom color + + settings + + + + + + + + + + + + + +
+ + + From 325b08eb4a4ef499d686820cee37c1acbd620a2e Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Thu, 21 May 2026 14:01:19 +0300 Subject: [PATCH 2/5] chore: sap icons font --- packages/main/test/pages/Icon_symbol.html | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/main/test/pages/Icon_symbol.html b/packages/main/test/pages/Icon_symbol.html index 8e2d7c874f74..4d28bad5a2bf 100644 --- a/packages/main/test/pages/Icon_symbol.html +++ b/packages/main/test/pages/Icon_symbol.html @@ -67,6 +67,19 @@ height: 1.5rem; } + + @@ -78,6 +91,7 @@

Icon - symbol slot

Scenario SAP Icons (SVG) + SAP Icons (font) Material Symbols Font Awesome Bootstrap Icons @@ -88,6 +102,11 @@

Icon - symbol slot

Decorative (default) + + + + + check_circle @@ -112,6 +131,11 @@

Icon - symbol slot

Image mode + accessible-name + + + + + home @@ -136,6 +160,11 @@

Icon - symbol slot

Interactive mode + + + + + add_circle @@ -160,6 +189,11 @@

Icon - symbol slot

design="Positive" + + + + + check_circle @@ -184,6 +218,11 @@

Icon - symbol slot

design="Negative" + + + + + cancel @@ -208,6 +247,11 @@

Icon - symbol slot

design="Critical" + + + + + warning @@ -232,6 +276,11 @@

Icon - symbol slot

3rem size + + + + + settings @@ -256,6 +305,11 @@

Icon - symbol slot

5rem×5rem, different font-size per icon
(tests font-size vs width/height independence) + + + + + settings @@ -280,6 +334,11 @@

Icon - symbol slot

Custom color + + + + + settings From 7751ed62f085cfb7e48c2f8ceb7b1b60dcbffe9d Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Thu, 21 May 2026 14:29:11 +0300 Subject: [PATCH 3/5] chore: since tag --- packages/main/src/Icon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/Icon.ts b/packages/main/src/Icon.ts index 83f25330ec3a..4c277a0a0d45 100644 --- a/packages/main/src/Icon.ts +++ b/packages/main/src/Icon.ts @@ -210,7 +210,7 @@ class Icon extends UI5Element implements IIcon { * * ``` * @public - * @since 2.12.0 + * @since 2.23.0 */ @slot({ type: HTMLElement }) symbol!: Slot; From af34974f92bb2daa40df40cb6cbc85b8955b7abe Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Thu, 21 May 2026 16:12:19 +0300 Subject: [PATCH 4/5] chore: rename slot --- packages/main/cypress/specs/Icon.cy.tsx | 24 +++--- packages/main/src/Icon.ts | 12 +-- packages/main/src/IconTemplate.tsx | 4 +- packages/main/test/pages/Icon_symbol.html | 90 +++++++++++------------ 4 files changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/main/cypress/specs/Icon.cy.tsx b/packages/main/cypress/specs/Icon.cy.tsx index ddedb1d5c888..287d362e4e5a 100644 --- a/packages/main/cypress/specs/Icon.cy.tsx +++ b/packages/main/cypress/specs/Icon.cy.tsx @@ -344,11 +344,11 @@ describe("Icon general interaction", () => { }); }); -describe("Icon symbol slot", () => { - it("renders a span root instead of svg when symbol slot is used", () => { +describe("Icon fontIcon slot", () => { + it("renders a span root instead of svg when fontIcon slot is used", () => { cy.mount( - + ); @@ -359,7 +359,7 @@ describe("Icon symbol slot", () => { it("Decorative mode: span root has role=presentation and aria-hidden=true", () => { cy.mount( - + ); @@ -371,7 +371,7 @@ describe("Icon symbol slot", () => { it("Image mode: span root has role=img and aria-label", () => { cy.mount( - + ); @@ -384,7 +384,7 @@ describe("Icon symbol slot", () => { it("Interactive mode: span root has role=button and tabindex=0", () => { cy.mount( - + ); @@ -397,7 +397,7 @@ describe("Icon symbol slot", () => { it("Interactive mode: fires ui5-click on mouse click", () => { cy.mount( - + ); @@ -412,7 +412,7 @@ describe("Icon symbol slot", () => { it("Interactive mode: fires ui5-click on Enter key", () => { cy.mount( - + ); @@ -428,7 +428,7 @@ describe("Icon symbol slot", () => { it("Interactive mode: fires ui5-click on Space key", () => { cy.mount( - + ); @@ -444,7 +444,7 @@ describe("Icon symbol slot", () => { it("Decorative mode: does not fire ui5-click on click", () => { cy.mount( - + ); @@ -459,7 +459,7 @@ describe("Icon symbol slot", () => { it("no accessible-name: aria-label is not set", () => { cy.mount( - + ); @@ -470,7 +470,7 @@ describe("Icon symbol slot", () => { it("accessible-name takes effect when set", () => { cy.mount( - + ); diff --git a/packages/main/src/Icon.ts b/packages/main/src/Icon.ts index 4c277a0a0d45..74571b184998 100644 --- a/packages/main/src/Icon.ts +++ b/packages/main/src/Icon.ts @@ -197,7 +197,7 @@ class Icon extends UI5Element implements IIcon { mode: `${IconMode}` = "Decorative"; /** - * Defines the symbol to be used as an icon. + * Defines the font icon to be used as an icon. * Intended for font-based icon libraries (e.g. Font Awesome, Material Icons) where * the application loads the font and provides a slotted element with the unicode character. * When this slot is used, the component renders a `` instead of an ``. @@ -206,14 +206,14 @@ class Icon extends UI5Element implements IIcon { * **Example:** * ```html * - * + * *
* ``` * @public * @since 2.23.0 */ @slot({ type: HTMLElement }) - symbol!: Slot; + fontIcon!: Slot; /** * @private @@ -309,7 +309,7 @@ class Icon extends UI5Element implements IIcon { } async onBeforeRendering() { - if (this.symbol.length) { + if (this.fontIcon.length) { // Font-based icon via slot — skip registry, accessibility is app's responsibility if (!this.accessibleName) { this.effectiveAccessibleName = undefined; @@ -375,8 +375,8 @@ class Icon extends UI5Element implements IIcon { } } - get hasSymbol() { - return this.symbol.length > 0; + get hasFontIcon() { + return this.fontIcon.length > 0; } get hasIconTooltip() { diff --git a/packages/main/src/IconTemplate.tsx b/packages/main/src/IconTemplate.tsx index 6705e67edbf9..9fb7aabf2eab 100644 --- a/packages/main/src/IconTemplate.tsx +++ b/packages/main/src/IconTemplate.tsx @@ -1,7 +1,7 @@ import type Icon from "./Icon.js"; export default function IconTemplate(this: Icon) { - if (this.hasSymbol) { + if (this.hasFontIcon) { return ( - + ); } diff --git a/packages/main/test/pages/Icon_symbol.html b/packages/main/test/pages/Icon_symbol.html index 4d28bad5a2bf..d54afe47b791 100644 --- a/packages/main/test/pages/Icon_symbol.html +++ b/packages/main/test/pages/Icon_symbol.html @@ -104,27 +104,27 @@

Icon - symbol slot

- + - check_circle + check_circle - + - + - + @@ -133,27 +133,27 @@

Icon - symbol slot

- + - home + home - + - + - + @@ -162,27 +162,27 @@

Icon - symbol slot

- + - add_circle + add_circle - + - + - + @@ -191,27 +191,27 @@

Icon - symbol slot

- + - check_circle + check_circle - + - + - + @@ -220,27 +220,27 @@

Icon - symbol slot

- + - cancel + cancel - + - + - + @@ -249,27 +249,27 @@

Icon - symbol slot

- + - warning + warning - + - + - + @@ -278,27 +278,27 @@

Icon - symbol slot

- + - settings + settings - + - + - + @@ -307,27 +307,27 @@

Icon - symbol slot

- + - settings + settings - + - + - + @@ -336,27 +336,27 @@

Icon - symbol slot

- + - settings + settings - + - + - + From b0a0b7f1b17656f6fdfc4049cff9b4560783832b Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Thu, 21 May 2026 16:13:10 +0300 Subject: [PATCH 5/5] chore: docs --- packages/main/src/Icon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/Icon.ts b/packages/main/src/Icon.ts index 74571b184998..1e24a0282f42 100644 --- a/packages/main/src/Icon.ts +++ b/packages/main/src/Icon.ts @@ -198,7 +198,7 @@ class Icon extends UI5Element implements IIcon { /** * Defines the font icon to be used as an icon. - * Intended for font-based icon libraries (e.g. Font Awesome, Material Icons) where + * Intended for font-based icon libraries where * the application loads the font and provides a slotted element with the unicode character. * When this slot is used, the component renders a `` instead of an ``. * Accessibility is fully delegated to the application — set `accessible-name` and `mode` explicitly.