license import { coerceBooleanProperty , coerceNumberProperty } from '@angular/cdk/coercion' ; import { AfterContentChecked , AfterContentInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChildren , ElementRef , EventEmitter , Input , OnDestroy , Output , QueryList , ViewChild , ViewEncapsulation , InjectionToken , Inject , Optional , } from '@angular/core' ; import { CanColor , CanColorCtor , CanDisableRipple , CanDisableRippleCtor , mixinColor , mixinDisableRipple , ThemePalette , } from '@angular/material/core' ; import { merge , Subscription } from 'rxjs' ; import { MatTab } from './tab' ; import { MatTabHeader } from './tab-header' ; /** 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 MatTabChangeEvent { /** Index of the currently-selected tab. */ index : number ; /** Reference to the currently-selected tab. */ tab : MatTab ; } /** Possible positions for the tab header. */ export type MatTabHeaderPosition = 'above' | 'below' ; /** Object that can be used to configure the default options for the tabs module. */ export interface MatTabsConfig { /** Duration for the tab animation. Must be a valid CSS value (e.g. 600ms). */ animationDuration ? : string ; } /** Injection token that can be used to provide the default options the tabs module. */ export const MAT_TABS_CONFIG = new InjectionToken ( 'MAT_TABS_CONFIG' ) ; docs-private export class MatTabGroupBase { constructor ( public _elementRef ) { } } export const _MatTabGroupMixinBase = mixinColor ( mixinDisableRipple ( MatTabGroupBase ) , 'primary' ) ; /**
 * Material design tab-group component.  Supports basic tab pairs (label + content) and includes
 * animated ink-bar, keyboard navigation, and screen reader.
 * See: https://material.io/design/components/tabs.html
 */ @ Component ( { moduleId : module . id , selector : 'mat-tab-group' , exportAs : 'matTabGroup' , templateUrl : 'tab-group.html' , styleUrls : [ 'tab-group.css' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , inputs : [ 'color' , 'disableRipple' ] , host : { 'class' : 'mat-tab-group' , '[class.mat-tab-group-dynamic-height]' : 'dynamicHeight' , '[class.mat-tab-group-inverted-header]' : 'headerPosition === "below"' , } , } ) export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentInit , AfterContentChecked , OnDestroy , CanColor , CanDisableRipple { @ ContentChildren ( MatTab ) _tabs : QueryList < MatTab > ; @ ViewChild ( 'tabBodyWrapper' ) _tabBodyWrapper : ElementRef ; @ ViewChild ( 'tabHeader' ) _tabHeader : MatTabHeader ; /** 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 = false ; /** 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 ; /** Position of the tab header. */ @ Input ( ) headerPosition : MatTabHeaderPosition = 'above' ; /** Duration for the tab animation. Must be a valid CSS value (e.g. 600ms). */ @ Input ( ) animationDuration : string ; /** Background color of the tab group. */ @ Input ( ) get backgroundColor ( ) : ThemePalette { return this . _backgroundColor ; } set backgroundColor ( value ) { const nativeElement = this . _elementRef . nativeElement ; nativeElement . classList . remove ( `mat-background-${ this . backgroundColor }` ) ; if ( value ) { nativeElement . classList . add ( `mat-background-${ value }` ) ; } this . _backgroundColor = value ; } private _backgroundColor : ThemePalette ; /** 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 < MatTabChangeEvent > = new EventEmitter < MatTabChangeEvent > ( ) ; /** 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 < MatTabChangeEvent > = new EventEmitter < MatTabChangeEvent > ( true ) ; private _groupId : number ; constructor ( elementRef , private _changeDetectorRef , @ Inject ( MAT_TABS_CONFIG ) @ Optional ( ) defaultConfig ? ) { super ( elementRef ) ; this . _groupId = nextId ++ ; this . animationDuration = defaultConfig && defaultConfig . animationDuration ? defaultConfig . animationDuration : '500ms' ; } /**
     * 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 . _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 . _subscribeToTabLabels ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; } ngOnDestroy ( ) { this . _tabsSubscription . unsubscribe ( ) ; this . _tabLabelSubscription . unsubscribe ( ) ; } /** Re-aligns the ink bar to the selected tab element. */ realignInkBar ( ) { if ( this . _tabHeader ) { this . _tabHeader . _alignInkBarToSelectedTab ( ) ; } } _focusChanged ( index ) { this . focusChange . emit ( this . _createChangeEvent ( index ) ) ; } private _createChangeEvent ( index ) { const event = new MatTabChangeEvent ; 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 MatTab component, whereas the data binding is inside the MatTabGroup. 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 `mat-tab-label-${ this . _groupId }-${ i }` ; } /** Returns a unique id for each tab content element */ _getTabContentId ( i ) { return `mat-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 ; } }