All files / element/element/projects/element-ng/tabs si-tabset.component.ts

94.59% Statements 35/37
80.55% Branches 29/36
85.71% Functions 12/14
100% Lines 26/26

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                                                                                                                77x     531x         25x   25x   25x             25x     76x     25x     25x         25x     10x       25x 28x   3x 3x                         2x   2x 2x 2x 2x 1x   2x 1x   2x             8x       3x      
/**
 * Copyright (c) Siemens 2016 - 2026
 * SPDX-License-Identifier: MIT
 */
import { FocusKeyManager } from '@angular/cdk/a11y';
import { CdkMenuTrigger } from '@angular/cdk/menu';
import { DomPortal } from '@angular/cdk/portal';
import { NgTemplateOutlet } from '@angular/common';
import {
  booleanAttribute,
  ChangeDetectionStrategy,
  Component,
  computed,
  contentChildren,
  effect,
  inject,
  INJECTOR,
  input,
  signal,
  viewChild
} from '@angular/core';
import { RouterLink } from '@angular/router';
import { elementOptions } from '@siemens/element-icons';
import { isRTL } from '@siemens/element-ng/common';
import { addIcons, SiIconComponent } from '@siemens/element-ng/icon';
import { SiMenuDirective, SiMenuItemComponent } from '@siemens/element-ng/menu';
import { SiResizeObserverModule } from '@siemens/element-ng/resize-observer';
 
import { SiTabBadgeComponent } from './si-tab-badge.component';
import { SiTabBaseDirective } from './si-tab-base.directive';
import { SiTabLinkComponent } from './si-tab-link.component';
import { SI_TABSET } from './si-tabs-tokens';
 
/**
 * A component to group multiple tabs together.
 * Can either be used with {@link SiTabLinkComponent} or {@link SiTabComponent} components.
 */
@Component({
  selector: 'si-tabset',
  imports: [
    SiMenuDirective,
    SiMenuItemComponent,
    CdkMenuTrigger,
    NgTemplateOutlet,
    SiResizeObserverModule,
    RouterLink,
    SiTabBadgeComponent,
    SiIconComponent
  ],
  templateUrl: './si-tabset.component.html',
  styleUrl: './si-tabset.component.scss',
  providers: [
    {
      provide: SI_TABSET,
      useExisting: SiTabsetComponent
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SiTabsetComponent {
  /**
   * When set, the overflowing content inside the tab will automatically scroll
   * @defaultValue false
   **/
  readonly contentOverflowAuto = input(false, { transform: booleanAttribute });
 
  protected readonly icons = addIcons({ elementOptions });
 
  private readonly contentNode = viewChild.required('contentNode');
  /**
   * A `DomPortal` wrapping the tab panel container. Used by {@link SiTabPortalComponent}
   * to render the active tab's content at a remote location in the DOM.
   *
   * @internal
   */
  readonly contentPortal = computed(() => new DomPortal(this.contentNode()));
 
  /** @internal */
  readonly activeTab = computed(() => this.tabPanels().find(tab => tab.active()));
 
  /** @internal */
  readonly tabPanels = contentChildren(SiTabBaseDirective);
 
  /** @internal */
  focusKeyManager = new FocusKeyManager(this.tabPanels, inject(INJECTOR))
    .withHorizontalOrientation(isRTL() ? 'rtl' : 'ltr')
    .withWrap(true);
 
  /** @internal */
  protected readonly showMenuButton = signal(false);
 
  protected tabIsLink(tab: unknown): tab is SiTabLinkComponent {
    return tab instanceof SiTabLinkComponent;
  }
 
  constructor() {
    effect(() => {
      if (this.showMenuButton() && this.activeTab()) {
        // wait for menu button to render on DOM
        setTimeout(() => {
          this.activeTab()?.scrollTabIntoView();
        });
      }
    });
  }
 
  /** @internal */
  removedTabByUser(index: number, active?: boolean): void {
    // The tab was already removed from the tabPanels list when this function is called.
    // We need to:
    // - focus another tab if the closed one was focused
    // - activate another tab if the closed one was active
    // If the closed tab was not focussed, there is no need to restore the focus as it could only be closed by mouse.
    for (let i = 0; i < this.tabPanels().length; i++) {
      // Get the actual index using modulo to wrap around
      const checkIndex = (index + i) % this.tabPanels().length;
      const checkTab = this.tabPanels()[checkIndex];
      Eif (!checkTab.disabledTab()) {
        if (this.focusKeyManager.activeItemIndex === index) {
          this.focusKeyManager.setActiveItem(checkIndex);
        }
        if (active) {
          checkTab.selectTab(true);
        }
        return;
      }
    }
  }
 
  protected resizeContainer(width: number, scrollWidth: number): void {
    // 48px is the width of the menu button.
    this.showMenuButton.set(scrollWidth > width + (this.showMenuButton() ? 48 : 0));
  }
 
  protected keydown(event: KeyboardEvent): void {
    this.focusKeyManager.onKeydown(event);
  }
}