All files / icon si-icon.component.ts

100% Statements 13/13
75% Branches 3/4
100% Functions 5/5
100% Lines 12/12

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                                                              1x   897x               5x                                                                                     1x                                           3012x   3012x 3012x   3012x 3000x       3012x 3063x       3063x            
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { NgClass } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  InjectionToken,
  input,
  Provider
} from '@angular/core';
 
import { IconService } from './si-icons';
 
/**
 * Global configuration for icons.
 *
 * @experimental
 */
export interface IconConfig {
  /**
   * If true, the si-icon component will always render the icon font instead of the svg.
   *
   * @defaultValue true
   */
  disableSvgIcons?: boolean;
}
 
const ICON_CONFIG = new InjectionToken<IconConfig>('ICON_CONFIG', {
  providedIn: 'root',
  factory: () => ({ disableSvgIcons: true })
});
 
/**
 * Configure how Element handles icons. Provide only once in your global configuration.
 *
 * @experimental
 */
export const provideIconConfig = (config: IconConfig): Provider => ({
  provide: ICON_CONFIG,
  useValue: config
});
 
/**
 * Component to render a font or SVG icon depending on the configuration.
 * If no SVG icon is found, the component will fall back to render the icon-font.
 * In that case, an application must ensure that the icon font is loaded.
 * This component will only attach the respective class.
 *
 * The content of this component is hidden in the a11y tree.
 * If needed, the consumer must set proper labels.
 *
 * @experimental
 */
@Component({
  selector: 'si-icon',
  imports: [NgClass],
  template: ` <div
    aria-hidden="true"
    [ngClass]="svgIcon() ? '' : fontIcon()"
    [innerHTML]="svgIcon()"
  ></div>`,
  styles: `
    :host {
      display: inline-flex;
      font-weight: normal;
      vertical-align: middle;
      line-height: 1;
 
      ::ng-deep svg {
        display: block;
        block-size: 1em;
        fill: currentColor;
      }
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    '[attr.data-icon]': 'icon()'
  }
})
export class SiIconComponent {
  /**
   * Define which icon should be rendered.
   * Provide using:
   * - value of the icon map provided by `addIcons`
   * - (not recommended): plain string in kebab-case or camelCase
   *
   * @example
   * ```ts
   * import { elementUser } from '@simpl/element-icons/ionic';
   *
   * @Component({template: `
   *   <si-icon [icon]="icons.elementUser" />
   *   <si-icon icon="element-user" />
   *   <si-icon icon="elementUser" />
   *
   * `})
   * class MyComponent {
   *   icons = addIcons(elementUser);
   * }
   * ```
   */
  readonly icon = input.required<string>();
 
  private readonly config = inject(ICON_CONFIG);
  private readonly iconService = inject(IconService);
 
  protected readonly svgIcon = computed(() =>
    this.config.disableSvgIcons ? undefined : this.iconService.getIcon(this.icon())
  );
 
  /** Icon class, which is ensured to be kebab-case. */
  protected readonly fontIcon = computed(() =>
    this.svgIcon() ? undefined : this.camelToKebabCase(this.icon())
  );
 
  private camelToKebabCase(str: string): string {
    return str
      ?.replace(/([a-z])([A-Z0-9])/g, '$1-$2')
      .replace(/([0-9])([A-Z])/g, '$1-$2')
      .toLowerCase();
  }
}