license import { FocusKeyManager , FocusOrigin } from '@angular/cdk/a11y' ; import { Direction } from '@angular/cdk/bidi' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { ESCAPE , LEFT_ARROW , RIGHT_ARROW , DOWN_ARROW , UP_ARROW } from '@angular/cdk/keycodes' ; import { AfterContentInit , ChangeDetectionStrategy , Component , ContentChild , ContentChildren , ElementRef , EventEmitter , Inject , InjectionToken , Input , NgZone , OnDestroy , Output , TemplateRef , QueryList , ViewChild , ViewEncapsulation , OnInit , } from '@angular/core' ; import { merge , Observable , Subject , Subscription } from 'rxjs' ; import { startWith , switchMap , take } from 'rxjs/operators' ; import { matMenuAnimations } from './menu-animations' ; import { MatMenuContent } from './menu-content' ; import { throwMatMenuInvalidPositionX , throwMatMenuInvalidPositionY } from './menu-errors' ; import { MatMenuItem } from './menu-item' ; import { MAT_MENU_PANEL , MatMenuPanel } from './menu-panel' ; import { MenuPositionX , MenuPositionY } from './menu-positions' ; import { AnimationEvent } from '@angular/animations' ; /** Default `mat-menu` options that can be overridden. */ export interface MatMenuDefaultOptions { /** The x-axis position of the menu. */ xPosition : MenuPositionX ; /** The y-axis position of the menu. */ yPosition : MenuPositionY ; /** Whether the menu should overlap the menu trigger. */ overlapTrigger : boolean ; /** Class to be applied to the menu's backdrop. */ backdropClass : string ; /** Whether the menu has a backdrop. */ hasBackdrop ? : boolean ; } /** Injection token to be used to override the default options for `mat-menu`. */ export const MAT_MENU_DEFAULT_OPTIONS = new InjectionToken < MatMenuDefaultOptions > ( 'mat-menu-default-options' , { providedIn : 'root' , factory : MAT_MENU_DEFAULT_OPTIONS_FACTORY } ) ; docs-private export function MAT_MENU_DEFAULT_OPTIONS_FACTORY ( ) { return { overlapTrigger : false , xPosition : 'after' , yPosition : 'below' , backdropClass : 'cdk-overlay-transparent-backdrop' , } ; } docs-private const MAT_MENU_BASE_ELEVATION = 4 ; @ Component ( { moduleId : module . id , selector : 'mat-menu' , templateUrl : 'menu.html' , styleUrls : [ 'menu.css' ] , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , exportAs : 'matMenu' , animations : [ matMenuAnimations . transformMenu , matMenuAnimations . fadeInItems ] , providers : [ { provide : MAT_MENU_PANEL , useExisting : MatMenu } ] } ) export class MatMenu implements AfterContentInit , MatMenuPanel < MatMenuItem > , OnInit , OnDestroy { private _keyManager : FocusKeyManager < MatMenuItem > ; private _xPosition : MenuPositionX = this . _defaultOptions . xPosition ; private _yPosition : MenuPositionY = this . _defaultOptions . yPosition ; private _previousElevation : string ; /** Menu items inside the current menu. */ private _items : MatMenuItem [ ] = [ ] ; /** Emits whenever the amount of menu items changes. */ private _itemChanges = new Subject < MatMenuItem [ ] > ( ) ; /** Subscription to tab events on the menu panel */ private _tabSubscription = Subscription . EMPTY ; /** Config object to be passed into the menu's ngClass */ _classList : { [ key ] : boolean ; } = { } ; /** Current state of the panel animation. */ _panelAnimationState : 'void' | 'enter' = 'void' ; /** Emits whenever an animation on the menu completes. */ _animationDone = new Subject < AnimationEvent > ( ) ; /** Whether the menu is animating. */ _isAnimating : boolean ; /** Parent menu of the current menu panel. */ parentMenu : MatMenuPanel | undefined ; /** Layout direction of the menu. */ direction : Direction ; /** Class to be added to the backdrop element. */ @ Input ( ) backdropClass : string = this . _defaultOptions . backdropClass ; /** Position of the menu in the X axis. */ @ Input ( ) get xPosition ( ) : MenuPositionX { return this . _xPosition ; } set xPosition ( value ) { if ( value !== 'before' && value !== 'after' ) { throwMatMenuInvalidPositionX ( ) ; } this . _xPosition = value ; this . setPositionClasses ( ) ; } /** Position of the menu in the Y axis. */ @ Input ( ) get yPosition ( ) : MenuPositionY { return this . _yPosition ; } set yPosition ( value ) { if ( value !== 'above' && value !== 'below' ) { throwMatMenuInvalidPositionY ( ) ; } this . _yPosition = value ; this . setPositionClasses ( ) ; } docs-private @ ViewChild ( TemplateRef ) templateRef : TemplateRef < any > ; deprecated breaking-change @ ContentChildren ( MatMenuItem ) items : QueryList < MatMenuItem > ; docs-private @ ContentChild ( MatMenuContent ) lazyContent : MatMenuContent ; /** Whether the menu should overlap its trigger. */ @ Input ( ) get overlapTrigger ( ) : boolean { return this . _overlapTrigger ; } set overlapTrigger ( value ) { this . _overlapTrigger = coerceBooleanProperty ( value ) ; } private _overlapTrigger : boolean = this . _defaultOptions . overlapTrigger ; /** Whether the menu has a backdrop. */ @ Input ( ) get hasBackdrop ( ) : boolean | undefined { return this . _hasBackdrop ; } set hasBackdrop ( value ) { this . _hasBackdrop = coerceBooleanProperty ( value ) ; } private _hasBackdrop : boolean | undefined = this . _defaultOptions . hasBackdrop ; param classes @ Input ( 'class' ) set panelClass ( classes ) { if ( classes && classes . length ) { this . _classList = classes . split ( ' ' ) . reduce ( ( obj , className ) => { obj [ className ] = true ; return obj ; } , { } ) ; this . _elementRef . nativeElement . className = '' ; } } deprecated breaking-change @ Input ( ) get classList ( ) : string { return this . panelClass ; } set classList ( classes ) { this . panelClass = classes ; } /** Event emitted when the menu is closed. */ @ Output ( ) readonly closed : EventEmitter < void | 'click' | 'keydown' | 'tab' > = new EventEmitter < void | 'click' | 'keydown' | 'tab' > ( ) ; deprecated breaking-change @ Output ( ) close = this . closed ; constructor ( private _elementRef , private _ngZone , @ Inject ( MAT_MENU_DEFAULT_OPTIONS ) private _defaultOptions ) { } ngOnInit ( ) { this . setPositionClasses ( ) ; } ngAfterContentInit ( ) { this . _keyManager = new FocusKeyManager < MatMenuItem > ( this . _items ) . withWrap ( ) . withTypeAhead ( ) ; this . _tabSubscription = this . _keyManager . tabOut . subscribe ( ( ) => this . closed . emit ( 'tab' ) ) ; } ngOnDestroy ( ) { this . _tabSubscription . unsubscribe ( ) ; this . closed . complete ( ) ; } /** Stream that emits whenever the hovered menu item changes. */ _hovered ( ) { return this . _itemChanges . pipe ( startWith ( this . _items ) , switchMap ( items => merge ( ... items . map ( item => item . _hovered ) ) ) ) ; } /** Handle a keyboard event from the menu, delegating to the appropriate action. */ _handleKeydown ( event ) { const keyCode = event . keyCode ; switch ( keyCode ) { case ESCAPE : this . closed . emit ( 'keydown' ) ; break ; case LEFT_ARROW : if ( this . parentMenu && this . direction === 'ltr' ) { this . closed . emit ( 'keydown' ) ; } break ; case RIGHT_ARROW : if ( this . parentMenu && this . direction === 'rtl' ) { this . closed . emit ( 'keydown' ) ; } break ; default : if ( keyCode === UP_ARROW || keyCode === DOWN_ARROW ) { this . _keyManager . setFocusOrigin ( 'keyboard' ) ; } this . _keyManager . onKeydown ( event ) ; } } param origin focusFirstItem ( origin = 'program' ) { if ( this . lazyContent ) { this . _ngZone . onStable . asObservable ( ) . pipe ( take ( 1 ) ) . subscribe ( ( ) => this . _keyManager . setFocusOrigin ( origin ) . setFirstItemActive ( ) ) ; } else { this . _keyManager . setFocusOrigin ( origin ) . setFirstItemActive ( ) ; } } /**
     * 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 ) ; } param depth setElevation ( depth ) { const newElevation = `mat-elevation-z${ MAT_MENU_BASE_ELEVATION + depth }` ; const customElevation = Object . keys ( this . _classList ) . find ( c => c . startsWith ( 'mat-elevation-z' ) ) ; if ( ! customElevation || customElevation === this . _previousElevation ) { if ( this . _previousElevation ) { this . _classList [ this . _previousElevation ] = false ; } this . _classList [ newElevation ] = true ; this . _previousElevation = newElevation ; } } docs-private addItem ( item ) { if ( this . _items . indexOf ( item ) === - 1 ) { this . _items . push ( item ) ; this . _itemChanges . next ( this . _items ) ; } } docs-private removeItem ( item ) { const index = this . _items . indexOf ( item ) ; if ( this . _items . indexOf ( item ) > - 1 ) { this . _items . splice ( index , 1 ) ; this . _itemChanges . next ( this . _items ) ; } } param posX param posY docs-private setPositionClasses ( posX = this . xPosition , posY = this . yPosition ) { const classes = this . _classList ; classes [ 'mat-menu-before' ] = posX === 'before' ; classes [ 'mat-menu-after' ] = posX === 'after' ; classes [ 'mat-menu-above' ] = posY === 'above' ; classes [ 'mat-menu-below' ] = posY === 'below' ; } /** Starts the enter animation. */ _startAnimation ( ) { this . _panelAnimationState = 'enter' ; } /** Resets the panel animation to its initial state. */ _resetAnimation ( ) { this . _panelAnimationState = 'void' ; } /** Callback that is invoked when the panel animation completes. */ _onAnimationDone ( event ) { this . _animationDone . next ( event ) ; this . _isAnimating = false ; } _onAnimationStart ( event ) { this . _isAnimating = true ; if ( event . toState === 'enter' && this . _keyManager . activeItemIndex === 0 ) { event . element . scrollTop = 0 ; } } }