import { AnimationEvent } from '@angular/animations' ; import { animate , state , style , transition , trigger } from '@angular/animations' ; import { FocusKeyManager , FocusOrigin } from '@angular/cdk/a11y' ; import { DOWN_ARROW , END , ESCAPE , hasModifierKey , HOME , UP_ARROW } from '@angular/cdk/keycodes' ; import { CdkPortal , CdkPortalOutlet } from '@angular/cdk/portal' ; import { AfterContentInit , ChangeDetectionStrategy , Component , ContentChildren , ElementRef , EventEmitter , HostListener , Inject , Input , OnDestroy , Output , QueryList , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { TypeRef } from '@sbb-esta/angular/core' ; import { Observable , Subject , Subscription } from 'rxjs' ; import { map , startWith , takeUntil } from 'rxjs/operators' ; import type { SbbHeaderLean } from './header' ; import { SbbHeaderMenuItem } from './header-menu-item' ; import { SBB_HEADER } from './header-token' ; let nextId = 0 ; @ Component ( { selector : 'sbb-header-menu' , templateUrl : './header-menu.html' , styleUrls : [ './header-menu.css' ] , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , exportAs : 'sbbHeaderMenu' , animations : [ trigger ( 'open' , [ state ( 'closed' , style ( { transform : 'translateX(-100vw)' } ) ) , state ( 'open-menu' , style ( { transform : 'translateX(0)' } ) ) , transition ( 'closed <=> open-menu' , animate ( '0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86)' ) ) , ] ) , ] , host : { class : 'sbb-header-menu' , '[id]' : 'this.id' , '[@open]' : 'this._animationState' , '[class.sbb-header-menus-collapsed]' : 'this._header._menusCollapsed' , } , } ) export class SbbHeaderMenu implements AfterContentInit , OnDestroy { _backButton : string = typeof $localize === 'function' ? $localize `:Go back to the app chooser navigation@@sbbHeaderMenuBack:Back` : 'Back' ; /** Unique ID to be used by menu trigger's "aria-owns" property. */ id : string = `sbb-header-menu-${ nextId ++ }` ; /** Event emitted when the menu is closed. */ @ Output ( ) readonly closed : EventEmitter < void | 'click' | 'keydown' | 'tab' > = new EventEmitter < void | 'click' | 'keydown' | 'tab' > ( ) ; /** Reference to the menu items. */ @ ContentChildren ( SbbHeaderMenuItem ) _items : QueryList < SbbHeaderMenuItem > ; docs-private @ ViewChild ( CdkPortal ) _panelPortal : CdkPortal ; docs-private @ ViewChild ( CdkPortalOutlet , { static : true } ) _panelPortalOutlet : CdkPortalOutlet ; /** Whether the menu panel is open. */ get open ( ) : boolean { return this . _open && ! ! this . showPanel ; } set open ( value ) { if ( value !== this . _open ) { this . _open = value ; if ( ! value ) { this . _animationState = 'closed' ; } else if ( ! this . _header . _menusCollapsed ) { this . _animationState = 'open-panel' ; } else { this . _animationState = 'open-menu' ; } } } private _open = false ; param classes @ Input ( 'class' ) set panelClass ( classes ) { const previousPanelClass = this . _previousPanelClass ; if ( previousPanelClass && previousPanelClass . length ) { previousPanelClass . split ( ' ' ) . forEach ( ( className ) => { this . _classList [ className ] = false ; } ) ; } this . _previousPanelClass = classes ; if ( classes && classes . length ) { classes . split ( ' ' ) . forEach ( ( className ) => { this . _classList [ className ] = true ; } ) ; this . _elementRef . nativeElement . className = '' ; } } private _previousPanelClass : string ; /** Whether the autocomplete panel should be visible, depending on option length. */ showPanel : boolean = false ; /** Manages active item in item list based on key events. */ _keyManager : FocusKeyManager < SbbHeaderMenuItem > ; /** Class list for the panel. */ _classList : { [ key ] : boolean ; } = { } ; docs-private _panel : CdkPortal | null = null ; docs-private _animationState : 'closed' | 'open-panel' | 'open-menu' = 'closed' ; /** Subscription to tab events on the menu panel */ private _tabSubscription = Subscription . EMPTY ; private _destroyed = new Subject < void > ( ) ; constructor ( private _elementRef , @ Inject ( SBB_HEADER ) public _header ) { this . closed . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => { if ( this . _animationState !== 'closed' ) { this . _animationState = 'closed' ; } } ) ; } ngAfterContentInit ( ) { ( this . _items . changes as Observable < SbbHeaderMenuItem [ ] > ) . pipe ( startWith ( this . _items . toArray ( ) ) , map ( ( i ) => ! ! i . length ) , takeUntil ( this . _destroyed ) ) . subscribe ( ( s ) => ( this . showPanel = s ) ) ; this . _keyManager = new FocusKeyManager ( this . _items ) . withWrap ( ) . withTypeAhead ( ) ; this . _tabSubscription = this . _keyManager . tabOut . subscribe ( ( ) => this . closed . emit ( 'tab' ) ) ; } ngOnDestroy ( ) { this . _tabSubscription . unsubscribe ( ) ; this . closed . complete ( ) ; this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } param origin focusFirstItem ( origin = 'program' ) { const manager = this . _keyManager ; manager . setFocusOrigin ( origin ) . setFirstItemActive ( ) ; if ( ! manager . activeItem && this . _items . length ) { let element = this . _items . first . _getHostElement ( ) . parentElement ; while ( element ) { if ( element . getAttribute ( 'role' ) === 'menu' ) { element . focus ( ) ; break ; } else { element = element . parentElement ; } } } } /**
     * Resets the active item in the menu. This is used when the menu is opened, allowing
     * the user to start from the first option when pressing the down arrow.
     */ resetActiveItem ( ) { this . _keyManager . setActiveItem ( - 1 ) ; } @ HostListener ( '@open.done' , [ '$event' ] ) _onAnimationDone ( event ) { if ( event . toState === 'closed' && event . fromState !== 'void' ) { this . closed . emit ( ) ; } } @ HostListener ( 'keydown' , [ '$event' ] ) _handleMenuKeydown ( event ) { if ( event . keyCode === ESCAPE ) { this . open = false ; } } /** Handle a keyboard event from the menu, delegating to the appropriate action. */ _handlePanelKeydown ( event ) { const keyCode = event . keyCode ; const manager = this . _keyManager ; switch ( keyCode ) { case ESCAPE : if ( ! hasModifierKey ( event ) ) { event . preventDefault ( ) ; this . closed . emit ( 'keydown' ) ; } break ; case HOME : case END : if ( ! hasModifierKey ( event ) ) { keyCode === HOME ? manager . setFirstItemActive ( ) : manager . setLastItemActive ( ) ; event . preventDefault ( ) ; } break ; default : if ( keyCode === UP_ARROW || keyCode === DOWN_ARROW ) { manager . setFocusOrigin ( 'keyboard' ) ; } else if ( ! this . _header . _menusCollapsed ) { manager . onKeydown ( event ) ; } } } }