UNPKG

84.3 kBJavaScriptView Raw
1import * as i0 from '@angular/core';
2import { Directive, InjectionToken, Optional, SkipSelf, Inject, Injectable, inject, Injector, ViewContainerRef, EventEmitter, NgZone, ElementRef, Input, Output, ContentChildren, NgModule } from '@angular/core';
3import { Overlay, OverlayConfig, STANDARD_DROPDOWN_BELOW_POSITIONS, STANDARD_DROPDOWN_ADJACENT_POSITIONS, OverlayModule } from '@angular/cdk/overlay';
4import { UP_ARROW, hasModifierKey, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE, TAB, ESCAPE } from '@angular/cdk/keycodes';
5import { startWith, debounceTime, distinctUntilChanged, filter, takeUntil, mergeMap, mapTo, mergeAll, switchMap, skip } from 'rxjs/operators';
6import { UniqueSelectionDispatcher } from '@angular/cdk/collections';
7import { Subject, merge, fromEvent, defer, partition } from 'rxjs';
8import { TemplatePortal } from '@angular/cdk/portal';
9import { InputModalityDetector, FocusKeyManager } from '@angular/cdk/a11y';
10import { coerceBooleanProperty } from '@angular/cdk/coercion';
11import { Directionality } from '@angular/cdk/bidi';
12import { _getEventTarget } from '@angular/cdk/platform';
13
14/**
15 * A grouping container for `CdkMenuItemRadio` instances, similar to a `role="radiogroup"` element.
16 */
17class CdkMenuGroup {
18 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
19 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuGroup, isStandalone: true, selector: "[cdkMenuGroup]", host: { attributes: { "role": "group" }, classAttribute: "cdk-menu-group" }, providers: [{ provide: UniqueSelectionDispatcher, useClass: UniqueSelectionDispatcher }], exportAs: ["cdkMenuGroup"], ngImport: i0 }); }
20}
21i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuGroup, decorators: [{
22 type: Directive,
23 args: [{
24 selector: '[cdkMenuGroup]',
25 exportAs: 'cdkMenuGroup',
26 standalone: true,
27 host: {
28 'role': 'group',
29 'class': 'cdk-menu-group',
30 },
31 providers: [{ provide: UniqueSelectionDispatcher, useClass: UniqueSelectionDispatcher }],
32 }]
33 }] });
34
35/** Injection token used to return classes implementing the Menu interface */
36const CDK_MENU = new InjectionToken('cdk-menu');
37
38/** Injection token used for an implementation of MenuStack. */
39const MENU_STACK = new InjectionToken('cdk-menu-stack');
40/** Provider that provides the parent menu stack, or a new menu stack if there is no parent one. */
41const PARENT_OR_NEW_MENU_STACK_PROVIDER = {
42 provide: MENU_STACK,
43 deps: [[new Optional(), new SkipSelf(), new Inject(MENU_STACK)]],
44 useFactory: (parentMenuStack) => parentMenuStack || new MenuStack(),
45};
46/** Provider that provides the parent menu stack, or a new inline menu stack if there is no parent one. */
47const PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER = (orientation) => ({
48 provide: MENU_STACK,
49 deps: [[new Optional(), new SkipSelf(), new Inject(MENU_STACK)]],
50 useFactory: (parentMenuStack) => parentMenuStack || MenuStack.inline(orientation),
51});
52/** The next available menu stack ID. */
53let nextId$2 = 0;
54/**
55 * MenuStack allows subscribers to listen for close events (when a MenuStackItem is popped off
56 * of the stack) in order to perform closing actions. Upon the MenuStack being empty it emits
57 * from the `empty` observable specifying the next focus action which the listener should perform
58 * as requested by the closer.
59 */
60class MenuStack {
61 constructor() {
62 /** The ID of this menu stack. */
63 this.id = `${nextId$2++}`;
64 /** All MenuStackItems tracked by this MenuStack. */
65 this._elements = [];
66 /** Emits the element which was popped off of the stack when requested by a closer. */
67 this._close = new Subject();
68 /** Emits once the MenuStack has become empty after popping off elements. */
69 this._empty = new Subject();
70 /** Emits whether any menu in the menu stack has focus. */
71 this._hasFocus = new Subject();
72 /** Observable which emits the MenuStackItem which has been requested to close. */
73 this.closed = this._close;
74 /** Observable which emits whether any menu in the menu stack has focus. */
75 this.hasFocus = this._hasFocus.pipe(startWith(false), debounceTime(0), distinctUntilChanged());
76 /**
77 * Observable which emits when the MenuStack is empty after popping off the last element. It
78 * emits a FocusNext event which specifies the action the closer has requested the listener
79 * perform.
80 */
81 this.emptied = this._empty;
82 /**
83 * Whether the inline menu associated with this menu stack is vertical or horizontal.
84 * `null` indicates there is no inline menu associated with this menu stack.
85 */
86 this._inlineMenuOrientation = null;
87 }
88 /** Creates a menu stack that originates from an inline menu. */
89 static inline(orientation) {
90 const stack = new MenuStack();
91 stack._inlineMenuOrientation = orientation;
92 return stack;
93 }
94 /**
95 * Adds an item to the menu stack.
96 * @param menu the MenuStackItem to put on the stack.
97 */
98 push(menu) {
99 this._elements.push(menu);
100 }
101 /**
102 * Pop items off of the stack up to and including `lastItem` and emit each on the close
103 * observable. If the stack is empty or `lastItem` is not on the stack it does nothing.
104 * @param lastItem the last item to pop off the stack.
105 * @param options Options that configure behavior on close.
106 */
107 close(lastItem, options) {
108 const { focusNextOnEmpty, focusParentTrigger } = { ...options };
109 if (this._elements.indexOf(lastItem) >= 0) {
110 let poppedElement;
111 do {
112 poppedElement = this._elements.pop();
113 this._close.next({ item: poppedElement, focusParentTrigger });
114 } while (poppedElement !== lastItem);
115 if (this.isEmpty()) {
116 this._empty.next(focusNextOnEmpty);
117 }
118 }
119 }
120 /**
121 * Pop items off of the stack up to but excluding `lastItem` and emit each on the close
122 * observable. If the stack is empty or `lastItem` is not on the stack it does nothing.
123 * @param lastItem the element which should be left on the stack
124 * @return whether or not an item was removed from the stack
125 */
126 closeSubMenuOf(lastItem) {
127 let removed = false;
128 if (this._elements.indexOf(lastItem) >= 0) {
129 removed = this.peek() !== lastItem;
130 while (this.peek() !== lastItem) {
131 this._close.next({ item: this._elements.pop() });
132 }
133 }
134 return removed;
135 }
136 /**
137 * Pop off all MenuStackItems and emit each one on the `close` observable one by one.
138 * @param options Options that configure behavior on close.
139 */
140 closeAll(options) {
141 const { focusNextOnEmpty, focusParentTrigger } = { ...options };
142 if (!this.isEmpty()) {
143 while (!this.isEmpty()) {
144 const menuStackItem = this._elements.pop();
145 if (menuStackItem) {
146 this._close.next({ item: menuStackItem, focusParentTrigger });
147 }
148 }
149 this._empty.next(focusNextOnEmpty);
150 }
151 }
152 /** Return true if this stack is empty. */
153 isEmpty() {
154 return !this._elements.length;
155 }
156 /** Return the length of the stack. */
157 length() {
158 return this._elements.length;
159 }
160 /** Get the top most element on the stack. */
161 peek() {
162 return this._elements[this._elements.length - 1];
163 }
164 /** Whether the menu stack is associated with an inline menu. */
165 hasInlineMenu() {
166 return this._inlineMenuOrientation != null;
167 }
168 /** The orientation of the associated inline menu. */
169 inlineMenuOrientation() {
170 return this._inlineMenuOrientation;
171 }
172 /** Sets whether the menu stack contains the focused element. */
173 setHasFocus(hasFocus) {
174 this._hasFocus.next(hasFocus);
175 }
176 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: MenuStack, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
177 static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: MenuStack }); }
178}
179i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: MenuStack, decorators: [{
180 type: Injectable
181 }] });
182
183/** Injection token used for an implementation of MenuStack. */
184const MENU_TRIGGER = new InjectionToken('cdk-menu-trigger');
185/**
186 * Abstract directive that implements shared logic common to all menu triggers.
187 * This class can be extended to create custom menu trigger types.
188 */
189class CdkMenuTriggerBase {
190 constructor() {
191 /** The DI injector for this component. */
192 this.injector = inject(Injector);
193 /** The view container ref for this component */
194 this.viewContainerRef = inject(ViewContainerRef);
195 /** The menu stack in which this menu resides. */
196 this.menuStack = inject(MENU_STACK);
197 /** Emits when the attached menu is requested to open */
198 this.opened = new EventEmitter();
199 /** Emits when the attached menu is requested to close */
200 this.closed = new EventEmitter();
201 /** A reference to the overlay which manages the triggered menu */
202 this.overlayRef = null;
203 /** Emits when this trigger is destroyed. */
204 this.destroyed = new Subject();
205 /** Emits when the outside pointer events listener on the overlay should be stopped. */
206 this.stopOutsideClicksListener = merge(this.closed, this.destroyed);
207 }
208 ngOnDestroy() {
209 this._destroyOverlay();
210 this.destroyed.next();
211 this.destroyed.complete();
212 }
213 /** Whether the attached menu is open. */
214 isOpen() {
215 return !!this.overlayRef?.hasAttached();
216 }
217 /** Registers a child menu as having been opened by this trigger. */
218 registerChildMenu(child) {
219 this.childMenu = child;
220 }
221 /**
222 * Get the portal to be attached to the overlay which contains the menu. Allows for the menu
223 * content to change dynamically and be reflected in the application.
224 */
225 getMenuContentPortal() {
226 const hasMenuContentChanged = this.menuTemplateRef !== this._menuPortal?.templateRef;
227 if (this.menuTemplateRef && (!this._menuPortal || hasMenuContentChanged)) {
228 this._menuPortal = new TemplatePortal(this.menuTemplateRef, this.viewContainerRef, this.menuData, this._getChildMenuInjector());
229 }
230 return this._menuPortal;
231 }
232 /**
233 * Whether the given element is inside the scope of this trigger's menu stack.
234 * @param element The element to check.
235 * @return Whether the element is inside the scope of this trigger's menu stack.
236 */
237 isElementInsideMenuStack(element) {
238 for (let el = element; el; el = el?.parentElement ?? null) {
239 if (el.getAttribute('data-cdk-menu-stack-id') === this.menuStack.id) {
240 return true;
241 }
242 }
243 return false;
244 }
245 /** Destroy and unset the overlay reference it if exists */
246 _destroyOverlay() {
247 if (this.overlayRef) {
248 this.overlayRef.dispose();
249 this.overlayRef = null;
250 }
251 }
252 /** Gets the injector to use when creating a child menu. */
253 _getChildMenuInjector() {
254 this._childMenuInjector =
255 this._childMenuInjector ||
256 Injector.create({
257 providers: [
258 { provide: MENU_TRIGGER, useValue: this },
259 { provide: MENU_STACK, useValue: this.menuStack },
260 ],
261 parent: this.injector,
262 });
263 return this._childMenuInjector;
264 }
265 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuTriggerBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
266 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuTriggerBase, host: { properties: { "attr.aria-controls": "childMenu?.id", "attr.data-cdk-menu-stack-id": "menuStack.id" } }, ngImport: i0 }); }
267}
268i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuTriggerBase, decorators: [{
269 type: Directive,
270 args: [{
271 host: {
272 '[attr.aria-controls]': 'childMenu?.id',
273 '[attr.data-cdk-menu-stack-id]': 'menuStack.id',
274 },
275 }]
276 }] });
277
278/**
279 * Throws an exception when an instance of the PointerFocusTracker is not provided.
280 * @docs-private
281 */
282function throwMissingPointerFocusTracker() {
283 throw Error('expected an instance of PointerFocusTracker to be provided');
284}
285/**
286 * Throws an exception when a reference to the parent menu is not provided.
287 * @docs-private
288 */
289function throwMissingMenuReference() {
290 throw Error('expected a reference to the parent menu');
291}
292
293/** Injection token used for an implementation of MenuAim. */
294const MENU_AIM = new InjectionToken('cdk-menu-aim');
295/** Capture every nth mouse move event. */
296const MOUSE_MOVE_SAMPLE_FREQUENCY = 3;
297/** The number of mouse move events to track. */
298const NUM_POINTS = 5;
299/**
300 * How long to wait before closing a sibling menu if a user stops short of the submenu they were
301 * predicted to go into.
302 */
303const CLOSE_DELAY = 300;
304/** Calculate the slope between point a and b. */
305function getSlope(a, b) {
306 return (b.y - a.y) / (b.x - a.x);
307}
308/** Calculate the y intercept for the given point and slope. */
309function getYIntercept(point, slope) {
310 return point.y - slope * point.x;
311}
312/**
313 * Whether the given mouse trajectory line defined by the slope and y intercept falls within the
314 * submenu as defined by `submenuPoints`
315 * @param submenuPoints the submenu DOMRect points.
316 * @param m the slope of the trajectory line.
317 * @param b the y intercept of the trajectory line.
318 * @return true if any point on the line falls within the submenu.
319 */
320function isWithinSubmenu(submenuPoints, m, b) {
321 const { left, right, top, bottom } = submenuPoints;
322 // Check for intersection with each edge of the submenu (left, right, top, bottom)
323 // by fixing one coordinate to that edge's coordinate (either x or y) and checking if the
324 // other coordinate is within bounds.
325 return ((m * left + b >= top && m * left + b <= bottom) ||
326 (m * right + b >= top && m * right + b <= bottom) ||
327 ((top - b) / m >= left && (top - b) / m <= right) ||
328 ((bottom - b) / m >= left && (bottom - b) / m <= right));
329}
330/**
331 * TargetMenuAim predicts if a user is moving into a submenu. It calculates the
332 * trajectory of the user's mouse movement in the current menu to determine if the
333 * mouse is moving towards an open submenu.
334 *
335 * The determination is made by calculating the slope of the users last NUM_POINTS moves where each
336 * pair of points determines if the trajectory line points into the submenu. It uses consensus
337 * approach by checking if at least NUM_POINTS / 2 pairs determine that the user is moving towards
338 * to submenu.
339 */
340class TargetMenuAim {
341 constructor() {
342 /** The Angular zone. */
343 this._ngZone = inject(NgZone);
344 /** The last NUM_POINTS mouse move events. */
345 this._points = [];
346 /** Emits when this service is destroyed. */
347 this._destroyed = new Subject();
348 }
349 ngOnDestroy() {
350 this._destroyed.next();
351 this._destroyed.complete();
352 }
353 /**
354 * Set the Menu and its PointerFocusTracker.
355 * @param menu The menu that this menu aim service controls.
356 * @param pointerTracker The `PointerFocusTracker` for the given menu.
357 */
358 initialize(menu, pointerTracker) {
359 this._menu = menu;
360 this._pointerTracker = pointerTracker;
361 this._subscribeToMouseMoves();
362 }
363 /**
364 * Calls the `doToggle` callback when it is deemed that the user is not moving towards
365 * the submenu.
366 * @param doToggle the function called when the user is not moving towards the submenu.
367 */
368 toggle(doToggle) {
369 // If the menu is horizontal the sub-menus open below and there is no risk of premature
370 // closing of any sub-menus therefore we automatically resolve the callback.
371 if (this._menu.orientation === 'horizontal') {
372 doToggle();
373 }
374 this._checkConfigured();
375 const siblingItemIsWaiting = !!this._timeoutId;
376 const hasPoints = this._points.length > 1;
377 if (hasPoints && !siblingItemIsWaiting) {
378 if (this._isMovingToSubmenu()) {
379 this._startTimeout(doToggle);
380 }
381 else {
382 doToggle();
383 }
384 }
385 else if (!siblingItemIsWaiting) {
386 doToggle();
387 }
388 }
389 /**
390 * Start the delayed toggle handler if one isn't running already.
391 *
392 * The delayed toggle handler executes the `doToggle` callback after some period of time iff the
393 * users mouse is on an item in the current menu.
394 *
395 * @param doToggle the function called when the user is not moving towards the submenu.
396 */
397 _startTimeout(doToggle) {
398 // If the users mouse is moving towards a submenu we don't want to immediately resolve.
399 // Wait for some period of time before determining if the previous menu should close in
400 // cases where the user may have moved towards the submenu but stopped on a sibling menu
401 // item intentionally.
402 const timeoutId = setTimeout(() => {
403 // Resolve if the user is currently moused over some element in the root menu
404 if (this._pointerTracker.activeElement && timeoutId === this._timeoutId) {
405 doToggle();
406 }
407 this._timeoutId = null;
408 }, CLOSE_DELAY);
409 this._timeoutId = timeoutId;
410 }
411 /** Whether the user is heading towards the open submenu. */
412 _isMovingToSubmenu() {
413 const submenuPoints = this._getSubmenuBounds();
414 if (!submenuPoints) {
415 return false;
416 }
417 let numMoving = 0;
418 const currPoint = this._points[this._points.length - 1];
419 // start from the second last point and calculate the slope between each point and the last
420 // point.
421 for (let i = this._points.length - 2; i >= 0; i--) {
422 const previous = this._points[i];
423 const slope = getSlope(currPoint, previous);
424 if (isWithinSubmenu(submenuPoints, slope, getYIntercept(currPoint, slope))) {
425 numMoving++;
426 }
427 }
428 return numMoving >= Math.floor(NUM_POINTS / 2);
429 }
430 /** Get the bounding DOMRect for the open submenu. */
431 _getSubmenuBounds() {
432 return this._pointerTracker?.previousElement?.getMenu()?.nativeElement.getBoundingClientRect();
433 }
434 /**
435 * Check if a reference to the PointerFocusTracker and menu element is provided.
436 * @throws an error if neither reference is provided.
437 */
438 _checkConfigured() {
439 if (typeof ngDevMode === 'undefined' || ngDevMode) {
440 if (!this._pointerTracker) {
441 throwMissingPointerFocusTracker();
442 }
443 if (!this._menu) {
444 throwMissingMenuReference();
445 }
446 }
447 }
448 /** Subscribe to the root menus mouse move events and update the tracked mouse points. */
449 _subscribeToMouseMoves() {
450 this._ngZone.runOutsideAngular(() => {
451 fromEvent(this._menu.nativeElement, 'mousemove')
452 .pipe(filter((_, index) => index % MOUSE_MOVE_SAMPLE_FREQUENCY === 0), takeUntil(this._destroyed))
453 .subscribe((event) => {
454 this._points.push({ x: event.clientX, y: event.clientY });
455 if (this._points.length > NUM_POINTS) {
456 this._points.shift();
457 }
458 });
459 });
460 }
461 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TargetMenuAim, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
462 static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TargetMenuAim }); }
463}
464i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TargetMenuAim, decorators: [{
465 type: Injectable
466 }] });
467/**
468 * CdkTargetMenuAim is a provider for the TargetMenuAim service. It can be added to an
469 * element with either the `cdkMenu` or `cdkMenuBar` directive and child menu items.
470 */
471class CdkTargetMenuAim {
472 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkTargetMenuAim, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
473 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkTargetMenuAim, isStandalone: true, selector: "[cdkTargetMenuAim]", providers: [{ provide: MENU_AIM, useClass: TargetMenuAim }], exportAs: ["cdkTargetMenuAim"], ngImport: i0 }); }
474}
475i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkTargetMenuAim, decorators: [{
476 type: Directive,
477 args: [{
478 selector: '[cdkTargetMenuAim]',
479 exportAs: 'cdkTargetMenuAim',
480 standalone: true,
481 providers: [{ provide: MENU_AIM, useClass: TargetMenuAim }],
482 }]
483 }] });
484
485/**
486 * A directive that turns its host element into a trigger for a popup menu.
487 * It can be combined with cdkMenuItem to create sub-menus. If the element is in a top level
488 * MenuBar it will open the menu on click, or if a sibling is already opened it will open on hover.
489 * If it is inside of a Menu it will open the attached Submenu on hover regardless of its sibling
490 * state.
491 */
492class CdkMenuTrigger extends CdkMenuTriggerBase {
493 constructor() {
494 super();
495 this._elementRef = inject(ElementRef);
496 this._overlay = inject(Overlay);
497 this._ngZone = inject(NgZone);
498 this._directionality = inject(Directionality, { optional: true });
499 this._inputModalityDetector = inject(InputModalityDetector);
500 /** The parent menu this trigger belongs to. */
501 this._parentMenu = inject(CDK_MENU, { optional: true });
502 /** The menu aim service used by this menu. */
503 this._menuAim = inject(MENU_AIM, { optional: true });
504 this._setRole();
505 this._registerCloseHandler();
506 this._subscribeToMenuStackClosed();
507 this._subscribeToMouseEnter();
508 this._subscribeToMenuStackHasFocus();
509 this._setType();
510 }
511 /** Toggle the attached menu. */
512 toggle() {
513 this.isOpen() ? this.close() : this.open();
514 }
515 /** Open the attached menu. */
516 open() {
517 if (!this.isOpen() && this.menuTemplateRef != null) {
518 this.opened.next();
519 this.overlayRef = this.overlayRef || this._overlay.create(this._getOverlayConfig());
520 this.overlayRef.attach(this.getMenuContentPortal());
521 this._subscribeToOutsideClicks();
522 }
523 }
524 /** Close the opened menu. */
525 close() {
526 if (this.isOpen()) {
527 this.closed.next();
528 this.overlayRef.detach();
529 }
530 this._closeSiblingTriggers();
531 }
532 /**
533 * Get a reference to the rendered Menu if the Menu is open and rendered in the DOM.
534 */
535 getMenu() {
536 return this.childMenu;
537 }
538 /**
539 * Handles keyboard events for the menu item.
540 * @param event The keyboard event to handle
541 */
542 _toggleOnKeydown(event) {
543 const isParentVertical = this._parentMenu?.orientation === 'vertical';
544 switch (event.keyCode) {
545 case SPACE:
546 case ENTER:
547 if (!hasModifierKey(event)) {
548 this.toggle();
549 this.childMenu?.focusFirstItem('keyboard');
550 }
551 break;
552 case RIGHT_ARROW:
553 if (!hasModifierKey(event)) {
554 if (this._parentMenu && isParentVertical && this._directionality?.value !== 'rtl') {
555 event.preventDefault();
556 this.open();
557 this.childMenu?.focusFirstItem('keyboard');
558 }
559 }
560 break;
561 case LEFT_ARROW:
562 if (!hasModifierKey(event)) {
563 if (this._parentMenu && isParentVertical && this._directionality?.value === 'rtl') {
564 event.preventDefault();
565 this.open();
566 this.childMenu?.focusFirstItem('keyboard');
567 }
568 }
569 break;
570 case DOWN_ARROW:
571 case UP_ARROW:
572 if (!hasModifierKey(event)) {
573 if (!isParentVertical) {
574 event.preventDefault();
575 this.open();
576 event.keyCode === DOWN_ARROW
577 ? this.childMenu?.focusFirstItem('keyboard')
578 : this.childMenu?.focusLastItem('keyboard');
579 }
580 }
581 break;
582 }
583 }
584 /** Handles clicks on the menu trigger. */
585 _handleClick() {
586 // Don't handle clicks originating from the keyboard since we
587 // already do the same on `keydown` events for enter and space.
588 if (this._inputModalityDetector.mostRecentModality !== 'keyboard') {
589 this.toggle();
590 this.childMenu?.focusFirstItem('mouse');
591 }
592 }
593 /**
594 * Sets whether the trigger's menu stack has focus.
595 * @param hasFocus Whether the menu stack has focus.
596 */
597 _setHasFocus(hasFocus) {
598 if (!this._parentMenu) {
599 this.menuStack.setHasFocus(hasFocus);
600 }
601 }
602 /**
603 * Subscribe to the mouseenter events and close any sibling menu items if this element is moused
604 * into.
605 */
606 _subscribeToMouseEnter() {
607 this._ngZone.runOutsideAngular(() => {
608 fromEvent(this._elementRef.nativeElement, 'mouseenter')
609 .pipe(filter(() => !this.menuStack.isEmpty() && !this.isOpen()), takeUntil(this.destroyed))
610 .subscribe(() => {
611 // Closes any sibling menu items and opens the menu associated with this trigger.
612 const toggleMenus = () => this._ngZone.run(() => {
613 this._closeSiblingTriggers();
614 this.open();
615 });
616 if (this._menuAim) {
617 this._menuAim.toggle(toggleMenus);
618 }
619 else {
620 toggleMenus();
621 }
622 });
623 });
624 }
625 /** Close out any sibling menu trigger menus. */
626 _closeSiblingTriggers() {
627 if (this._parentMenu) {
628 // If nothing was removed from the stack and the last element is not the parent item
629 // that means that the parent menu is a menu bar since we don't put the menu bar on the
630 // stack
631 const isParentMenuBar = !this.menuStack.closeSubMenuOf(this._parentMenu) &&
632 this.menuStack.peek() !== this._parentMenu;
633 if (isParentMenuBar) {
634 this.menuStack.closeAll();
635 }
636 }
637 else {
638 this.menuStack.closeAll();
639 }
640 }
641 /** Get the configuration object used to create the overlay. */
642 _getOverlayConfig() {
643 return new OverlayConfig({
644 positionStrategy: this._getOverlayPositionStrategy(),
645 scrollStrategy: this._overlay.scrollStrategies.reposition(),
646 direction: this._directionality || undefined,
647 });
648 }
649 /** Build the position strategy for the overlay which specifies where to place the menu. */
650 _getOverlayPositionStrategy() {
651 return this._overlay
652 .position()
653 .flexibleConnectedTo(this._elementRef)
654 .withLockedPosition()
655 .withGrowAfterOpen()
656 .withPositions(this._getOverlayPositions());
657 }
658 /** Get the preferred positions for the opened menu relative to the menu item. */
659 _getOverlayPositions() {
660 return (this.menuPosition ??
661 (!this._parentMenu || this._parentMenu.orientation === 'horizontal'
662 ? STANDARD_DROPDOWN_BELOW_POSITIONS
663 : STANDARD_DROPDOWN_ADJACENT_POSITIONS));
664 }
665 /**
666 * Subscribe to the MenuStack close events if this is a standalone trigger and close out the menu
667 * this triggers when requested.
668 */
669 _registerCloseHandler() {
670 if (!this._parentMenu) {
671 this.menuStack.closed.pipe(takeUntil(this.destroyed)).subscribe(({ item }) => {
672 if (item === this.childMenu) {
673 this.close();
674 }
675 });
676 }
677 }
678 /**
679 * Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
680 * click occurs outside the menus.
681 */
682 _subscribeToOutsideClicks() {
683 if (this.overlayRef) {
684 this.overlayRef
685 .outsidePointerEvents()
686 .pipe(takeUntil(this.stopOutsideClicksListener))
687 .subscribe(event => {
688 const target = _getEventTarget(event);
689 const element = this._elementRef.nativeElement;
690 if (target !== element && !element.contains(target)) {
691 if (!this.isElementInsideMenuStack(target)) {
692 this.menuStack.closeAll();
693 }
694 else {
695 this._closeSiblingTriggers();
696 }
697 }
698 });
699 }
700 }
701 /** Subscribe to the MenuStack hasFocus events. */
702 _subscribeToMenuStackHasFocus() {
703 if (!this._parentMenu) {
704 this.menuStack.hasFocus.pipe(takeUntil(this.destroyed)).subscribe(hasFocus => {
705 if (!hasFocus) {
706 this.menuStack.closeAll();
707 }
708 });
709 }
710 }
711 /** Subscribe to the MenuStack closed events. */
712 _subscribeToMenuStackClosed() {
713 if (!this._parentMenu) {
714 this.menuStack.closed.subscribe(({ focusParentTrigger }) => {
715 if (focusParentTrigger && !this.menuStack.length()) {
716 this._elementRef.nativeElement.focus();
717 }
718 });
719 }
720 }
721 /** Sets the role attribute for this trigger if needed. */
722 _setRole() {
723 // If this trigger is part of another menu, the cdkMenuItem directive will handle setting the
724 // role, otherwise this is a standalone trigger, and we should ensure it has role="button".
725 if (!this._parentMenu) {
726 this._elementRef.nativeElement.setAttribute('role', 'button');
727 }
728 }
729 /** Sets thte `type` attribute of the trigger. */
730 _setType() {
731 const element = this._elementRef.nativeElement;
732 if (element.nodeName === 'BUTTON' && !element.getAttribute('type')) {
733 // Prevents form submissions.
734 element.setAttribute('type', 'button');
735 }
736 }
737 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
738 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.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: [
739 { provide: MENU_TRIGGER, useExisting: CdkMenuTrigger },
740 PARENT_OR_NEW_MENU_STACK_PROVIDER,
741 ], exportAs: ["cdkMenuTriggerFor"], usesInheritance: true, ngImport: i0 }); }
742}
743i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuTrigger, decorators: [{
744 type: Directive,
745 args: [{
746 selector: '[cdkMenuTriggerFor]',
747 exportAs: 'cdkMenuTriggerFor',
748 standalone: true,
749 host: {
750 'class': 'cdk-menu-trigger',
751 '[attr.aria-haspopup]': 'menuTemplateRef ? "menu" : null',
752 '[attr.aria-expanded]': 'menuTemplateRef == null ? null : isOpen()',
753 '(focusin)': '_setHasFocus(true)',
754 '(focusout)': '_setHasFocus(false)',
755 '(keydown)': '_toggleOnKeydown($event)',
756 '(click)': '_handleClick()',
757 },
758 inputs: [
759 'menuTemplateRef: cdkMenuTriggerFor',
760 'menuPosition: cdkMenuPosition',
761 'menuData: cdkMenuTriggerData',
762 ],
763 outputs: ['opened: cdkMenuOpened', 'closed: cdkMenuClosed'],
764 providers: [
765 { provide: MENU_TRIGGER, useExisting: CdkMenuTrigger },
766 PARENT_OR_NEW_MENU_STACK_PROVIDER,
767 ],
768 }]
769 }], ctorParameters: function () { return []; } });
770
771/**
772 * Directive which provides the ability for an element to be focused and navigated to using the
773 * keyboard when residing in a CdkMenu, CdkMenuBar, or CdkMenuGroup. It performs user defined
774 * behavior when clicked.
775 */
776class CdkMenuItem {
777 /** Whether the CdkMenuItem is disabled - defaults to false */
778 get disabled() {
779 return this._disabled;
780 }
781 set disabled(value) {
782 this._disabled = coerceBooleanProperty(value);
783 }
784 /** Whether the menu item opens a menu. */
785 get hasMenu() {
786 return this._menuTrigger?.menuTemplateRef != null;
787 }
788 constructor() {
789 this._dir = inject(Directionality, { optional: true });
790 this._inputModalityDetector = inject(InputModalityDetector);
791 this._elementRef = inject(ElementRef);
792 this._ngZone = inject(NgZone);
793 /** The menu aim service used by this menu. */
794 this._menuAim = inject(MENU_AIM, { optional: true });
795 /** The stack of menus this menu belongs to. */
796 this._menuStack = inject(MENU_STACK);
797 /** The parent menu in which this menuitem resides. */
798 this._parentMenu = inject(CDK_MENU, { optional: true });
799 /** Reference to the CdkMenuItemTrigger directive if one is added to the same element */
800 this._menuTrigger = inject(CdkMenuTrigger, { optional: true, self: true });
801 this._disabled = false;
802 /**
803 * If this MenuItem is a regular MenuItem, outputs when it is triggered by a keyboard or mouse
804 * event.
805 */
806 this.triggered = new EventEmitter();
807 /**
808 * The tabindex for this menu item managed internally and used for implementing roving a
809 * tab index.
810 */
811 this._tabindex = -1;
812 /** Whether the item should close the menu if triggered by the spacebar. */
813 this.closeOnSpacebarTrigger = true;
814 /** Emits when the menu item is destroyed. */
815 this.destroyed = new Subject();
816 this._setupMouseEnter();
817 this._setType();
818 if (this._isStandaloneItem()) {
819 this._tabindex = 0;
820 }
821 }
822 ngOnDestroy() {
823 this.destroyed.next();
824 this.destroyed.complete();
825 }
826 /** Place focus on the element. */
827 focus() {
828 this._elementRef.nativeElement.focus();
829 }
830 /**
831 * If the menu item is not disabled and the element does not have a menu trigger attached, emit
832 * on the cdkMenuItemTriggered emitter and close all open menus.
833 * @param options Options the configure how the item is triggered
834 * - keepOpen: specifies that the menu should be kept open after triggering the item.
835 */
836 trigger(options) {
837 const { keepOpen } = { ...options };
838 if (!this.disabled && !this.hasMenu) {
839 this.triggered.next();
840 if (!keepOpen) {
841 this._menuStack.closeAll({ focusParentTrigger: true });
842 }
843 }
844 }
845 /** Return true if this MenuItem has an attached menu and it is open. */
846 isMenuOpen() {
847 return !!this._menuTrigger?.isOpen();
848 }
849 /**
850 * Get a reference to the rendered Menu if the Menu is open and it is visible in the DOM.
851 * @return the menu if it is open, otherwise undefined.
852 */
853 getMenu() {
854 return this._menuTrigger?.getMenu();
855 }
856 /** Get the CdkMenuTrigger associated with this element. */
857 getMenuTrigger() {
858 return this._menuTrigger;
859 }
860 /** Get the label for this element which is required by the FocusableOption interface. */
861 getLabel() {
862 return this.typeaheadLabel || this._elementRef.nativeElement.textContent?.trim() || '';
863 }
864 /** Reset the tabindex to -1. */
865 _resetTabIndex() {
866 if (!this._isStandaloneItem()) {
867 this._tabindex = -1;
868 }
869 }
870 /**
871 * Set the tab index to 0 if not disabled and it's a focus event, or a mouse enter if this element
872 * is not in a menu bar.
873 */
874 _setTabIndex(event) {
875 if (this.disabled) {
876 return;
877 }
878 // don't set the tabindex if there are no open sibling or parent menus
879 if (!event || !this._menuStack.isEmpty()) {
880 this._tabindex = 0;
881 }
882 }
883 /**
884 * Handles keyboard events for the menu item, specifically either triggering the user defined
885 * callback or opening/closing the current menu based on whether the left or right arrow key was
886 * pressed.
887 * @param event the keyboard event to handle
888 */
889 _onKeydown(event) {
890 switch (event.keyCode) {
891 case SPACE:
892 case ENTER:
893 if (!hasModifierKey(event)) {
894 this.trigger({ keepOpen: event.keyCode === SPACE && !this.closeOnSpacebarTrigger });
895 }
896 break;
897 case RIGHT_ARROW:
898 if (!hasModifierKey(event)) {
899 if (this._parentMenu && this._isParentVertical()) {
900 if (this._dir?.value !== 'rtl') {
901 this._forwardArrowPressed(event);
902 }
903 else {
904 this._backArrowPressed(event);
905 }
906 }
907 }
908 break;
909 case LEFT_ARROW:
910 if (!hasModifierKey(event)) {
911 if (this._parentMenu && this._isParentVertical()) {
912 if (this._dir?.value !== 'rtl') {
913 this._backArrowPressed(event);
914 }
915 else {
916 this._forwardArrowPressed(event);
917 }
918 }
919 }
920 break;
921 }
922 }
923 /** Handles clicks on the menu item. */
924 _handleClick() {
925 // Don't handle clicks originating from the keyboard since we
926 // already do the same on `keydown` events for enter and space.
927 if (this._inputModalityDetector.mostRecentModality !== 'keyboard') {
928 this.trigger();
929 }
930 }
931 /** Whether this menu item is standalone or within a menu or menu bar. */
932 _isStandaloneItem() {
933 return !this._parentMenu;
934 }
935 /**
936 * Handles the user pressing the back arrow key.
937 * @param event The keyboard event.
938 */
939 _backArrowPressed(event) {
940 const parentMenu = this._parentMenu;
941 if (this._menuStack.hasInlineMenu() || this._menuStack.length() > 1) {
942 event.preventDefault();
943 this._menuStack.close(parentMenu, {
944 focusNextOnEmpty: this._menuStack.inlineMenuOrientation() === 'horizontal'
945 ? 1 /* FocusNext.previousItem */
946 : 2 /* FocusNext.currentItem */,
947 focusParentTrigger: true,
948 });
949 }
950 }
951 /**
952 * Handles the user pressing the forward arrow key.
953 * @param event The keyboard event.
954 */
955 _forwardArrowPressed(event) {
956 if (!this.hasMenu && this._menuStack.inlineMenuOrientation() === 'horizontal') {
957 event.preventDefault();
958 this._menuStack.closeAll({
959 focusNextOnEmpty: 0 /* FocusNext.nextItem */,
960 focusParentTrigger: true,
961 });
962 }
963 }
964 /**
965 * Subscribe to the mouseenter events and close any sibling menu items if this element is moused
966 * into.
967 */
968 _setupMouseEnter() {
969 if (!this._isStandaloneItem()) {
970 const closeOpenSiblings = () => this._ngZone.run(() => this._menuStack.closeSubMenuOf(this._parentMenu));
971 this._ngZone.runOutsideAngular(() => fromEvent(this._elementRef.nativeElement, 'mouseenter')
972 .pipe(filter(() => !this._menuStack.isEmpty() && !this.hasMenu), takeUntil(this.destroyed))
973 .subscribe(() => {
974 if (this._menuAim) {
975 this._menuAim.toggle(closeOpenSiblings);
976 }
977 else {
978 closeOpenSiblings();
979 }
980 }));
981 }
982 }
983 /**
984 * Return true if the enclosing parent menu is configured in a horizontal orientation, false
985 * otherwise or if no parent.
986 */
987 _isParentVertical() {
988 return this._parentMenu?.orientation === 'vertical';
989 }
990 /** Sets the `type` attribute of the menu item. */
991 _setType() {
992 const element = this._elementRef.nativeElement;
993 if (element.nodeName === 'BUTTON' && !element.getAttribute('type')) {
994 // Prevent form submissions.
995 element.setAttribute('type', 'button');
996 }
997 }
998 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
999 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuItem, isStandalone: true, selector: "[cdkMenuItem]", inputs: { disabled: ["cdkMenuItemDisabled", "disabled"], typeaheadLabel: ["cdkMenuitemTypeaheadLabel", "typeaheadLabel"] }, outputs: { triggered: "cdkMenuItemTriggered" }, host: { attributes: { "role": "menuitem" }, listeners: { "blur": "_resetTabIndex()", "focus": "_setTabIndex()", "click": "_handleClick()", "keydown": "_onKeydown($event)" }, properties: { "tabindex": "_tabindex", "attr.aria-disabled": "disabled || null" }, classAttribute: "cdk-menu-item" }, exportAs: ["cdkMenuItem"], ngImport: i0 }); }
1000}
1001i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItem, decorators: [{
1002 type: Directive,
1003 args: [{
1004 selector: '[cdkMenuItem]',
1005 exportAs: 'cdkMenuItem',
1006 standalone: true,
1007 host: {
1008 'role': 'menuitem',
1009 'class': 'cdk-menu-item',
1010 '[tabindex]': '_tabindex',
1011 '[attr.aria-disabled]': 'disabled || null',
1012 '(blur)': '_resetTabIndex()',
1013 '(focus)': '_setTabIndex()',
1014 '(click)': '_handleClick()',
1015 '(keydown)': '_onKeydown($event)',
1016 },
1017 }]
1018 }], ctorParameters: function () { return []; }, propDecorators: { disabled: [{
1019 type: Input,
1020 args: ['cdkMenuItemDisabled']
1021 }], typeaheadLabel: [{
1022 type: Input,
1023 args: ['cdkMenuitemTypeaheadLabel']
1024 }], triggered: [{
1025 type: Output,
1026 args: ['cdkMenuItemTriggered']
1027 }] } });
1028
1029/**
1030 * PointerFocusTracker keeps track of the currently active item under mouse focus. It also has
1031 * observables which emit when the users mouse enters and leaves a tracked element.
1032 */
1033class PointerFocusTracker {
1034 constructor(
1035 /** The list of items being tracked. */
1036 _items) {
1037 this._items = _items;
1038 /** Emits when an element is moused into. */
1039 this.entered = this._getItemPointerEntries();
1040 /** Emits when an element is moused out. */
1041 this.exited = this._getItemPointerExits();
1042 /** Emits when this is destroyed. */
1043 this._destroyed = new Subject();
1044 this.entered.subscribe(element => (this.activeElement = element));
1045 this.exited.subscribe(() => {
1046 this.previousElement = this.activeElement;
1047 this.activeElement = undefined;
1048 });
1049 }
1050 /** Stop the managers listeners. */
1051 destroy() {
1052 this._destroyed.next();
1053 this._destroyed.complete();
1054 }
1055 /**
1056 * Gets a stream of pointer (mouse) entries into the given items.
1057 * This should typically run outside the Angular zone.
1058 */
1059 _getItemPointerEntries() {
1060 return defer(() => this._items.changes.pipe(startWith(this._items), mergeMap((list) => list.map(element => fromEvent(element._elementRef.nativeElement, 'mouseenter').pipe(mapTo(element), takeUntil(this._items.changes)))), mergeAll()));
1061 }
1062 /**
1063 * Gets a stream of pointer (mouse) exits out of the given items.
1064 * This should typically run outside the Angular zone.
1065 */
1066 _getItemPointerExits() {
1067 return defer(() => this._items.changes.pipe(startWith(this._items), mergeMap((list) => list.map(element => fromEvent(element._elementRef.nativeElement, 'mouseout').pipe(mapTo(element), takeUntil(this._items.changes)))), mergeAll()));
1068 }
1069}
1070
1071/** Counter used to create unique IDs for menus. */
1072let nextId$1 = 0;
1073/**
1074 * Abstract directive that implements shared logic common to all menus.
1075 * This class can be extended to create custom menu types.
1076 */
1077class CdkMenuBase extends CdkMenuGroup {
1078 constructor() {
1079 super(...arguments);
1080 /** The menu's native DOM host element. */
1081 this.nativeElement = inject(ElementRef).nativeElement;
1082 /** The Angular zone. */
1083 this.ngZone = inject(NgZone);
1084 /** The stack of menus this menu belongs to. */
1085 this.menuStack = inject(MENU_STACK);
1086 /** The menu aim service used by this menu. */
1087 this.menuAim = inject(MENU_AIM, { optional: true, self: true });
1088 /** The directionality (text direction) of the current page. */
1089 this.dir = inject(Directionality, { optional: true });
1090 /** The id of the menu's host element. */
1091 this.id = `cdk-menu-${nextId$1++}`;
1092 /** The direction items in the menu flow. */
1093 this.orientation = 'vertical';
1094 /**
1095 * Whether the menu is displayed inline (i.e. always present vs a conditional popup that the
1096 * user triggers with a trigger element).
1097 */
1098 this.isInline = false;
1099 /** Emits when the MenuBar is destroyed. */
1100 this.destroyed = new Subject();
1101 /** Whether this menu's menu stack has focus. */
1102 this._menuStackHasFocus = false;
1103 }
1104 ngAfterContentInit() {
1105 if (!this.isInline) {
1106 this.menuStack.push(this);
1107 }
1108 this._setKeyManager();
1109 this._subscribeToMenuStackHasFocus();
1110 this._subscribeToMenuOpen();
1111 this._subscribeToMenuStackClosed();
1112 this._setUpPointerTracker();
1113 }
1114 ngOnDestroy() {
1115 this.keyManager?.destroy();
1116 this.destroyed.next();
1117 this.destroyed.complete();
1118 this.pointerTracker?.destroy();
1119 }
1120 /**
1121 * Place focus on the first MenuItem in the menu and set the focus origin.
1122 * @param focusOrigin The origin input mode of the focus event.
1123 */
1124 focusFirstItem(focusOrigin = 'program') {
1125 this.keyManager.setFocusOrigin(focusOrigin);
1126 this.keyManager.setFirstItemActive();
1127 }
1128 /**
1129 * Place focus on the last MenuItem in the menu and set the focus origin.
1130 * @param focusOrigin The origin input mode of the focus event.
1131 */
1132 focusLastItem(focusOrigin = 'program') {
1133 this.keyManager.setFocusOrigin(focusOrigin);
1134 this.keyManager.setLastItemActive();
1135 }
1136 /** Gets the tabindex for this menu. */
1137 _getTabIndex() {
1138 const tabindexIfInline = this._menuStackHasFocus ? -1 : 0;
1139 return this.isInline ? tabindexIfInline : null;
1140 }
1141 /**
1142 * Close the open menu if the current active item opened the requested MenuStackItem.
1143 * @param menu The menu requested to be closed.
1144 * @param options Options to configure the behavior on close.
1145 * - `focusParentTrigger` Whether to focus the parent trigger after closing the menu.
1146 */
1147 closeOpenMenu(menu, options) {
1148 const { focusParentTrigger } = { ...options };
1149 const keyManager = this.keyManager;
1150 const trigger = this.triggerItem;
1151 if (menu === trigger?.getMenuTrigger()?.getMenu()) {
1152 trigger?.getMenuTrigger()?.close();
1153 // If the user has moused over a sibling item we want to focus the element under mouse focus
1154 // not the trigger which previously opened the now closed menu.
1155 if (focusParentTrigger) {
1156 if (trigger) {
1157 keyManager.setActiveItem(trigger);
1158 }
1159 else {
1160 keyManager.setFirstItemActive();
1161 }
1162 }
1163 }
1164 }
1165 /** Setup the FocusKeyManager with the correct orientation for the menu. */
1166 _setKeyManager() {
1167 this.keyManager = new FocusKeyManager(this.items).withWrap().withTypeAhead().withHomeAndEnd();
1168 if (this.orientation === 'horizontal') {
1169 this.keyManager.withHorizontalOrientation(this.dir?.value || 'ltr');
1170 }
1171 else {
1172 this.keyManager.withVerticalOrientation();
1173 }
1174 }
1175 /**
1176 * Subscribe to the menu trigger's open events in order to track the trigger which opened the menu
1177 * and stop tracking it when the menu is closed.
1178 */
1179 _subscribeToMenuOpen() {
1180 const exitCondition = merge(this.items.changes, this.destroyed);
1181 this.items.changes
1182 .pipe(startWith(this.items), mergeMap((list) => list
1183 .filter(item => item.hasMenu)
1184 .map(item => item.getMenuTrigger().opened.pipe(mapTo(item), takeUntil(exitCondition)))), mergeAll(), switchMap((item) => {
1185 this.triggerItem = item;
1186 return item.getMenuTrigger().closed;
1187 }), takeUntil(this.destroyed))
1188 .subscribe(() => (this.triggerItem = undefined));
1189 }
1190 /** Subscribe to the MenuStack close events. */
1191 _subscribeToMenuStackClosed() {
1192 this.menuStack.closed
1193 .pipe(takeUntil(this.destroyed))
1194 .subscribe(({ item, focusParentTrigger }) => this.closeOpenMenu(item, { focusParentTrigger }));
1195 }
1196 /** Subscribe to the MenuStack hasFocus events. */
1197 _subscribeToMenuStackHasFocus() {
1198 if (this.isInline) {
1199 this.menuStack.hasFocus.pipe(takeUntil(this.destroyed)).subscribe(hasFocus => {
1200 this._menuStackHasFocus = hasFocus;
1201 });
1202 }
1203 }
1204 /**
1205 * Set the PointerFocusTracker and ensure that when mouse focus changes the key manager is updated
1206 * with the latest menu item under mouse focus.
1207 */
1208 _setUpPointerTracker() {
1209 if (this.menuAim) {
1210 this.ngZone.runOutsideAngular(() => {
1211 this.pointerTracker = new PointerFocusTracker(this.items);
1212 });
1213 this.menuAim.initialize(this, this.pointerTracker);
1214 }
1215 }
1216 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuBase, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1217 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuBase, inputs: { id: "id" }, host: { attributes: { "role": "menu" }, listeners: { "focus": "focusFirstItem()", "focusin": "menuStack.setHasFocus(true)", "focusout": "menuStack.setHasFocus(false)" }, properties: { "tabindex": "_getTabIndex()", "id": "id", "attr.aria-orientation": "orientation", "attr.data-cdk-menu-stack-id": "menuStack.id" } }, queries: [{ propertyName: "items", predicate: CdkMenuItem, descendants: true }], usesInheritance: true, ngImport: i0 }); }
1218}
1219i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuBase, decorators: [{
1220 type: Directive,
1221 args: [{
1222 host: {
1223 'role': 'menu',
1224 'class': '',
1225 '[tabindex]': '_getTabIndex()',
1226 '[id]': 'id',
1227 '[attr.aria-orientation]': 'orientation',
1228 '[attr.data-cdk-menu-stack-id]': 'menuStack.id',
1229 '(focus)': 'focusFirstItem()',
1230 '(focusin)': 'menuStack.setHasFocus(true)',
1231 '(focusout)': 'menuStack.setHasFocus(false)',
1232 },
1233 }]
1234 }], propDecorators: { id: [{
1235 type: Input
1236 }], items: [{
1237 type: ContentChildren,
1238 args: [CdkMenuItem, { descendants: true }]
1239 }] } });
1240
1241/**
1242 * Directive which configures the element as a Menu which should contain child elements marked as
1243 * CdkMenuItem or CdkMenuGroup. Sets the appropriate role and aria-attributes for a menu and
1244 * contains accessible keyboard and mouse handling logic.
1245 *
1246 * It also acts as a RadioGroup for elements marked with role `menuitemradio`.
1247 */
1248class CdkMenu extends CdkMenuBase {
1249 constructor() {
1250 super();
1251 this._parentTrigger = inject(MENU_TRIGGER, { optional: true });
1252 /** Event emitted when the menu is closed. */
1253 this.closed = new EventEmitter();
1254 /** The direction items in the menu flow. */
1255 this.orientation = 'vertical';
1256 /** Whether the menu is displayed inline (i.e. always present vs a conditional popup that the user triggers with a trigger element). */
1257 this.isInline = !this._parentTrigger;
1258 this.destroyed.subscribe(this.closed);
1259 this._parentTrigger?.registerChildMenu(this);
1260 }
1261 ngAfterContentInit() {
1262 super.ngAfterContentInit();
1263 this._subscribeToMenuStackEmptied();
1264 }
1265 ngOnDestroy() {
1266 super.ngOnDestroy();
1267 this.closed.complete();
1268 }
1269 /**
1270 * Handle keyboard events for the Menu.
1271 * @param event The keyboard event to be handled.
1272 */
1273 _handleKeyEvent(event) {
1274 const keyManager = this.keyManager;
1275 switch (event.keyCode) {
1276 case LEFT_ARROW:
1277 case RIGHT_ARROW:
1278 if (!hasModifierKey(event)) {
1279 event.preventDefault();
1280 keyManager.setFocusOrigin('keyboard');
1281 keyManager.onKeydown(event);
1282 }
1283 break;
1284 case ESCAPE:
1285 if (!hasModifierKey(event)) {
1286 event.preventDefault();
1287 this.menuStack.close(this, {
1288 focusNextOnEmpty: 2 /* FocusNext.currentItem */,
1289 focusParentTrigger: true,
1290 });
1291 }
1292 break;
1293 case TAB:
1294 if (!hasModifierKey(event, 'altKey', 'metaKey', 'ctrlKey')) {
1295 this.menuStack.closeAll({ focusParentTrigger: true });
1296 }
1297 break;
1298 default:
1299 keyManager.onKeydown(event);
1300 }
1301 }
1302 /**
1303 * Set focus the either the current, previous or next item based on the FocusNext event.
1304 * @param focusNext The element to focus.
1305 */
1306 _toggleMenuFocus(focusNext) {
1307 const keyManager = this.keyManager;
1308 switch (focusNext) {
1309 case 0 /* FocusNext.nextItem */:
1310 keyManager.setFocusOrigin('keyboard');
1311 keyManager.setNextItemActive();
1312 break;
1313 case 1 /* FocusNext.previousItem */:
1314 keyManager.setFocusOrigin('keyboard');
1315 keyManager.setPreviousItemActive();
1316 break;
1317 case 2 /* FocusNext.currentItem */:
1318 if (keyManager.activeItem) {
1319 keyManager.setFocusOrigin('keyboard');
1320 keyManager.setActiveItem(keyManager.activeItem);
1321 }
1322 break;
1323 }
1324 }
1325 /** Subscribe to the MenuStack emptied events. */
1326 _subscribeToMenuStackEmptied() {
1327 this.menuStack.emptied
1328 .pipe(takeUntil(this.destroyed))
1329 .subscribe(event => this._toggleMenuFocus(event));
1330 }
1331 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1332 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenu, isStandalone: true, selector: "[cdkMenu]", outputs: { closed: "closed" }, host: { attributes: { "role": "menu" }, listeners: { "keydown": "_handleKeyEvent($event)" }, properties: { "class.cdk-menu-inline": "isInline" }, classAttribute: "cdk-menu" }, providers: [
1333 { provide: CdkMenuGroup, useExisting: CdkMenu },
1334 { provide: CDK_MENU, useExisting: CdkMenu },
1335 PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER('vertical'),
1336 ], exportAs: ["cdkMenu"], usesInheritance: true, ngImport: i0 }); }
1337}
1338i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenu, decorators: [{
1339 type: Directive,
1340 args: [{
1341 selector: '[cdkMenu]',
1342 exportAs: 'cdkMenu',
1343 standalone: true,
1344 host: {
1345 'role': 'menu',
1346 'class': 'cdk-menu',
1347 '[class.cdk-menu-inline]': 'isInline',
1348 '(keydown)': '_handleKeyEvent($event)',
1349 },
1350 providers: [
1351 { provide: CdkMenuGroup, useExisting: CdkMenu },
1352 { provide: CDK_MENU, useExisting: CdkMenu },
1353 PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER('vertical'),
1354 ],
1355 }]
1356 }], ctorParameters: function () { return []; }, propDecorators: { closed: [{
1357 type: Output
1358 }] } });
1359
1360/**
1361 * Directive applied to an element which configures it as a MenuBar by setting the appropriate
1362 * role, aria attributes, and accessible keyboard and mouse handling logic. The component that
1363 * this directive is applied to should contain components marked with CdkMenuItem.
1364 *
1365 */
1366class CdkMenuBar extends CdkMenuBase {
1367 constructor() {
1368 super(...arguments);
1369 /** The direction items in the menu flow. */
1370 this.orientation = 'horizontal';
1371 /** Whether the menu is displayed inline (i.e. always present vs a conditional popup that the user triggers with a trigger element). */
1372 this.isInline = true;
1373 }
1374 ngAfterContentInit() {
1375 super.ngAfterContentInit();
1376 this._subscribeToMenuStackEmptied();
1377 }
1378 /**
1379 * Handle keyboard events for the Menu.
1380 * @param event The keyboard event to be handled.
1381 */
1382 _handleKeyEvent(event) {
1383 const keyManager = this.keyManager;
1384 switch (event.keyCode) {
1385 case UP_ARROW:
1386 case DOWN_ARROW:
1387 case LEFT_ARROW:
1388 case RIGHT_ARROW:
1389 if (!hasModifierKey(event)) {
1390 const horizontalArrows = event.keyCode === LEFT_ARROW || event.keyCode === RIGHT_ARROW;
1391 // For a horizontal menu if the left/right keys were clicked, or a vertical menu if the
1392 // up/down keys were clicked: if the current menu is open, close it then focus and open the
1393 // next menu.
1394 if (horizontalArrows) {
1395 event.preventDefault();
1396 const prevIsOpen = keyManager.activeItem?.isMenuOpen();
1397 keyManager.activeItem?.getMenuTrigger()?.close();
1398 keyManager.setFocusOrigin('keyboard');
1399 keyManager.onKeydown(event);
1400 if (prevIsOpen) {
1401 keyManager.activeItem?.getMenuTrigger()?.open();
1402 }
1403 }
1404 }
1405 break;
1406 case ESCAPE:
1407 if (!hasModifierKey(event)) {
1408 event.preventDefault();
1409 keyManager.activeItem?.getMenuTrigger()?.close();
1410 }
1411 break;
1412 case TAB:
1413 if (!hasModifierKey(event, 'altKey', 'metaKey', 'ctrlKey')) {
1414 keyManager.activeItem?.getMenuTrigger()?.close();
1415 }
1416 break;
1417 default:
1418 keyManager.onKeydown(event);
1419 }
1420 }
1421 /**
1422 * Set focus to either the current, previous or next item based on the FocusNext event, then
1423 * open the previous or next item.
1424 * @param focusNext The element to focus.
1425 */
1426 _toggleOpenMenu(focusNext) {
1427 const keyManager = this.keyManager;
1428 switch (focusNext) {
1429 case 0 /* FocusNext.nextItem */:
1430 keyManager.setFocusOrigin('keyboard');
1431 keyManager.setNextItemActive();
1432 keyManager.activeItem?.getMenuTrigger()?.open();
1433 break;
1434 case 1 /* FocusNext.previousItem */:
1435 keyManager.setFocusOrigin('keyboard');
1436 keyManager.setPreviousItemActive();
1437 keyManager.activeItem?.getMenuTrigger()?.open();
1438 break;
1439 case 2 /* FocusNext.currentItem */:
1440 if (keyManager.activeItem) {
1441 keyManager.setFocusOrigin('keyboard');
1442 keyManager.setActiveItem(keyManager.activeItem);
1443 }
1444 break;
1445 }
1446 }
1447 /** Subscribe to the MenuStack emptied events. */
1448 _subscribeToMenuStackEmptied() {
1449 this.menuStack?.emptied
1450 .pipe(takeUntil(this.destroyed))
1451 .subscribe(event => this._toggleOpenMenu(event));
1452 }
1453 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuBar, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1454 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuBar, isStandalone: true, selector: "[cdkMenuBar]", host: { attributes: { "role": "menubar" }, listeners: { "keydown": "_handleKeyEvent($event)" }, classAttribute: "cdk-menu-bar" }, providers: [
1455 { provide: CdkMenuGroup, useExisting: CdkMenuBar },
1456 { provide: CDK_MENU, useExisting: CdkMenuBar },
1457 { provide: MENU_STACK, useFactory: () => MenuStack.inline('horizontal') },
1458 ], exportAs: ["cdkMenuBar"], usesInheritance: true, ngImport: i0 }); }
1459}
1460i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuBar, decorators: [{
1461 type: Directive,
1462 args: [{
1463 selector: '[cdkMenuBar]',
1464 exportAs: 'cdkMenuBar',
1465 standalone: true,
1466 host: {
1467 'role': 'menubar',
1468 'class': 'cdk-menu-bar',
1469 '(keydown)': '_handleKeyEvent($event)',
1470 },
1471 providers: [
1472 { provide: CdkMenuGroup, useExisting: CdkMenuBar },
1473 { provide: CDK_MENU, useExisting: CdkMenuBar },
1474 { provide: MENU_STACK, useFactory: () => MenuStack.inline('horizontal') },
1475 ],
1476 }]
1477 }] });
1478
1479/** Base class providing checked state for selectable MenuItems. */
1480class CdkMenuItemSelectable extends CdkMenuItem {
1481 constructor() {
1482 super(...arguments);
1483 this._checked = false;
1484 /** Whether the item should close the menu if triggered by the spacebar. */
1485 this.closeOnSpacebarTrigger = false;
1486 }
1487 /** Whether the element is checked */
1488 get checked() {
1489 return this._checked;
1490 }
1491 set checked(value) {
1492 this._checked = coerceBooleanProperty(value);
1493 }
1494 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemSelectable, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1495 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuItemSelectable, inputs: { checked: ["cdkMenuItemChecked", "checked"] }, host: { properties: { "attr.aria-checked": "!!checked", "attr.aria-disabled": "disabled || null" } }, usesInheritance: true, ngImport: i0 }); }
1496}
1497i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemSelectable, decorators: [{
1498 type: Directive,
1499 args: [{
1500 host: {
1501 '[attr.aria-checked]': '!!checked',
1502 '[attr.aria-disabled]': 'disabled || null',
1503 },
1504 }]
1505 }], propDecorators: { checked: [{
1506 type: Input,
1507 args: ['cdkMenuItemChecked']
1508 }] } });
1509
1510/** Counter used to set a unique id and name for a selectable item */
1511let nextId = 0;
1512/**
1513 * A directive providing behavior for the "menuitemradio" ARIA role, which behaves similarly to
1514 * a conventional radio-button. Any sibling `CdkMenuItemRadio` instances within the same `CdkMenu`
1515 * or `CdkMenuGroup` comprise a radio group with unique selection enforced.
1516 */
1517class CdkMenuItemRadio extends CdkMenuItemSelectable {
1518 constructor() {
1519 super();
1520 /** The unique selection dispatcher for this radio's `CdkMenuGroup`. */
1521 this._selectionDispatcher = inject(UniqueSelectionDispatcher);
1522 /** An ID to identify this radio item to the `UniqueSelectionDispatcher`. */
1523 this._id = `${nextId++}`;
1524 this._registerDispatcherListener();
1525 }
1526 ngOnDestroy() {
1527 super.ngOnDestroy();
1528 this._removeDispatcherListener();
1529 }
1530 /**
1531 * Toggles the checked state of the radio-button.
1532 * @param options Options the configure how the item is triggered
1533 * - keepOpen: specifies that the menu should be kept open after triggering the item.
1534 */
1535 trigger(options) {
1536 super.trigger(options);
1537 if (!this.disabled) {
1538 this._selectionDispatcher.notify(this._id, '');
1539 }
1540 }
1541 /** Configure the unique selection dispatcher listener in order to toggle the checked state */
1542 _registerDispatcherListener() {
1543 this._removeDispatcherListener = this._selectionDispatcher.listen((id) => {
1544 this.checked = this._id === id;
1545 });
1546 }
1547 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemRadio, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1548 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuItemRadio, isStandalone: true, selector: "[cdkMenuItemRadio]", host: { attributes: { "role": "menuitemradio" }, properties: { "class.cdk-menu-item-radio": "true" } }, providers: [
1549 { provide: CdkMenuItemSelectable, useExisting: CdkMenuItemRadio },
1550 { provide: CdkMenuItem, useExisting: CdkMenuItemSelectable },
1551 ], exportAs: ["cdkMenuItemRadio"], usesInheritance: true, ngImport: i0 }); }
1552}
1553i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemRadio, decorators: [{
1554 type: Directive,
1555 args: [{
1556 selector: '[cdkMenuItemRadio]',
1557 exportAs: 'cdkMenuItemRadio',
1558 standalone: true,
1559 host: {
1560 'role': 'menuitemradio',
1561 '[class.cdk-menu-item-radio]': 'true',
1562 },
1563 providers: [
1564 { provide: CdkMenuItemSelectable, useExisting: CdkMenuItemRadio },
1565 { provide: CdkMenuItem, useExisting: CdkMenuItemSelectable },
1566 ],
1567 }]
1568 }], ctorParameters: function () { return []; } });
1569
1570/**
1571 * A directive providing behavior for the "menuitemcheckbox" ARIA role, which behaves similarly to a
1572 * conventional checkbox.
1573 */
1574class CdkMenuItemCheckbox extends CdkMenuItemSelectable {
1575 /**
1576 * Toggle the checked state of the checkbox.
1577 * @param options Options the configure how the item is triggered
1578 * - keepOpen: specifies that the menu should be kept open after triggering the item.
1579 */
1580 trigger(options) {
1581 super.trigger(options);
1582 if (!this.disabled) {
1583 this.checked = !this.checked;
1584 }
1585 }
1586 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemCheckbox, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1587 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuItemCheckbox, isStandalone: true, selector: "[cdkMenuItemCheckbox]", host: { attributes: { "role": "menuitemcheckbox" }, properties: { "class.cdk-menu-item-checkbox": "true" } }, providers: [
1588 { provide: CdkMenuItemSelectable, useExisting: CdkMenuItemCheckbox },
1589 { provide: CdkMenuItem, useExisting: CdkMenuItemSelectable },
1590 ], exportAs: ["cdkMenuItemCheckbox"], usesInheritance: true, ngImport: i0 }); }
1591}
1592i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemCheckbox, decorators: [{
1593 type: Directive,
1594 args: [{
1595 selector: '[cdkMenuItemCheckbox]',
1596 exportAs: 'cdkMenuItemCheckbox',
1597 standalone: true,
1598 host: {
1599 'role': 'menuitemcheckbox',
1600 '[class.cdk-menu-item-checkbox]': 'true',
1601 },
1602 providers: [
1603 { provide: CdkMenuItemSelectable, useExisting: CdkMenuItemCheckbox },
1604 { provide: CdkMenuItem, useExisting: CdkMenuItemSelectable },
1605 ],
1606 }]
1607 }] });
1608
1609/** The preferred menu positions for the context menu. */
1610const CONTEXT_MENU_POSITIONS = STANDARD_DROPDOWN_BELOW_POSITIONS.map(position => {
1611 // In cases where the first menu item in the context menu is a trigger the submenu opens on a
1612 // hover event. We offset the context menu 2px by default to prevent this from occurring.
1613 const offsetX = position.overlayX === 'start' ? 2 : -2;
1614 const offsetY = position.overlayY === 'top' ? 2 : -2;
1615 return { ...position, offsetX, offsetY };
1616});
1617/** Tracks the last open context menu trigger across the entire application. */
1618class ContextMenuTracker {
1619 /**
1620 * Close the previous open context menu and set the given one as being open.
1621 * @param trigger The trigger for the currently open Context Menu.
1622 */
1623 update(trigger) {
1624 if (ContextMenuTracker._openContextMenuTrigger !== trigger) {
1625 ContextMenuTracker._openContextMenuTrigger?.close();
1626 ContextMenuTracker._openContextMenuTrigger = trigger;
1627 }
1628 }
1629 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ContextMenuTracker, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1630 static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ContextMenuTracker, providedIn: 'root' }); }
1631}
1632i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ContextMenuTracker, decorators: [{
1633 type: Injectable,
1634 args: [{ providedIn: 'root' }]
1635 }] });
1636/**
1637 * A directive that opens a menu when a user right-clicks within its host element.
1638 * It is aware of nested context menus and will trigger only the lowest level non-disabled context menu.
1639 */
1640class CdkContextMenuTrigger extends CdkMenuTriggerBase {
1641 /** Whether the context menu is disabled. */
1642 get disabled() {
1643 return this._disabled;
1644 }
1645 set disabled(value) {
1646 this._disabled = coerceBooleanProperty(value);
1647 }
1648 constructor() {
1649 super();
1650 /** The CDK overlay service. */
1651 this._overlay = inject(Overlay);
1652 /** The directionality of the page. */
1653 this._directionality = inject(Directionality, { optional: true });
1654 /** The app's context menu tracking registry */
1655 this._contextMenuTracker = inject(ContextMenuTracker);
1656 this._disabled = false;
1657 this._setMenuStackCloseListener();
1658 }
1659 /**
1660 * Open the attached menu at the specified location.
1661 * @param coordinates where to open the context menu
1662 */
1663 open(coordinates) {
1664 this._open(coordinates, false);
1665 }
1666 /** Close the currently opened context menu. */
1667 close() {
1668 this.menuStack.closeAll();
1669 }
1670 /**
1671 * Open the context menu and closes any previously open menus.
1672 * @param event the mouse event which opens the context menu.
1673 */
1674 _openOnContextMenu(event) {
1675 if (!this.disabled) {
1676 // Prevent the native context menu from opening because we're opening a custom one.
1677 event.preventDefault();
1678 // Stop event propagation to ensure that only the closest enabled context menu opens.
1679 // Otherwise, any context menus attached to containing elements would *also* open,
1680 // resulting in multiple stacked context menus being displayed.
1681 event.stopPropagation();
1682 this._contextMenuTracker.update(this);
1683 this._open({ x: event.clientX, y: event.clientY }, true);
1684 // A context menu can be triggered via a mouse right click or a keyboard shortcut.
1685 if (event.button === 2) {
1686 this.childMenu?.focusFirstItem('mouse');
1687 }
1688 else if (event.button === 0) {
1689 this.childMenu?.focusFirstItem('keyboard');
1690 }
1691 else {
1692 this.childMenu?.focusFirstItem('program');
1693 }
1694 }
1695 }
1696 /**
1697 * Get the configuration object used to create the overlay.
1698 * @param coordinates the location to place the opened menu
1699 */
1700 _getOverlayConfig(coordinates) {
1701 return new OverlayConfig({
1702 positionStrategy: this._getOverlayPositionStrategy(coordinates),
1703 scrollStrategy: this._overlay.scrollStrategies.reposition(),
1704 direction: this._directionality || undefined,
1705 });
1706 }
1707 /**
1708 * Get the position strategy for the overlay which specifies where to place the menu.
1709 * @param coordinates the location to place the opened menu
1710 */
1711 _getOverlayPositionStrategy(coordinates) {
1712 return this._overlay
1713 .position()
1714 .flexibleConnectedTo(coordinates)
1715 .withLockedPosition()
1716 .withGrowAfterOpen()
1717 .withPositions(this.menuPosition ?? CONTEXT_MENU_POSITIONS);
1718 }
1719 /** Subscribe to the menu stack close events and close this menu when requested. */
1720 _setMenuStackCloseListener() {
1721 this.menuStack.closed.pipe(takeUntil(this.destroyed)).subscribe(({ item }) => {
1722 if (item === this.childMenu && this.isOpen()) {
1723 this.closed.next();
1724 this.overlayRef.detach();
1725 }
1726 });
1727 }
1728 /**
1729 * Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
1730 * click occurs outside the menus.
1731 * @param ignoreFirstAuxClick Whether to ignore the first auxclick event outside the menu.
1732 */
1733 _subscribeToOutsideClicks(ignoreFirstAuxClick) {
1734 if (this.overlayRef) {
1735 let outsideClicks = this.overlayRef.outsidePointerEvents();
1736 // If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
1737 // because it fires when the mouse is released on the same click that opened the menu.
1738 if (ignoreFirstAuxClick) {
1739 const [auxClicks, nonAuxClicks] = partition(outsideClicks, ({ type }) => type === 'auxclick');
1740 outsideClicks = merge(nonAuxClicks, auxClicks.pipe(skip(1)));
1741 }
1742 outsideClicks.pipe(takeUntil(this.stopOutsideClicksListener)).subscribe(event => {
1743 if (!this.isElementInsideMenuStack(_getEventTarget(event))) {
1744 this.menuStack.closeAll();
1745 }
1746 });
1747 }
1748 }
1749 /**
1750 * Open the attached menu at the specified location.
1751 * @param coordinates where to open the context menu
1752 * @param ignoreFirstOutsideAuxClick Whether to ignore the first auxclick outside the menu after opening.
1753 */
1754 _open(coordinates, ignoreFirstOutsideAuxClick) {
1755 if (this.disabled) {
1756 return;
1757 }
1758 if (this.isOpen()) {
1759 // since we're moving this menu we need to close any submenus first otherwise they end up
1760 // disconnected from this one.
1761 this.menuStack.closeSubMenuOf(this.childMenu);
1762 this.overlayRef.getConfig().positionStrategy.setOrigin(coordinates);
1763 this.overlayRef.updatePosition();
1764 }
1765 else {
1766 this.opened.next();
1767 if (this.overlayRef) {
1768 this.overlayRef.getConfig().positionStrategy.setOrigin(coordinates);
1769 this.overlayRef.updatePosition();
1770 }
1771 else {
1772 this.overlayRef = this._overlay.create(this._getOverlayConfig(coordinates));
1773 }
1774 this.overlayRef.attach(this.getMenuContentPortal());
1775 this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick);
1776 }
1777 }
1778 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkContextMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1779 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkContextMenuTrigger, isStandalone: true, selector: "[cdkContextMenuTriggerFor]", inputs: { menuTemplateRef: ["cdkContextMenuTriggerFor", "menuTemplateRef"], menuPosition: ["cdkContextMenuPosition", "menuPosition"], menuData: ["cdkContextMenuTriggerData", "menuData"], disabled: ["cdkContextMenuDisabled", "disabled"] }, outputs: { opened: "cdkContextMenuOpened", closed: "cdkContextMenuClosed" }, host: { listeners: { "contextmenu": "_openOnContextMenu($event)" }, properties: { "attr.data-cdk-menu-stack-id": "null" } }, providers: [
1780 { provide: MENU_TRIGGER, useExisting: CdkContextMenuTrigger },
1781 { provide: MENU_STACK, useClass: MenuStack },
1782 ], exportAs: ["cdkContextMenuTriggerFor"], usesInheritance: true, ngImport: i0 }); }
1783}
1784i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkContextMenuTrigger, decorators: [{
1785 type: Directive,
1786 args: [{
1787 selector: '[cdkContextMenuTriggerFor]',
1788 exportAs: 'cdkContextMenuTriggerFor',
1789 standalone: true,
1790 host: {
1791 '[attr.data-cdk-menu-stack-id]': 'null',
1792 '(contextmenu)': '_openOnContextMenu($event)',
1793 },
1794 inputs: [
1795 'menuTemplateRef: cdkContextMenuTriggerFor',
1796 'menuPosition: cdkContextMenuPosition',
1797 'menuData: cdkContextMenuTriggerData',
1798 ],
1799 outputs: ['opened: cdkContextMenuOpened', 'closed: cdkContextMenuClosed'],
1800 providers: [
1801 { provide: MENU_TRIGGER, useExisting: CdkContextMenuTrigger },
1802 { provide: MENU_STACK, useClass: MenuStack },
1803 ],
1804 }]
1805 }], ctorParameters: function () { return []; }, propDecorators: { disabled: [{
1806 type: Input,
1807 args: ['cdkContextMenuDisabled']
1808 }] } });
1809
1810const MENU_DIRECTIVES = [
1811 CdkMenuBar,
1812 CdkMenu,
1813 CdkMenuItem,
1814 CdkMenuItemRadio,
1815 CdkMenuItemCheckbox,
1816 CdkMenuTrigger,
1817 CdkMenuGroup,
1818 CdkContextMenuTrigger,
1819 CdkTargetMenuAim,
1820];
1821/** Module that declares components and directives for the CDK menu. */
1822class CdkMenuModule {
1823 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1824 static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuModule, imports: [OverlayModule, CdkMenuBar,
1825 CdkMenu,
1826 CdkMenuItem,
1827 CdkMenuItemRadio,
1828 CdkMenuItemCheckbox,
1829 CdkMenuTrigger,
1830 CdkMenuGroup,
1831 CdkContextMenuTrigger,
1832 CdkTargetMenuAim], exports: [CdkMenuBar,
1833 CdkMenu,
1834 CdkMenuItem,
1835 CdkMenuItemRadio,
1836 CdkMenuItemCheckbox,
1837 CdkMenuTrigger,
1838 CdkMenuGroup,
1839 CdkContextMenuTrigger,
1840 CdkTargetMenuAim] }); }
1841 static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuModule, imports: [OverlayModule] }); }
1842}
1843i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuModule, decorators: [{
1844 type: NgModule,
1845 args: [{
1846 imports: [OverlayModule, ...MENU_DIRECTIVES],
1847 exports: MENU_DIRECTIVES,
1848 }]
1849 }] });
1850
1851/**
1852 * Generated bundle index. Do not edit.
1853 */
1854
1855export { CDK_MENU, CdkContextMenuTrigger, CdkMenu, CdkMenuBar, CdkMenuBase, CdkMenuGroup, CdkMenuItem, CdkMenuItemCheckbox, CdkMenuItemRadio, CdkMenuItemSelectable, CdkMenuModule, CdkMenuTrigger, CdkMenuTriggerBase, CdkTargetMenuAim, ContextMenuTracker, MENU_AIM, MENU_STACK, MENU_TRIGGER, MenuStack, PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER, PARENT_OR_NEW_MENU_STACK_PROVIDER, PointerFocusTracker, TargetMenuAim };
1856//# sourceMappingURL=menu.mjs.map