All files / header-dropdown si-header-dropdown.component.ts

94.11% Statements 16/17
100% Branches 12/12
83.33% Functions 5/6
94.11% Lines 16/17

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                                                                      1x 90x   90x 90x 90x 90x         90x 65x 7x 7x   58x         7x   58x             953x       953x       133x                      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { A11yModule, CdkTrapFocus } from '@angular/cdk/a11y';
import {
  ChangeDetectionStrategy,
  Component,
  HostBinding,
  HostListener,
  inject,
  viewChild,
  DOCUMENT
} from '@angular/core';
 
import { SiHeaderDropdownTriggerDirective } from './si-header-dropdown-trigger.directive';
import { SI_HEADER_DROPDOWN_OPTIONS } from './si-header.model';
 
/**
 * Wrapper component for {@link SiHeaderDropdownItemComponent}.
 * Must only be opened using an {@link SiHeaderDropdownTriggerDirective}.
 */
@Component({
  selector: 'si-header-dropdown',
  imports: [A11yModule],
  templateUrl: './si-header-dropdown.component.html',
  styles: ':host.sub-menu {min-inline-size: 200px}',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'dropdown-menu position-static',
    role: 'group',
    '[id]': 'trigger.ariaControls',
    '[attr.aria-labelledby]': 'trigger.id'
  }
})
export class SiHeaderDropdownComponent {
  protected trigger = inject(SiHeaderDropdownTriggerDirective);
 
  private readonly focusTrap = viewChild.required(CdkTrapFocus);
  private previousElement: Element | null = null;
  private readonly document = inject(DOCUMENT);
  private readonly options = inject(SI_HEADER_DROPDOWN_OPTIONS, { optional: true });
 
  constructor() {
    // The autoFocus feature of the focus trap is not enough, as this component is not newly created when opened in mobile (inline).
    // But we still need autofocus in desktop mode, as the close event is never executed (component is destroyed before).
    this.trigger.openChange.subscribe(change => {
      if (!this.trigger.isOverlay && this.trapFocus && change) {
        this.previousElement = this.document.activeElement;
        this.focusTrap().focusTrap.focusFirstTabbableElementWhenReady();
      } else {
        if (
          this.previousElement &&
          'focus' in this.previousElement &&
          typeof this.previousElement.focus === 'function'
        ) {
          this.previousElement.focus();
        }
        this.previousElement = null;
      }
    });
  }
 
  @HostBinding('class.show')
  protected get show(): boolean {
    return this.trigger.isOpen;
  }
 
  @HostBinding('class.sub-menu') protected get submenu(): boolean {
    return this.trigger.level > 1;
  }
 
  protected get trapFocus(): boolean {
    return (
      this.trigger.isOverlay ||
      (!this.options?.disableRootFocusTrapForInlineMode && this.trigger.level === 1)
    );
  }
 
  @HostListener('keydown.escape')
  protected escape(): void {
    this.trigger?.close();
  }
}