@angular/cdk
Version:
Angular Material Component Development Kit
1,141 lines (1,131 loc) • 89.3 kB
JavaScript
import * as i0 from '@angular/core';
import { Directive, InjectionToken, Optional, SkipSelf, Inject, inject, Injectable, Injector, ViewContainerRef, EventEmitter, NgZone, RendererFactory2, ElementRef, ChangeDetectorRef, Renderer2, booleanAttribute, Input, Output, signal, computed, ContentChildren, NgModule } from '@angular/core';
import { startWith, debounceTime, distinctUntilChanged, takeUntil, mergeMap, mapTo, mergeAll, switchMap, skipWhile, skip } from 'rxjs/operators';
import { U as UniqueSelectionDispatcher } from './unique-selection-dispatcher-28a150e1.mjs';
import { Subject, merge, partition } from 'rxjs';
import { _ as _IdGenerator } from './id-generator-0b91c6f7.mjs';
import { a as Overlay, c as OverlayConfig, S as STANDARD_DROPDOWN_BELOW_POSITIONS, e as STANDARD_DROPDOWN_ADJACENT_POSITIONS, d as OverlayModule } from './overlay-module-1d184db0.mjs';
import { T as TemplatePortal } from './portal-directives-dced6d68.mjs';
import { h as ENTER, k as SPACE, U as UP_ARROW, D as DOWN_ARROW, L as LEFT_ARROW, R as RIGHT_ARROW, T as TAB, e as ESCAPE } from './keycodes-0e4398c6.mjs';
import { I as InputModalityDetector, d as FocusMonitor } from './focus-monitor-28b6c826.mjs';
import { D as Directionality } from './directionality-9d44e426.mjs';
import { h as hasModifierKey } from './modifiers-3e8908bb.mjs';
import { _ as _getEventTarget } from './shadow-dom-318658ae.mjs';
import { F as FocusKeyManager } from './focus-key-manager-ff488563.mjs';
import '@angular/common';
import './platform-20fc4de8.mjs';
import './backwards-compatibility-08253a84.mjs';
import './test-environment-f6f8bc13.mjs';
import './style-loader-09eecacc.mjs';
import './css-pixel-value-5d0cae55.mjs';
import './array-6239d2f8.mjs';
import './scrolling-module-722545e3.mjs';
import './element-15999318.mjs';
import './scrolling-59340c46.mjs';
import './recycle-view-repeater-strategy-0f32b0a8.mjs';
import './data-source-d79c6e09.mjs';
import './bidi-module-04c03e58.mjs';
import './fake-event-detection-84590b88.mjs';
import './passive-listeners-93cf8be8.mjs';
import './list-key-manager-f9c3e90c.mjs';
import './typeahead-0113d27c.mjs';
/**
* A grouping container for `CdkMenuItemRadio` instances, similar to a `role="radiogroup"` element.
*/
class CdkMenuGroup {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkMenuGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: CdkMenuGroup, isStandalone: true, selector: "[cdkMenuGroup]", host: { attributes: { "role": "group" }, classAttribute: "cdk-menu-group" }, providers: [{ provide: UniqueSelectionDispatcher, useClass: UniqueSelectionDispatcher }], exportAs: ["cdkMenuGroup"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkMenuGroup, decorators: [{
type: Directive,
args: [{
selector: '[cdkMenuGroup]',
exportAs: 'cdkMenuGroup',
host: {
'role': 'group',
'class': 'cdk-menu-group',
},
providers: [{ provide: UniqueSelectionDispatcher, useClass: UniqueSelectionDispatcher }],
}]
}] });
/** Injection token used to return classes implementing the Menu interface */
const CDK_MENU = new InjectionToken('cdk-menu');
/** The relative item in the inline menu to focus after closing all popup menus. */
var FocusNext;
(function (FocusNext) {
FocusNext[FocusNext["nextItem"] = 0] = "nextItem";
FocusNext[FocusNext["previousItem"] = 1] = "previousItem";
FocusNext[FocusNext["currentItem"] = 2] = "currentItem";
})(FocusNext || (FocusNext = {}));
/** Injection token used for an implementation of MenuStack. */
const MENU_STACK = new InjectionToken('cdk-menu-stack');
/** Provider that provides the parent menu stack, or a new menu stack if there is no parent one. */
const PARENT_OR_NEW_MENU_STACK_PROVIDER = {
provide: MENU_STACK,
deps: [[new Optional(), new SkipSelf(), new Inject(MENU_STACK)]],
useFactory: (parentMenuStack) => parentMenuStack || new MenuStack(),
};
/** Provider that provides the parent menu stack, or a new inline menu stack if there is no parent one. */
const PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER = (orientation) => ({
provide: MENU_STACK,
deps: [[new Optional(), new SkipSelf(), new Inject(MENU_STACK)]],
useFactory: (parentMenuStack) => parentMenuStack || MenuStack.inline(orientation),
});
/**
* MenuStack allows subscribers to listen for close events (when a MenuStackItem is popped off
* of the stack) in order to perform closing actions. Upon the MenuStack being empty it emits
* from the `empty` observable specifying the next focus action which the listener should perform
* as requested by the closer.
*/
class MenuStack {
/** The ID of this menu stack. */
id = inject(_IdGenerator).getId('cdk-menu-stack-');
/** All MenuStackItems tracked by this MenuStack. */
_elements = [];
/** Emits the element which was popped off of the stack when requested by a closer. */
_close = new Subject();
/** Emits once the MenuStack has become empty after popping off elements. */
_empty = new Subject();
/** Emits whether any menu in the menu stack has focus. */
_hasFocus = new Subject();
/** Observable which emits the MenuStackItem which has been requested to close. */
closed = this._close;
/** Observable which emits whether any menu in the menu stack has focus. */
hasFocus = this._hasFocus.pipe(startWith(false), debounceTime(0), distinctUntilChanged());
/**
* Observable which emits when the MenuStack is empty after popping off the last element. It
* emits a FocusNext event which specifies the action the closer has requested the listener
* perform.
*/
emptied = this._empty;
/**
* Whether the inline menu associated with this menu stack is vertical or horizontal.
* `null` indicates there is no inline menu associated with this menu stack.
*/
_inlineMenuOrientation = null;
/** Creates a menu stack that originates from an inline menu. */
static inline(orientation) {
const stack = new MenuStack();
stack._inlineMenuOrientation = orientation;
return stack;
}
/**
* Adds an item to the menu stack.
* @param menu the MenuStackItem to put on the stack.
*/
push(menu) {
this._elements.push(menu);
}
/**
* Pop items off of the stack up to and including `lastItem` and emit each on the close
* observable. If the stack is empty or `lastItem` is not on the stack it does nothing.
* @param lastItem the last item to pop off the stack.
* @param options Options that configure behavior on close.
*/
close(lastItem, options) {
const { focusNextOnEmpty, focusParentTrigger } = { ...options };
if (this._elements.indexOf(lastItem) >= 0) {
let poppedElement;
do {
poppedElement = this._elements.pop();
this._close.next({ item: poppedElement, focusParentTrigger });
} while (poppedElement !== lastItem);
if (this.isEmpty()) {
this._empty.next(focusNextOnEmpty);
}
}
}
/**
* Pop items off of the stack up to but excluding `lastItem` and emit each on the close
* observable. If the stack is empty or `lastItem` is not on the stack it does nothing.
* @param lastItem the element which should be left on the stack
* @return whether or not an item was removed from the stack
*/
closeSubMenuOf(lastItem) {
let removed = false;
if (this._elements.indexOf(lastItem) >= 0) {
removed = this.peek() !== lastItem;
while (this.peek() !== lastItem) {
this._close.next({ item: this._elements.pop() });
}
}
return removed;
}
/**
* Pop off all MenuStackItems and emit each one on the `close` observable one by one.
* @param options Options that configure behavior on close.
*/
closeAll(options) {
const { focusNextOnEmpty, focusParentTrigger } = { ...options };
if (!this.isEmpty()) {
while (!this.isEmpty()) {
const menuStackItem = this._elements.pop();
if (menuStackItem) {
this._close.next({ item: menuStackItem, focusParentTrigger });
}
}
this._empty.next(focusNextOnEmpty);
}
}
/** Return true if this stack is empty. */
isEmpty() {
return !this._elements.length;
}
/** Return the length of the stack. */
length() {
return this._elements.length;
}
/** Get the top most element on the stack. */
peek() {
return this._elements[this._elements.length - 1];
}
/** Whether the menu stack is associated with an inline menu. */
hasInlineMenu() {
return this._inlineMenuOrientation != null;
}
/** The orientation of the associated inline menu. */
inlineMenuOrientation() {
return this._inlineMenuOrientation;
}
/** Sets whether the menu stack contains the focused element. */
setHasFocus(hasFocus) {
this._hasFocus.next(hasFocus);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MenuStack, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MenuStack });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MenuStack, decorators: [{
type: Injectable
}] });
/** Injection token used for an implementation of MenuStack. */
const MENU_TRIGGER = new InjectionToken('cdk-menu-trigger');
/** Injection token used to configure the behavior of the menu when the page is scrolled. */
const MENU_SCROLL_STRATEGY = new InjectionToken('cdk-menu-scroll-strategy', {
providedIn: 'root',
factory: () => {
const overlay = inject(Overlay);
return () => overlay.scrollStrategies.reposition();
},
});
/**
* Abstract directive that implements shared logic common to all menu triggers.
* This class can be extended to create custom menu trigger types.
*/
class CdkMenuTriggerBase {
/** The DI injector for this component. */
injector = inject(Injector);
/** The view container ref for this component */
viewContainerRef = inject(ViewContainerRef);
/** The menu stack in which this menu resides. */
menuStack = inject(MENU_STACK);
/** Function used to configure the scroll strategy for the menu. */
menuScrollStrategy = inject(MENU_SCROLL_STRATEGY);
/**
* A list of preferred menu positions to be used when constructing the
* `FlexibleConnectedPositionStrategy` for this trigger's menu.
*/
menuPosition;
/** Emits when the attached menu is requested to open */
opened = new EventEmitter();
/** Emits when the attached menu is requested to close */
closed = new EventEmitter();
/** Template reference variable to the menu this trigger opens */
menuTemplateRef;
/** Context data to be passed along to the menu template */
menuData;
/** A reference to the overlay which manages the triggered menu */
overlayRef = null;
/** Emits when this trigger is destroyed. */
destroyed = new Subject();
/** Emits when the outside pointer events listener on the overlay should be stopped. */
stopOutsideClicksListener = merge(this.closed, this.destroyed);
/** The child menu opened by this trigger. */
childMenu;
/** The content of the menu panel opened by this trigger. */
_menuPortal;
/** The injector to use for the child menu opened by this trigger. */
_childMenuInjector;
ngOnDestroy() {
this._destroyOverlay();
this.destroyed.next();
this.destroyed.complete();
}
/** Whether the attached menu is open. */
isOpen() {
return !!this.overlayRef?.hasAttached();
}
/** Registers a child menu as having been opened by this trigger. */
registerChildMenu(child) {
this.childMenu = child;
}
/**
* Get the portal to be attached to the overlay which contains the menu. Allows for the menu
* content to change dynamically and be reflected in the application.
*/
getMenuContentPortal() {
const hasMenuContentChanged = this.menuTemplateRef !== this._menuPortal?.templateRef;
if (this.menuTemplateRef && (!this._menuPortal || hasMenuContentChanged)) {
this._menuPortal = new TemplatePortal(this.menuTemplateRef, this.viewContainerRef, this.menuData, this._getChildMenuInjector());
}
return this._menuPortal;
}
/**
* Whether the given element is inside the scope of this trigger's menu stack.
* @param element The element to check.
* @return Whether the element is inside the scope of this trigger's menu stack.
*/
isElementInsideMenuStack(element) {
for (let el = element; el; el = el?.parentElement ?? null) {
if (el.getAttribute('data-cdk-menu-stack-id') === this.menuStack.id) {
return true;
}
}
return false;
}
/** Destroy and unset the overlay reference it if exists */
_destroyOverlay() {
if (this.overlayRef) {
this.overlayRef.dispose();
this.overlayRef = null;
}
}
/** Gets the injector to use when creating a child menu. */
_getChildMenuInjector() {
this._childMenuInjector =
this._childMenuInjector ||
Injector.create({
providers: [
{ provide: MENU_TRIGGER, useValue: this },
{ provide: MENU_STACK, useValue: this.menuStack },
],
parent: this.injector,
});
return this._childMenuInjector;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkMenuTriggerBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: CdkMenuTriggerBase, isStandalone: true, host: { properties: { "attr.aria-controls": "childMenu?.id", "attr.data-cdk-menu-stack-id": "menuStack.id" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkMenuTriggerBase, decorators: [{
type: Directive,
args: [{
host: {
'[attr.aria-controls]': 'childMenu?.id',
'[attr.data-cdk-menu-stack-id]': 'menuStack.id',
},
}]
}] });
/**
* Throws an exception when an instance of the PointerFocusTracker is not provided.
* @docs-private
*/
function throwMissingPointerFocusTracker() {
throw Error('expected an instance of PointerFocusTracker to be provided');
}
/**
* Throws an exception when a reference to the parent menu is not provided.
* @docs-private
*/
function throwMissingMenuReference() {
throw Error('expected a reference to the parent menu');
}
/** Injection token used for an implementation of MenuAim. */
const MENU_AIM = new InjectionToken('cdk-menu-aim');
/** Capture every nth mouse move event. */
const MOUSE_MOVE_SAMPLE_FREQUENCY = 3;
/** The number of mouse move events to track. */
const NUM_POINTS = 5;
/**
* How long to wait before closing a sibling menu if a user stops short of the submenu they were
* predicted to go into.
*/
const CLOSE_DELAY = 300;
/** Calculate the slope between point a and b. */
function getSlope(a, b) {
return (b.y - a.y) / (b.x - a.x);
}
/** Calculate the y intercept for the given point and slope. */
function getYIntercept(point, slope) {
return point.y - slope * point.x;
}
/**
* Whether the given mouse trajectory line defined by the slope and y intercept falls within the
* submenu as defined by `submenuPoints`
* @param submenuPoints the submenu DOMRect points.
* @param m the slope of the trajectory line.
* @param b the y intercept of the trajectory line.
* @return true if any point on the line falls within the submenu.
*/
function isWithinSubmenu(submenuPoints, m, b) {
const { left, right, top, bottom } = submenuPoints;
// Check for intersection with each edge of the submenu (left, right, top, bottom)
// by fixing one coordinate to that edge's coordinate (either x or y) and checking if the
// other coordinate is within bounds.
return ((m * left + b >= top && m * left + b <= bottom) ||
(m * right + b >= top && m * right + b <= bottom) ||
((top - b) / m >= left && (top - b) / m <= right) ||
((bottom - b) / m >= left && (bottom - b) / m <= right));
}
/**
* TargetMenuAim predicts if a user is moving into a submenu. It calculates the
* trajectory of the user's mouse movement in the current menu to determine if the
* mouse is moving towards an open submenu.
*
* The determination is made by calculating the slope of the users last NUM_POINTS moves where each
* pair of points determines if the trajectory line points into the submenu. It uses consensus
* approach by checking if at least NUM_POINTS / 2 pairs determine that the user is moving towards
* to submenu.
*/
class TargetMenuAim {
_ngZone = inject(NgZone);
_renderer = inject(RendererFactory2).createRenderer(null, null);
_cleanupMousemove;
/** The last NUM_POINTS mouse move events. */
_points = [];
/** Reference to the root menu in which we are tracking mouse moves. */
_menu;
/** Reference to the root menu's mouse manager. */
_pointerTracker;
/** The id associated with the current timeout call waiting to resolve. */
_timeoutId;
/** Emits when this service is destroyed. */
_destroyed = new Subject();
ngOnDestroy() {
this._cleanupMousemove?.();
this._destroyed.next();
this._destroyed.complete();
}
/**
* Set the Menu and its PointerFocusTracker.
* @param menu The menu that this menu aim service controls.
* @param pointerTracker The `PointerFocusTracker` for the given menu.
*/
initialize(menu, pointerTracker) {
this._menu = menu;
this._pointerTracker = pointerTracker;
this._subscribeToMouseMoves();
}
/**
* Calls the `doToggle` callback when it is deemed that the user is not moving towards
* the submenu.
* @param doToggle the function called when the user is not moving towards the submenu.
*/
toggle(doToggle) {
// If the menu is horizontal the sub-menus open below and there is no risk of premature
// closing of any sub-menus therefore we automatically resolve the callback.
if (this._menu.orientation === 'horizontal') {
doToggle();
}
this._checkConfigured();
const siblingItemIsWaiting = !!this._timeoutId;
const hasPoints = this._points.length > 1;
if (hasPoints && !siblingItemIsWaiting) {
if (this._isMovingToSubmenu()) {
this._startTimeout(doToggle);
}
else {
doToggle();
}
}
else if (!siblingItemIsWaiting) {
doToggle();
}
}
/**
* Start the delayed toggle handler if one isn't running already.
*
* The delayed toggle handler executes the `doToggle` callback after some period of time iff the
* users mouse is on an item in the current menu.
*
* @param doToggle the function called when the user is not moving towards the submenu.
*/
_startTimeout(doToggle) {
// If the users mouse is moving towards a submenu we don't want to immediately resolve.
// Wait for some period of time before determining if the previous menu should close in
// cases where the user may have moved towards the submenu but stopped on a sibling menu
// item intentionally.
const timeoutId = setTimeout(() => {
// Resolve if the user is currently moused over some element in the root menu
if (this._pointerTracker.activeElement && timeoutId === this._timeoutId) {
doToggle();
}
this._timeoutId = null;
}, CLOSE_DELAY);
this._timeoutId = timeoutId;
}
/** Whether the user is heading towards the open submenu. */
_isMovingToSubmenu() {
const submenuPoints = this._getSubmenuBounds();
if (!submenuPoints) {
return false;
}
let numMoving = 0;
const currPoint = this._points[this._points.length - 1];
// start from the second last point and calculate the slope between each point and the last
// point.
for (let i = this._points.length - 2; i >= 0; i--) {
const previous = this._points[i];
const slope = getSlope(currPoint, previous);
if (isWithinSubmenu(submenuPoints, slope, getYIntercept(currPoint, slope))) {
numMoving++;
}
}
return numMoving >= Math.floor(NUM_POINTS / 2);
}
/** Get the bounding DOMRect for the open submenu. */
_getSubmenuBounds() {
return this._pointerTracker?.previousElement?.getMenu()?.nativeElement.getBoundingClientRect();
}
/**
* Check if a reference to the PointerFocusTracker and menu element is provided.
* @throws an error if neither reference is provided.
*/
_checkConfigured() {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!this._pointerTracker) {
throwMissingPointerFocusTracker();
}
if (!this._menu) {
throwMissingMenuReference();
}
}
}
/** Subscribe to the root menus mouse move events and update the tracked mouse points. */
_subscribeToMouseMoves() {
this._cleanupMousemove?.();
this._cleanupMousemove = this._ngZone.runOutsideAngular(() => {
let eventIndex = 0;
return this._renderer.listen(this._menu.nativeElement, 'mousemove', (event) => {
if (eventIndex % MOUSE_MOVE_SAMPLE_FREQUENCY === 0) {
this._points.push({ x: event.clientX, y: event.clientY });
if (this._points.length > NUM_POINTS) {
this._points.shift();
}
}
eventIndex++;
});
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: TargetMenuAim, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: TargetMenuAim });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: TargetMenuAim, decorators: [{
type: Injectable
}] });
/**
* CdkTargetMenuAim is a provider for the TargetMenuAim service. It can be added to an
* element with either the `cdkMenu` or `cdkMenuBar` directive and child menu items.
*/
class CdkTargetMenuAim {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkTargetMenuAim, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: CdkTargetMenuAim, isStandalone: true, selector: "[cdkTargetMenuAim]", providers: [{ provide: MENU_AIM, useClass: TargetMenuAim }], exportAs: ["cdkTargetMenuAim"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkTargetMenuAim, decorators: [{
type: Directive,
args: [{
selector: '[cdkTargetMenuAim]',
exportAs: 'cdkTargetMenuAim',
providers: [{ provide: MENU_AIM, useClass: TargetMenuAim }],
}]
}] });
/** Checks whether a keyboard event will trigger a native `click` event on an element. */
function eventDispatchesNativeClick(elementRef, event) {
// Synthetic events won't trigger clicks.
if (!event.isTrusted) {
return false;
}
const el = elementRef.nativeElement;
const keyCode = event.keyCode;
// Buttons trigger clicks both on space and enter events.
if (el.nodeName === 'BUTTON' && !el.disabled) {
return keyCode === ENTER || keyCode === SPACE;
}
// Links only trigger clicks on enter.
if (el.nodeName === 'A') {
return keyCode === ENTER;
}
// Any other elements won't dispatch clicks from keyboard events.
return false;
}
/**
* A directive that turns its host element into a trigger for a popup menu.
* It can be combined with cdkMenuItem to create sub-menus. If the element is in a top level
* MenuBar it will open the menu on click, or if a sibling is already opened it will open on hover.
* If it is inside of a Menu it will open the attached Submenu on hover regardless of its sibling
* state.
*/
class CdkMenuTrigger extends CdkMenuTriggerBase {
_elementRef = inject(ElementRef);
_overlay = inject(Overlay);
_ngZone = inject(NgZone);
_changeDetectorRef = inject(ChangeDetectorRef);
_inputModalityDetector = inject(InputModalityDetector);
_directionality = inject(Directionality, { optional: true });
_renderer = inject(Renderer2);
_cleanupMouseenter;
/** The parent menu this trigger belongs to. */
_parentMenu = inject(CDK_MENU, { optional: true });
/** The menu aim service used by this menu. */
_menuAim = inject(MENU_AIM, { optional: true });
constructor() {
super();
this._setRole();
this._registerCloseHandler();
this._subscribeToMenuStackClosed();
this._subscribeToMouseEnter();
this._subscribeToMenuStackHasFocus();
this._setType();
}
/** Toggle the attached menu. */
toggle() {
this.isOpen() ? this.close() : this.open();
}
/** Open the attached menu. */
open() {
if (!this.isOpen() && this.menuTemplateRef != null) {
this.opened.next();
this.overlayRef = this.overlayRef || this._overlay.create(this._getOverlayConfig());
this.overlayRef.attach(this.getMenuContentPortal());
this._changeDetectorRef.markForCheck();
this._subscribeToOutsideClicks();
}
}
/** Close the opened menu. */
close() {
if (this.isOpen()) {
this.closed.next();
this.overlayRef.detach();
this._changeDetectorRef.markForCheck();
}
this._closeSiblingTriggers();
}
/**
* Get a reference to the rendered Menu if the Menu is open and rendered in the DOM.
*/
getMenu() {
return this.childMenu;
}
ngOnChanges(changes) {
if (changes['menuPosition'] && this.overlayRef) {
this.overlayRef.updatePositionStrategy(this._getOverlayPositionStrategy());
}
}
ngOnDestroy() {
this._cleanupMouseenter();
super.ngOnDestroy();
}
/**
* Handles keyboard events for the menu item.
* @param event The keyboard event to handle
*/
_toggleOnKeydown(event) {
const isParentVertical = this._parentMenu?.orientation === 'vertical';
switch (event.keyCode) {
case SPACE:
case ENTER:
// Skip events that will trigger clicks so the handler doesn't get triggered twice.
if (!hasModifierKey(event) && !eventDispatchesNativeClick(this._elementRef, event)) {
this.toggle();
this.childMenu?.focusFirstItem('keyboard');
}
break;
case RIGHT_ARROW:
if (!hasModifierKey(event)) {
if (this._parentMenu && isParentVertical && this._directionality?.value !== 'rtl') {
event.preventDefault();
this.open();
this.childMenu?.focusFirstItem('keyboard');
}
}
break;
case LEFT_ARROW:
if (!hasModifierKey(event)) {
if (this._parentMenu && isParentVertical && this._directionality?.value === 'rtl') {
event.preventDefault();
this.open();
this.childMenu?.focusFirstItem('keyboard');
}
}
break;
case DOWN_ARROW:
case UP_ARROW:
if (!hasModifierKey(event)) {
if (!isParentVertical) {
event.preventDefault();
this.open();
event.keyCode === DOWN_ARROW
? this.childMenu?.focusFirstItem('keyboard')
: this.childMenu?.focusLastItem('keyboard');
}
}
break;
}
}
/** Handles clicks on the menu trigger. */
_handleClick() {
this.toggle();
this.childMenu?.focusFirstItem('mouse');
}
/**
* Sets whether the trigger's menu stack has focus.
* @param hasFocus Whether the menu stack has focus.
*/
_setHasFocus(hasFocus) {
if (!this._parentMenu) {
this.menuStack.setHasFocus(hasFocus);
}
}
/**
* Subscribe to the mouseenter events and close any sibling menu items if this element is moused
* into.
*/
_subscribeToMouseEnter() {
this._cleanupMouseenter = this._ngZone.runOutsideAngular(() => {
return this._renderer.listen(this._elementRef.nativeElement, 'mouseenter', () => {
if (
// Skip fake `mouseenter` events dispatched by touch devices.
this._inputModalityDetector.mostRecentModality !== 'touch' &&
!this.menuStack.isEmpty() &&
!this.isOpen()) {
// Closes any sibling menu items and opens the menu associated with this trigger.
const toggleMenus = () => this._ngZone.run(() => {
this._closeSiblingTriggers();
this.open();
});
if (this._menuAim) {
this._menuAim.toggle(toggleMenus);
}
else {
toggleMenus();
}
}
});
});
}
/** Close out any sibling menu trigger menus. */
_closeSiblingTriggers() {
if (this._parentMenu) {
// If nothing was removed from the stack and the last element is not the parent item
// that means that the parent menu is a menu bar since we don't put the menu bar on the
// stack
const isParentMenuBar = !this.menuStack.closeSubMenuOf(this._parentMenu) &&
this.menuStack.peek() !== this._parentMenu;
if (isParentMenuBar) {
this.menuStack.closeAll();
}
}
else {
this.menuStack.closeAll();
}
}
/** Get the configuration object used to create the overlay. */
_getOverlayConfig() {
return new OverlayConfig({
positionStrategy: this._getOverlayPositionStrategy(),
scrollStrategy: this.menuScrollStrategy(),
direction: this._directionality || undefined,
});
}
/** Build the position strategy for the overlay which specifies where to place the menu. */
_getOverlayPositionStrategy() {
return this._overlay
.position()
.flexibleConnectedTo(this._elementRef)
.withLockedPosition()
.withFlexibleDimensions(false)
.withPositions(this._getOverlayPositions());
}
/** Get the preferred positions for the opened menu relative to the menu item. */
_getOverlayPositions() {
return (this.menuPosition ??
(!this._parentMenu || this._parentMenu.orientation === 'horizontal'
? STANDARD_DROPDOWN_BELOW_POSITIONS
: STANDARD_DROPDOWN_ADJACENT_POSITIONS));
}
/**
* Subscribe to the MenuStack close events if this is a standalone trigger and close out the menu
* this triggers when requested.
*/
_registerCloseHandler() {
if (!this._parentMenu) {
this.menuStack.closed.pipe(takeUntil(this.destroyed)).subscribe(({ item }) => {
if (item === this.childMenu) {
this.close();
}
});
}
}
/**
* Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
* click occurs outside the menus.
*/
_subscribeToOutsideClicks() {
if (this.overlayRef) {
this.overlayRef
.outsidePointerEvents()
.pipe(takeUntil(this.stopOutsideClicksListener))
.subscribe(event => {
const target = _getEventTarget(event);
const element = this._elementRef.nativeElement;
if (target !== element && !element.contains(target)) {
if (!this.isElementInsideMenuStack(target)) {
this.menuStack.closeAll();
}
else {
this._closeSiblingTriggers();
}
}
});
}
}
/** Subscribe to the MenuStack hasFocus events. */
_subscribeToMenuStackHasFocus() {
if (!this._parentMenu) {
this.menuStack.hasFocus.pipe(takeUntil(this.destroyed)).subscribe(hasFocus => {
if (!hasFocus) {
this.menuStack.closeAll();
}
});
}
}
/** Subscribe to the MenuStack closed events. */
_subscribeToMenuStackClosed() {
if (!this._parentMenu) {
this.menuStack.closed.subscribe(({ focusParentTrigger }) => {
if (focusParentTrigger && !this.menuStack.length()) {
this._elementRef.nativeElement.focus();
}
});
}
}
/** Sets the role attribute for this trigger if needed. */
_setRole() {
// If this trigger is part of another menu, the cdkMenuItem directive will handle setting the
// role, otherwise this is a standalone trigger, and we should ensure it has role="button".
if (!this._parentMenu) {
this._elementRef.nativeElement.setAttribute('role', 'button');
}
}
/** Sets thte `type` attribute of the trigger. */
_setType() {
const element = this._elementRef.nativeElement;
if (element.nodeName === 'BUTTON' && !element.getAttribute('type')) {
// Prevents form submissions.
element.setAttribute('type', 'button');
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: CdkMenuTrigger, isStandalone: true, selector: "[cdkMenuTriggerFor]", inputs: { menuTemplateRef: ["cdkMenuTriggerFor", "menuTemplateRef"], menuPosition: ["cdkMenuPosition", "menuPosition"], menuData: ["cdkMenuTriggerData", "menuData"] }, outputs: { opened: "cdkMenuOpened", closed: "cdkMenuClosed" }, host: { listeners: { "focusin": "_setHasFocus(true)", "focusout": "_setHasFocus(false)", "keydown": "_toggleOnKeydown($event)", "click": "_handleClick()" }, properties: { "attr.aria-haspopup": "menuTemplateRef ? \"menu\" : null", "attr.aria-expanded": "menuTemplateRef == null ? null : isOpen()" }, classAttribute: "cdk-menu-trigger" }, providers: [
{ provide: MENU_TRIGGER, useExisting: CdkMenuTrigger },
PARENT_OR_NEW_MENU_STACK_PROVIDER,
], exportAs: ["cdkMenuTriggerFor"], usesInheritance: true, usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkMenuTrigger, decorators: [{
type: Directive,
args: [{
selector: '[cdkMenuTriggerFor]',
exportAs: 'cdkMenuTriggerFor',
host: {
'class': 'cdk-menu-trigger',
'[attr.aria-haspopup]': 'menuTemplateRef ? "menu" : null',
'[attr.aria-expanded]': 'menuTemplateRef == null ? null : isOpen()',
'(focusin)': '_setHasFocus(true)',
'(focusout)': '_setHasFocus(false)',
'(keydown)': '_toggleOnKeydown($event)',
'(click)': '_handleClick()',
},
inputs: [
{ name: 'menuTemplateRef', alias: 'cdkMenuTriggerFor' },
{ name: 'menuPosition', alias: 'cdkMenuPosition' },
{ name: 'menuData', alias: 'cdkMenuTriggerData' },
],
outputs: ['opened: cdkMenuOpened', 'closed: cdkMenuClosed'],
providers: [
{ provide: MENU_TRIGGER, useExisting: CdkMenuTrigger },
PARENT_OR_NEW_MENU_STACK_PROVIDER,
],
}]
}], ctorParameters: () => [] });
/**
* Directive which provides the ability for an element to be focused and navigated to using the
* keyboard when residing in a CdkMenu, CdkMenuBar, or CdkMenuGroup. It performs user defined
* behavior when clicked.
*/
class CdkMenuItem {
_dir = inject(Directionality, { optional: true });
_elementRef = inject(ElementRef);
_ngZone = inject(NgZone);
_inputModalityDetector = inject(InputModalityDetector);
_renderer = inject(Renderer2);
_cleanupMouseEnter;
/** The menu aim service used by this menu. */
_menuAim = inject(MENU_AIM, { optional: true });
/** The stack of menus this menu belongs to. */
_menuStack = inject(MENU_STACK);
/** The parent menu in which this menuitem resides. */
_parentMenu = inject(CDK_MENU, { optional: true });
/** Reference to the CdkMenuItemTrigger directive if one is added to the same element */
_menuTrigger = inject(CdkMenuTrigger, { optional: true, self: true });
/** Whether the CdkMenuItem is disabled - defaults to false */
disabled = false;
/**
* The text used to locate this item during menu typeahead. If not specified,
* the `textContent` of the item will be used.
*/
typeaheadLabel;
/**
* If this MenuItem is a regular MenuItem, outputs when it is triggered by a keyboard or mouse
* event.
*/
triggered = new EventEmitter();
/** Whether the menu item opens a menu. */
get hasMenu() {
return this._menuTrigger?.menuTemplateRef != null;
}
/**
* The tabindex for this menu item managed internally and used for implementing roving a
* tab index.
*/
_tabindex = -1;
/** Whether the item should close the menu if triggered by the spacebar. */
closeOnSpacebarTrigger = true;
/** Emits when the menu item is destroyed. */
destroyed = new Subject();
constructor() {
this._setupMouseEnter();
this._setType();
if (this._isStandaloneItem()) {
this._tabindex = 0;
}
}
ngOnDestroy() {
this._cleanupMouseEnter?.();
this.destroyed.next();
this.destroyed.complete();
}
/** Place focus on the element. */
focus() {
this._elementRef.nativeElement.focus();
}
/**
* If the menu item is not disabled and the element does not have a menu trigger attached, emit
* on the cdkMenuItemTriggered emitter and close all open menus.
* @param options Options the configure how the item is triggered
* - keepOpen: specifies that the menu should be kept open after triggering the item.
*/
trigger(options) {
const { keepOpen } = { ...options };
if (!this.disabled && !this.hasMenu) {
this.triggered.next();
if (!keepOpen) {
this._menuStack.closeAll({ focusParentTrigger: true });
}
}
}
/** Return true if this MenuItem has an attached menu and it is open. */
isMenuOpen() {
return !!this._menuTrigger?.isOpen();
}
/**
* Get a reference to the rendered Menu if the Menu is open and it is visible in the DOM.
* @return the menu if it is open, otherwise undefined.
*/
getMenu() {
return this._menuTrigger?.getMenu();
}
/** Get the CdkMenuTrigger associated with this element. */
getMenuTrigger() {
return this._menuTrigger;
}
/** Get the label for this element which is required by the FocusableOption interface. */
getLabel() {
return this.typeaheadLabel || this._elementRef.nativeElement.textContent?.trim() || '';
}
/** Reset the tabindex to -1. */
_resetTabIndex() {
if (!this._isStandaloneItem()) {
this._tabindex = -1;
}
}
/**
* Set the tab index to 0 if not disabled and it's a focus event, or a mouse enter if this element
* is not in a menu bar.
*/
_setTabIndex(event) {
if (this.disabled) {
return;
}
// don't set the tabindex if there are no open sibling or parent menus
if (!event || !this._menuStack.isEmpty()) {
this._tabindex = 0;
}
}
/**
* Handles keyboard events for the menu item, specifically either triggering the user defined
* callback or opening/closing the current menu based on whether the left or right arrow key was
* pressed.
* @param event the keyboard event to handle
*/
_onKeydown(event) {
switch (event.keyCode) {
case SPACE:
case ENTER:
// Skip events that will trigger clicks so the handler doesn't get triggered twice.
if (!hasModifierKey(event) && !eventDispatchesNativeClick(this._elementRef, event)) {
const nodeName = this._elementRef.nativeElement.nodeName;
// Avoid repeat events on non-native elements (see #30250). Note that we don't do this
// on the native elements so we don't interfere with their behavior (see #26296).
if (nodeName !== 'A' && nodeName !== 'BUTTON') {
event.preventDefault();
}
this.trigger({ keepOpen: event.keyCode === SPACE && !this.closeOnSpacebarTrigger });
}
break;
case RIGHT_ARROW:
if (!hasModifierKey(event)) {
if (this._parentMenu && this._isParentVertical()) {
if (this._dir?.value !== 'rtl') {
this._forwardArrowPressed(event);
}
else {
this._backArrowPressed(event);
}
}
}
break;
case LEFT_ARROW:
if (!hasModifierKey(event)) {
if (this._parentMenu && this._isParentVertical()) {
if (this._dir?.value !== 'rtl') {
this._backArrowPressed(event);
}
else {
this._forwardArrowPressed(event);
}
}
}
break;
}
}
/** Whether this menu item is standalone or within a menu or menu bar. */
_isStandaloneItem() {
return !this._parentMenu;
}
/**
* Handles the user pressing the back arrow key.
* @param event The keyboard event.
*/
_backArrowPressed(event) {
const parentMenu = this._parentMenu;
if (this._menuStack.hasInlineMenu() || this._menuStack.length() > 1) {
event.preventDefault();
this._menuStack.close(parentMenu, {
focusNextOnEmpty: this._menuStack.inlineMenuOrientation() === 'horizontal'
? FocusNext.previousItem
: FocusNext.currentItem,
focusParentTrigger: true,
});
}
}
/**
* Handles the user pressing the forward arrow key.
* @param event The keyboard event.
*/
_forwardArrowPressed(event) {
if (!this.hasMenu && this._menuStack.inlineMenuOrientation() === 'horizontal') {
event.preventDefault();
this._menuStack.closeAll({
focusNextOnEmpty: FocusNext.nextItem,
focusParentTrigger: true,
});
}
}
/**
* Subscribe to the mouseenter events and close any sibling menu items if this element is moused
* into.
*/
_setupMouseEnter() {
if (!this._isStandaloneItem()) {
const closeOpenSiblings = () => this._ngZone.run(() => this._menuStack.closeSubMenuOf(this._parentMenu));
this._cleanupMouseEnter = this._ngZone.runOutsideAngular(() => this._renderer.listen(this._elementRef.nativeElement, 'mouseenter', () => {
// Skip fake `mouseenter` events dispatched by touch devices.
if (this._inputModalityDetector.mostRecentModality !== 'touch' &&
!this._menuStack.isEmpty() &&
!this.hasMenu) {
if (this._menuAim) {
this._menuAim.toggle(closeOpenSiblings);
}
else {
closeOpenSiblings();
}
}
}));
}
}
/**
* Return true if the enclosing parent menu is configured in a horizontal orientation, false
* otherwise or if no parent.
*/
_isParentVertical() {
return this._parentMenu?.orientation === 'vertical';
}
/** Sets the `type` attribute of the menu item. */
_setType() {
const element = this._elementRef.nativeElement;
if (element.nodeName === 'BUTTON' && !element.getAttribute('type')) {
// Prevent form submissions.
element.setAttribute('type', 'button');
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.0", type: CdkMenuItem, isStandalone: true, selector: "[cdkMenuItem]", inputs: { disabled: ["cdkMenuItemDisabled", "disabled", booleanAttribute], typeaheadLabel: ["cdkMenuitemTypeaheadLabel", "typeaheadLabel"] }, outputs: { triggered: "cdkMenuItemTriggered" }, host: { attributes: { "role": "menuitem" }, listeners: { "blur": "_resetTabIndex()", "focus": "_setTabIndex()", "click": "trigger()", "keydown": "_onKeydown($event)" }, properties: { "tabindex": "_tabindex", "attr.aria-disabled": "disabled || null" }, classAttribute: "cdk-menu-item" }, exportAs: ["cdkMenuItem"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: CdkMenuItem, decorators: [{
type: Directive,
args: [{
selector: '[cdkMenuItem]',
exportAs: 'cdkMenuItem',
host: {
'role': 'menuitem',
'class': 'cdk-menu-item',
'[tabindex]': '_tabindex',
'[attr.aria-disabled]': 'disabled || null',
'(blur)': '_resetTabIndex()',
'(focus)': '_setTabIndex()',
'(click)': 'trigger()',
'(keydown)': '_onKeydown($event)',
},
}]
}], ctorParameters: () => [], propDecorators: { disabled: [{
type: Input,
args: [{ alias: 'cdkMenuItemDisabled', transform: booleanAttribute }]
}], typeaheadLabel: [{
type: Input,
args: ['cdkMenuitemTypeaheadLabel']
}], triggered: [{
type: Output,
args: ['cdkMenuItemTriggered']
}] } });
/**
* PointerFocusTracker keeps track of the currently active item under mouse focus. It also has
* observables which emit when the users mouse enters and leaves a tracked element.
*/
class PointerFocusTracker {
_renderer;
_items;
_eventCleanups;
_itemsSubscription;
/** Emits when an element is moused into. */
entered = new Subject();
/** Emits when an element is moused out. */
exited = new Subject();
/** The element currently under mouse focus. */
activeElement;
/** The element previously under mouse focus. */
previousElement;
constructor(_renderer, _items) {
this._renderer = _renderer;
this._items = _items;
this._bindEvents();
this.entered.subscribe(element => (this.activeElement = element));
this.exited.subscribe(() => {
this.previousElemen