@angular/material
Version:
Angular Material
871 lines (864 loc) • 72.6 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, Component, ChangeDetectionStrategy, ViewEncapsulation, Inject, Optional, Input, Directive, QueryList, EventEmitter, TemplateRef, ContentChildren, ViewChild, ContentChild, Output, Self, NgModule } from '@angular/core';
import * as i1 from '@angular/cdk/a11y';
import { FocusKeyManager, isFakeTouchstartFromScreenReader, isFakeMousedownFromScreenReader } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { UP_ARROW, DOWN_ARROW, RIGHT_ARROW, LEFT_ARROW, ESCAPE, hasModifierKey, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Subject, merge, Subscription, of, asapScheduler } from 'rxjs';
import { startWith, switchMap, take, takeUntil, filter, delay } from 'rxjs/operators';
import * as i3 from '@angular/material/core';
import { mixinDisableRipple, mixinDisabled, MatRippleModule, MatCommonModule } from '@angular/material/core';
import * as i2 from '@angular/common';
import { DOCUMENT, CommonModule } from '@angular/common';
import { TemplatePortal, DomPortalOutlet } from '@angular/cdk/portal';
import { trigger, state, style, transition, animate } from '@angular/animations';
import * as i3$1 from '@angular/cdk/bidi';
import * as i1$1 from '@angular/cdk/overlay';
import { Overlay, OverlayConfig, OverlayModule } from '@angular/cdk/overlay';
import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
import { CdkScrollableModule } from '@angular/cdk/scrolling';
/**
* @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
*/
/**
* Injection token used to provide the parent menu to menu-specific components.
* @docs-private
*/
const MAT_MENU_PANEL = new InjectionToken('MAT_MENU_PANEL');
/**
* @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
*/
// Boilerplate for applying mixins to MatMenuItem.
/** @docs-private */
const _MatMenuItemBase = mixinDisableRipple(mixinDisabled(class {
}));
/**
* Single item inside of a `mat-menu`. Provides the menu item styling and accessibility treatment.
*/
class MatMenuItem extends _MatMenuItemBase {
constructor(_elementRef, _document, _focusMonitor, _parentMenu, _changeDetectorRef) {
super();
this._elementRef = _elementRef;
this._document = _document;
this._focusMonitor = _focusMonitor;
this._parentMenu = _parentMenu;
this._changeDetectorRef = _changeDetectorRef;
/** ARIA role for the menu item. */
this.role = 'menuitem';
/** Stream that emits when the menu item is hovered. */
this._hovered = new Subject();
/** Stream that emits when the menu item is focused. */
this._focused = new Subject();
/** Whether the menu item is highlighted. */
this._highlighted = false;
/** Whether the menu item acts as a trigger for a sub-menu. */
this._triggersSubmenu = false;
_parentMenu?.addItem?.(this);
}
/** Focuses the menu item. */
focus(origin, options) {
if (this._focusMonitor && origin) {
this._focusMonitor.focusVia(this._getHostElement(), origin, options);
}
else {
this._getHostElement().focus(options);
}
this._focused.next(this);
}
ngAfterViewInit() {
if (this._focusMonitor) {
// Start monitoring the element so it gets the appropriate focused classes. We want
// to show the focus style for menu items only when the focus was not caused by a
// mouse or touch interaction.
this._focusMonitor.monitor(this._elementRef, false);
}
}
ngOnDestroy() {
if (this._focusMonitor) {
this._focusMonitor.stopMonitoring(this._elementRef);
}
if (this._parentMenu && this._parentMenu.removeItem) {
this._parentMenu.removeItem(this);
}
this._hovered.complete();
this._focused.complete();
}
/** Used to set the `tabindex`. */
_getTabIndex() {
return this.disabled ? '-1' : '0';
}
/** Returns the host DOM element. */
_getHostElement() {
return this._elementRef.nativeElement;
}
/** Prevents the default element actions if it is disabled. */
_checkDisabled(event) {
if (this.disabled) {
event.preventDefault();
event.stopPropagation();
}
}
/** Emits to the hover stream. */
_handleMouseEnter() {
this._hovered.next(this);
}
/** Gets the label to be used when determining whether the option should be focused. */
getLabel() {
const clone = this._elementRef.nativeElement.cloneNode(true);
const icons = clone.querySelectorAll('mat-icon, .material-icons');
// Strip away icons so they don't show up in the text.
for (let i = 0; i < icons.length; i++) {
icons[i].remove();
}
return clone.textContent?.trim() || '';
}
_setHighlighted(isHighlighted) {
// We need to mark this for check for the case where the content is coming from a
// `matMenuContent` whose change detection tree is at the declaration position,
// not the insertion position. See #23175.
// @breaking-change 12.0.0 Remove null check for `_changeDetectorRef`.
this._highlighted = isHighlighted;
this._changeDetectorRef?.markForCheck();
}
_setTriggersSubmenu(triggersSubmenu) {
// @breaking-change 12.0.0 Remove null check for `_changeDetectorRef`.
this._triggersSubmenu = triggersSubmenu;
this._changeDetectorRef?.markForCheck();
}
_hasFocus() {
return this._document && this._document.activeElement === this._getHostElement();
}
}
MatMenuItem.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatMenuItem, deps: [{ token: i0.ElementRef }, { token: DOCUMENT }, { token: i1.FocusMonitor }, { token: MAT_MENU_PANEL, optional: true }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
MatMenuItem.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatMenuItem, selector: "[mat-menu-item]", inputs: { disabled: "disabled", disableRipple: "disableRipple", role: "role" }, host: { listeners: { "click": "_checkDisabled($event)", "mouseenter": "_handleMouseEnter()" }, properties: { "attr.role": "role", "class.mat-mdc-menu-item-highlighted": "_highlighted", "class.mat-mdc-menu-item-submenu-trigger": "_triggersSubmenu", "attr.tabindex": "_getTabIndex()", "attr.aria-disabled": "disabled", "attr.disabled": "disabled || null" }, classAttribute: "mat-mdc-menu-item mat-mdc-focus-indicator mdc-list-item" }, exportAs: ["matMenuItem"], usesInheritance: true, ngImport: i0, template: "<ng-content select=\"mat-icon\"></ng-content>\n<span class=\"mdc-list-item__primary-text\"><ng-content></ng-content></span>\n<div class=\"mat-mdc-menu-ripple\" matRipple\n [matRippleDisabled]=\"disableRipple || disabled\"\n [matRippleTrigger]=\"_getHostElement()\">\n</div>\n<svg\n *ngIf=\"_triggersSubmenu\"\n class=\"mat-mdc-menu-submenu-icon\"\n viewBox=\"0 0 5 10\"\n focusable=\"false\"><polygon points=\"0,0 5,5 0,10\"/></svg>\n", dependencies: [{ kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatMenuItem, decorators: [{
type: Component,
args: [{ selector: '[mat-menu-item]', exportAs: 'matMenuItem', inputs: ['disabled', 'disableRipple'], host: {
'[attr.role]': 'role',
'class': 'mat-mdc-menu-item mat-mdc-focus-indicator mdc-list-item',
'[class.mat-mdc-menu-item-highlighted]': '_highlighted',
'[class.mat-mdc-menu-item-submenu-trigger]': '_triggersSubmenu',
'[attr.tabindex]': '_getTabIndex()',
'[attr.aria-disabled]': 'disabled',
'[attr.disabled]': 'disabled || null',
'(click)': '_checkDisabled($event)',
'(mouseenter)': '_handleMouseEnter()',
}, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<ng-content select=\"mat-icon\"></ng-content>\n<span class=\"mdc-list-item__primary-text\"><ng-content></ng-content></span>\n<div class=\"mat-mdc-menu-ripple\" matRipple\n [matRippleDisabled]=\"disableRipple || disabled\"\n [matRippleTrigger]=\"_getHostElement()\">\n</div>\n<svg\n *ngIf=\"_triggersSubmenu\"\n class=\"mat-mdc-menu-submenu-icon\"\n viewBox=\"0 0 5 10\"\n focusable=\"false\"><polygon points=\"0,0 5,5 0,10\"/></svg>\n" }]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: i1.FocusMonitor }, { type: undefined, decorators: [{
type: Inject,
args: [MAT_MENU_PANEL]
}, {
type: Optional
}] }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { role: [{
type: Input
}] } });
/**
* @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
*/
/**
* Throws an exception for the case when menu's x-position value isn't valid.
* In other words, it doesn't match 'before' or 'after'.
* @docs-private
*/
function throwMatMenuInvalidPositionX() {
throw Error(`xPosition value must be either 'before' or after'.
Example: <mat-menu xPosition="before" #menu="matMenu"></mat-menu>`);
}
/**
* Throws an exception for the case when menu's y-position value isn't valid.
* In other words, it doesn't match 'above' or 'below'.
* @docs-private
*/
function throwMatMenuInvalidPositionY() {
throw Error(`yPosition value must be either 'above' or below'.
Example: <mat-menu yPosition="above" #menu="matMenu"></mat-menu>`);
}
/**
* Throws an exception for the case when a menu is assigned
* to a trigger that is placed inside the same menu.
* @docs-private
*/
function throwMatMenuRecursiveError() {
throw Error(`matMenuTriggerFor: menu cannot contain its own trigger. Assign a menu that is ` +
`not a parent of the trigger or move the trigger outside of the menu.`);
}
/**
* @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
*/
/**
* Injection token that can be used to reference instances of `MatMenuContent`. It serves
* as alternative token to the actual `MatMenuContent` class which could cause unnecessary
* retention of the class and its directive metadata.
*/
const MAT_MENU_CONTENT = new InjectionToken('MatMenuContent');
class _MatMenuContentBase {
constructor(_template, _componentFactoryResolver, _appRef, _injector, _viewContainerRef, _document, _changeDetectorRef) {
this._template = _template;
this._componentFactoryResolver = _componentFactoryResolver;
this._appRef = _appRef;
this._injector = _injector;
this._viewContainerRef = _viewContainerRef;
this._document = _document;
this._changeDetectorRef = _changeDetectorRef;
/** Emits when the menu content has been attached. */
this._attached = new Subject();
}
/**
* Attaches the content with a particular context.
* @docs-private
*/
attach(context = {}) {
if (!this._portal) {
this._portal = new TemplatePortal(this._template, this._viewContainerRef);
}
this.detach();
if (!this._outlet) {
this._outlet = new DomPortalOutlet(this._document.createElement('div'), this._componentFactoryResolver, this._appRef, this._injector);
}
const element = this._template.elementRef.nativeElement;
// Because we support opening the same menu from different triggers (which in turn have their
// own `OverlayRef` panel), we have to re-insert the host element every time, otherwise we
// risk it staying attached to a pane that's no longer in the DOM.
element.parentNode.insertBefore(this._outlet.outletElement, element);
// When `MatMenuContent` is used in an `OnPush` component, the insertion of the menu
// content via `createEmbeddedView` does not cause the content to be seen as "dirty"
// by Angular. This causes the `@ContentChildren` for menu items within the menu to
// not be updated by Angular. By explicitly marking for check here, we tell Angular that
// it needs to check for new menu items and update the `@ContentChild` in `MatMenu`.
// @breaking-change 9.0.0 Make change detector ref required
this._changeDetectorRef?.markForCheck();
this._portal.attach(this._outlet, context);
this._attached.next();
}
/**
* Detaches the content.
* @docs-private
*/
detach() {
if (this._portal.isAttached) {
this._portal.detach();
}
}
ngOnDestroy() {
if (this._outlet) {
this._outlet.dispose();
}
}
}
_MatMenuContentBase.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatMenuContentBase, deps: [{ token: i0.TemplateRef }, { token: i0.ComponentFactoryResolver }, { token: i0.ApplicationRef }, { token: i0.Injector }, { token: i0.ViewContainerRef }, { token: DOCUMENT }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive });
_MatMenuContentBase.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: _MatMenuContentBase, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatMenuContentBase, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.TemplateRef }, { type: i0.ComponentFactoryResolver }, { type: i0.ApplicationRef }, { type: i0.Injector }, { type: i0.ViewContainerRef }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: i0.ChangeDetectorRef }]; } });
/** Menu content that will be rendered lazily once the menu is opened. */
class MatMenuContent extends _MatMenuContentBase {
}
MatMenuContent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatMenuContent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
MatMenuContent.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatMenuContent, selector: "ng-template[matMenuContent]", providers: [{ provide: MAT_MENU_CONTENT, useExisting: MatMenuContent }], usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatMenuContent, decorators: [{
type: Directive,
args: [{
selector: 'ng-template[matMenuContent]',
providers: [{ provide: MAT_MENU_CONTENT, useExisting: MatMenuContent }],
}]
}] });
/**
* @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
*/
/**
* Animations used by the mat-menu component.
* Animation duration and timing values are based on:
* https://material.io/guidelines/components/menus.html#menus-usage
* @docs-private
*/
const matMenuAnimations = {
/**
* This animation controls the menu panel's entry and exit from the page.
*
* When the menu panel is added to the DOM, it scales in and fades in its border.
*
* When the menu panel is removed from the DOM, it simply fades out after a brief
* delay to display the ripple.
*/
transformMenu: trigger('transformMenu', [
state('void', style({
opacity: 0,
transform: 'scale(0.8)',
})),
transition('void => enter', animate('120ms cubic-bezier(0, 0, 0.2, 1)', style({
opacity: 1,
transform: 'scale(1)',
}))),
transition('* => void', animate('100ms 25ms linear', style({ opacity: 0 }))),
]),
/**
* This animation fades in the background color and content of the menu panel
* after its containing element is scaled in.
*/
fadeInItems: trigger('fadeInItems', [
// TODO(crisbeto): this is inside the `transformMenu`
// now. Remove next time we do breaking changes.
state('showing', style({ opacity: 1 })),
transition('void => *', [
style({ opacity: 0 }),
animate('400ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)'),
]),
]),
};
/**
* @deprecated
* @breaking-change 8.0.0
* @docs-private
*/
const fadeInItems = matMenuAnimations.fadeInItems;
/**
* @deprecated
* @breaking-change 8.0.0
* @docs-private
*/
const transformMenu = matMenuAnimations.transformMenu;
/**
* @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
*/
let menuPanelUid = 0;
/** Injection token to be used to override the default options for `mat-menu`. */
const MAT_MENU_DEFAULT_OPTIONS = new InjectionToken('mat-menu-default-options', {
providedIn: 'root',
factory: MAT_MENU_DEFAULT_OPTIONS_FACTORY,
});
/** @docs-private */
function MAT_MENU_DEFAULT_OPTIONS_FACTORY() {
return {
overlapTrigger: false,
xPosition: 'after',
yPosition: 'below',
backdropClass: 'cdk-overlay-transparent-backdrop',
};
}
/** Base class with all of the `MatMenu` functionality. */
class _MatMenuBase {
constructor(_elementRef, _ngZone, _defaultOptions,
// @breaking-change 15.0.0 `_changeDetectorRef` to become a required parameter.
_changeDetectorRef) {
this._elementRef = _elementRef;
this._ngZone = _ngZone;
this._defaultOptions = _defaultOptions;
this._changeDetectorRef = _changeDetectorRef;
this._xPosition = this._defaultOptions.xPosition;
this._yPosition = this._defaultOptions.yPosition;
/** Only the direct descendant menu items. */
this._directDescendantItems = new QueryList();
/** Config object to be passed into the menu's ngClass */
this._classList = {};
/** Current state of the panel animation. */
this._panelAnimationState = 'void';
/** Emits whenever an animation on the menu completes. */
this._animationDone = new Subject();
/** Class or list of classes to be added to the overlay panel. */
this.overlayPanelClass = this._defaultOptions.overlayPanelClass || '';
/** Class to be added to the backdrop element. */
this.backdropClass = this._defaultOptions.backdropClass;
this._overlapTrigger = this._defaultOptions.overlapTrigger;
this._hasBackdrop = this._defaultOptions.hasBackdrop;
/** Event emitted when the menu is closed. */
this.closed = new EventEmitter();
/**
* Event emitted when the menu is closed.
* @deprecated Switch to `closed` instead
* @breaking-change 8.0.0
*/
this.close = this.closed;
this.panelId = `mat-menu-panel-${menuPanelUid++}`;
}
/** Position of the menu in the X axis. */
get xPosition() {
return this._xPosition;
}
set xPosition(value) {
if (value !== 'before' &&
value !== 'after' &&
(typeof ngDevMode === 'undefined' || ngDevMode)) {
throwMatMenuInvalidPositionX();
}
this._xPosition = value;
this.setPositionClasses();
}
/** Position of the menu in the Y axis. */
get yPosition() {
return this._yPosition;
}
set yPosition(value) {
if (value !== 'above' && value !== 'below' && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throwMatMenuInvalidPositionY();
}
this._yPosition = value;
this.setPositionClasses();
}
/** Whether the menu should overlap its trigger. */
get overlapTrigger() {
return this._overlapTrigger;
}
set overlapTrigger(value) {
this._overlapTrigger = coerceBooleanProperty(value);
}
/** Whether the menu has a backdrop. */
get hasBackdrop() {
return this._hasBackdrop;
}
set hasBackdrop(value) {
this._hasBackdrop = coerceBooleanProperty(value);
}
/**
* 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
*/
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 = '';
}
}
/**
* 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
*/
get classList() {
return this.panelClass;
}
set classList(classes) {
this.panelClass = classes;
}
ngOnInit() {
this.setPositionClasses();
}
ngAfterContentInit() {
this._updateDirectDescendants();
this._keyManager = new FocusKeyManager(this._directDescendantItems)
.withWrap()
.withTypeAhead()
.withHomeAndEnd();
this._keyManager.tabOut.subscribe(() => this.closed.emit('tab'));
// If a user manually (programmatically) focuses a menu item, we need to reflect that focus
// change back to the key manager. Note that we don't need to unsubscribe here because _focused
// is internal and we know that it gets completed on destroy.
this._directDescendantItems.changes
.pipe(startWith(this._directDescendantItems), switchMap(items => merge(...items.map((item) => item._focused))))
.subscribe(focusedItem => this._keyManager.updateActiveItem(focusedItem));
this._directDescendantItems.changes.subscribe((itemsList) => {
// Move focus to another item, if the active item is removed from the list.
// We need to debounce the callback, because multiple items might be removed
// in quick succession.
const manager = this._keyManager;
if (this._panelAnimationState === 'enter' && manager.activeItem?._hasFocus()) {
const items = itemsList.toArray();
const index = Math.max(0, Math.min(items.length - 1, manager.activeItemIndex || 0));
if (items[index] && !items[index].disabled) {
manager.setActiveItem(index);
}
else {
manager.setNextItemActive();
}
}
});
}
ngOnDestroy() {
this._keyManager?.destroy();
this._directDescendantItems.destroy();
this.closed.complete();
this._firstItemFocusSubscription?.unsubscribe();
}
/** Stream that emits whenever the hovered menu item changes. */
_hovered() {
// Coerce the `changes` property because Angular types it as `Observable<any>`
const itemChanges = this._directDescendantItems.changes;
return itemChanges.pipe(startWith(this._directDescendantItems), switchMap(items => merge(...items.map((item) => item._hovered))));
}
/*
* Registers a menu item with the menu.
* @docs-private
* @deprecated No longer being used. To be removed.
* @breaking-change 9.0.0
*/
addItem(_item) { }
/**
* Removes an item from the menu.
* @docs-private
* @deprecated No longer being used. To be removed.
* @breaking-change 9.0.0
*/
removeItem(_item) { }
/** Handle a keyboard event from the menu, delegating to the appropriate action. */
_handleKeydown(event) {
const keyCode = event.keyCode;
const manager = this._keyManager;
switch (keyCode) {
case ESCAPE:
if (!hasModifierKey(event)) {
event.preventDefault();
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) {
manager.setFocusOrigin('keyboard');
}
manager.onKeydown(event);
return;
}
// Don't allow the event to propagate if we've already handled it, or it may
// end up reaching other overlays that were opened earlier (see #22694).
event.stopPropagation();
}
/**
* Focus the first item in the menu.
* @param origin Action from which the focus originated. Used to set the correct styling.
*/
focusFirstItem(origin = 'program') {
// Wait for `onStable` to ensure iOS VoiceOver screen reader focuses the first item (#24735).
this._firstItemFocusSubscription?.unsubscribe();
this._firstItemFocusSubscription = this._ngZone.onStable.pipe(take(1)).subscribe(() => {
let menuPanel = null;
if (this._directDescendantItems.length) {
// Because the `mat-menuPanel` is at the DOM insertion point, not inside the overlay, we don't
// have a nice way of getting a hold of the menuPanel panel. We can't use a `ViewChild` either
// because the panel is inside an `ng-template`. We work around it by starting from one of
// the items and walking up the DOM.
menuPanel = this._directDescendantItems.first._getHostElement().closest('[role="menu"]');
}
// If an item in the menuPanel is already focused, avoid overriding the focus.
if (!menuPanel || !menuPanel.contains(document.activeElement)) {
const manager = this._keyManager;
manager.setFocusOrigin(origin).setFirstItemActive();
// If there's no active item at this point, it means that all the items are disabled.
// Move focus to the menuPanel panel so keyboard events like Escape still work. Also this will
// give _some_ feedback to screen readers.
if (!manager.activeItem && menuPanel) {
menuPanel.focus();
}
}
});
}
/**
* 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) {
// The elevation starts at the base and increases by one for each level.
// Capped at 24 because that's the maximum elevation defined in the Material design spec.
const elevation = Math.min(this._baseElevation + depth, 24);
const newElevation = `${this._elevationPrefix}${elevation}`;
const customElevation = Object.keys(this._classList).find(className => {
return className.startsWith(this._elevationPrefix);
});
if (!customElevation || customElevation === this._previousElevation) {
if (this._previousElevation) {
this._classList[this._previousElevation] = false;
}
this._classList[newElevation] = true;
this._previousElevation = newElevation;
}
}
/**
* 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 = 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';
// @breaking-change 15.0.0 Remove null check for `_changeDetectorRef`.
this._changeDetectorRef?.markForCheck();
}
/** 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) {
this._animationDone.next(event);
this._isAnimating = false;
}
_onAnimationStart(event) {
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;
}
}
/**
* Sets up a stream that will keep track of any newly-added menu items and will update the list
* of direct descendants. We collect the descendants this way, because `_allItems` can include
* items that are part of child menus, and using a custom way of registering items is unreliable
* when it comes to maintaining the item order.
*/
_updateDirectDescendants() {
this._allItems.changes
.pipe(startWith(this._allItems))
.subscribe((items) => {
this._directDescendantItems.reset(items.filter(item => item._parentMenu === this));
this._directDescendantItems.notifyOnChanges();
});
}
}
_MatMenuBase.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatMenuBase, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: MAT_MENU_DEFAULT_OPTIONS }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive });
_MatMenuBase.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: _MatMenuBase, inputs: { backdropClass: "backdropClass", ariaLabel: ["aria-label", "ariaLabel"], ariaLabelledby: ["aria-labelledby", "ariaLabelledby"], ariaDescribedby: ["aria-describedby", "ariaDescribedby"], xPosition: "xPosition", yPosition: "yPosition", overlapTrigger: "overlapTrigger", hasBackdrop: "hasBackdrop", panelClass: ["class", "panelClass"], classList: "classList" }, outputs: { closed: "closed", close: "close" }, queries: [{ propertyName: "lazyContent", first: true, predicate: MAT_MENU_CONTENT, descendants: true }, { propertyName: "_allItems", predicate: MatMenuItem, descendants: true }, { propertyName: "items", predicate: MatMenuItem }], viewQueries: [{ propertyName: "templateRef", first: true, predicate: TemplateRef, descendants: true }], ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: _MatMenuBase, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: undefined, decorators: [{
type: Inject,
args: [MAT_MENU_DEFAULT_OPTIONS]
}] }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { _allItems: [{
type: ContentChildren,
args: [MatMenuItem, { descendants: true }]
}], backdropClass: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], ariaLabelledby: [{
type: Input,
args: ['aria-labelledby']
}], ariaDescribedby: [{
type: Input,
args: ['aria-describedby']
}], xPosition: [{
type: Input
}], yPosition: [{
type: Input
}], templateRef: [{
type: ViewChild,
args: [TemplateRef]
}], items: [{
type: ContentChildren,
args: [MatMenuItem, { descendants: false }]
}], lazyContent: [{
type: ContentChild,
args: [MAT_MENU_CONTENT]
}], overlapTrigger: [{
type: Input
}], hasBackdrop: [{
type: Input
}], panelClass: [{
type: Input,
args: ['class']
}], classList: [{
type: Input
}], closed: [{
type: Output
}], close: [{
type: Output
}] } });
class MatMenu extends _MatMenuBase {
constructor(_elementRef, _ngZone, _defaultOptions, changeDetectorRef) {
super(_elementRef, _ngZone, _defaultOptions, changeDetectorRef);
this._elevationPrefix = 'mat-elevation-z';
this._baseElevation = 8;
}
}
MatMenu.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatMenu, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: MAT_MENU_DEFAULT_OPTIONS }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
MatMenu.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.0.0-rc.1", type: MatMenu, selector: "mat-menu", host: { properties: { "attr.aria-label": "null", "attr.aria-labelledby": "null", "attr.aria-describedby": "null" } }, providers: [{ provide: MAT_MENU_PANEL, useExisting: MatMenu }], exportAs: ["matMenu"], usesInheritance: true, ngImport: i0, template: "<ng-template>\n <div\n class=\"mat-mdc-menu-panel mdc-menu-surface mdc-menu-surface--open mat-mdc-elevation-specific\"\n [id]=\"panelId\"\n [ngClass]=\"_classList\"\n (keydown)=\"_handleKeydown($event)\"\n (click)=\"closed.emit('click')\"\n [@transformMenu]=\"_panelAnimationState\"\n (@transformMenu.start)=\"_onAnimationStart($event)\"\n (@transformMenu.done)=\"_onAnimationDone($event)\"\n tabindex=\"-1\"\n role=\"menu\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.aria-labelledby]=\"ariaLabelledby || null\"\n [attr.aria-describedby]=\"ariaDescribedby || null\">\n <div class=\"mat-mdc-menu-content mdc-list\">\n <ng-content></ng-content>\n </div>\n </div>\n</ng-template>\n", styles: [".mdc-menu-surface{display:none;position:absolute;box-sizing:border-box;max-width:var(--mdc-menu-max-width, calc(100vw - 32px));max-height:var(--mdc-menu-max-height, calc(100vh - 32px));margin:0;padding:0;transform:scale(1);transform-origin:top left;opacity:0;overflow:auto;will-change:transform,opacity;z-index:8;border-radius:var(--mdc-shape-medium, 4px);transform-origin-left:top left;transform-origin-right:top right}.mdc-menu-surface:focus{outline:none}.mdc-menu-surface--animating-open{display:inline-block;transform:scale(0.8);opacity:0}.mdc-menu-surface--open{display:inline-block;transform:scale(1);opacity:1}.mdc-menu-surface--animating-closed{display:inline-block;opacity:0}[dir=rtl] .mdc-menu-surface,.mdc-menu-surface[dir=rtl]{transform-origin-left:top right;transform-origin-right:top left}.mdc-menu-surface--anchor{position:relative;overflow:visible}.mdc-menu-surface--fixed{position:fixed}.mdc-menu-surface--fullwidth{width:100%}mat-menu{display:none}.mat-mdc-menu-content{margin:0;padding:8px 0;list-style-type:none}.mat-mdc-menu-content:focus{outline:none}.cdk-high-contrast-active .mat-mdc-menu-panel{outline:solid 1px}.mat-mdc-menu-panel.mat-mdc-menu-panel{min-width:112px;max-width:280px;overflow:auto;-webkit-overflow-scrolling:touch;position:relative}.mat-mdc-menu-item{display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;padding:0;padding-left:16px;padding-right:16px;-webkit-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:rgba(0,0,0,0);cursor:pointer;width:100%;text-align:left;box-sizing:border-box;color:inherit;font-size:inherit;background:none;text-decoration:none;margin:0;min-height:48px}.mat-mdc-menu-item:focus{outline:none}[dir=rtl] .mat-mdc-menu-item,.mat-mdc-menu-item[dir=rtl]{padding-left:16px;padding-right:16px}.mat-mdc-menu-item::-moz-focus-inner{border:0}.mat-mdc-menu-item.mdc-list-item{align-items:center}.mat-mdc-menu-item[disabled]{cursor:default;opacity:.38}.mat-mdc-menu-item[disabled]::after{display:block;position:absolute;content:\"\";top:0;left:0;bottom:0;right:0}.mat-mdc-menu-item .mat-icon{margin-right:16px}[dir=rtl] .mat-mdc-menu-item{text-align:right}[dir=rtl] .mat-mdc-menu-item .mat-icon{margin-right:0;margin-left:16px}.mat-mdc-menu-item .mdc-list-item__primary-text{white-space:normal}.mat-mdc-menu-item.mat-mdc-menu-item-submenu-trigger{padding-right:32px}[dir=rtl] .mat-mdc-menu-item.mat-mdc-menu-item-submenu-trigger{padding-right:16px;padding-left:32px}.cdk-high-contrast-active .mat-mdc-menu-item{margin-top:1px}.mat-mdc-menu-submenu-icon{position:absolute;top:50%;right:16px;transform:translateY(-50%);width:5px;height:10px;fill:currentColor}[dir=rtl] .mat-mdc-menu-submenu-icon{right:auto;left:16px;transform:translateY(-50%) scaleX(-1)}.cdk-high-contrast-active .mat-mdc-menu-submenu-icon{fill:CanvasText}.mat-mdc-menu-item .mat-mdc-menu-ripple{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none}"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], animations: [matMenuAnimations.transformMenu, matMenuAnimations.fadeInItems], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.0-rc.1", ngImport: i0, type: MatMenu, decorators: [{
type: Component,
args: [{ selector: 'mat-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, exportAs: 'matMenu', host: {
'[attr.aria-label]': 'null',
'[attr.aria-labelledby]': 'null',
'[attr.aria-describedby]': 'null',
}, animations: [matMenuAnimations.transformMenu, matMenuAnimations.fadeInItems], providers: [{ provide: MAT_MENU_PANEL, useExisting: MatMenu }], template: "<ng-template>\n <div\n class=\"mat-mdc-menu-panel mdc-menu-surface mdc-menu-surface--open mat-mdc-elevation-specific\"\n [id]=\"panelId\"\n [ngClass]=\"_classList\"\n (keydown)=\"_handleKeydown($event)\"\n (click)=\"closed.emit('click')\"\n [@transformMenu]=\"_panelAnimationState\"\n (@transformMenu.start)=\"_onAnimationStart($event)\"\n (@transformMenu.done)=\"_onAnimationDone($event)\"\n tabindex=\"-1\"\n role=\"menu\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.aria-labelledby]=\"ariaLabelledby || null\"\n [attr.aria-describedby]=\"ariaDescribedby || null\">\n <div class=\"mat-mdc-menu-content mdc-list\">\n <ng-content></ng-content>\n </div>\n </div>\n</ng-template>\n", styles: [".mdc-menu-surface{display:none;position:absolute;box-sizing:border-box;max-width:var(--mdc-menu-max-width, calc(100vw - 32px));max-height:var(--mdc-menu-max-height, calc(100vh - 32px));margin:0;padding:0;transform:scale(1);transform-origin:top left;opacity:0;overflow:auto;will-change:transform,opacity;z-index:8;border-radius:var(--mdc-shape-medium, 4px);transform-origin-left:top left;transform-origin-right:top right}.mdc-menu-surface:focus{outline:none}.mdc-menu-surface--animating-open{display:inline-block;transform:scale(0.8);opacity:0}.mdc-menu-surface--open{display:inline-block;transform:scale(1);opacity:1}.mdc-menu-surface--animating-closed{display:inline-block;opacity:0}[dir=rtl] .mdc-menu-surface,.mdc-menu-surface[dir=rtl]{transform-origin-left:top right;transform-origin-right:top left}.mdc-menu-surface--anchor{position:relative;overflow:visible}.mdc-menu-surface--fixed{position:fixed}.mdc-menu-surface--fullwidth{width:100%}mat-menu{display:none}.mat-mdc-menu-content{margin:0;padding:8px 0;list-style-type:none}.mat-mdc-menu-content:focus{outline:none}.cdk-high-contrast-active .mat-mdc-menu-panel{outline:solid 1px}.mat-mdc-menu-panel.mat-mdc-menu-panel{min-width:112px;max-width:280px;overflow:auto;-webkit-overflow-scrolling:touch;position:relative}.mat-mdc-menu-item{display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;padding:0;padding-left:16px;padding-right:16px;-webkit-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:rgba(0,0,0,0);cursor:pointer;width:100%;text-align:left;box-sizing:border-box;color:inherit;font-size:inherit;background:none;text-decoration:none;margin:0;min-height:48px}.mat-mdc-menu-item:focus{outline:none}[dir=rtl] .mat-mdc-menu-item,.mat-mdc-menu-item[dir=rtl]{padding-left:16px;padding-right:16px}.mat-mdc-menu-item::-moz-focus-inner{border:0}.mat-mdc-menu-item.mdc-list-item{align-items:center}.mat-mdc-menu-item[disabled]{cursor:default;opacity:.38}.mat-mdc-menu-item[disabled]::after{display:block;position:absolute;content:\"\";top:0;left:0;bottom:0;right:0}.mat-mdc-menu-item .mat-icon{margin-right:16px}[dir=rtl] .mat-mdc-menu-item{text-align:right}[dir=rtl] .mat-mdc-menu-item .mat-icon{margin-right:0;margin-left:16px}.mat-mdc-menu-item .mdc-list-item__primary-text{white-space:normal}.mat-mdc-menu-item.mat-mdc-menu-item-submenu-trigger{padding-right:32px}[dir=rtl] .mat-mdc-menu-item.mat-mdc-menu-item-submenu-trigger{padding-right:16px;padding-left:32px}.cdk-high-contrast-active .mat-mdc-menu-item{margin-top:1px}.mat-mdc-menu-submenu-icon{position:absolute;top:50%;right:16px;transform:translateY(-50%);width:5px;height:10px;fill:currentColor}[dir=rtl] .mat-mdc-menu-submenu-icon{right:auto;left:16px;transform:translateY(-50%) scaleX(-1)}.cdk-high-contrast-active .mat-mdc-menu-submenu-icon{fill:CanvasText}.mat-mdc-menu-item .mat-mdc-menu-ripple{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none}"] }]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: undefined, decorators: [{
type: Inject,
args: [MAT_MENU_DEFAULT_OPTIONS]
}] }, { type: i0.ChangeDetectorRef }]; } });
/**
* @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
*/
/** Injection token that determines the scroll handling while the menu is open. */
const MAT_MENU_SCROLL_STRATEGY = new InjectionToken('mat-menu-scroll-strategy');
/** @docs-private */
function MAT_MENU_SCROLL_STRATEGY_FACTORY(overlay) {
return () => overlay.scrollStrategies.reposition();
}
/** @docs-private */
const MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER = {
provide: MAT_MENU_SCROLL_STRATEGY,
deps: [Overlay],
useFactory: MAT_MENU_SCROLL_STRATEGY_FACTORY,
};
/** Options for binding a passive event listener. */
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
/**
* Default top padding of the menu panel.
* @deprecated No longer being used. Will be removed.
* @breaking-change 15.0.0
*/
const MENU_PANEL_TOP_PADDING = 8;
class _MatMenuTriggerBase {
constructor(_overlay, _element, _viewContainerRef, scrollStrategy, parentMenu,
// `MatMenuTrigger` is commonly used in combination with a `MatMenuItem`.
// tslint:disable-next-line: lightweight-tokens
_menuItemInstance, _dir, _focusMonitor, _ngZone) {
this._overlay = _overlay;
this._element = _element;
this._viewContainerRef = _viewContainerRef;
this._menuItemInstance = _menuItemInstance;
this._dir = _dir;
this._focusMonitor = _focusMonitor;
this._ngZone = _ngZone;
this._overlayRef = null;
this._menuOpen = false;
this._closingActionsSubscription = Subscription.EMPTY;
this._hoverSubscription = Subscription.EMPTY;
this._menuCloseSubscription = Subscription.EMPTY;
/**
* Handles touch start events on the trigger.
* Needs to be an arrow function so we can easily use addEventListener and removeEventListener.
*/
this._handleTouchStart = (event) => {
if (!isFakeTouchstartFromScreenReader(event)) {
this._openedBy = 'touch';
}
};
// Tracking input type is necessary so it's possible to only auto-focus
// the first item of the list when the menu is opened via the keyboard
this._openedBy = undefined;
/**
* Whether focus should be restored when the menu is closed.
* Note that disabling this option can have accessibility implications
* and it's up to you to manage focus, if you decide to turn it off.
*/
this.restoreFocus = true;
/** Event emitted when the associated menu is opened. */
this.menuOpened = new EventEmitter();
/**
* Event emitted when the associated menu is opened.
* @deprecated Switch to `menuOpened` instead
* @breaking-change 8.0.0
*/
// tslint:disable-next-line:no-output-on-prefix
this.onMenuOpen = this.menuOpened;
/** Event emitted when the associated menu is closed. */
this.menuClosed = new EventEmitter();
/**
* Event emitted when the associated menu is closed.
* @deprecated Switch to `menuClosed` instead
* @breaking-change 8.0.0
*/
// tslint:disable-next-line:no-output-on-prefix
this.onMenuClose = this.menuClosed;
this._scrollStrategy = scrollStrategy;
this._parentMaterialMenu = parentMenu instanceof _MatMenuBase ? parentMenu : undefined;
_element.nativeElement.addEventListener('touchstart', this._handleTouchStart, passiveEventListenerOptions);
}
/**
* @deprecated
* @breaking-change 8.0.0
*/
get _deprecatedMatMenuTriggerFor() {
return this.menu;
}
set _deprecatedMatMenuTriggerFor(v) {
this.menu = v;
}
/** References the menu instance that the trigger is associated with. */
get menu() {
return this._menu;
}
set menu(menu) {
if (menu === this._menu) {
return;
}
this._menu = menu;
this._menuCloseSubscription.unsubscribe();
if (menu) {
if (menu === this._parentMaterialMenu && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throwMatMenuRecursiveError();
}
this._menuCloseSubscription = menu.close.subscribe((reason) => {
this._destroyMenu(reason);
// If a click closed the menu, we should close the entire chain of nested menus.
if ((reason === 'click' || reason === 'tab') && this._parentMaterialMenu) {
this._parentMaterialMenu.closed.emit(reason);
}
});
}
this._menuItemInstan