diff --git a/api-goldens/element-ng/icon/index.api.md b/api-goldens/element-ng/icon/index.api.md index a5ddcb7d9..510946514 100644 --- a/api-goldens/element-ng/icon/index.api.md +++ b/api-goldens/element-ng/icon/index.api.md @@ -5,7 +5,6 @@ ```ts import * as _angular_core from '@angular/core'; -import * as _angular_platform_browser from '@angular/platform-browser'; import { EntityStatusType } from '@siemens/element-ng/common'; import { InjectionToken } from '@angular/core'; import { Provider } from '@angular/core'; diff --git a/api-goldens/element-ng/theme/index.api.md b/api-goldens/element-ng/theme/index.api.md index 8a8d21d2d..6fc9c8a72 100644 --- a/api-goldens/element-ng/theme/index.api.md +++ b/api-goldens/element-ng/theme/index.api.md @@ -7,7 +7,6 @@ import { EventEmitter } from '@angular/core'; import * as i0 from '@angular/core'; import { Observable } from 'rxjs'; -import { SafeHtml } from '@angular/platform-browser'; // @public (undocumented) export const ELEMENT_THEME_NAME = "element"; @@ -54,7 +53,7 @@ export class SiThemeService { get resolvedColorScheme(): ThemeType | undefined; setActiveTheme(name?: string, type?: ThemeType): Observable; readonly themeChange: EventEmitter; - readonly themeIcons: i0.WritableSignal>; + readonly themeIcons: i0.WritableSignal>; readonly themeNames$: Observable; get themeNames(): string[]; updateProperty(name: string, value: string, type: keyof ThemeColorSchemes): void; diff --git a/projects/element-ng/avatar/si-avatar.component.scss b/projects/element-ng/avatar/si-avatar.component.scss index 7b4bcfcc4..b8ddb308a 100644 --- a/projects/element-ng/avatar/si-avatar.component.scss +++ b/projects/element-ng/avatar/si-avatar.component.scss @@ -67,14 +67,7 @@ img { inset-inline-end: var(--indicator-offset-x); font-size: var(--indicator-size); - .drop-shadow { - ::ng-deep { - svg { - filter: drop-shadow(1px 0 0 variables.$element-base-1) - drop-shadow(-1px 0 0 variables.$element-base-1) - drop-shadow(0 1px 0 variables.$element-base-1) - drop-shadow(0 -1px 0 variables.$element-base-1); - } - } - } + filter: drop-shadow(1px 0 0 variables.$element-base-1) + drop-shadow(-1px 0 0 variables.$element-base-1) drop-shadow(0 1px 0 variables.$element-base-1) + drop-shadow(0 -1px 0 variables.$element-base-1); } diff --git a/projects/element-ng/icon/si-icon.component.scss b/projects/element-ng/icon/si-icon.component.scss index da5f31320..646b530f7 100644 --- a/projects/element-ng/icon/si-icon.component.scss +++ b/projects/element-ng/icon/si-icon.component.scss @@ -3,10 +3,14 @@ font-weight: normal; vertical-align: middle; line-height: 1; +} - ::ng-deep svg { - display: block; - block-size: 1em; - fill: currentColor; - } +.svg-element-icon { + background-color: currentColor; + mask-image: var(--svg-element-icon); + mask-repeat: no-repeat; + mask-size: 100% 100%; + mask-position: center; + block-size: 1em; + inline-size: 1em; } diff --git a/projects/element-ng/icon/si-icon.component.spec.ts b/projects/element-ng/icon/si-icon.component.spec.ts index 57d7f49d3..17c6fcd94 100644 --- a/projects/element-ng/icon/si-icon.component.spec.ts +++ b/projects/element-ng/icon/si-icon.component.spec.ts @@ -19,6 +19,11 @@ describe('SiSvgIconComponent', () => { let component: TestHostComponent; let fixture: ComponentFixture; + const getIconHostStyle = (): string => + (fixture.nativeElement.querySelector('si-icon') as HTMLElement).style.getPropertyValue( + '--svg-element-icon' + ); + @Component({ imports: [SiIconComponent], template: ` ` @@ -60,7 +65,7 @@ describe('SiSvgIconComponent', () => { }); it('should load icon', () => { - expect(document.getElementById('svg-2')).toBeTruthy(); + expect(getIconHostStyle()).toContain('svg-2'); }); it('should load icon override from theme', () => { @@ -74,7 +79,7 @@ describe('SiSvgIconComponent', () => { } }); fixture.detectChanges(); - expect(document.getElementById('svg-oem')).toBeTruthy(); + expect(getIconHostStyle()).toContain('svg-oem'); }); }); @@ -85,7 +90,7 @@ describe('SiSvgIconComponent', () => { }); it('should load icon', () => { - expect(document.getElementById('svg')).toBeTruthy(); + expect(getIconHostStyle()).toContain('svg'); }); it('should load icon override from theme', () => { @@ -99,7 +104,7 @@ describe('SiSvgIconComponent', () => { } }); fixture.detectChanges(); - expect(document.getElementById('svg-oem')).toBeTruthy(); + expect(getIconHostStyle()).toContain('svg-oem'); }); }); }); @@ -114,7 +119,7 @@ describe('SiSvgIconComponent', () => { it('should always use the icon-font', () => { component.icon.set('element-svg'); fixture.detectChanges(); - expect(document.getElementById('svg')).toBeFalsy(); + expect(getIconHostStyle()).toBe(''); expect(fixture.debugElement.query(By.css('.element-svg'))).toBeTruthy(); }); }); diff --git a/projects/element-ng/icon/si-icon.component.ts b/projects/element-ng/icon/si-icon.component.ts index 69d7c047a..e9c5e7adf 100644 --- a/projects/element-ng/icon/si-icon.component.ts +++ b/projects/element-ng/icon/si-icon.component.ts @@ -56,15 +56,12 @@ export const provideIconConfig = (config: IconConfig): Provider => ({ */ @Component({ selector: 'si-icon', - template: ` `, + template: ` `, styleUrl: './si-icon.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, host: { - '[attr.data-icon]': 'icon()' + '[attr.data-icon]': 'icon()', + '[style.--svg-element-icon]': 'svgIcon()' } }) export class SiIconComponent { diff --git a/projects/element-ng/icon/si-icons.ts b/projects/element-ng/icon/si-icons.ts index dd31d69c1..81bcd813e 100644 --- a/projects/element-ng/icon/si-icons.ts +++ b/projects/element-ng/icon/si-icons.ts @@ -3,23 +3,15 @@ * SPDX-License-Identifier: MIT */ import { DestroyRef, inject, Injectable } from '@angular/core'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { SiThemeService } from '@siemens/element-ng/theme'; interface RegisteredIcon { - content: SafeHtml | undefined; + content: string | undefined; // Count how often an icon was registered to only remove it if it is no longer in use. referenceCount: number; } -const parseDataSvgIcon = (icon: string, domSanitizer: DomSanitizer): SafeHtml => { - const parsed = /^data:image\/svg\+xml;utf8,(.*)$/.exec(icon); - if (!parsed) { - console.error('Failed to parse icon', icon); - return ''; - } - return domSanitizer.bypassSecurityTrustHtml(parsed[1]); -}; +const parseDataSvgIcon = (icon: string): string => `url("${icon}")`; const registeredIcons = new Map(); @@ -46,10 +38,9 @@ const registeredIcons = new Map(); */ export const addIcons = (icons: Record): Record => { const iconMap = {} as Record; - const domSanitizer = inject(DomSanitizer); for (const [key, rawContent] of Object.entries(icons)) { const registeredIcon = registeredIcons.get(key) ?? { - content: parseDataSvgIcon(rawContent, domSanitizer), + content: parseDataSvgIcon(rawContent), referenceCount: 0 }; registeredIcon.referenceCount++; @@ -74,13 +65,13 @@ export const addIcons = (icons: Record): Record registeredIcons.get(key)?.content; +const getIcon = (key: string): string | undefined => registeredIcons.get(key)?.content; @Injectable({ providedIn: 'root' }) export class IconService { private themeService = inject(SiThemeService); - getIcon(name: string): SafeHtml | undefined { + getIcon(name: string): string | undefined { const camelCaseName = this.kebabToCamelCase(name); return this.themeService.themeIcons()[camelCaseName] ?? getIcon(camelCaseName); } diff --git a/projects/element-ng/theme/si-theme.service.ts b/projects/element-ng/theme/si-theme.service.ts index 28ba109a8..9a8b32a8e 100644 --- a/projects/element-ng/theme/si-theme.service.ts +++ b/projects/element-ng/theme/si-theme.service.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: MIT */ import { isPlatformBrowser } from '@angular/common'; -import { EventEmitter, inject, Injectable, PLATFORM_ID, signal, DOCUMENT } from '@angular/core'; -import { DomSanitizer, Meta, SafeHtml } from '@angular/platform-browser'; +import { DOCUMENT, EventEmitter, inject, Injectable, PLATFORM_ID, signal } from '@angular/core'; +import { Meta } from '@angular/platform-browser'; import { Observable, of, ReplaySubject, throwError } from 'rxjs'; import { map, switchMap, take, tap } from 'rxjs/operators'; @@ -75,7 +75,7 @@ export class SiThemeService { * {} * ``` */ - readonly themeIcons = signal>({}); + readonly themeIcons = signal>({}); private themes: Map = new Map(); private darkMediaQuery?: MediaQueryList; @@ -87,7 +87,6 @@ export class SiThemeService { inject(SiThemeStore, { optional: true }) ?? new SiDefaultThemeStore(this.isBrowser); private meta = inject(Meta); private document = inject(DOCUMENT); - private domSanitizer = inject(DomSanitizer); constructor() { this.resolvedColorScheme$.subscribe(scheme => (this._resolvedColorScheme = scheme)); @@ -468,17 +467,8 @@ export class SiThemeService { ); } - private parseDataSvgIcon(icon: string): SafeHtml { + private parseDataSvgIcon(icon: string): string { // This method is currently a copy of parseDataSvgIcon in si-icon.registry.ts. - // Those are likely to diverge in the future, as this version will get support for other formats like: - // - URLs - // - Plain SVG data - // - Promises to enable lazy icon loading using import() - const parsed = /^data:image\/svg\+xml;utf8,(.*)$/.exec(icon); - if (!parsed) { - console.error('Failed to parse icon', icon); - return ''; - } - return this.domSanitizer.bypassSecurityTrustHtml(parsed[1]); + return `url("${icon}")`; } }