All files / circle-status si-circle-status.component.ts

90.69% Statements 39/43
84.84% Branches 28/33
80% Functions 8/10
90.24% Lines 37/41

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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152                                                                        1x 6x       6x                   6x           6x             6x             6x         6x             6x         6x         6x   9x 11x 6x 10x 10x 10x 10x 10x       6x 9x 9x   6x 6x 6x     6x   6x     7x 2x   2x 2x 2x 5x           7x 2x 2x           7x                      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { NgClass } from '@angular/common';
import {
  booleanAttribute,
  ChangeDetectionStrategy,
  Component,
  computed,
  ElementRef,
  inject,
  input,
  OnChanges,
  OnDestroy,
  signal,
  SimpleChanges,
  viewChild
} from '@angular/core';
import { BlinkService, EntityStatusType, StatusIcon } from '@siemens/element-ng/common';
import {
  addIcons,
  elementRight4,
  SiIconComponent,
  STATUS_ICON_CONFIG
} from '@siemens/element-ng/icon';
import { SiTranslatePipe, TranslatableString } from '@siemens/element-translate-ng/translate';
import { Observable, Subscription } from 'rxjs';
 
@Component({
  selector: 'si-circle-status',
  imports: [NgClass, SiIconComponent, SiTranslatePipe],
  templateUrl: './si-circle-status.component.html',
  styleUrl: './si-circle-status.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SiCircleStatusComponent implements OnChanges, OnDestroy {
  private readonly statusIcons = inject(STATUS_ICON_CONFIG);
  /**
   * The status (success, info, warning, danger) to be visualized.
   */
  readonly status = input<EntityStatusType>();
 
  /* DO NOT REMOVE: Even though the input is marked as deprecated, the core-team decided not to remove the
   input. The possibility to have custom color is often requested by projects, so we keep it.
   however in order to discourage it's use, we keep it marked deprecated.
   */
  /**
   * A custom color (e.g. `#fefefe`) for exceptional cases.
   * @deprecated use the semantic `status` input instead.
   */
  readonly color = input<string>();
 
  /**
   * Set a domain type icon (e.g. `element-door`) for which the status shall be shown.
   * Leave undefined for visualizing the status without an icon.
   */
  readonly icon = input<string>();
 
  /**
   * Set the size using either regular or small only works when used together with `icon`
   *
   * @defaultValue 'regular'
   */
  readonly size = input<'regular' | 'small'>('regular');
 
  /**
   * event direction is out
   *
   * @defaultValue false
   */
  readonly eventOut = input(false, { transform: booleanAttribute });
 
  /**
   * Custom icon class for event out
   */
  readonly eventIcon = input<string>();
 
  /**
   * Whether the status should appear with a pulsing circle around the badge.
   *
   * @defaultValue false
   */
  readonly blink = input(false, { transform: booleanAttribute });
 
  /**
   * Blink pulse generator for synchronized blinking with other components
   */
  readonly blinkPulse = input<Observable<boolean>>();
 
  /**
   * Aria label for icon and status combo. Needed for a11y
   */
  readonly ariaLabel = input<TranslatableString>();
 
  protected readonly backgroundClass = computed(() => this.statusIcon()?.background ?? '');
  protected readonly theAriaLabel = computed(() => this.ariaLabel() ?? this.autoLabel());
  protected readonly autoLabel = computed(() => {
    const status = this.status();
    const statusName = status && this.statusIcons[status] ? status : 'none';
    const direction = this.eventOut() ? ' out' : '';
    const iconName = this.icon()?.replace(/^element-{0,1}(.+)/, '$1 ') ?? '';
    return `${iconName.toLocaleLowerCase()}${
      this.status() && this.icon() ? 'in ' : ''
    }status ${statusName}${direction}`;
  });
  protected readonly statusIcon = computed<StatusIcon | undefined>(() => {
    const status = this.status();
    return status ? this.statusIcons[status] : undefined;
  });
  protected readonly blinkOn = signal(false);
  protected readonly contrastFix = signal(false);
  protected readonly icons = addIcons({ elementRight4 });
  private blinkSubs?: Subscription;
 
  private readonly bg = viewChild.required<ElementRef>('bg');
 
  private blinkService = inject(BlinkService);
 
  ngOnChanges(changes: SimpleChanges): void {
    if (this.blinkService && changes.blink) {
      this.blinkSubs?.unsubscribe();
 
      if (this.blink()) {
        const pulse = this.blinkPulse() ?? this.blinkService.pulse$;
        this.blinkSubs = pulse.subscribe(onOff => {
          this.blinkOn.set(onOff);
        });
      } else E{
        this.blinkOn.set(false);
      }
    }
    if (changes.color || changes.blink) {
      queueMicrotask(() => {
        this.contrastFix.set(!!this.color() && this.blink() && this.calculateContrastFix());
      });
    }
  }
 
  ngOnDestroy(): void {
    this.blinkSubs?.unsubscribe();
  }
 
  private calculateContrastFix(): boolean {
    // see https://www.w3.org/TR/AERT/#color-contrast
    const rgb = getComputedStyle(this.bg().nativeElement)
      .backgroundColor?.match(/\d+/g)
      ?.map(v => +v);
    return !!rgb && Math.round((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000) <= 128;
  }
}