From a800c2a68014c77d0f29b5229a1938bed050813a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 24 Apr 2019 05:07:11 +0200 Subject: [PATCH] chore(menu): minor file reorganization (#15850) Reorganizes the files under `material/menu` so they make a bit more sense. The file for the `mat-menu` **component** isn't called `menu-directive` anymore. Also replaces the unnecessary `menu.ts` which was only re-exporting symbols. --- src/lib/menu/menu-directive.ts | 429 --------------------------------- src/lib/menu/menu-module.ts | 2 +- src/lib/menu/menu-trigger.ts | 2 +- src/lib/menu/menu.ts | 425 +++++++++++++++++++++++++++++++- src/lib/menu/public-api.ts | 7 +- 5 files changed, 427 insertions(+), 438 deletions(-) delete mode 100644 src/lib/menu/menu-directive.ts diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts deleted file mode 100644 index 1a66709ee27..00000000000 --- a/src/lib/menu/menu-directive.ts +++ /dev/null @@ -1,429 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/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, - HOME, - END, - hasModifierKey, -} 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('mat-menu-default-options', { - providedIn: 'root', - factory: MAT_MENU_DEFAULT_OPTIONS_FACTORY - }); - -/** @docs-private */ -export function MAT_MENU_DEFAULT_OPTIONS_FACTORY(): MatMenuDefaultOptions { - return { - overlapTrigger: false, - xPosition: 'after', - yPosition: 'below', - backdropClass: 'cdk-overlay-transparent-backdrop', - }; -} -/** - * Start elevation for the menu panel. - * @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, OnInit, OnDestroy { - private _keyManager: FocusKeyManager; - 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(); - - /** Subscription to tab events on the menu panel */ - private _tabSubscription = Subscription.EMPTY; - - /** Config object to be passed into the menu's ngClass */ - _classList: {[key: string]: boolean} = {}; - - /** Current state of the panel animation. */ - _panelAnimationState: 'void' | 'enter' = 'void'; - - /** Emits whenever an animation on the menu completes. */ - _animationDone = new Subject(); - - /** 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: MenuPositionX) { - 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: MenuPositionY) { - if (value !== 'above' && value !== 'below') { - throwMatMenuInvalidPositionY(); - } - this._yPosition = value; - this.setPositionClasses(); - } - - /** @docs-private */ - @ViewChild(TemplateRef, {static: false}) templateRef: TemplateRef; - - /** - * List of the items inside of a menu. - * @deprecated - * @breaking-change 8.0.0 - */ - @ContentChildren(MatMenuItem) items: QueryList; - - /** - * Menu content that will be rendered lazily. - * @docs-private - */ - @ContentChild(MatMenuContent, {static: false}) lazyContent: MatMenuContent; - - /** Whether the menu should overlap its trigger. */ - @Input() - get overlapTrigger(): boolean { return this._overlapTrigger; } - set overlapTrigger(value: boolean) { - 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: boolean | undefined) { - this._hasBackdrop = coerceBooleanProperty(value); - } - private _hasBackdrop: boolean | undefined = this._defaultOptions.hasBackdrop; - - /** - * This method takes classes set on the host mat-menu element and applies them on the - * menu template that displays in the overlay container. Otherwise, it's difficult - * to style the containing menu from outside the component. - * @param classes list of class names - */ - @Input('class') - set panelClass(classes: string) { - const previousPanelClass = this._previousPanelClass; - - if (previousPanelClass && previousPanelClass.length) { - previousPanelClass.split(' ').forEach((className: string) => { - this._classList[className] = false; - }); - } - - this._previousPanelClass = classes; - - if (classes && classes.length) { - classes.split(' ').forEach((className: string) => { - this._classList[className] = true; - }); - - this._elementRef.nativeElement.className = ''; - } - } - private _previousPanelClass: string; - - /** - * This method takes classes set on the host mat-menu element and applies them on the - * menu template that displays in the overlay container. Otherwise, it's difficult - * to style the containing menu from outside the component. - * @deprecated Use `panelClass` instead. - * @breaking-change 8.0.0 - */ - @Input() - get classList(): string { return this.panelClass; } - set classList(classes: string) { this.panelClass = classes; } - - /** Event emitted when the menu is closed. */ - @Output() readonly closed: EventEmitter = - new EventEmitter(); - - /** - * Event emitted when the menu is closed. - * @deprecated Switch to `closed` instead - * @breaking-change 8.0.0 - */ - @Output() close = this.closed; - - constructor( - private _elementRef: ElementRef, - private _ngZone: NgZone, - @Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { } - - ngOnInit() { - this.setPositionClasses(); - } - - ngAfterContentInit() { - 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(); - } - - /** Stream that emits whenever the hovered menu item changes. */ - _hovered(): Observable { - 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: KeyboardEvent) { - const keyCode = event.keyCode; - const manager = this._keyManager; - - 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; - 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'); - } - - manager.onKeydown(event); - } - } - - /** - * Focus the first item in the menu. - * @param origin Action from which the focus originated. Used to set the correct styling. - */ - focusFirstItem(origin: FocusOrigin = 'program'): void { - // When the content is rendered lazily, it takes a bit before the items are inside the DOM. - 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); - } - - /** - * Sets the menu panel elevation. - * @param depth Number of parent menus that come before the menu. - */ - setElevation(depth: number): void { - // The elevation starts at the base and increases by one for each level. - 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; - } - } - - /** - * Registers a menu item with the menu. - * @docs-private - */ - addItem(item: MatMenuItem) { - // We register the items through this method, rather than picking them up through - // `ContentChildren`, because we need the items to be picked up by their closest - // `mat-menu` ancestor. If we used `@ContentChildren(MatMenuItem, {descendants: true})`, - // all descendant items will bleed into the top-level menu in the case where the consumer - // has `mat-menu` instances nested inside each other. - if (this._items.indexOf(item) === -1) { - this._items.push(item); - this._itemChanges.next(this._items); - } - } - - /** - * Removes an item from the menu. - * @docs-private - */ - removeItem(item: MatMenuItem) { - const index = this._items.indexOf(item); - - if (this._items.indexOf(item) > -1) { - this._items.splice(index, 1); - this._itemChanges.next(this._items); - } - } - - /** - * Adds classes to the menu panel based on its position. Can be used by - * consumers to add specific styling based on the position. - * @param posX Position of the menu along the x axis. - * @param posY Position of the menu along the y axis. - * @docs-private - */ - setPositionClasses(posX: MenuPositionX = this.xPosition, posY: MenuPositionY = 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() { - // @breaking-change 8.0.0 Combine with _resetAnimation. - this._panelAnimationState = 'enter'; - } - - /** Resets the panel animation to its initial state. */ - _resetAnimation() { - // @breaking-change 8.0.0 Combine with _startAnimation. - this._panelAnimationState = 'void'; - } - - /** Callback that is invoked when the panel animation completes. */ - _onAnimationDone(event: AnimationEvent) { - this._animationDone.next(event); - this._isAnimating = false; - } - - _onAnimationStart(event: AnimationEvent) { - this._isAnimating = true; - - // Scroll the content element to the top as soon as the animation starts. This is necessary, - // because we move focus to the first item while it's still being animated, which can throw - // the browser off when it determines the scroll position. Alternatively we can move focus - // when the animation is done, however moving focus asynchronously will interrupt screen - // readers which are in the process of reading out the menu already. We take the `element` - // from the `event` since we can't use a `ViewChild` to access the pane. - if (event.toState === 'enter' && this._keyManager.activeItemIndex === 0) { - event.element.scrollTop = 0; - } - } -} diff --git a/src/lib/menu/menu-module.ts b/src/lib/menu/menu-module.ts index 66e76040336..d0e65b5c7f1 100644 --- a/src/lib/menu/menu-module.ts +++ b/src/lib/menu/menu-module.ts @@ -11,7 +11,7 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {MatCommonModule, MatRippleModule} from '@angular/material/core'; import {MatMenuContent} from './menu-content'; -import {MatMenu} from './menu-directive'; +import {MatMenu} from './menu'; import {MatMenuItem} from './menu-item'; import { MatMenuTrigger, diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index 16264cf885c..9c655e22cbd 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -36,7 +36,7 @@ import { 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 {MatMenu} from './menu'; import {throwMatMenuMissingError} from './menu-errors'; import {MatMenuItem} from './menu-item'; import {MatMenuPanel} from './menu-panel'; diff --git a/src/lib/menu/menu.ts b/src/lib/menu/menu.ts index 28c202b7187..0b07bfe943a 100644 --- a/src/lib/menu/menu.ts +++ b/src/lib/menu/menu.ts @@ -6,8 +6,423 @@ * found in the LICENSE file at https://angular.io/license */ -export {MatMenu, MatMenuDefaultOptions, MAT_MENU_DEFAULT_OPTIONS} from './menu-directive'; -export {MatMenuItem} from './menu-item'; -export {MatMenuTrigger} from './menu-trigger'; -export {MatMenuPanel} from './menu-panel'; -export {MenuPositionX, MenuPositionY} from './menu-positions'; +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, + HOME, + END, + hasModifierKey, +} 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 {MenuPositionX, MenuPositionY} from './menu-positions'; +import {throwMatMenuInvalidPositionX, throwMatMenuInvalidPositionY} from './menu-errors'; +import {MatMenuItem} from './menu-item'; +import {MAT_MENU_PANEL, MatMenuPanel} from './menu-panel'; +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('mat-menu-default-options', { + providedIn: 'root', + factory: MAT_MENU_DEFAULT_OPTIONS_FACTORY + }); + +/** @docs-private */ +export function MAT_MENU_DEFAULT_OPTIONS_FACTORY(): MatMenuDefaultOptions { + return { + overlapTrigger: false, + xPosition: 'after', + yPosition: 'below', + backdropClass: 'cdk-overlay-transparent-backdrop', + }; +} +/** + * Start elevation for the menu panel. + * @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, OnInit, OnDestroy { + private _keyManager: FocusKeyManager; + 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(); + + /** Subscription to tab events on the menu panel */ + private _tabSubscription = Subscription.EMPTY; + + /** Config object to be passed into the menu's ngClass */ + _classList: {[key: string]: boolean} = {}; + + /** Current state of the panel animation. */ + _panelAnimationState: 'void' | 'enter' = 'void'; + + /** Emits whenever an animation on the menu completes. */ + _animationDone = new Subject(); + + /** 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: MenuPositionX) { + 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: MenuPositionY) { + if (value !== 'above' && value !== 'below') { + throwMatMenuInvalidPositionY(); + } + this._yPosition = value; + this.setPositionClasses(); + } + + /** @docs-private */ + @ViewChild(TemplateRef, {static: false}) templateRef: TemplateRef; + + /** + * List of the items inside of a menu. + * @deprecated + * @breaking-change 8.0.0 + */ + @ContentChildren(MatMenuItem) items: QueryList; + + /** + * Menu content that will be rendered lazily. + * @docs-private + */ + @ContentChild(MatMenuContent, {static: false}) lazyContent: MatMenuContent; + + /** Whether the menu should overlap its trigger. */ + @Input() + get overlapTrigger(): boolean { return this._overlapTrigger; } + set overlapTrigger(value: boolean) { + 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: boolean | undefined) { + this._hasBackdrop = coerceBooleanProperty(value); + } + private _hasBackdrop: boolean | undefined = this._defaultOptions.hasBackdrop; + + /** + * This method takes classes set on the host mat-menu element and applies them on the + * menu template that displays in the overlay container. Otherwise, it's difficult + * to style the containing menu from outside the component. + * @param classes list of class names + */ + @Input('class') + set panelClass(classes: string) { + const previousPanelClass = this._previousPanelClass; + + if (previousPanelClass && previousPanelClass.length) { + previousPanelClass.split(' ').forEach((className: string) => { + this._classList[className] = false; + }); + } + + this._previousPanelClass = classes; + + if (classes && classes.length) { + classes.split(' ').forEach((className: string) => { + this._classList[className] = true; + }); + + this._elementRef.nativeElement.className = ''; + } + } + private _previousPanelClass: string; + + /** + * This method takes classes set on the host mat-menu element and applies them on the + * menu template that displays in the overlay container. Otherwise, it's difficult + * to style the containing menu from outside the component. + * @deprecated Use `panelClass` instead. + * @breaking-change 8.0.0 + */ + @Input() + get classList(): string { return this.panelClass; } + set classList(classes: string) { this.panelClass = classes; } + + /** Event emitted when the menu is closed. */ + @Output() readonly closed: EventEmitter = + new EventEmitter(); + + /** + * Event emitted when the menu is closed. + * @deprecated Switch to `closed` instead + * @breaking-change 8.0.0 + */ + @Output() close = this.closed; + + constructor( + private _elementRef: ElementRef, + private _ngZone: NgZone, + @Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { } + + ngOnInit() { + this.setPositionClasses(); + } + + ngAfterContentInit() { + 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(); + } + + /** Stream that emits whenever the hovered menu item changes. */ + _hovered(): Observable { + 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: KeyboardEvent) { + const keyCode = event.keyCode; + const manager = this._keyManager; + + 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; + 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'); + } + + manager.onKeydown(event); + } + } + + /** + * Focus the first item in the menu. + * @param origin Action from which the focus originated. Used to set the correct styling. + */ + focusFirstItem(origin: FocusOrigin = 'program'): void { + // When the content is rendered lazily, it takes a bit before the items are inside the DOM. + 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); + } + + /** + * Sets the menu panel elevation. + * @param depth Number of parent menus that come before the menu. + */ + setElevation(depth: number): void { + // The elevation starts at the base and increases by one for each level. + 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; + } + } + + /** + * Registers a menu item with the menu. + * @docs-private + */ + addItem(item: MatMenuItem) { + // We register the items through this method, rather than picking them up through + // `ContentChildren`, because we need the items to be picked up by their closest + // `mat-menu` ancestor. If we used `@ContentChildren(MatMenuItem, {descendants: true})`, + // all descendant items will bleed into the top-level menu in the case where the consumer + // has `mat-menu` instances nested inside each other. + if (this._items.indexOf(item) === -1) { + this._items.push(item); + this._itemChanges.next(this._items); + } + } + + /** + * Removes an item from the menu. + * @docs-private + */ + removeItem(item: MatMenuItem) { + const index = this._items.indexOf(item); + + if (this._items.indexOf(item) > -1) { + this._items.splice(index, 1); + this._itemChanges.next(this._items); + } + } + + /** + * Adds classes to the menu panel based on its position. Can be used by + * consumers to add specific styling based on the position. + * @param posX Position of the menu along the x axis. + * @param posY Position of the menu along the y axis. + * @docs-private + */ + setPositionClasses(posX: MenuPositionX = this.xPosition, posY: MenuPositionY = 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() { + // @breaking-change 8.0.0 Combine with _resetAnimation. + this._panelAnimationState = 'enter'; + } + + /** Resets the panel animation to its initial state. */ + _resetAnimation() { + // @breaking-change 8.0.0 Combine with _startAnimation. + this._panelAnimationState = 'void'; + } + + /** Callback that is invoked when the panel animation completes. */ + _onAnimationDone(event: AnimationEvent) { + this._animationDone.next(event); + this._isAnimating = false; + } + + _onAnimationStart(event: AnimationEvent) { + this._isAnimating = true; + + // Scroll the content element to the top as soon as the animation starts. This is necessary, + // because we move focus to the first item while it's still being animated, which can throw + // the browser off when it determines the scroll position. Alternatively we can move focus + // when the animation is done, however moving focus asynchronously will interrupt screen + // readers which are in the process of reading out the menu already. We take the `element` + // from the `event` since we can't use a `ViewChild` to access the pane. + if (event.toState === 'enter' && this._keyManager.activeItemIndex === 0) { + event.element.scrollTop = 0; + } + } +} diff --git a/src/lib/menu/public-api.ts b/src/lib/menu/public-api.ts index e0462d4094d..92feabe3ce8 100644 --- a/src/lib/menu/public-api.ts +++ b/src/lib/menu/public-api.ts @@ -6,8 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +export {MatMenu, MatMenuDefaultOptions, MAT_MENU_DEFAULT_OPTIONS} from './menu'; +export {MatMenuItem} from './menu-item'; +export {MatMenuTrigger, MAT_MENU_SCROLL_STRATEGY} from './menu-trigger'; +export {MatMenuPanel} from './menu-panel'; export * from './menu-module'; -export * from './menu'; export * from './menu-animations'; export * from './menu-content'; -export {MAT_MENU_SCROLL_STRATEGY} from './menu-trigger'; +export * from './menu-positions';