UNPKG

@angular/material

Version:
827 lines (820 loc) 71.8 kB
import * as i0 from '@angular/core'; import { InjectionToken, inject, ElementRef, ChangeDetectorRef, booleanAttribute, Component, ChangeDetectionStrategy, ViewEncapsulation, Input, TemplateRef, ApplicationRef, Injector, ViewContainerRef, Directive, QueryList, EventEmitter, ANIMATION_MODULE_TYPE, afterNextRender, ContentChildren, ViewChild, ContentChild, Output, NgZone, Renderer2, NgModule } from '@angular/core'; import { FocusMonitor, _IdGenerator, FocusKeyManager, isFakeTouchstartFromScreenReader, isFakeMousedownFromScreenReader } from '@angular/cdk/a11y'; import { UP_ARROW, DOWN_ARROW, RIGHT_ARROW, LEFT_ARROW, ESCAPE, hasModifierKey, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Subject, merge, Subscription, of } from 'rxjs'; import { startWith, switchMap, takeUntil, take, filter } from 'rxjs/operators'; import { DOCUMENT } from '@angular/common'; import { _CdkPrivateStyleLoader } from '@angular/cdk/private'; import { _ as _StructuralStylesLoader } from './structural-styles-7c66c8fc.mjs'; import { M as MatRipple } from './ripple-acd53c76.mjs'; import { TemplatePortal, DomPortalOutlet } from '@angular/cdk/portal'; import { Directionality } from '@angular/cdk/bidi'; import { Overlay, OverlayConfig, OverlayModule } from '@angular/cdk/overlay'; import { _bindEventWithOptions } from '@angular/cdk/platform'; import { CdkScrollableModule } from '@angular/cdk/scrolling'; import { M as MatRippleModule } from './index-5decf30c.mjs'; import { M as MatCommonModule } from './common-module-43c0ba57.mjs'; import '@angular/cdk/coercion'; /** * Injection token used to provide the parent menu to menu-specific components. * @docs-private */ const MAT_MENU_PANEL = new InjectionToken('MAT_MENU_PANEL'); /** * Single item inside a `mat-menu`. Provides the menu item styling and accessibility treatment. */ class MatMenuItem { _elementRef = inject(ElementRef); _document = inject(DOCUMENT); _focusMonitor = inject(FocusMonitor); _parentMenu = inject(MAT_MENU_PANEL, { optional: true }); _changeDetectorRef = inject(ChangeDetectorRef); /** ARIA role for the menu item. */ role = 'menuitem'; /** Whether the menu item is disabled. */ disabled = false; /** Whether ripples are disabled on the menu item. */ disableRipple = false; /** Stream that emits when the menu item is hovered. */ _hovered = new Subject(); /** Stream that emits when the menu item is focused. */ _focused = new Subject(); /** Whether the menu item is highlighted. */ _highlighted = false; /** Whether the menu item acts as a trigger for a sub-menu. */ _triggersSubmenu = false; constructor() { inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader); this._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. this._highlighted = isHighlighted; this._changeDetectorRef.markForCheck(); } _setTriggersSubmenu(triggersSubmenu) { this._triggersSubmenu = triggersSubmenu; this._changeDetectorRef.markForCheck(); } _hasFocus() { return this._document && this._document.activeElement === this._getHostElement(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: MatMenuItem, isStandalone: true, selector: "[mat-menu-item]", inputs: { role: "role", disabled: ["disabled", "disabled", booleanAttribute], disableRipple: ["disableRipple", "disableRipple", booleanAttribute] }, 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-focus-indicator" }, exportAs: ["matMenuItem"], ngImport: i0, template: "<ng-content select=\"mat-icon, [matMenuItemIcon]\"></ng-content>\n<span class=\"mat-mdc-menu-item-text\"><ng-content></ng-content></span>\n<div class=\"mat-mdc-menu-ripple\" matRipple\n [matRippleDisabled]=\"disableRipple || disabled\"\n [matRippleTrigger]=\"_getHostElement()\">\n</div>\n\n@if (_triggersSubmenu) {\n <svg\n class=\"mat-mdc-menu-submenu-icon\"\n viewBox=\"0 0 5 10\"\n focusable=\"false\"\n aria-hidden=\"true\"><polygon points=\"0,0 5,5 0,10\"/></svg>\n}\n", dependencies: [{ kind: "directive", type: 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: "19.2.0", ngImport: i0, type: MatMenuItem, decorators: [{ type: Component, args: [{ selector: '[mat-menu-item]', exportAs: 'matMenuItem', host: { '[attr.role]': 'role', 'class': 'mat-mdc-menu-item mat-focus-indicator', '[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, imports: [MatRipple], template: "<ng-content select=\"mat-icon, [matMenuItemIcon]\"></ng-content>\n<span class=\"mat-mdc-menu-item-text\"><ng-content></ng-content></span>\n<div class=\"mat-mdc-menu-ripple\" matRipple\n [matRippleDisabled]=\"disableRipple || disabled\"\n [matRippleTrigger]=\"_getHostElement()\">\n</div>\n\n@if (_triggersSubmenu) {\n <svg\n class=\"mat-mdc-menu-submenu-icon\"\n viewBox=\"0 0 5 10\"\n focusable=\"false\"\n aria-hidden=\"true\"><polygon points=\"0,0 5,5 0,10\"/></svg>\n}\n" }] }], ctorParameters: () => [], propDecorators: { role: [{ type: Input }], disabled: [{ type: Input, args: [{ transform: booleanAttribute }] }], disableRipple: [{ type: Input, args: [{ transform: booleanAttribute }] }] } }); /** * 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.`); } /** * 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'); /** Menu content that will be rendered lazily once the menu is opened. */ class MatMenuContent { _template = inject(TemplateRef); _appRef = inject(ApplicationRef); _injector = inject(Injector); _viewContainerRef = inject(ViewContainerRef); _document = inject(DOCUMENT); _changeDetectorRef = inject(ChangeDetectorRef); _portal; _outlet; /** Emits when the menu content has been attached. */ _attached = new Subject(); constructor() { } /** * 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'), null, 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`. 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() { this.detach(); this._outlet?.dispose(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatMenuContent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: MatMenuContent, isStandalone: true, selector: "ng-template[matMenuContent]", providers: [{ provide: MAT_MENU_CONTENT, useExisting: MatMenuContent }], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatMenuContent, decorators: [{ type: Directive, args: [{ selector: 'ng-template[matMenuContent]', providers: [{ provide: MAT_MENU_CONTENT, useExisting: MatMenuContent }], }] }], ctorParameters: () => [] }); /** 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 * @deprecated No longer used, will be removed. * @breaking-change 21.0.0 */ function MAT_MENU_DEFAULT_OPTIONS_FACTORY() { return { overlapTrigger: false, xPosition: 'after', yPosition: 'below', backdropClass: 'cdk-overlay-transparent-backdrop', }; } /** Name of the enter animation `@keyframes`. */ const ENTER_ANIMATION = '_mat-menu-enter'; /** Name of the exit animation `@keyframes`. */ const EXIT_ANIMATION = '_mat-menu-exit'; class MatMenu { _elementRef = inject(ElementRef); _changeDetectorRef = inject(ChangeDetectorRef); _injector = inject(Injector); _keyManager; _xPosition; _yPosition; _firstItemFocusRef; _exitFallbackTimeout; /** Whether animations are currently disabled. */ _animationsDisabled; /** All items inside the menu. Includes items nested inside another menu. */ _allItems; /** Only the direct descendant menu items. */ _directDescendantItems = new QueryList(); /** Classes to be applied to the menu panel. */ _classList = {}; /** Current state of the panel animation. */ _panelAnimationState = 'void'; /** Emits whenever an animation on the menu completes. */ _animationDone = new Subject(); /** Whether the menu is animating. */ _isAnimating = false; /** Parent menu of the current menu panel. */ parentMenu; /** Layout direction of the menu. */ direction; /** Class or list of classes to be added to the overlay panel. */ overlayPanelClass; /** Class to be added to the backdrop element. */ backdropClass; /** aria-label for the menu panel. */ ariaLabel; /** aria-labelledby for the menu panel. */ ariaLabelledby; /** aria-describedby for the menu panel. */ ariaDescribedby; /** 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(); } /** @docs-private */ templateRef; /** * List of the items inside of a menu. * @deprecated * @breaking-change 8.0.0 */ items; /** * Menu content that will be rendered lazily. * @docs-private */ lazyContent; /** Whether the menu should overlap its trigger. */ overlapTrigger; /** Whether the menu has a backdrop. */ 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 */ set panelClass(classes) { const previousPanelClass = this._previousPanelClass; const newClassList = { ...this._classList }; if (previousPanelClass && previousPanelClass.length) { previousPanelClass.split(' ').forEach((className) => { newClassList[className] = false; }); } this._previousPanelClass = classes; if (classes && classes.length) { classes.split(' ').forEach((className) => { newClassList[className] = true; }); this._elementRef.nativeElement.className = ''; } this._classList = newClassList; } _previousPanelClass; /** * 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; } /** Event emitted when the menu is closed. */ closed = new EventEmitter(); /** * Event emitted when the menu is closed. * @deprecated Switch to `closed` instead * @breaking-change 8.0.0 */ close = this.closed; panelId = inject(_IdGenerator).getId('mat-menu-panel-'); constructor() { const defaultOptions = inject(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; this._animationsDisabled = inject(ANIMATION_MODULE_TYPE, { optional: true }) === 'NoopAnimations'; } 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._firstItemFocusRef?.destroy(); clearTimeout(this._exitFallbackTimeout); } /** 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; } } /** * 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 `afterNextRender` to ensure iOS VoiceOver screen reader focuses the first item (#24735). this._firstItemFocusRef?.destroy(); this._firstItemFocusRef = afterNextRender(() => { const menuPanel = this._resolvePanel(); // 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) { } /** * 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) { 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(); } /** Callback that is invoked when the panel animation completes. */ _onAnimationDone(state) { const isExit = state === EXIT_ANIMATION; if (isExit || state === ENTER_ANIMATION) { if (isExit) { clearTimeout(this._exitFallbackTimeout); this._exitFallbackTimeout = undefined; } this._animationDone.next(isExit ? 'void' : 'enter'); this._isAnimating = false; } } _onAnimationStart(state) { if (state === ENTER_ANIMATION || state === EXIT_ANIMATION) { this._isAnimating = true; } } _setIsOpen(isOpen) { this._panelAnimationState = isOpen ? 'enter' : 'void'; if (isOpen) { if (this._keyManager.activeItemIndex === 0) { // 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. const menuPanel = this._resolvePanel(); if (menuPanel) { menuPanel.scrollTop = 0; } } } else if (!this._animationsDisabled) { // Some apps do `* { animation: none !important; }` in tests which will prevent the // `animationend` event from firing. Since the exit animation is loading-bearing for // removing the content from the DOM, add a fallback timer. this._exitFallbackTimeout = setTimeout(() => this._onAnimationDone(EXIT_ANIMATION), 200); } // Animation events won't fire when animations are disabled so we simulate them. if (this._animationsDisabled) { setTimeout(() => { this._onAnimationDone(isOpen ? ENTER_ANIMATION : EXIT_ANIMATION); }); } this._changeDetectorRef.markForCheck(); } /** * 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(); }); } /** Gets the menu panel DOM node. */ _resolvePanel() { 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"]'); } return menuPanel; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MatMenu, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "19.2.0", type: MatMenu, isStandalone: true, selector: "mat-menu", inputs: { backdropClass: "backdropClass", ariaLabel: ["aria-label", "ariaLabel"], ariaLabelledby: ["aria-labelledby", "ariaLabelledby"], ariaDescribedby: ["aria-describedby", "ariaDescribedby"], xPosition: "xPosition", yPosition: "yPosition", overlapTrigger: ["overlapTrigger", "overlapTrigger", booleanAttribute], hasBackdrop: ["hasBackdrop", "hasBackdrop", (value) => (value == null ? null : booleanAttribute(value))], panelClass: ["class", "panelClass"], classList: "classList" }, outputs: { closed: "closed", close: "close" }, host: { properties: { "attr.aria-label": "null", "attr.aria-labelledby": "null", "attr.aria-describedby": "null" } }, providers: [{ provide: MAT_MENU_PANEL, useExisting: MatMenu }], 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 }], exportAs: ["matMenu"], ngImport: i0, template: "<ng-template>\n <div\n class=\"mat-mdc-menu-panel\"\n [id]=\"panelId\"\n [class]=\"_classList\"\n [class.mat-menu-panel-animations-disabled]=\"_animationsDisabled\"\n [class.mat-menu-panel-exit-animation]=\"_panelAnimationState === 'void'\"\n [class.mat-menu-panel-animating]=\"_isAnimating\"\n (click)=\"closed.emit('click')\"\n tabindex=\"-1\"\n role=\"menu\"\n (animationstart)=\"_onAnimationStart($event.animationName)\"\n (animationend)=\"_onAnimationDone($event.animationName)\"\n (animationcancel)=\"_onAnimationDone($event.animationName)\"\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\">\n <ng-content></ng-content>\n </div>\n </div>\n</ng-template>\n", styles: ["mat-menu{display:none}.mat-mdc-menu-content{margin:0;padding:8px 0;outline:0}.mat-mdc-menu-content,.mat-mdc-menu-content .mat-mdc-menu-item .mat-mdc-menu-item-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;flex:1;white-space:normal;font-family:var(--mat-menu-item-label-text-font, var(--mat-sys-label-large-font));line-height:var(--mat-menu-item-label-text-line-height, var(--mat-sys-label-large-line-height));font-size:var(--mat-menu-item-label-text-size, var(--mat-sys-label-large-size));letter-spacing:var(--mat-menu-item-label-text-tracking, var(--mat-sys-label-large-tracking));font-weight:var(--mat-menu-item-label-text-weight, var(--mat-sys-label-large-weight))}@keyframes _mat-menu-enter{from{opacity:0;transform:scale(0.8)}to{opacity:1;transform:none}}@keyframes _mat-menu-exit{from{opacity:1}to{opacity:0}}.mat-mdc-menu-panel{min-width:112px;max-width:280px;overflow:auto;box-sizing:border-box;outline:0;animation:_mat-menu-enter 120ms cubic-bezier(0, 0, 0.2, 1);border-radius:var(--mat-menu-container-shape, var(--mat-sys-corner-extra-small));background-color:var(--mat-menu-container-color, var(--mat-sys-surface-container));box-shadow:var(--mat-menu-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));will-change:transform,opacity}.mat-mdc-menu-panel.mat-menu-panel-exit-animation{animation:_mat-menu-exit 100ms 25ms linear forwards}.mat-mdc-menu-panel.mat-menu-panel-animations-disabled{animation:none}.mat-mdc-menu-panel.mat-menu-panel-animating{pointer-events:none}.mat-mdc-menu-panel.mat-menu-panel-animating:has(.mat-mdc-menu-content:empty){display:none}@media(forced-colors: active){.mat-mdc-menu-panel{outline:solid 1px}}.mat-mdc-menu-panel .mat-divider{color:var(--mat-menu-divider-color, var(--mat-sys-surface-variant));margin-bottom:var(--mat-menu-divider-bottom-spacing, 8px);margin-top:var(--mat-menu-divider-top-spacing, 8px)}.mat-mdc-menu-item{display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;padding: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;padding-left:var(--mat-menu-item-leading-spacing, 12px);padding-right:var(--mat-menu-item-trailing-spacing, 12px);-webkit-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-menu-item::-moz-focus-inner{border:0}[dir=rtl] .mat-mdc-menu-item{padding-left:var(--mat-menu-item-trailing-spacing, 12px);padding-right:var(--mat-menu-item-leading-spacing, 12px)}.mat-mdc-menu-item:has(.material-icons,mat-icon,[matButtonIcon]){padding-left:var(--mat-menu-item-with-icon-leading-spacing, 12px);padding-right:var(--mat-menu-item-with-icon-trailing-spacing, 12px)}[dir=rtl] .mat-mdc-menu-item:has(.material-icons,mat-icon,[matButtonIcon]){padding-left:var(--mat-menu-item-with-icon-trailing-spacing, 12px);padding-right:var(--mat-menu-item-with-icon-leading-spacing, 12px)}.mat-mdc-menu-item,.mat-mdc-menu-item:visited,.mat-mdc-menu-item:link{color:var(--mat-menu-item-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-menu-item .mat-icon-no-color,.mat-mdc-menu-item .mat-mdc-menu-submenu-icon{color:var(--mat-menu-item-icon-color, var(--mat-sys-on-surface-variant))}.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:focus{outline:0}.mat-mdc-menu-item .mat-icon{flex-shrink:0;margin-right:var(--mat-menu-item-spacing, 12px);height:var(--mat-menu-item-icon-size, 24px);width:var(--mat-menu-item-icon-size, 24px)}[dir=rtl] .mat-mdc-menu-item{text-align:right}[dir=rtl] .mat-mdc-menu-item .mat-icon{margin-right:0;margin-left:var(--mat-menu-item-spacing, 12px)}.mat-mdc-menu-item:not([disabled]):hover{background-color:var(--mat-menu-item-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}.mat-mdc-menu-item:not([disabled]).cdk-program-focused,.mat-mdc-menu-item:not([disabled]).cdk-keyboard-focused,.mat-mdc-menu-item:not([disabled]).mat-mdc-menu-item-highlighted{background-color:var(--mat-menu-item-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent))}@media(forced-colors: active){.mat-mdc-menu-item{margin-top:1px}}.mat-mdc-menu-submenu-icon{width:var(--mat-menu-item-icon-size, 24px);height:10px;fill:currentColor;padding-left:var(--mat-menu-item-spacing, 12px)}[dir=rtl] .mat-mdc-menu-submenu-icon{padding-right:var(--mat-menu-item-spacing, 12px);padding-left:0}[dir=rtl] .mat-mdc-menu-submenu-icon polygon{transform:scaleX(-1);transform-origin:center}@media(forced-colors: 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}"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", 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', }, providers: [{ provide: MAT_MENU_PANEL, useExisting: MatMenu }], template: "<ng-template>\n <div\n class=\"mat-mdc-menu-panel\"\n [id]=\"panelId\"\n [class]=\"_classList\"\n [class.mat-menu-panel-animations-disabled]=\"_animationsDisabled\"\n [class.mat-menu-panel-exit-animation]=\"_panelAnimationState === 'void'\"\n [class.mat-menu-panel-animating]=\"_isAnimating\"\n (click)=\"closed.emit('click')\"\n tabindex=\"-1\"\n role=\"menu\"\n (animationstart)=\"_onAnimationStart($event.animationName)\"\n (animationend)=\"_onAnimationDone($event.animationName)\"\n (animationcancel)=\"_onAnimationDone($event.animationName)\"\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\">\n <ng-content></ng-content>\n </div>\n </div>\n</ng-template>\n", styles: ["mat-menu{display:none}.mat-mdc-menu-content{margin:0;padding:8px 0;outline:0}.mat-mdc-menu-content,.mat-mdc-menu-content .mat-mdc-menu-item .mat-mdc-menu-item-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;flex:1;white-space:normal;font-family:var(--mat-menu-item-label-text-font, var(--mat-sys-label-large-font));line-height:var(--mat-menu-item-label-text-line-height, var(--mat-sys-label-large-line-height));font-size:var(--mat-menu-item-label-text-size, var(--mat-sys-label-large-size));letter-spacing:var(--mat-menu-item-label-text-tracking, var(--mat-sys-label-large-tracking));font-weight:var(--mat-menu-item-label-text-weight, var(--mat-sys-label-large-weight))}@keyframes _mat-menu-enter{from{opacity:0;transform:scale(0.8)}to{opacity:1;transform:none}}@keyframes _mat-menu-exit{from{opacity:1}to{opacity:0}}.mat-mdc-menu-panel{min-width:112px;max-width:280px;overflow:auto;box-sizing:border-box;outline:0;animation:_mat-menu-enter 120ms cubic-bezier(0, 0, 0.2, 1);border-radius:var(--mat-menu-container-shape, var(--mat-sys-corner-extra-small));background-color:var(--mat-menu-container-color, var(--mat-sys-surface-container));box-shadow:var(--mat-menu-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));will-change:transform,opacity}.mat-mdc-menu-panel.mat-menu-panel-exit-animation{animation:_mat-menu-exit 100ms 25ms linear forwards}.mat-mdc-menu-panel.mat-menu-panel-animations-disabled{animation:none}.mat-mdc-menu-panel.mat-menu-panel-animating{pointer-events:none}.mat-mdc-menu-panel.mat-menu-panel-animating:has(.mat-mdc-menu-content:empty){display:none}@media(forced-colors: active){.mat-mdc-menu-panel{outline:solid 1px}}.mat-mdc-menu-panel .mat-divider{color:var(--mat-menu-divider-color, var(--mat-sys-surface-variant));margin-bottom:var(--mat-menu-divider-bottom-spacing, 8px);margin-top:var(--mat-menu-divider-top-spacing, 8px)}.mat-mdc-menu-item{display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;padding: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;padding-left:var(--mat-menu-item-leading-spacing, 12px);padding-right:var(--mat-menu-item-trailing-spacing, 12px);-webkit-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-menu-item::-moz-focus-inner{border:0}[dir=rtl] .mat-mdc-menu-item{padding-left:var(--mat-menu-item-trailing-spacing, 12px);padding-right:var(--mat-menu-item-leading-spacing, 12px)}.mat-mdc-menu-item:has(.material-icons,mat-icon,[matButtonIcon]){padding-left:var(--mat-menu-item-with-icon-leading-spacing, 12px);padding-right:var(--mat-menu-item-with-icon-trailing-spacing, 12px)}[dir=rtl] .mat-mdc-menu-item:has(.material-icons,mat-icon,[matButtonIcon]){padding-left:var(--mat-menu-item-with-icon-trailing-spacing, 12px);padding-right:var(--mat-menu-item-with-icon-leading-spacing, 12px)}.mat-mdc-menu-item,.mat-mdc-menu-item:visited,.mat-mdc-menu-item:link{color:var(--mat-menu-item-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-menu-item .mat-icon-no-color,.mat-mdc-menu-item .mat-mdc-menu-submenu-icon{color:var(--mat-menu-item-icon-color, var(--mat-sys-on-surface-variant))}.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:focus{outline:0}.mat-mdc-menu-item .mat-icon{flex-shrink:0;margin-right:var(--mat-menu-item-spacing, 12px);height:var(--mat-menu-item-icon-size, 24px);width:var(--mat-menu-item-icon-size, 24px)}[dir=rtl] .mat-mdc-menu-item{text-align:right}[dir=rtl] .mat-mdc-menu-item .mat-icon{margin-right:0;margin-left:var(--mat-menu-item-spacing, 12px)}.mat-mdc-menu-item:not([disabled]):hover{background-color:var(--mat-menu-item-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}.mat-mdc-menu-item:not([disabled]).cdk-program-focused,.mat-mdc-menu-item:not([disabled]).cdk-keyboard-focused,.mat-mdc-menu-item:not([disabled]).mat-mdc-menu-item-highlighted{background-color:var(--mat-menu-item-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent))}@media(forced-colors: active){.mat-mdc-menu-item{margin-top:1px}}.mat-mdc-menu-submenu-icon{width:var(--mat-menu-item-icon-size, 24px);height:10px;fill:currentColor;padding-left:var(--mat-menu-item-spacing, 12px)}[dir=rtl] .mat-mdc-menu-submenu-icon{padding-right:var(--mat-menu-item-spacing, 12px);padding-left:0}[dir=rtl] .mat-mdc-menu-submenu-icon polygon{transform:scaleX(-1);transform-origin:center}@media(forced-colors: 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: () => [], 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, args: [{ transform: booleanAttribute }] }], hasBackdrop: [{ type: Input, args: [{ transform: (value) => (value == null ? null : booleanAttribute(value)) }] }], panelClass: [{ type: Input, args: ['class'] }], classList: [{ type: Input }], closed: [{ type: Output }], close: [{ type: Output }] } }); /** Injection token that determines the scroll handling while the menu is open. */ const MAT_MENU_SCROLL_STRATEGY = new InjectionToken('mat-menu-scroll-strategy', { providedIn: 'root', factory: () => { const overlay = inject(Overlay); return () => overlay.scrollStrategies.reposition(); }, }); /** * @docs-private * @deprecated No longer used, will be removed. * @breaking-change 21.0.0 */ function MAT_MENU_SCROLL_STRATEGY_FACTORY(overlay) { return () => overlay.scrollStrategies.reposition(); } /** * @docs-private * @deprecated No longer used, will be removed. * @breaking-change 21.0.0 */ 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 = { 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; /** Mapping between menu panels and the last trigger that opened them. */ const PANELS_TO_TRIGGERS = new WeakMap(); /** Directive applied to an element that should trigger a `mat-menu`. */ class MatMenuTrigger { _overlay = inject(Overlay); _element = inject(ElementRef); _viewContainerRef = inject(ViewContainerRef); _menuItemInstance = inject(MatMenuItem, { optional: true, self: true }); _dir = inject(Directionality, { optional: true }); _focusMonitor = inject(FocusMonitor); _ngZone = inject(NgZone); _scrollStrategy = inject(MAT_MENU_SCROLL_STRATEGY); _changeDetectorRef = inject(ChangeDetectorRef); _cleanupTouchstart; _portal; _overlayRef = null; _menuOpen = false; _closingActionsSubscription = Subscription.EMPTY; _hoverSubscription = Subscription.EMPTY; _menuCloseSubscription = Subscription.EMPTY; _pendingRemoval; /** * We're specifically looking for a `MatMenu` here since the generic `MatMenuPanel` * interface lacks some functionality around nested menus and animations. */ _parentMaterialMenu; /** * Cached value of the padding of the parent menu panel. * Used to offset sub-menus to compensate for the padding. */ _parentInnerPadding; // 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 _openedBy = undefined; /** * @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._menuItemInstance?._setTriggersSubmenu(this.triggersSubmenu()); } _menu; /** Data to be passed along to any lazily-rendered content. */ menuData; /** * 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. */ restoreFocus = true; /** Event emitted when the associated menu is opened. */ 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 onMenuOpen = this.menuOpened; /** Event emitted when the associated menu is closed. */ 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 onMenuClose = this.menuClosed; constructor() { const parentMenu = inject(MAT_MENU_PANEL, { optional: true }); const renderer = inject(Renderer2); this._parentMaterialMenu = parentMenu instanceof MatMenu ? parentMenu : undefined; this._cleanupTouchstart = _bindEventWithOptions(renderer, this._element.nativeElement, 'touchstart', (event) => { if (!isFakeTouchstartFromScreenReader(event)) { this._openedBy = 'touch'; } }, passiveEventListenerOptions); } ngAfterContentInit() { this._handleHover(); } ngOnDestroy() { if (this.menu && this._ownsMenu(this.menu)) { PANELS_TO_TRIGGERS.delete(this.menu); } this._cleanupTouchstart(); this._pendingRemoval?.unsubscribe(); this._menuCloseSubscription.unsubscribe(); this._closingActionsSubscription.unsubscribe(); this._hoverSubscription.unsubscribe(); if (this._overlayRef) { this._overlayRef.dispose(); this._overlayRef = null; } } /** Whether the menu is open. */