All files / popover si-popover.component.ts

100% Statements 35/35
87.5% Branches 7/8
100% Functions 7/7
100% Lines 35/35

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                                                                      1x 7x 7x           7x 7x 7x 6x 6x   7x 7x   7x 7x   7x     7x 7x 7x 1x   7x 7x   7x       7x 7x         7x           7x   7x 7x 7x       1x         7x 7x 7x 7x 7x 6x 6x          
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
import { ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
import { NgClass, NgTemplateOutlet } from '@angular/common';
import {
  Component,
  ElementRef,
  inject,
  Injector,
  input,
  OnDestroy,
  OnInit,
  signal,
  TemplateRef,
  viewChild,
  DOCUMENT,
  computed
} from '@angular/core';
import { calculateOverlayArrowPosition, OverlayArrowPosition } from '@siemens/element-ng/common';
import { SiIconComponent } from '@siemens/element-ng/icon';
import { SiTranslateModule } from '@siemens/element-translate-ng/translate';
 
import { SiPopoverDirective } from './si-popover.directive';
 
@Component({
  selector: 'si-popover',
  imports: [NgClass, NgTemplateOutlet, SiIconComponent, SiTranslateModule],
  templateUrl: './si-popover.component.html',
  host: {
    '[id]': 'this.popoverDirective().popoverId'
  }
})
export class PopoverComponent implements OnInit, OnDestroy {
  readonly popoverDirective = input.required<SiPopoverDirective>();
  readonly popoverWrapper = viewChild.required<ElementRef>('popoverWrapper');
 
  /** @internal */
  labelledBy: string | undefined;
  /** @internal */
  describedBy: string | undefined;
  protected readonly positionClass = signal('');
  protected readonly arrowPos = signal<OverlayArrowPosition | undefined>(undefined);
  protected readonly description = computed(() => {
    const description = this.popoverDirective().siPopover();
    return !(description instanceof TemplateRef) ? description : undefined;
  });
  protected popoverTemplate: TemplateRef<any> | null = null;
  protected injector = inject(Injector);
 
  private elementRef = inject(ElementRef);
  private focusTrapFactory = inject(ConfigurableFocusTrapFactory);
  private focusTrap?: ConfigurableFocusTrap;
  private readonly previouslyActiveElement = inject(DOCUMENT).activeElement;
 
  ngOnInit(): void {
    const popoverDirective = this.popoverDirective();
    const popover = popoverDirective.siPopover();
    if (popover instanceof TemplateRef) {
      this.popoverTemplate = popover;
    }
    this.labelledBy = `__popover-title_${popoverDirective.popoverCounter}`;
    this.describedBy = `__popover-body_${popoverDirective.popoverCounter}`;
 
    this.applyFocus();
  }
 
  ngOnDestroy(): void {
    this.focusTrap?.destroy();
    if (
      this.previouslyActiveElement &&
      'focus' in this.previouslyActiveElement &&
      typeof this.previouslyActiveElement.focus === 'function'
    ) {
      this.previouslyActiveElement.focus();
    }
  }
 
  /** @internal */
  updateArrow(change: ConnectedOverlayPositionChange, anchor?: ElementRef): void {
    const positionClass = `popover-${change.connectionPair.overlayX}-${change.connectionPair.overlayY}`;
    // need two updates as class changes affect the position
    this.positionClass.set(positionClass);
    const arrowPos = calculateOverlayArrowPosition(change, this.elementRef, anchor);
    this.arrowPos.set(arrowPos);
  }
 
  hide(): void {
    this.popoverDirective().hide();
  }
 
  private applyFocus(): void {
    // Using setTimeout ensures that SR first read `expanded` before we move the focus.
    setTimeout(async () => {
      const popoverWrapperEl = this.popoverWrapper().nativeElement;
      this.focusTrap = this.focusTrapFactory.create(this.popoverWrapper().nativeElement);
      const moved = await this.focusTrap.focusFirstTabbableElementWhenReady();
      if (!moved) {
        popoverWrapperEl.tabIndex = 0;
        popoverWrapperEl.focus();
      }
    });
  }
}