From bc8fb52a7e2e68f1804bc7241374ab8e1e1920d5 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 30 Jan 2025 10:32:21 +0100 Subject: [PATCH 01/15] post merge --- .changeset/fuzzy-carrots-drive.md | 5 ++ README.md | 21 +++++++- src/components/icon/resolveIcon.ts | 54 ++++++++++++++++++- src/components/icon/test/ix-icon.spec.tsx | 2 +- src/components/icon/test/resolveIcon.spec.ts | 33 +++++++++--- .../{rocker-example.ts => rocket-example.ts} | 0 src/index.ts | 1 + 7 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 .changeset/fuzzy-carrots-drive.md rename src/components/icon/test/{rocker-example.ts => rocket-example.ts} (100%) diff --git a/.changeset/fuzzy-carrots-drive.md b/.changeset/fuzzy-carrots-drive.md new file mode 100644 index 00000000..8b4aa664 --- /dev/null +++ b/.changeset/fuzzy-carrots-drive.md @@ -0,0 +1,5 @@ +--- +"@siemens/ix-icons": minor +--- + +Allow preloading of specific icons. diff --git a/README.md b/README.md index c5cf469d..c9e6dcfa 100755 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ SPDX-License-Identifier: MIT ## Usage -Using icons within your project. You need to: +### Installation + +First install the package `@siemens/ix-icons` in your project (e.g. `npm install --save @siemens/ix-icons`). -- Install `@siemens/ix-icons` e.g. `npm install --save @siemens/ix-icons` +Then load the icon component: ```javascript import { defineCustomElements } from '@siemens/ix-icons/loader'; @@ -24,6 +26,21 @@ import { defineCustomElements } from '@siemens/ix-icons/loader'; })(); ``` +Icons are loaded once and then cached for the entire duration of the single-page application. +Additionally, icons can be preloaded to ensure they are immediately available from the cache when needed later: + +```javascript +import { loadIcons } from '@siemens/ix-icons'; + +const icons = [ + 'star', + 'star-filled', + // ... +]; + +loadIcons(icons) +``` + ### Angular / Web Components ```html diff --git a/src/components/icon/resolveIcon.ts b/src/components/icon/resolveIcon.ts index ba4ece75..70db8fe4 100644 --- a/src/components/icon/resolveIcon.ts +++ b/src/components/icon/resolveIcon.ts @@ -28,6 +28,7 @@ export const getIconCacheMap = (): Map => { window.IxIcons = window.IxIcons || {}; fetchCache = window.IxIcons.map = window.IxIcons.map || new Map(); } + return fetchCache; }; @@ -85,12 +86,15 @@ async function fetchSVG(url: string) { const svgContent = parseSVGDataContent(responseText); cache.set(url, svgContent); + requests.delete(url); + return svgContent; }); requests.set(url, fetching); return fetching; } + const urlRegex = /^(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:www\.)?(?:\S+\.\S+)(?:\S*)$/i; function isValidUrl(url: string) { @@ -99,11 +103,13 @@ function isValidUrl(url: string) { export function getIconUrl(name: string) { const customAssetUrl = getCustomAssetUrl(); + if (customAssetUrl) { return `${customAssetUrl}/${name}.svg`; } let url: string = `svg/${name}.svg`; + try { url = getAssetPath(url); } catch (error) { @@ -124,6 +130,16 @@ export async function resolveIcon(iconName: string) { return parseSVGDataContent(iconName); } + return await loadIcon(iconName); +} + +async function loadIcon(iconName: string) { + const cache = getIconCacheMap(); + + if (cache.has(iconName)) { + return cache.get(iconName); + } + if (isValidUrl(iconName)) { try { return fetchSVG(iconName); @@ -135,6 +151,42 @@ export async function resolveIcon(iconName: string) { try { return fetchSVG(getIconUrl(iconName)); } catch (error) { - throw Error('Cannot resolve any icon'); + throw Error(`Could not resolve ${iconName}`); + } +} + +function removePrefix(name: string, prefix: string) { + if (name.startsWith(prefix)) { + name = name.slice(prefix.length); + return name.replace(/^(\w)/, (_match, p1) => p1.toLowerCase()); + } + + return name; +} + +export function addIcons(icons: { [name: string]: any }) { + Object.keys(icons).forEach(name => { + const icon = icons[name]; + name = removePrefix(name, 'icon'); + + addIconToCache(name, icon); + }); +} + +export function addIconToCache(name: string, icon: string) { + const cache = getIconCacheMap(); + + if (cache.has(name)) { + console.warn(`Icon name '${name}' already in cache. Overwritting with new icon data.`); + } + + const svg = parseSVGDataContent(icon); + + cache.set(name, svg); + + const toKebabCase = name.replace(/([a-z0-9]|(?=[A-Z]))([A-Z0-9])/g, '$1-$2').toLowerCase(); + + if (name != toKebabCase) { + cache.set(toKebabCase, svg); } } diff --git a/src/components/icon/test/ix-icon.spec.tsx b/src/components/icon/test/ix-icon.spec.tsx index f32bbe3d..40303431 100644 --- a/src/components/icon/test/ix-icon.spec.tsx +++ b/src/components/icon/test/ix-icon.spec.tsx @@ -3,7 +3,7 @@ */ import { newSpecPage } from '@stencil/core/testing'; import { Icon } from '../icon'; -import { rocket } from './rocker-example'; +import { rocket } from './rocket-example'; //@ts-ignore global.fetch = jest.fn(() => diff --git a/src/components/icon/test/resolveIcon.spec.ts b/src/components/icon/test/resolveIcon.spec.ts index 9d22b291..9fb960a1 100644 --- a/src/components/icon/test/resolveIcon.spec.ts +++ b/src/components/icon/test/resolveIcon.spec.ts @@ -1,8 +1,9 @@ /* * COPYRIGHT (c) Siemens AG 2018-2023 ALL RIGHTS RESERVED. */ -import { iconStar } from '../icons'; -import { resolveIcon, getIconCacheMap, getIconUrl, parseSVGDataContent } from '../resolveIcon'; +import { iconAdd, iconStar } from '../icons'; +import { resolveIcon, getIconCacheMap, getIconUrl, parseSVGDataContent, addIcons } from '../resolveIcon'; + const exampleSvg = ` @@ -22,6 +23,9 @@ const invalidexampleSvg = ` `; +const urlInvalid = 'http://localhost/invalid.svg'; +const urlTest = 'http://localhost/test.svg'; + jest.mock('../meta-tag'); jest.mock('../icons', () => ({ iconStar: exampleSvg, @@ -43,14 +47,14 @@ let fetch = (global.fetch = jest.fn((url: string) => { }); } - if (url === 'http://localhost/test.svg') { + if (url === urlTest) { return Promise.resolve({ text: () => Promise.resolve(exampleSvg), ok: true, }); } - if (url === 'http://localhost/invalid.svg') { + if (url === urlInvalid) { return Promise.resolve({ text: () => Promise.resolve(invalidexampleSvg), ok: true, @@ -72,7 +76,7 @@ describe('resolve icon', () => { }); it('should resolve svg from src', async () => { - const expectedName = await resolveIcon('http://localhost/test.svg'); + const expectedName = await resolveIcon(urlTest); expect(expectedName).toEqual( ` add `, @@ -80,7 +84,7 @@ describe('resolve icon', () => { }); it('should not resolve invalid svg from src', async () => { - const icon = 'http://localhost/invalid.svg'; + const icon = urlInvalid; await expect(resolveIcon(icon)).rejects.toThrow('No valid svg data provided'); }); @@ -92,8 +96,6 @@ test('fill cache map if not loaded', async () => { const cacheMap = getIconCacheMap(); cacheMap.clear(); - expect(cacheMap.size).toBe(0); - const data = await resolveIcon('star'); expect(data).toBe(parseSVGDataContent(iconStar)); @@ -111,3 +113,18 @@ test('preload custom icon', async () => { const data = await resolveIcon('star'); expect(data).toBe('Test'); }); + +test('add icons', async () => { + fetch.mockClear(); + + const cacheMap = getIconCacheMap(); + cacheMap.clear(); + + addIcons({ + iconAdd, + }); + + expect(cacheMap.size).toBe(2); + expect(cacheMap.has('iconAdd')).toBeTruthy(); + expect(cacheMap.has('add')).toBeTruthy(); +}); diff --git a/src/components/icon/test/rocker-example.ts b/src/components/icon/test/rocket-example.ts similarity index 100% rename from src/components/icon/test/rocker-example.ts rename to src/components/icon/test/rocket-example.ts diff --git a/src/index.ts b/src/index.ts index df11febd..5684bbdd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export type { Components, JSX } from './components'; export * from './components/icon/icon'; +export { addIcons, addIconToCache } from './components/icon/resolveIcon'; export { setAssetPath } from '@stencil/core'; From b2bcde71a237ebc0f24c8244210ec4b40b85a139 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 30 Jan 2025 10:40:20 +0100 Subject: [PATCH 02/15] test: update ct --- src/components/icon/test/resolveIcon.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/icon/test/resolveIcon.spec.ts b/src/components/icon/test/resolveIcon.spec.ts index 9fb960a1..2f233728 100644 --- a/src/components/icon/test/resolveIcon.spec.ts +++ b/src/components/icon/test/resolveIcon.spec.ts @@ -1,7 +1,7 @@ /* * COPYRIGHT (c) Siemens AG 2018-2023 ALL RIGHTS RESERVED. */ -import { iconAdd, iconStar } from '../icons'; +import { iconStar, iconStarFilled } from '../icons'; import { resolveIcon, getIconCacheMap, getIconUrl, parseSVGDataContent, addIcons } from '../resolveIcon'; const exampleSvg = ` @@ -121,10 +121,10 @@ test('add icons', async () => { cacheMap.clear(); addIcons({ - iconAdd, + iconStarFilled, }); expect(cacheMap.size).toBe(2); - expect(cacheMap.has('iconAdd')).toBeTruthy(); - expect(cacheMap.has('add')).toBeTruthy(); + expect(cacheMap.has('starFilled')).toBeTruthy(); + expect(cacheMap.has('star-filled')).toBeTruthy(); }); From 640908ab45bf6c47a2fd37927766e1df711deba9 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 30 Jan 2025 10:45:36 +0100 Subject: [PATCH 03/15] docs: update readme.md --- .changeset/fuzzy-carrots-drive.md | 2 +- README.md | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.changeset/fuzzy-carrots-drive.md b/.changeset/fuzzy-carrots-drive.md index 8b4aa664..2fc677b8 100644 --- a/.changeset/fuzzy-carrots-drive.md +++ b/.changeset/fuzzy-carrots-drive.md @@ -2,4 +2,4 @@ "@siemens/ix-icons": minor --- -Allow preloading of specific icons. +Allow users to put specific icons into the cache. diff --git a/README.md b/README.md index c9e6dcfa..ac46be2d 100755 --- a/README.md +++ b/README.md @@ -26,21 +26,6 @@ import { defineCustomElements } from '@siemens/ix-icons/loader'; })(); ``` -Icons are loaded once and then cached for the entire duration of the single-page application. -Additionally, icons can be preloaded to ensure they are immediately available from the cache when needed later: - -```javascript -import { loadIcons } from '@siemens/ix-icons'; - -const icons = [ - 'star', - 'star-filled', - // ... -]; - -loadIcons(icons) -``` - ### Angular / Web Components ```html From a39c758c95290c1c5619b02a4503f344ee205996 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 30 Jan 2025 14:09:20 +0100 Subject: [PATCH 04/15] ci: fix snapshot workflow --- .github/workflows/snapshot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 99eb6462..cb15873a 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -77,7 +77,7 @@ jobs: echo "$(git diff --name-only --diff-filter=A ${{ steps.comment-branch.outputs.base_sha }} ${{ steps.parse-sha.outputs.sha }} .changeset/*.md)" >> "${GITHUB_OUTPUT}" echo "${delimiter}" >> "${GITHUB_OUTPUT}" - - uses: ./.github/workflows/actions/turbo + - uses: ./.github/workflows/actions/build - name: Check for pre.json file existence id: check_files From 95e6c1da46a79b0cf8362ca2d55f366a4cd7fc39 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 30 Jan 2025 15:03:43 +0100 Subject: [PATCH 05/15] ci: fix version --- .github/workflows/snapshot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index cb15873a..0af2ed95 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -114,7 +114,7 @@ jobs: - name: Get released version if: ${{ steps.added-files.outputs.changesets != '' }} id: get-version - run: echo "version=$(node -p "require('./packages/core/package.json').version")" >> "$GITHUB_OUTPUT" + run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" - name: Create comment if: ${{ steps.added-files.outputs.changesets != '' }} From 6f066d422917e1c44ec56db3293f4caae8310d76 Mon Sep 17 00:00:00 2001 From: Daniel Leroux Date: Fri, 31 Jan 2025 14:40:04 +0100 Subject: [PATCH 06/15] docs: update readme --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ac46be2d..37c2c2b8 100755 --- a/README.md +++ b/README.md @@ -12,38 +12,63 @@ SPDX-License-Identifier: MIT ## Usage -### Installation +If you're using [Siemens Industrial Experience](https://github.com/siemens/ix/) library you don't have to setup your project this will be done via `@siemens/ix-angular`, `@siemens/ix-react` or `@siemens/ix-vue`, so no additional setup is necessary. -First install the package `@siemens/ix-icons` in your project (e.g. `npm install --save @siemens/ix-icons`). +You want to use `@siemens/ix-icons` without `@siemens/ix` you need to follow these steps. -Then load the icon component: +### Using CDN -```javascript -import { defineCustomElements } from '@siemens/ix-icons/loader'; +Place the following `
@@ -51,7 +51,8 @@ describe('ix-icon', () => { global.fetch = jest.fn(() => Promise.resolve({ ok: false, - text: () => Promise.resolve(`ERROR!`), + text: () => 'error', + status: '500', }), ); @@ -60,7 +61,7 @@ describe('ix-icon', () => { html: ``, }); - expect(page.root.shadowRoot).toEqualHtml(` + expect(page.root!.shadowRoot).toEqualHtml(`
diff --git a/src/components/icon/test/resolveIcon.spec.ts b/src/components/icon/test/resolveIcon.spec.ts index 2f233728..83ac4f00 100644 --- a/src/components/icon/test/resolveIcon.spec.ts +++ b/src/components/icon/test/resolveIcon.spec.ts @@ -1,8 +1,13 @@ /* * COPYRIGHT (c) Siemens AG 2018-2023 ALL RIGHTS RESERVED. */ -import { iconStar, iconStarFilled } from '../icons'; -import { resolveIcon, getIconCacheMap, getIconUrl, parseSVGDataContent, addIcons } from '../resolveIcon'; +import { resolveIcon, getIconCacheMap, getIconUrl, addIcons } from '../resolveIcon'; +import { parseSVGDataContent } from '../parser'; + +export const iconStarFilled = + "data:image/svg+xml;utf8,"; +export const iconStar = + "data:image/svg+xml;utf8,"; const exampleSvg = ` @@ -31,8 +36,6 @@ jest.mock('../icons', () => ({ iconStar: exampleSvg, })); let fetch = (global.fetch = jest.fn((url: string) => { - console.log(url); - if (url === '/svg/star.svg') { return Promise.resolve({ text: () => Promise.resolve(iconStar), @@ -63,12 +66,15 @@ let fetch = (global.fetch = jest.fn((url: string) => { }) as jest.Mock); describe('resolve icon', () => { + let element: HTMLIxIconElement = null!; + beforeEach(() => { fetch.mockClear(); + element = document.createElement('ix-icon'); }); it('should resolve svg from name', async () => { - const data = await resolveIcon('bulb'); + const data = await resolveIcon(element, 'bulb'); expect(data).toBe( ` add `, @@ -76,7 +82,7 @@ describe('resolve icon', () => { }); it('should resolve svg from src', async () => { - const expectedName = await resolveIcon(urlTest); + const expectedName = await resolveIcon(element, urlTest); expect(expectedName).toEqual( ` add `, @@ -86,23 +92,26 @@ describe('resolve icon', () => { it('should not resolve invalid svg from src', async () => { const icon = urlInvalid; - await expect(resolveIcon(icon)).rejects.toThrow('No valid svg data provided'); + const content = await resolveIcon(element, icon); + expect(content).toEqual(''); }); }); test('fill cache map if not loaded', async () => { + const element = document.createElement('ix-icon'); fetch.mockClear(); const cacheMap = getIconCacheMap(); cacheMap.clear(); - const data = await resolveIcon('star'); + const data = await resolveIcon(element, 'star'); expect(data).toBe(parseSVGDataContent(iconStar)); expect(cacheMap.get(getIconUrl('star'))).toBe(parseSVGDataContent(iconStar)); }); test('preload custom icon', async () => { + const element = document.createElement('ix-icon'); fetch.mockClear(); const cacheMap = getIconCacheMap(); @@ -110,7 +119,7 @@ test('preload custom icon', async () => { cacheMap.set(getIconUrl('star'), 'Test'); - const data = await resolveIcon('star'); + const data = await resolveIcon(element, 'star'); expect(data).toBe('Test'); }); diff --git a/tsconfig.json b/tsconfig.json index 5971d5da..a8815317 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "noUnusedLocals": true, "noUnusedParameters": true, "jsx": "react", - "jsxFactory": "h" + "jsxFactory": "h", + "strict": true }, "include": ["src"], "exclude": ["node_modules"] From b06998a0e6aeaf8476c80342729f379697bddbbe Mon Sep 17 00:00:00 2001 From: Daniel Leroux Date: Wed, 5 Feb 2025 15:43:09 +0100 Subject: [PATCH 12/15] test: remove unused mock --- src/components/icon/test/resolveIcon.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/icon/test/resolveIcon.spec.ts b/src/components/icon/test/resolveIcon.spec.ts index 83ac4f00..80570fad 100644 --- a/src/components/icon/test/resolveIcon.spec.ts +++ b/src/components/icon/test/resolveIcon.spec.ts @@ -32,9 +32,7 @@ const urlInvalid = 'http://localhost/invalid.svg'; const urlTest = 'http://localhost/test.svg'; jest.mock('../meta-tag'); -jest.mock('../icons', () => ({ - iconStar: exampleSvg, -})); + let fetch = (global.fetch = jest.fn((url: string) => { if (url === '/svg/star.svg') { return Promise.resolve({ From 9917f3be765852bd4ef863c4bc6a45299d7491cd Mon Sep 17 00:00:00 2001 From: Daniel Leroux Date: Wed, 5 Feb 2025 16:24:19 +0100 Subject: [PATCH 13/15] fix: fix error branch which results in object string --- src/components/icon/resolveIcon.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/icon/resolveIcon.ts b/src/components/icon/resolveIcon.ts index e0eb28a1..9e52218f 100644 --- a/src/components/icon/resolveIcon.ts +++ b/src/components/icon/resolveIcon.ts @@ -129,7 +129,13 @@ async function loadIcon(iconName: string): Promise { return fetchSVG(iconName); } - return fetchSVG(getIconUrl(iconName)); + const iconUrl = getIconUrl(iconName); + + if (!iconUrl) { + return ''; + } + + return fetchSVG(iconUrl); } function removePrefix(name: string, prefix: string) { From c55b0b95029c291b410ec391693878483953112c Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 13 Feb 2025 10:58:37 +0100 Subject: [PATCH 14/15] Apply suggestions from code review Co-authored-by: matthiashader <144090716+matthiashader@users.noreply.github.com> --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b5b1af30..1dfcb441 100755 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ SPDX-License-Identifier: MIT ## Usage -### With Siemens Industrial Experience design system +### Settng up with Siemens Industrial Experience design system -If you are also using the library [Siemens Industrial Experience](https://github.com/siemens/ix/), no additional project setup will be neccessary. The packages `@siemens/ix-angular`, `@siemens/ix-react` or `@siemens/ix-vue` will take care of setting up the icon library for you. +If you are also using the library [Siemens Industrial Experience](https://github.com/siemens/ix/), no additional project setup will be necessary. The packages `@siemens/ix-angular`, `@siemens/ix-react` or `@siemens/ix-vue` will take care of setting up the icon library for you. -### Without Siemens Industrial Experience design system +### Setting up without Siemens Industrial Experience design system If you want to use `@siemens/ix-icons` without `@siemens/ix` you need to follow these steps: @@ -29,7 +29,7 @@ Place the following `