license import { Direction , Directionality } from '@angular/cdk/bidi' ; import { coerceNumberProperty } from '@angular/cdk/coercion' ; import { END , ENTER , HOME , SPACE } from '@angular/cdk/keycodes' ; import { ViewportRuler } from '@angular/cdk/scrolling' ; import { AfterContentChecked , AfterContentInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChildren , ElementRef , EventEmitter , Input , NgZone , OnDestroy , Optional , Output , QueryList , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { CanDisableRipple , CanDisableRippleCtor , mixinDisableRipple } from '@angular/material/core' ; import { merge , of as observableOf , Subject } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { MatInkBar } from './ink-bar' ; import { MatTabLabelWrapper } from './tab-label-wrapper' ; import { FocusKeyManager } from '@angular/cdk/a11y' ; /**
 * The directions that scrolling can go in when the header's tabs exceed the header width. 'After'
 * will scroll the header towards the end of the tabs list and 'before' will scroll towards the
 * beginning of the list.
 */ export type ScrollDirection = 'after' | 'before' ; /**
 * The distance in pixels that will be overshot when scrolling a tab label into view. This helps
 * provide a small affordance to the label next to it.
 */ const EXAGGERATED_OVERSCROLL = 60 ; docs-private export class MatTabHeaderBase { } export const _MatTabHeaderMixinBase = mixinDisableRipple ( MatTabHeaderBase ) ; docs-private @ Component ( { moduleId : module . id , selector : 'mat-tab-header' , templateUrl : 'tab-header.html' , styleUrls : [ 'tab-header.css' ] , inputs : [ 'disableRipple' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , host : { 'class' : 'mat-tab-header' , '[class.mat-tab-header-pagination-controls-enabled]' : '_showPaginationControls' , '[class.mat-tab-header-rtl]' : "_getLayoutDirection() == 'rtl'" , } , } ) export class MatTabHeader extends _MatTabHeaderMixinBase implements AfterContentChecked , AfterContentInit , OnDestroy , CanDisableRipple { @ ContentChildren ( MatTabLabelWrapper ) _labelWrappers : QueryList < MatTabLabelWrapper > ; @ ViewChild ( MatInkBar ) _inkBar : MatInkBar ; @ ViewChild ( 'tabListContainer' ) _tabListContainer : ElementRef ; @ ViewChild ( 'tabList' ) _tabList : ElementRef ; /** The distance in pixels that the tab labels should be translated to the left. */ private _scrollDistance = 0 ; /** Whether the header should scroll to the selected index after the view has been checked. */ private _selectedIndexChanged = false ; /** Emits when the component is destroyed. */ private readonly _destroyed = new Subject < void > ( ) ; /** Whether the controls for pagination should be displayed */ _showPaginationControls = false ; /** Whether the tab list can be scrolled more towards the end of the tab label list. */ _disableScrollAfter = true ; /** Whether the tab list can be scrolled more towards the beginning of the tab label list. */ _disableScrollBefore = true ; /**
     * The number of tab labels that are displayed on the header. When this changes, the header
     * should re-evaluate the scroll position.
     */ private _tabLabelCount : number ; /** Whether the scroll distance has changed and should be applied after the view is checked. */ private _scrollDistanceChanged : boolean ; /** Used to manage focus between the tabs. */ private _keyManager : FocusKeyManager < MatTabLabelWrapper > ; /** Cached text content of the header. */ private _currentTextContent : string ; /** The index of the active tab. */ @ Input ( ) get selectedIndex ( ) : number { return this . _selectedIndex ; } set selectedIndex ( value ) { value = coerceNumberProperty ( value ) ; this . _selectedIndexChanged = this . _selectedIndex != value ; this . _selectedIndex = value ; if ( this . _keyManager ) { this . _keyManager . updateActiveItemIndex ( value ) ; } } private _selectedIndex : number = 0 ; /** Event emitted when the option is selected. */ @ Output ( ) readonly selectFocusedIndex = new EventEmitter ( ) ; /** Event emitted when a label is focused. */ @ Output ( ) readonly indexFocused = new EventEmitter ( ) ; constructor ( private _elementRef , private _changeDetectorRef , private _viewportRuler , @ Optional ( ) private _dir , private _ngZone ? ) { super ( ) ; } ngAfterContentChecked ( ) { if ( this . _tabLabelCount != this . _labelWrappers . length ) { this . updatePagination ( ) ; this . _tabLabelCount = this . _labelWrappers . length ; this . _changeDetectorRef . markForCheck ( ) ; } if ( this . _selectedIndexChanged ) { this . _scrollToLabel ( this . _selectedIndex ) ; this . _checkScrollingControls ( ) ; this . _alignInkBarToSelectedTab ( ) ; this . _selectedIndexChanged = false ; this . _changeDetectorRef . markForCheck ( ) ; } if ( this . _scrollDistanceChanged ) { this . _updateTabScrollPosition ( ) ; this . _scrollDistanceChanged = false ; this . _changeDetectorRef . markForCheck ( ) ; } } _handleKeydown ( event ) { switch ( event . keyCode ) { case HOME : this . _keyManager . setFirstItemActive ( ) ; event . preventDefault ( ) ; break ; case END : this . _keyManager . setLastItemActive ( ) ; event . preventDefault ( ) ; break ; case ENTER : case SPACE : this . selectFocusedIndex . emit ( this . focusIndex ) ; event . preventDefault ( ) ; break ; default : this . _keyManager . onKeydown ( event ) ; } } /**
     * Aligns the ink bar to the selected tab on load.
     */ ngAfterContentInit ( ) { const dirChange = this . _dir ? this . _dir . change : observableOf ( null ) ; const resize = this . _viewportRuler . change ( 150 ) ; const realign = ( ) => { this . updatePagination ( ) ; this . _alignInkBarToSelectedTab ( ) ; } ; this . _keyManager = new FocusKeyManager ( this . _labelWrappers ) . withHorizontalOrientation ( this . _getLayoutDirection ( ) ) . withWrap ( ) ; this . _keyManager . updateActiveItem ( 0 ) ; typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame ( realign ) : realign ( ) ; merge ( dirChange , resize ) . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => { realign ( ) ; this . _keyManager . withHorizontalOrientation ( this . _getLayoutDirection ( ) ) ; } ) ; this . _keyManager . change . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( newFocusIndex => { this . indexFocused . emit ( newFocusIndex ) ; this . _setTabFocus ( newFocusIndex ) ; } ) ; } ngOnDestroy ( ) { this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } /**
     * Callback for when the MutationObserver detects that the content has changed.
     */ _onContentChanges ( ) { const textContent = this . _elementRef . nativeElement . textContent ; if ( textContent !== this . _currentTextContent ) { this . _currentTextContent = textContent ; const zoneCallback = ( ) => { this . updatePagination ( ) ; this . _alignInkBarToSelectedTab ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ; this . _ngZone ? this . _ngZone . run ( zoneCallback ) : zoneCallback ( ) ; } } /**
     * Updates the view whether pagination should be enabled or not.
     *
     * WARNING: Calling this method can be very costly in terms of performance.  It should be called
     * as infrequently as possible from outside of the Tabs component as it causes a reflow of the
     * page.
     */ updatePagination ( ) { this . _checkPaginationEnabled ( ) ; this . _checkScrollingControls ( ) ; this . _updateTabScrollPosition ( ) ; } /** Tracks which element has focus; used for keyboard navigation */ get focusIndex ( ) : number { return this . _keyManager ? this . _keyManager . activeItemIndex ! : 0 ; } /** When the focus index is set, we must manually send focus to the correct label */ set focusIndex ( value ) { if ( ! this . _isValidIndex ( value ) || this . focusIndex === value || ! this . _keyManager ) { return ; } this . _keyManager . setActiveItem ( value ) ; } /**
     * Determines if an index is valid.  If the tabs are not ready yet, we assume that the user is
     * providing a valid index and return true.
     */ _isValidIndex ( index ) { if ( ! this . _labelWrappers ) { return true ; } const tab = this . _labelWrappers ? this . _labelWrappers . toArray ( ) [ index ] : null ; return ! ! tab && ! tab . disabled ; } /**
     * Sets focus on the HTML element for the label wrapper and scrolls it into the view if
     * scrolling is enabled.
     */ _setTabFocus ( tabIndex ) { if ( this . _showPaginationControls ) { this . _scrollToLabel ( tabIndex ) ; } if ( this . _labelWrappers && this . _labelWrappers . length ) { this . _labelWrappers . toArray ( ) [ tabIndex ] . focus ( ) ; const containerEl = this . _tabListContainer . nativeElement ; const dir = this . _getLayoutDirection ( ) ; if ( dir == 'ltr' ) { containerEl . scrollLeft = 0 ; } else { containerEl . scrollLeft = containerEl . scrollWidth - containerEl . offsetWidth ; } } } /** The layout direction of the containing app. */ _getLayoutDirection ( ) { return this . _dir && this . _dir . value === 'rtl' ? 'rtl' : 'ltr' ; } /** Performs the CSS transformation on the tab list that will cause the list to scroll. */ _updateTabScrollPosition ( ) { const scrollDistance = this . scrollDistance ; const translateX = this . _getLayoutDirection ( ) === 'ltr' ? - scrollDistance : scrollDistance ; this . _tabList . nativeElement . style . transform = `translateX(${ Math . round ( translateX ) }px)` ; this . _tabListContainer . nativeElement . scrollLeft = 0 ; } /** Sets the distance in pixels that the tab header should be transformed in the X-axis. */ get scrollDistance ( ) : number { return this . _scrollDistance ; } set scrollDistance ( v ) { this . _scrollDistance = Math . max ( 0 , Math . min ( this . _getMaxScrollDistance ( ) , v ) ) ; this . _scrollDistanceChanged = true ; this . _checkScrollingControls ( ) ; } /**
     * Moves the tab list in the 'before' or 'after' direction (towards the beginning of the list or
     * the end of the list, respectively). The distance to scroll is computed to be a third of the
     * length of the tab list view window.
     *
     * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
     * should be called sparingly.
     */ _scrollHeader ( scrollDir ) { const viewLength = this . _tabListContainer . nativeElement . offsetWidth ; this . scrollDistance += ( scrollDir == 'before' ? - 1 : 1 ) * viewLength / 3 ; } /**
     * Moves the tab list such that the desired tab label (marked by index) is moved into view.
     *
     * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
     * should be called sparingly.
     */ _scrollToLabel ( labelIndex ) { const selectedLabel = this . _labelWrappers ? this . _labelWrappers . toArray ( ) [ labelIndex ] : null ; if ( ! selectedLabel ) { return ; } const viewLength = this . _tabListContainer . nativeElement . offsetWidth ; let labelBeforePos , labelAfterPos ; if ( this . _getLayoutDirection ( ) == 'ltr' ) { labelBeforePos = selectedLabel . getOffsetLeft ( ) ; labelAfterPos = labelBeforePos + selectedLabel . getOffsetWidth ( ) ; } else { labelAfterPos = this . _tabList . nativeElement . offsetWidth - selectedLabel . getOffsetLeft ( ) ; labelBeforePos = labelAfterPos - selectedLabel . getOffsetWidth ( ) ; } const beforeVisiblePos = this . scrollDistance ; const afterVisiblePos = this . scrollDistance + viewLength ; if ( labelBeforePos < beforeVisiblePos ) { this . scrollDistance -= beforeVisiblePos - labelBeforePos + EXAGGERATED_OVERSCROLL ; } else if ( labelAfterPos > afterVisiblePos ) { this . scrollDistance += labelAfterPos - afterVisiblePos + EXAGGERATED_OVERSCROLL ; } } /**
     * Evaluate whether the pagination controls should be displayed. If the scroll width of the
     * tab list is wider than the size of the header container, then the pagination controls should
     * be shown.
     *
     * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
     * should be called sparingly.
     */ _checkPaginationEnabled ( ) { const isEnabled = this . _tabList . nativeElement . scrollWidth > this . _elementRef . nativeElement . offsetWidth ; if ( ! isEnabled ) { this . scrollDistance = 0 ; } if ( isEnabled !== this . _showPaginationControls ) { this . _changeDetectorRef . markForCheck ( ) ; } this . _showPaginationControls = isEnabled ; } /**
     * Evaluate whether the before and after controls should be enabled or disabled.
     * If the header is at the beginning of the list (scroll distance is equal to 0) then disable the
     * before button. If the header is at the end of the list (scroll distance is equal to the
     * maximum distance we can scroll), then disable the after button.
     *
     * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
     * should be called sparingly.
     */ _checkScrollingControls ( ) { this . _disableScrollBefore = this . scrollDistance == 0 ; this . _disableScrollAfter = this . scrollDistance == this . _getMaxScrollDistance ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } /**
     * Determines what is the maximum length in pixels that can be set for the scroll distance. This
     * is equal to the difference in width between the tab list container and tab header container.
     *
     * This is an expensive call that forces a layout reflow to compute box and scroll metrics and
     * should be called sparingly.
     */ _getMaxScrollDistance ( ) { const lengthOfTabList = this . _tabList . nativeElement . scrollWidth ; const viewLength = this . _tabListContainer . nativeElement . offsetWidth ; return ( lengthOfTabList - viewLength ) || 0 ; } /** Tells the ink-bar to align itself to the current label wrapper */ _alignInkBarToSelectedTab ( ) { const selectedLabelWrapper = this . _labelWrappers && this . _labelWrappers . length ? this . _labelWrappers . toArray ( ) [ this . selectedIndex ] . elementRef . nativeElement : null ; this . _inkBar . alignToElement ( selectedLabelWrapper ! ) ; } }