Skip to content

feat(ui5-icon): display custom SVG, defined as JSX template #11966

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
67 changes: 32 additions & 35 deletions docs/2-advanced/03-using-icons.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,61 +110,58 @@ After the SVG icons collection is registered, you can use the custom icons every

## Custom SVG icons

In case you need to use a fully custom SVG with multiple SVG elements like `circle` and `rect` instead of only a custom `path`, you can provide a custom renderer and register it for usage in `<ui5-icon>`.
### with JSX Templates

First, create a template for the icon you need:
In case you need to use a fully custom SVG, that can be used `ui5-icon`, `ui5-button` or any component that offers API to display an icon via icon name, you can provide a custom JSX template, rendering the custom SVG and register it under a custom name.

`BakeryDining.hbs`
```html
<g>
<rect fill="none" height="24" width="24" y="0" />
</g>
<g>
<g>
<path
d="M7.6,8.67l-2.01,0.8c-0.22,0.09-0.34,0.31-0.31,0.54l2.4,5.98h1.23l-0.62-6.9C8.25,8.75,7.91,8.54,7.6,8.67 z"
opacity=".3" />
<path d="M3.07,16.1c-0.27,0.53,0.29,1.09,0.82,0.83l1.68-0.84l-1.08-2.71L3.07,16.1z" opacity=".3" />
<path
d="M13.36,6.99h-2.71c-0.27,0-0.53,0.23-0.5,0.54l0.77,8.45h2.17l0.77-8.45C13.88,7.22,13.63,6.99,13.36,6.99z"
opacity=".3" />
<path
d="M18.41,9.47l-2.01-0.8c-0.31-0.12-0.65,0.09-0.68,0.42l-0.62,6.9h1.23l2.4-5.98 C18.75,9.78,18.63,9.56,18.41,9.47z"
opacity=".3" />
<path d="M19.52,13.39l-1.08,2.7l1.68,0.84c0.52,0.26,1.09-0.3,0.82-0.83L19.52,13.39z" opacity=".3" />
<path
d="M20.5,10.94c0.13-0.32,0.1-0.23,0.15-0.39c0.3-1.21-0.34-2.47-1.5-2.93l-2.01-0.8c-0.46-0.18-0.95-0.21-1.41-0.12 c-0.11-0.33-0.29-0.63-0.52-0.89C14.73,5.29,14.06,5,13.36,5h-2.71C9.94,5,9.27,5.29,8.8,5.81C8.56,6.07,8.38,6.37,8.27,6.69 C7.81,6.6,7.32,6.63,6.86,6.81l-2.01,0.8c-1.16,0.46-1.8,1.72-1.5,2.93l0.15,0.38C1.1,15.55,1,15.55,1,16.38 c0,0.91,0.46,1.74,1.24,2.22c1.42,0.88,2.49,0.14,4-0.61h11.53c1.52,0.76,1.86,1.01,2.63,1.01c1,0,2.61-0.77,2.61-2.61 C23,15.54,22.88,15.51,20.5,10.94z M3.88,16.93c-0.53,0.26-1.09-0.3-0.82-0.83l1.41-2.72l1.08,2.71L3.88,16.93z M7.68,15.99 l-2.4-5.98C5.25,9.78,5.37,9.56,5.59,9.47l2.01-0.8c0.31-0.12,0.65,0.08,0.68,0.42l0.62,6.9H7.68z M13.09,15.99h-2.17l-0.77-8.45 c-0.03-0.31,0.23-0.54,0.5-0.54h2.71c0.27,0,0.53,0.23,0.5,0.54L13.09,15.99z M16.32,15.99h-1.23l0.62-6.9 c0.03-0.33,0.37-0.54,0.68-0.42l2.01,0.8c0.22,0.09,0.34,0.31,0.31,0.54L16.32,15.99z M20.12,16.93l-1.68-0.84l1.08-2.7l1.41,2.71 C21.21,16.63,20.64,17.19,20.12,16.93z" />
</g>
</g>

#### 1. Create JSX template

First, create a JSX template for the icon you need:

```tsx
// MyPensilSVGTemplate.tsx
export default function MyPensilSVGTemplate() {
return (
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2221_23716)"><path d="M11.3333 1.99998C11.503 1.79933 11.7131 1.63601 11.9499 1.52043C12.1868 1.40485 12.4453 1.33953 12.709 1.32865C12.9727 1.31777 13.2358 1.36156 13.4815 1.45723C13.7272 1.55291 13.9502 1.69836 14.1361 1.88432C14.3221 2.07029 14.467 2.29268 14.5616 2.53734C14.6562 2.78199 14.6985 3.04353 14.6857 3.3053C14.6728 3.56706 14.6052 3.8233 14.4872 4.05769C14.3691 4.29207 14.2032 4.49947 13.9999 4.66664L4.99992 13.6666L1.33325 14.6666L2.33325 11L11.3333 1.99998Z" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 3.33331L12.6667 5.99998" stroke-linecap="round" stroke-linejoin="round"/></g>
<defs>
<clipPath id="clip0_2221_23716"><rect width="16" height="16"/></clipPath>
</defs>
</svg>
)
};
```

The `.hbs` file must start exactly with the content `"<g>"` or `"<g "` for correct compilation. The HBS compiler will generate a template that you can then import and register with the icon regsitry.
#### 2. Register the Custom Icon

You can use the `registerIcon` to register the custom icon as follows:

`bundle.esm.js`
```js
import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";
import iconBakeryDiningTemplate from "./dist/generated/templates/BakeryDiningTemplate.lit.js";
import myPensilSVGTemplate from "./MyPensilSVGTemplate.js";

// create the icon data for registration
const iconBakeryDining = {
customTemplate: iconBakeryDiningTemplate,
viewBox: "0 0 24 24",
const iconPensil = {
customTemplate: myPensilSVGTemplate,
collection: "custom",
viewBox: "0 0 24 24", // optional
}

// register the icon
registerIcon("bakery-dining", iconBakeryDining);
registerIcon("pensil", iconPensil);
```

The icon data object should fill the `customTemplate` property with a template that will be included inside the SVG of the `<ui5-icon>`. In that case, a `path` won't be rendered. You can also specify a custom `viewBox` size, as the default one is `0 0 512 512`.
#### 3. Use the Custom Icon

Finally, the icon can be used anywhere.
```html
<ui5-icon name="custom/backery-dining"></ui5-icon>
<ui5-avatar icon="custom/backery-dining" size="XS"></ui5-avatar>
<ui5-icon name="custom/pensil"></ui5-icon>
<ui5-button icon="custom/pensil"></ui5-button>
<ui5-avatar icon="custom/pensil" size="XS"></ui5-avatar>
```

Tip: for multi-colored icons, you can specify multiple SVG elements and put a fill/color attribute with a specific value on each element.
**Tip:** for multi-colored icons, you can specify multiple SVG elements and put a fill/color attribute with a specific value on each element.
```html
<g fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" fill="aqua"/>
Expand Down
35 changes: 32 additions & 3 deletions packages/main/src/IconTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type Icon from "./Icon.js";

type LegacySVGTemplate = {
strings: string[],
values?: Array<string | LegacySVGTemplate>
}

export default function IconTemplate(this: Icon) {
return (
<svg
Expand All @@ -22,9 +27,7 @@ export default function IconTemplate(this: Icon) {
}

<g role="presentation">
{this.customSvg &&
<g dangerouslySetInnerHTML={{ __html: (this.customSvg as { strings?: string[] }).strings?.join("") ?? "" }}></g>
}
{this.customSvg && svgTemplate.call(this, this.customSvg)}

{this.pathData.map(path => (
<path d={path}></path>
Expand All @@ -33,3 +36,29 @@ export default function IconTemplate(this: Icon) {
</svg>
);
}

function svgTemplate(this: Icon, template: object | LegacySVGTemplate) {
if ((template as LegacySVGTemplate).strings) {
return <g dangerouslySetInnerHTML={{ __html: renderLegacySVGTemplate(this.customSvg as LegacySVGTemplate) ?? "" }}></g>;
}
return template;
}

// Renders legacy (lit) SVG template
function renderLegacySVGTemplate(customTemplate: LegacySVGTemplate): string {
const { strings, values } = customTemplate;

return strings.map((str: string, i: number) => {
const value = values && values[i];

if (typeof value === "string") {
return str + value;
}

if (typeof value === "object" && value?.strings) {
return str + renderLegacySVGTemplate(value);
}

return str;
}).join("");
}
10 changes: 10 additions & 0 deletions packages/main/src/bundle-assets/IconPensilJSXTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function IconPensilJSXTemplate() {
return (
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2221_23716)"><path d="M11.3333 1.99998C11.503 1.79933 11.7131 1.63601 11.9499 1.52043C12.1868 1.40485 12.4453 1.33953 12.709 1.32865C12.9727 1.31777 13.2358 1.36156 13.4815 1.45723C13.7272 1.55291 13.9502 1.69836 14.1361 1.88432C14.3221 2.07029 14.467 2.29268 14.5616 2.53734C14.6562 2.78199 14.6985 3.04353 14.6857 3.3053C14.6728 3.56706 14.6052 3.8233 14.4872 4.05769C14.3691 4.29207 14.2032 4.49947 13.9999 4.66664L4.99992 13.6666L1.33325 14.6666L2.33325 11L11.3333 1.99998Z" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 3.33331L12.6667 5.99998" stroke-linecap="round" stroke-linejoin="round"/></g>
<defs>
<clipPath id="clip0_2221_23716"><rect width="16" height="16"/></clipPath>
</defs>
</svg>
);
}
13 changes: 13 additions & 0 deletions packages/main/src/bundle-assets/IconPensilLitTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { html, svg } from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";

function block0(this: any) {
return html`<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">${blockSVG1.call(this)}</svg>`;
}

function blockSVG1(this: any) {
return svg`<g clip-path="url(#clip0_2221_23716)"><path d="M11.3333 1.99998C11.503 1.79933 11.7131 1.63601 11.9499 1.52043C12.1868 1.40485 12.4453 1.33953 12.709 1.32865C12.9727 1.31777 13.2358 1.36156 13.4815 1.45723C13.7272 1.55291 13.9502 1.69836 14.1361 1.88432C14.3221 2.07029 14.467 2.29268 14.5616 2.53734C14.6562 2.78199 14.6985 3.04353 14.6857 3.3053C14.6728 3.56706 14.6052 3.8233 14.4872 4.05769C14.3691 4.29207 14.2032 4.49947 13.9999 4.66664L4.99992 13.6666L1.33325 14.6666L2.33325 11L11.3333 1.99998Z" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 3.33331L12.6667 5.99998" stroke-linecap="round" stroke-linejoin="round"/></g><defs><clipPath id="clip0_2221_23716"><rect width="16" height="16"/></clipPath></defs>`;
}

export default function IconPensilLitTemplate(this: any) {
return block0.call(this);
}
22 changes: 21 additions & 1 deletion packages/main/src/bundle.esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// eslint-disable-next-line
import testAssetsCommon from "./bundle.common.bootstrap.js"; // code that needs to be executed before other modules

import { registerIconLoader } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";
import { registerIconLoader, registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";

// SAP Icons
import accept, { getPathData } from "@ui5/webcomponents-icons/dist/accept.js";
Expand Down Expand Up @@ -128,6 +128,10 @@ import ListItemCustom from "./ListItemCustom.js";
import ListItemGroupHeader from "./ListItemGroupHeader.js";
import ListItemGroup from "./ListItemGroup.js";

// custom SVG template (Lit or JSX), registered as an icon
import IconPensilJSXTemplate from "./bundle-assets/IconPensilJSXTemplate.js";
import IconPensilLitTemplate from "./bundle-assets/IconPensilLitTemplate.js";

const icons = [accept, acceptv4, acceptv5, actor, actorv2, actorv3, icon3d, icon3dv1, icon3dv2];

const testAssets = {
Expand Down Expand Up @@ -215,6 +219,22 @@ registerIconLoader("my-icons", () => {
}]);
});

registerIcon("pencil", {
customTemplate: IconPensilJSXTemplate,
viewBox: "0 0 16 16",
packageName: "custom-svg-icon",
collection: "custom-svg-icons",
pathData: "pencil",
});

registerIcon("pencil2", {
customTemplate: IconPensilLitTemplate,
viewBox: "0 0 16 16",
packageName: "custom-svg-icon",
collection: "custom-svg-icons",
pathData: "pencil2",
});

// @ts-ignore
window["sap-ui-webcomponents-bundle"] = testAssets;

Expand Down
2 changes: 2 additions & 0 deletions packages/main/test/pages/Icon_custom.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@
<ui5-icon name="home"></ui5-icon>home<br>
<ui5-icon name="tnt/actor"></ui5-icon>tnt/actor<br>
<ui5-icon name="my-icons/mark"></ui5-icon>my-icons/mark<br>
<ui5-icon name="custom-svg-icons/pencil"></ui5-icon>pensil<br>
<ui5-icon name="custom-svg-icons/pencil2"></ui5-icon>pensil2<br>
</body>
</html>
Loading