506 lines
16 KiB
TypeScript
506 lines
16 KiB
TypeScript
|
|
/**
|
||
|
|
* @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.dev/license
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {
|
||
|
|
AfterContentInit,
|
||
|
|
ChangeDetectionStrategy,
|
||
|
|
Component,
|
||
|
|
ContentChild,
|
||
|
|
ContentChildren,
|
||
|
|
ElementRef,
|
||
|
|
EventEmitter,
|
||
|
|
InjectionToken,
|
||
|
|
Input,
|
||
|
|
OnDestroy,
|
||
|
|
Output,
|
||
|
|
TemplateRef,
|
||
|
|
QueryList,
|
||
|
|
ViewChild,
|
||
|
|
ViewEncapsulation,
|
||
|
|
OnInit,
|
||
|
|
ChangeDetectorRef,
|
||
|
|
booleanAttribute,
|
||
|
|
afterNextRender,
|
||
|
|
AfterRenderRef,
|
||
|
|
inject,
|
||
|
|
Injector,
|
||
|
|
} from '@angular/core';
|
||
|
|
import {AnimationEvent} from '@angular/animations';
|
||
|
|
import {_IdGenerator, FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y';
|
||
|
|
import {Direction} from '@angular/cdk/bidi';
|
||
|
|
import {
|
||
|
|
ESCAPE,
|
||
|
|
LEFT_ARROW,
|
||
|
|
RIGHT_ARROW,
|
||
|
|
DOWN_ARROW,
|
||
|
|
UP_ARROW,
|
||
|
|
hasModifierKey,
|
||
|
|
} from '@angular/cdk/keycodes';
|
||
|
|
import {merge, Observable, Subject} from 'rxjs';
|
||
|
|
import {startWith, switchMap} from 'rxjs/operators';
|
||
|
|
import {MatMenuItem} from './menu-item';
|
||
|
|
import {MatMenuPanel, MAT_MENU_PANEL} from './menu-panel';
|
||
|
|
import {MenuPositionX, MenuPositionY} from './menu-positions';
|
||
|
|
import {throwMatMenuInvalidPositionX, throwMatMenuInvalidPositionY} from './menu-errors';
|
||
|
|
import {MatMenuContent, MAT_MENU_CONTENT} from './menu-content';
|
||
|
|
import {matMenuAnimations} from './menu-animations';
|
||
|
|
|
||
|
|
/** Reason why the menu was closed. */
|
||
|
|
export type MenuCloseReason = void | 'click' | 'keydown' | 'tab';
|
||
|
|
|
||
|
|
/** 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;
|
||
|
|
|
||
|
|
/** Class or list of classes to be applied to the menu's overlay panel. */
|
||
|
|
overlayPanelClass?: string | 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',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
selector: 'mat-menu',
|
||
|
|
templateUrl: 'menu.html',
|
||
|
|
styleUrl: 'menu.css',
|
||
|
|
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}],
|
||
|
|
})
|
||
|
|
export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnInit, OnDestroy {
|
||
|
|
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
||
|
|
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||
|
|
|
||
|
|
private _keyManager: FocusKeyManager<MatMenuItem>;
|
||
|
|
private _xPosition: MenuPositionX;
|
||
|
|
private _yPosition: MenuPositionY;
|
||
|
|
private _firstItemFocusRef?: AfterRenderRef;
|
||
|
|
|
||
|
|
/** All items inside the menu. Includes items nested inside another menu. */
|
||
|
|
@ContentChildren(MatMenuItem, {descendants: true}) _allItems: QueryList<MatMenuItem>;
|
||
|
|
|
||
|
|
/** Only the direct descendant menu items. */
|
||
|
|
_directDescendantItems = new QueryList<MatMenuItem>();
|
||
|
|
|
||
|
|
/** Classes to be applied to the menu panel. */
|
||
|
|
_classList: {[key: string]: boolean} = {};
|
||
|
|
|
||
|
|
/** Current state of the panel animation. */
|
||
|
|
_panelAnimationState: 'void' | 'enter' = 'void';
|
||
|
|
|
||
|
|
/** Emits whenever an animation on the menu completes. */
|
||
|
|
readonly _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 or list of classes to be added to the overlay panel. */
|
||
|
|
overlayPanelClass: string | string[];
|
||
|
|
|
||
|
|
/** Class to be added to the backdrop element. */
|
||
|
|
@Input() backdropClass: string;
|
||
|
|
|
||
|
|
/** aria-label for the menu panel. */
|
||
|
|
@Input('aria-label') ariaLabel: string;
|
||
|
|
|
||
|
|
/** aria-labelledby for the menu panel. */
|
||
|
|
@Input('aria-labelledby') ariaLabelledby: string;
|
||
|
|
|
||
|
|
/** aria-describedby for the menu panel. */
|
||
|
|
@Input('aria-describedby') ariaDescribedby: string;
|
||
|
|
|
||
|
|
/** Position of the menu in the X axis. */
|
||
|
|
@Input()
|
||
|
|
get xPosition(): MenuPositionX {
|
||
|
|
return this._xPosition;
|
||
|
|
}
|
||
|
|
set xPosition(value: MenuPositionX) {
|
||
|
|
if (
|
||
|
|
value !== 'before' &&
|
||
|
|
value !== 'after' &&
|
||
|
|
(typeof ngDevMode === 'undefined' || ngDevMode)
|
||
|
|
) {
|
||
|
|
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' && (typeof ngDevMode === 'undefined' || ngDevMode)) {
|
||
|
|
throwMatMenuInvalidPositionY();
|
||
|
|
}
|
||
|
|
this._yPosition = value;
|
||
|
|
this.setPositionClasses();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @docs-private */
|
||
|
|
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* List of the items inside of a menu.
|
||
|
|
* @deprecated
|
||
|
|
* @breaking-change 8.0.0
|
||
|
|
*/
|
||
|
|
@ContentChildren(MatMenuItem, {descendants: false}) items: QueryList<MatMenuItem>;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Menu content that will be rendered lazily.
|
||
|
|
* @docs-private
|
||
|
|
*/
|
||
|
|
@ContentChild(MAT_MENU_CONTENT) lazyContent: MatMenuContent;
|
||
|
|
|
||
|
|
/** Whether the menu should overlap its trigger. */
|
||
|
|
@Input({transform: booleanAttribute}) overlapTrigger: boolean;
|
||
|
|
|
||
|
|
/** Whether the menu has a backdrop. */
|
||
|
|
@Input({transform: (value: any) => (value == null ? null : booleanAttribute(value))})
|
||
|
|
hasBackdrop?: boolean;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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;
|
||
|
|
const newClassList = {...this._classList};
|
||
|
|
|
||
|
|
if (previousPanelClass && previousPanelClass.length) {
|
||
|
|
previousPanelClass.split(' ').forEach((className: string) => {
|
||
|
|
newClassList[className] = false;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
this._previousPanelClass = classes;
|
||
|
|
|
||
|
|
if (classes && classes.length) {
|
||
|
|
classes.split(' ').forEach((className: string) => {
|
||
|
|
newClassList[className] = true;
|
||
|
|
});
|
||
|
|
|
||
|
|
this._elementRef.nativeElement.className = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
this._classList = newClassList;
|
||
|
|
}
|
||
|
|
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<MenuCloseReason> = new EventEmitter<MenuCloseReason>();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Event emitted when the menu is closed.
|
||
|
|
* @deprecated Switch to `closed` instead
|
||
|
|
* @breaking-change 8.0.0
|
||
|
|
*/
|
||
|
|
@Output() readonly close: EventEmitter<MenuCloseReason> = this.closed;
|
||
|
|
|
||
|
|
readonly panelId: string = inject(_IdGenerator).getId('mat-menu-panel-');
|
||
|
|
|
||
|
|
private _injector = inject(Injector);
|
||
|
|
|
||
|
|
constructor(...args: unknown[]);
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
const defaultOptions = inject<MatMenuDefaultOptions>(MAT_MENU_DEFAULT_OPTIONS);
|
||
|
|
this.overlayPanelClass = defaultOptions.overlayPanelClass || '';
|
||
|
|
this._xPosition = defaultOptions.xPosition;
|
||
|
|
this._yPosition = defaultOptions.yPosition;
|
||
|
|
this.backdropClass = defaultOptions.backdropClass;
|
||
|
|
this.overlapTrigger = defaultOptions.overlapTrigger;
|
||
|
|
this.hasBackdrop = defaultOptions.hasBackdrop;
|
||
|
|
}
|
||
|
|
|
||
|
|
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: MatMenuItem) => item._focused))),
|
||
|
|
)
|
||
|
|
.subscribe(focusedItem => this._keyManager.updateActiveItem(focusedItem as MatMenuItem));
|
||
|
|
|
||
|
|
this._directDescendantItems.changes.subscribe((itemsList: QueryList<MatMenuItem>) => {
|
||
|
|
// 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._firstItemFocusRef?.destroy();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Stream that emits whenever the hovered menu item changes. */
|
||
|
|
_hovered(): Observable<MatMenuItem> {
|
||
|
|
// Coerce the `changes` property because Angular types it as `Observable<any>`
|
||
|
|
const itemChanges = this._directDescendantItems.changes as Observable<QueryList<MatMenuItem>>;
|
||
|
|
return itemChanges.pipe(
|
||
|
|
startWith(this._directDescendantItems),
|
||
|
|
switchMap(items => merge(...items.map((item: MatMenuItem) => item._hovered))),
|
||
|
|
) as Observable<MatMenuItem>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Registers a menu item with the menu.
|
||
|
|
* @docs-private
|
||
|
|
* @deprecated No longer being used. To be removed.
|
||
|
|
* @breaking-change 9.0.0
|
||
|
|
*/
|
||
|
|
addItem(_item: MatMenuItem) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Removes an item from the menu.
|
||
|
|
* @docs-private
|
||
|
|
* @deprecated No longer being used. To be removed.
|
||
|
|
* @breaking-change 9.0.0
|
||
|
|
*/
|
||
|
|
removeItem(_item: MatMenuItem) {}
|
||
|
|
|
||
|
|
/** 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:
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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 {
|
||
|
|
// Wait for `afterNextRender` to ensure iOS VoiceOver screen reader focuses the first item (#24735).
|
||
|
|
this._firstItemFocusRef?.destroy();
|
||
|
|
this._firstItemFocusRef = afterNextRender(
|
||
|
|
() => {
|
||
|
|
let menuPanel: HTMLElement | null = 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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{injector: this._injector},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated No longer used and will be removed.
|
||
|
|
* @breaking-change 21.0.0
|
||
|
|
*/
|
||
|
|
setElevation(_depth: number): void {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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) {
|
||
|
|
this._classList = {
|
||
|
|
...this._classList,
|
||
|
|
['mat-menu-before']: posX === 'before',
|
||
|
|
['mat-menu-after']: posX === 'after',
|
||
|
|
['mat-menu-above']: posY === 'above',
|
||
|
|
['mat-menu-below']: posY === 'below',
|
||
|
|
};
|
||
|
|
|
||
|
|
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: 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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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.
|
||
|
|
*/
|
||
|
|
private _updateDirectDescendants() {
|
||
|
|
this._allItems.changes
|
||
|
|
.pipe(startWith(this._allItems))
|
||
|
|
.subscribe((items: QueryList<MatMenuItem>) => {
|
||
|
|
this._directDescendantItems.reset(items.filter(item => item._parentMenu === this));
|
||
|
|
this._directDescendantItems.notifyOnChanges();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|