mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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.
This commit is contained in:
committed by
mmalerba
parent
3a7beb9382
commit
a800c2a680
@@ -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<MatMenuDefaultOptions>('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<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: string]: 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: 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<any>;
|
||||
|
||||
/**
|
||||
* List of the items inside of a menu.
|
||||
* @deprecated
|
||||
* @breaking-change 8.0.0
|
||||
*/
|
||||
@ContentChildren(MatMenuItem) items: QueryList<MatMenuItem>;
|
||||
|
||||
/**
|
||||
* 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<void | 'click' | 'keydown' | 'tab'> =
|
||||
new EventEmitter<void | 'click' | 'keydown' | 'tab'>();
|
||||
|
||||
/**
|
||||
* 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<HTMLElement>,
|
||||
private _ngZone: NgZone,
|
||||
@Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { }
|
||||
|
||||
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(): Observable<MatMenuItem> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
+420
-5
@@ -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<MatMenuDefaultOptions>('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<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: string]: 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: 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<any>;
|
||||
|
||||
/**
|
||||
* List of the items inside of a menu.
|
||||
* @deprecated
|
||||
* @breaking-change 8.0.0
|
||||
*/
|
||||
@ContentChildren(MatMenuItem) items: QueryList<MatMenuItem>;
|
||||
|
||||
/**
|
||||
* 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<void | 'click' | 'keydown' | 'tab'> =
|
||||
new EventEmitter<void | 'click' | 'keydown' | 'tab'>();
|
||||
|
||||
/**
|
||||
* 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<HTMLElement>,
|
||||
private _ngZone: NgZone,
|
||||
@Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { }
|
||||
|
||||
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(): Observable<MatMenuItem> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user