import { FocusOrigin } from '@angular/cdk/a11y' ; import { BooleanInput , coerceBooleanProperty , coerceNumberProperty , NumberInput , } from '@angular/cdk/coercion' ; import { AfterContentChecked , AfterContentInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChildren , Directive , ElementRef , EventEmitter , Inject , Input , OnDestroy , Optional , Output , QueryList , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations' ; import { merge , Subscription } from 'rxjs' ; import { startWith } from 'rxjs/operators' ; import { SbbTab , SBB_TAB_GROUP } from './tab' ; import { SbbTabsConfig , SBB_TABS_CONFIG } from './tab-config' ; /** Used to generate unique ID's for each tab component */ let nextId = 0 ; /** A simple change event emitted on focus or selection changes. */ export class SbbTabChangeEvent { /** Index of the currently-selected tab. */ index : number ; /** Reference to the currently-selected tab. */ tab : SbbTab ; } interface SbbTabGroupBaseHeader { focusIndex : number ; } docs-private @ Directive ( ) export abstract class SbbTabGroupBase implements AfterContentInit , AfterContentChecked , OnDestroy { /**
     * All tabs inside the tab group. This includes tabs that belong to groups that are nested
     * inside the current one. We filter out only the tabs that belong to this group in `_tabs`.
     */ abstract _allTabs : QueryList < SbbTab > ; abstract _tabBodyWrapper : ElementRef ; abstract _tabHeader : SbbTabGroupBaseHeader ; /** All of the tabs that belong to the group. */ _tabs : QueryList < SbbTab > = new QueryList < SbbTab > ( ) ; /** The tab index that should be selected after the content has been checked. */ private _indexToSelect : number | null = 0 ; /** Snapshot of the height of the tab body wrapper before another tab is activated. */ private _tabBodyWrapperHeight : number = 0 ; /** Subscription to tabs being added/removed. */ private _tabsSubscription = Subscription . EMPTY ; /** Subscription to changes in the tab labels. */ private _tabLabelSubscription = Subscription . EMPTY ; /** Whether the tab group should grow to the size of the active tab. */ @ Input ( ) get dynamicHeight ( ) : boolean { return this . _dynamicHeight ; } set dynamicHeight ( value ) { this . _dynamicHeight = coerceBooleanProperty ( value ) ; } private _dynamicHeight : boolean ; /** The index of the active tab. */ @ Input ( ) get selectedIndex ( ) : number | null { return this . _selectedIndex ; } set selectedIndex ( value ) { this . _indexToSelect = coerceNumberProperty ( value , null ) ; } private _selectedIndex : number | null = null ; /** Output to enable support for two-way binding on `[(selectedIndex)]` */ @ Output ( ) readonly selectedIndexChange : EventEmitter < number > = new EventEmitter < number > ( ) ; /** Event emitted when focus has changed within a tab group. */ @ Output ( ) readonly focusChange : EventEmitter < SbbTabChangeEvent > = new EventEmitter < SbbTabChangeEvent > ( ) ; /** Event emitted when the body animation has completed */ @ Output ( ) readonly animationDone : EventEmitter < void > = new EventEmitter < void > ( ) ; /** Event emitted when the tab selection has changed. */ @ Output ( ) readonly selectedTabChange : EventEmitter < SbbTabChangeEvent > = new EventEmitter < SbbTabChangeEvent > ( true ) ; private _groupId : number ; constructor ( protected _changeDetectorRef , @ Inject ( SBB_TABS_CONFIG ) @ Optional ( ) defaultConfig ? , @ Optional ( ) @ Inject ( ANIMATION_MODULE_TYPE ) public _animationMode ? ) { this . _groupId = nextId ++ ; this . dynamicHeight = defaultConfig && defaultConfig . dynamicHeight != null ? defaultConfig . dynamicHeight : false ; } /**
     * After the content is checked, this component knows what tabs have been defined
     * and what the selected index should be. This is where we can know exactly what position
     * each tab should be in according to the new selected index, and additionally we know how
     * a new selected tab should transition in (from the left or right).
     */ ngAfterContentChecked ( ) { const indexToSelect = ( this . _indexToSelect = this . _clampTabIndex ( this . _indexToSelect ) ) ; if ( this . _selectedIndex !== indexToSelect ) { const isFirstRun = this . _selectedIndex == null ; if ( ! isFirstRun ) { this . selectedTabChange . emit ( this . _createChangeEvent ( indexToSelect ) ) ; } Promise . resolve ( ) . then ( ( ) => { this . _tabs . forEach ( ( tab , index ) => ( tab . isActive = index === indexToSelect ) ) ; if ( ! isFirstRun ) { this . selectedIndexChange . emit ( indexToSelect ) ; } } ) ; } this . _tabs . forEach ( ( tab , index ) => { tab . position = index - indexToSelect ; if ( this . _selectedIndex != null && tab . position === 0 && ! tab . origin ) { tab . origin = indexToSelect - this . _selectedIndex ; } } ) ; if ( this . _selectedIndex !== indexToSelect ) { this . _selectedIndex = indexToSelect ; this . _changeDetectorRef . markForCheck ( ) ; } } ngAfterContentInit ( ) { this . _subscribeToAllTabChanges ( ) ; this . _subscribeToTabLabels ( ) ; this . _tabsSubscription = this . _tabs . changes . subscribe ( ( ) => { const indexToSelect = this . _clampTabIndex ( this . _indexToSelect ) ; if ( indexToSelect === this . _selectedIndex ) { const tabs = this . _tabs . toArray ( ) ; for ( let i = 0 ; i < tabs . length ; i ++ ) { if ( tabs [ i ] . isActive ) { this . _indexToSelect = this . _selectedIndex = i ; break ; } } } this . _changeDetectorRef . markForCheck ( ) ; } ) ; } /** Listens to changes in all of the tabs. */ private _subscribeToAllTabChanges ( ) { this . _allTabs . changes . pipe ( startWith ( this . _allTabs ) ) . subscribe ( ( tabs ) => { this . _tabs . reset ( tabs . filter ( ( tab ) => tab . _closestTabGroup === this ) ) ; this . _tabs . notifyOnChanges ( ) ; } ) ; } ngOnDestroy ( ) { this . _tabs . destroy ( ) ; this . _tabsSubscription . unsubscribe ( ) ; this . _tabLabelSubscription . unsubscribe ( ) ; } param index focusTab ( index ) { const header = this . _tabHeader ; if ( header ) { header . focusIndex = index ; } } _focusChanged ( index ) { this . focusChange . emit ( this . _createChangeEvent ( index ) ) ; } private _createChangeEvent ( index ) { const event = new SbbTabChangeEvent ( ) ; event . index = index ; if ( this . _tabs && this . _tabs . length ) { event . tab = this . _tabs . toArray ( ) [ index ] ; } return event ; } /**
     * Subscribes to changes in the tab labels. This is needed, because the @Input for the label is
     * on the SbbTab component, whereas the data binding is inside the SbbTabGroup. In order for the
     * binding to be updated, we need to subscribe to changes in it and trigger change detection
     * manually.
     */ private _subscribeToTabLabels ( ) { if ( this . _tabLabelSubscription ) { this . _tabLabelSubscription . unsubscribe ( ) ; } this . _tabLabelSubscription = merge ( ... this . _tabs . map ( ( tab ) => tab . _stateChanges ) ) . subscribe ( ( ) => this . _changeDetectorRef . markForCheck ( ) ) ; } /** Clamps the given index to the bounds of 0 and the tabs length. */ private _clampTabIndex ( index ) { return Math . min ( this . _tabs . length - 1 , Math . max ( index || 0 , 0 ) ) ; } /** Returns a unique id for each tab label element */ _getTabLabelId ( i ) { return `sbb-tab-label-${ this . _groupId }-${ i }` ; } /** Returns a unique id for each tab content element */ _getTabContentId ( i ) { return `sbb-tab-content-${ this . _groupId }-${ i }` ; } /**
     * Sets the height of the body wrapper to the height of the activating tab if dynamic
     * height property is true.
     */ _setTabBodyWrapperHeight ( tabHeight ) { if ( ! this . _dynamicHeight || ! this . _tabBodyWrapperHeight ) { return ; } const wrapper = this . _tabBodyWrapper . nativeElement ; wrapper . style . height = this . _tabBodyWrapperHeight + 'px' ; if ( this . _tabBodyWrapper . nativeElement . offsetHeight ) { wrapper . style . height = tabHeight + 'px' ; } } /** Removes the height of the tab body wrapper. */ _removeTabBodyWrapperHeight ( ) { const wrapper = this . _tabBodyWrapper . nativeElement ; this . _tabBodyWrapperHeight = wrapper . clientHeight ; wrapper . style . height = '' ; this . animationDone . emit ( ) ; } /** Handle click events, setting new selected index if appropriate. */ _handleClick ( tab , tabHeader , index ) { if ( ! tab . disabled ) { this . selectedIndex = tabHeader . focusIndex = index ; } } /** Retrieves the tabindex for the tab. */ _getTabIndex ( tab , idx ) { if ( tab . disabled ) { return null ; } return this . selectedIndex === idx ? 0 : - 1 ; } /** Callback for when the focused state of a tab has changed. */ _tabFocusChanged ( focusOrigin , index ) { if ( focusOrigin ) { this . _tabHeader . focusIndex = index ; } } } /**
 * SBB design tab-group component. Supports basic tab pairs (label + content) and
 * includes keyboard navigation and screen reader.
 */ @ Component ( { selector : 'sbb-tab-group' , exportAs : 'sbbTabGroup' , templateUrl : 'tab-group.html' , styleUrls : [ 'tab-group.css' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . Default , providers : [ { provide : SBB_TAB_GROUP , useExisting : SbbTabGroup , } , ] , host : { class : 'sbb-tab-group' , '[class.sbb-tab-group-dynamic-height]' : 'dynamicHeight' , } , } ) export class SbbTabGroup extends SbbTabGroupBase { @ ContentChildren ( SbbTab , { descendants : true } ) _allTabs : QueryList < SbbTab > ; @ ViewChild ( 'tabBodyWrapper' ) _tabBodyWrapper : ElementRef ; @ ViewChild ( 'tabHeader' ) _tabHeader : SbbTabGroupBaseHeader ; constructor ( changeDetectorRef , @ Inject ( SBB_TABS_CONFIG ) @ Optional ( ) defaultConfig ? , @ Optional ( ) @ Inject ( ANIMATION_MODULE_TYPE ) animationMode ? ) { super ( changeDetectorRef , defaultConfig , animationMode ) ; } }