license import { FocusMonitor , FocusOrigin , isFakeMousedownFromScreenReader } from '@angular/cdk/a11y' ; import { Direction , Directionality } from '@angular/cdk/bidi' ; import { LEFT_ARROW , RIGHT_ARROW } from '@angular/cdk/keycodes' ; import { FlexibleConnectedPositionStrategy , HorizontalConnectionPos , Overlay , OverlayConfig , OverlayRef , VerticalConnectionPos , ScrollStrategy , } from '@angular/cdk/overlay' ; import { TemplatePortal } from '@angular/cdk/portal' ; import { AfterContentInit , Directive , ElementRef , EventEmitter , Inject , InjectionToken , Input , OnDestroy , Optional , Output , Self , ViewContainerRef , } from '@angular/core' ; import { normalizePassiveListenerOptions } from '@angular/cdk/platform' ; import { asapScheduler , merge , of as observableOf , Subscription } from 'rxjs' ; import { delay , filter , take , takeUntil } from 'rxjs/operators' ; import { MatMenu } from './menu-directive' ; import { throwMatMenuMissingError } from './menu-errors' ; import { MatMenuItem } from './menu-item' ; import { MatMenuPanel } from './menu-panel' ; import { MenuPositionX , MenuPositionY } from './menu-positions' ; /** Injection token that determines the scroll handling while the menu is open. */ export const MAT_MENU_SCROLL_STRATEGY = new InjectionToken < ( ) => ScrollStrategy > ( 'mat-menu-scroll-strategy' ) ; docs-private export function MAT_MENU_SCROLL_STRATEGY_FACTORY ( overlay ) { return ( ) => overlay . scrollStrategies . reposition ( ) ; } docs-private export const MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER = { provide : MAT_MENU_SCROLL_STRATEGY , deps : [ Overlay ] , useFactory : MAT_MENU_SCROLL_STRATEGY_FACTORY , } ; /** Default top padding of the menu panel. */ export const MENU_PANEL_TOP_PADDING = 8 ; /** Options for binding a passive event listener. */ const passiveEventListenerOptions = normalizePassiveListenerOptions ( { passive : true } ) ; /**
 * This directive is intended to be used in conjunction with an mat-menu tag.  It is
 * responsible for toggling the display of the provided menu instance.
 */ @ Directive ( { selector : `[mat-menu-trigger-for], [matMenuTriggerFor]` , host : { 'aria-haspopup' : 'true' , '[attr.aria-expanded]' : 'menuOpen || null' , '(mousedown)' : '_handleMousedown($event)' , '(keydown)' : '_handleKeydown($event)' , '(click)' : '_handleClick($event)' , } , exportAs : 'matMenuTrigger' } ) export class MatMenuTrigger implements AfterContentInit , OnDestroy { private _portal : TemplatePortal ; private _overlayRef : OverlayRef | null = null ; private _menuOpen : boolean = false ; private _closeSubscription = Subscription . EMPTY ; private _hoverSubscription = Subscription . EMPTY ; private _menuCloseSubscription = Subscription . EMPTY ; private _scrollStrategy : ( ) => ScrollStrategy ; /**
     * Handles touch start events on the trigger.
     * Needs to be an arrow function so we can easily use addEventListener and removeEventListener.
     */ private _handleTouchStart = ( ) => this . _openedBy = 'touch' ; _openedBy : 'mouse' | 'touch' | null = null ; deprecated breaking-change @ Input ( 'mat-menu-trigger-for' ) get _deprecatedMatMenuTriggerFor ( ) : MatMenuPanel { return this . menu ; } set _deprecatedMatMenuTriggerFor ( v ) { this . menu = v ; } /** References the menu instance that the trigger is associated with. */ @ Input ( 'matMenuTriggerFor' ) get menu ( ) { return this . _menu ; } set menu ( menu ) { if ( menu === this . _menu ) { return ; } this . _menu = menu ; this . _menuCloseSubscription . unsubscribe ( ) ; if ( menu ) { this . _menuCloseSubscription = menu . close . asObservable ( ) . subscribe ( reason => { this . _destroyMenu ( ) ; if ( ( reason === 'click' || reason === 'tab' ) && this . _parentMenu ) { this . _parentMenu . closed . emit ( reason ) ; } } ) ; } } private _menu : MatMenuPanel ; /** Data to be passed along to any lazily-rendered content. */ @ Input ( 'matMenuTriggerData' ) menuData : any ; /** Event emitted when the associated menu is opened. */ @ Output ( ) readonly menuOpened : EventEmitter < void > = new EventEmitter < void > ( ) ; deprecated breaking-change @ Output ( ) readonly onMenuOpen : EventEmitter < void > = this . menuOpened ; /** Event emitted when the associated menu is closed. */ @ Output ( ) readonly menuClosed : EventEmitter < void > = new EventEmitter < void > ( ) ; deprecated breaking-change @ Output ( ) readonly onMenuClose : EventEmitter < void > = this . menuClosed ; constructor ( private _overlay , private _element , private _viewContainerRef , @ Inject ( MAT_MENU_SCROLL_STRATEGY ) scrollStrategy , @ Optional ( ) private _parentMenu , @ Optional ( ) @ Self ( ) private _menuItemInstance , @ Optional ( ) private _dir , private _focusMonitor ? ) { _element . nativeElement . addEventListener ( 'touchstart' , this . _handleTouchStart , passiveEventListenerOptions ) ; if ( _menuItemInstance ) { _menuItemInstance . _triggersSubmenu = this . triggersSubmenu ( ) ; } this . _scrollStrategy = scrollStrategy ; } ngAfterContentInit ( ) { this . _checkMenu ( ) ; this . _handleHover ( ) ; } ngOnDestroy ( ) { if ( this . _overlayRef ) { this . _overlayRef . dispose ( ) ; this . _overlayRef = null ; } this . _element . nativeElement . removeEventListener ( 'touchstart' , this . _handleTouchStart , passiveEventListenerOptions ) ; this . _cleanUpSubscriptions ( ) ; } /** Whether the menu is open. */ get menuOpen ( ) : boolean { return this . _menuOpen ; } /** The text direction of the containing app. */ get dir ( ) : Direction { return this . _dir && this . _dir . value === 'rtl' ? 'rtl' : 'ltr' ; } /** Whether the menu triggers a sub-menu or a top-level one. */ triggersSubmenu ( ) { return ! ! ( this . _menuItemInstance && this . _parentMenu ) ; } /** Toggles the menu between the open and closed states. */ toggleMenu ( ) { return this . _menuOpen ? this . closeMenu ( ) : this . openMenu ( ) ; } /** Opens the menu. */ openMenu ( ) { if ( this . _menuOpen ) { return ; } this . _checkMenu ( ) ; const overlayRef = this . _createOverlay ( ) ; const overlayConfig = overlayRef . getConfig ( ) ; this . _setPosition ( overlayConfig . positionStrategy as FlexibleConnectedPositionStrategy ) ; overlayConfig . hasBackdrop = this . menu . hasBackdrop == null ? ! this . triggersSubmenu ( ) : this . menu . hasBackdrop ; overlayRef . attach ( this . _getPortal ( ) ) ; if ( this . menu . lazyContent ) { this . menu . lazyContent . attach ( this . menuData ) ; } this . _closeSubscription = this . _menuClosingActions ( ) . subscribe ( ( ) => this . closeMenu ( ) ) ; this . _initMenu ( ) ; if ( this . menu instanceof MatMenu ) { this . menu . _startAnimation ( ) ; } } /** Closes the menu. */ closeMenu ( ) { this . menu . close . emit ( ) ; } param origin focus ( origin = 'program' ) { if ( this . _focusMonitor ) { this . _focusMonitor . focusVia ( this . _element , origin ) ; } else { this . _element . nativeElement . focus ( ) ; } } /** Closes the menu and does the necessary cleanup. */ private _destroyMenu ( ) { if ( ! this . _overlayRef || ! this . menuOpen ) { return ; } const menu = this . menu ; this . _closeSubscription . unsubscribe ( ) ; this . _overlayRef . detach ( ) ; if ( menu instanceof MatMenu ) { menu . _resetAnimation ( ) ; if ( menu . lazyContent ) { menu . _animationDone . pipe ( filter ( event => event . toState === 'void' ) , take ( 1 ) , takeUntil ( menu . lazyContent . _attached ) ) . subscribe ( ( ) => menu . lazyContent ! . detach ( ) , undefined , ( ) => { this . _resetMenu ( ) ; } ) ; } else { this . _resetMenu ( ) ; } } else { this . _resetMenu ( ) ; if ( menu . lazyContent ) { menu . lazyContent . detach ( ) ; } } } /**
     * This method sets the menu state to open and focuses the first item if
     * the menu was opened via the keyboard.
     */ private _initMenu ( ) { this . menu . parentMenu = this . triggersSubmenu ( ) ? this . _parentMenu : undefined ; this . menu . direction = this . dir ; this . _setMenuElevation ( ) ; this . _setIsMenuOpen ( true ) ; this . menu . focusFirstItem ( this . _openedBy || 'program' ) ; } /** Updates the menu elevation based on the amount of parent menus that it has. */ private _setMenuElevation ( ) { if ( this . menu . setElevation ) { let depth = 0 ; let parentMenu = this . menu . parentMenu ; while ( parentMenu ) { depth ++ ; parentMenu = parentMenu . parentMenu ; } this . menu . setElevation ( depth ) ; } } /**
     * This method resets the menu when it's closed, most importantly restoring
     * focus to the menu trigger if the menu was opened via the keyboard.
     */ private _resetMenu ( ) { this . _setIsMenuOpen ( false ) ; if ( ! this . _openedBy ) { this . focus ( ) ; } else if ( ! this . triggersSubmenu ( ) ) { this . focus ( this . _openedBy ) ; } this . _openedBy = null ; } private _setIsMenuOpen ( isOpen ) { this . _menuOpen = isOpen ; this . _menuOpen ? this . menuOpened . emit ( ) : this . menuClosed . emit ( ) ; if ( this . triggersSubmenu ( ) ) { this . _menuItemInstance . _highlighted = isOpen ; } } /**
     * This method checks that a valid instance of MatMenu has been passed into
     * matMenuTriggerFor. If not, an exception is thrown.
     */ private _checkMenu ( ) { if ( ! this . menu ) { throwMatMenuMissingError ( ) ; } } /**
     * This method creates the overlay from the provided menu's template and saves its
     * OverlayRef so that it can be attached to the DOM when openMenu is called.
     */ private _createOverlay ( ) { if ( ! this . _overlayRef ) { const config = this . _getOverlayConfig ( ) ; this . _subscribeToPositions ( config . positionStrategy as FlexibleConnectedPositionStrategy ) ; this . _overlayRef = this . _overlay . create ( config ) ; this . _overlayRef . keydownEvents ( ) . subscribe ( ) ; } return this . _overlayRef ; } returns private _getOverlayConfig ( ) { return new OverlayConfig ( { positionStrategy : this . _overlay . position ( ) . flexibleConnectedTo ( this . _element ) . withLockedPosition ( ) . withTransformOriginOn ( '.mat-menu-panel' ) , backdropClass : this . menu . backdropClass || 'cdk-overlay-transparent-backdrop' , scrollStrategy : this . _scrollStrategy ( ) , direction : this . _dir } ) ; } /**
     * Listens to changes in the position of the overlay and sets the correct classes
     * on the menu based on the new position. This ensures the animation origin is always
     * correct, even if a fallback position is used for the overlay.
     */ private _subscribeToPositions ( position ) { if ( this . menu . setPositionClasses ) { position . positionChanges . subscribe ( change => { const posX = change . connectionPair . overlayX === 'start' ? 'after' : 'before' ; const posY = change . connectionPair . overlayY === 'top' ? 'below' : 'above' ; this . menu . setPositionClasses ! ( posX , posY ) ; } ) ; } } param positionStrategy private _setPosition ( positionStrategy ) { let [ originX , originFallbackX ] = this . menu . xPosition === 'before' ? [ 'end' , 'start' ] : [ 'start' , 'end' ] ; let [ overlayY , overlayFallbackY ] = this . menu . yPosition === 'above' ? [ 'bottom' , 'top' ] : [ 'top' , 'bottom' ] ; let [ originY , originFallbackY ] = [ overlayY , overlayFallbackY ] ; let [ overlayX , overlayFallbackX ] = [ originX , originFallbackX ] ; let offsetY = 0 ; if ( this . triggersSubmenu ( ) ) { overlayFallbackX = originX = this . menu . xPosition === 'before' ? 'start' : 'end' ; originFallbackX = overlayX = originX === 'end' ? 'start' : 'end' ; offsetY = overlayY === 'bottom' ? MENU_PANEL_TOP_PADDING : - MENU_PANEL_TOP_PADDING ; } else if ( ! this . menu . overlapTrigger ) { originY = overlayY === 'top' ? 'bottom' : 'top' ; originFallbackY = overlayFallbackY === 'top' ? 'bottom' : 'top' ; } positionStrategy . withPositions ( [ { originX , originY , overlayX , overlayY , offsetY } , { originX : originFallbackX , originY , overlayX : overlayFallbackX , overlayY , offsetY } , { originX , originY : originFallbackY , overlayX , overlayY : overlayFallbackY , offsetY : - offsetY } , { originX : originFallbackX , originY : originFallbackY , overlayX : overlayFallbackX , overlayY : overlayFallbackY , offsetY : - offsetY } ] ) ; } /** Cleans up the active subscriptions. */ private _cleanUpSubscriptions ( ) { this . _closeSubscription . unsubscribe ( ) ; this . _hoverSubscription . unsubscribe ( ) ; } /** Returns a stream that emits whenever an action that should close the menu occurs. */ private _menuClosingActions ( ) { const backdrop = this . _overlayRef ! . backdropClick ( ) ; const detachments = this . _overlayRef ! . detachments ( ) ; const parentClose = this . _parentMenu ? this . _parentMenu . closed : observableOf ( ) ; const hover = this . _parentMenu ? this . _parentMenu . _hovered ( ) . pipe ( filter ( active => active !== this . _menuItemInstance ) , filter ( ( ) => this . _menuOpen ) ) : observableOf ( ) ; return merge ( backdrop , parentClose , hover , detachments ) ; } /** Handles mouse presses on the trigger. */ _handleMousedown ( event ) { if ( ! isFakeMousedownFromScreenReader ( event ) ) { this . _openedBy = event . button === 0 ? 'mouse' : null ; if ( this . triggersSubmenu ( ) ) { event . preventDefault ( ) ; } } } /** Handles key presses on the trigger. */ _handleKeydown ( event ) { const keyCode = event . keyCode ; if ( this . triggersSubmenu ( ) && ( ( keyCode === RIGHT_ARROW && this . dir === 'ltr' ) || ( keyCode === LEFT_ARROW && this . dir === 'rtl' ) ) ) { this . openMenu ( ) ; } } /** Handles click events on the trigger. */ _handleClick ( event ) { if ( this . triggersSubmenu ( ) ) { event . stopPropagation ( ) ; this . openMenu ( ) ; } else { this . toggleMenu ( ) ; } } /** Handles the cases where the user hovers over the trigger. */ private _handleHover ( ) { if ( ! this . triggersSubmenu ( ) ) { return ; } this . _hoverSubscription = this . _parentMenu . _hovered ( ) . pipe ( filter ( active => active === this . _menuItemInstance && ! active . disabled ) , delay ( 0 , asapScheduler ) ) . subscribe ( ( ) => { this . _openedBy = 'mouse' ; if ( this . menu instanceof MatMenu && this . menu . _isAnimating ) { this . menu . _animationDone . pipe ( take ( 1 ) , delay ( 0 , asapScheduler ) , takeUntil ( this . _parentMenu . _hovered ( ) ) ) . subscribe ( ( ) => this . openMenu ( ) ) ; } else { this . openMenu ( ) ; } } ) ; } /** Gets the portal that should be attached to the overlay. */ private _getPortal ( ) { if ( ! this . _portal || this . _portal . templateRef !== this . menu . templateRef ) { this . _portal = new TemplatePortal ( this . menu . templateRef , this . _viewContainerRef ) ; } return this . _portal ; } }