Skip to content

Commit b8ccf03

Browse files
authored
fix(cdk-experimental/menu): API, code, and docs cleanup pass (#24745)
* fix(cdk-experimental/menu): API, code, and docs cleanup pass * fixup! fix(cdk-experimental/menu): API, code, and docs cleanup pass
1 parent 7f59aee commit b8ccf03

29 files changed

+922
-783
lines changed

src/cdk-experimental/menu/context-menu.spec.ts renamed to src/cdk-experimental/menu/context-menu-trigger.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import {Component, ViewChild, ElementRef, Type, ViewChildren, QueryList} from '@
22
import {CdkMenuModule} from './menu-module';
33
import {TestBed, waitForAsync, ComponentFixture} from '@angular/core/testing';
44
import {CdkMenu} from './menu';
5-
import {CdkContextMenuTrigger} from './context-menu';
5+
import {CdkContextMenuTrigger} from './context-menu-trigger';
66
import {dispatchKeyboardEvent, dispatchMouseEvent} from '../../cdk/testing/private';
77
import {By} from '@angular/platform-browser';
88
import {CdkMenuItem} from './menu-item';
9-
import {CdkMenuItemTrigger} from './menu-item-trigger';
9+
import {CdkMenuTrigger} from './menu-trigger';
1010
import {CdkMenuBar} from './menu-bar';
1111
import {LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
1212

@@ -491,7 +491,7 @@ class NestedContextMenu {
491491
})
492492
class ContextMenuWithSubmenu {
493493
@ViewChild(CdkContextMenuTrigger, {read: ElementRef}) context: ElementRef<HTMLElement>;
494-
@ViewChild(CdkMenuItemTrigger, {read: ElementRef}) triggerNativeElement: ElementRef<HTMLElement>;
494+
@ViewChild(CdkMenuTrigger, {read: ElementRef}) triggerNativeElement: ElementRef<HTMLElement>;
495495

496496
@ViewChild('cut_menu', {read: CdkMenu}) cutMenu: CdkMenu;
497497
@ViewChild('copy_menu', {read: CdkMenu}) copyMenu: CdkMenu;
@@ -548,7 +548,7 @@ class ContextMenuWithMenuBarAndInlineMenu {
548548
`,
549549
})
550550
class MenuBarAndContextTriggerShareMenu {
551-
@ViewChild(CdkMenuItemTrigger) menuBarTrigger: CdkMenuItemTrigger;
551+
@ViewChild(CdkMenuTrigger) menuBarTrigger: CdkMenuTrigger;
552552
@ViewChild(CdkContextMenuTrigger) contextTrigger: CdkContextMenuTrigger;
553553
@ViewChildren(CdkMenu) menus: QueryList<CdkMenu>;
554554
}

src/cdk-experimental/menu/context-menu.ts renamed to src/cdk-experimental/menu/context-menu-trigger.ts

Lines changed: 67 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,16 @@ import {
2323
OverlayConfig,
2424
STANDARD_DROPDOWN_BELOW_POSITIONS,
2525
} from '@angular/cdk/overlay';
26-
import {Portal, TemplatePortal} from '@angular/cdk/portal';
2726
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
2827
import {merge, partition} from 'rxjs';
2928
import {skip, takeUntil} from 'rxjs/operators';
3029
import {MENU_STACK, MenuStack} from './menu-stack';
31-
import {isClickInsideMenuOverlay} from './menu-item-trigger';
32-
import {MENU_TRIGGER, MenuTrigger} from './menu-trigger';
30+
import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base';
3331

34-
// In cases where the first menu item in the context menu is a trigger the submenu opens on a
35-
// hover event. We offset the context menu 2px by default to prevent this from occurring.
32+
/** The preferred menu positions for the context menu. */
3633
const CONTEXT_MENU_POSITIONS = STANDARD_DROPDOWN_BELOW_POSITIONS.map(position => {
34+
// In cases where the first menu item in the context menu is a trigger the submenu opens on a
35+
// hover event. We offset the context menu 2px by default to prevent this from occurring.
3736
const offsetX = position.overlayX === 'start' ? 2 : -2;
3837
const offsetY = position.overlayY === 'top' ? 2 : -2;
3938
return {...position, offsetX, offsetY};
@@ -47,7 +46,7 @@ export class ContextMenuTracker {
4746

4847
/**
4948
* Close the previous open context menu and set the given one as being open.
50-
* @param trigger the trigger for the currently open Context Menu.
49+
* @param trigger The trigger for the currently open Context Menu.
5150
*/
5251
update(trigger: CdkContextMenuTrigger) {
5352
if (ContextMenuTracker._openContextMenuTrigger !== trigger) {
@@ -57,29 +56,29 @@ export class ContextMenuTracker {
5756
}
5857
}
5958

60-
/** The coordinates of where the context menu should open. */
59+
/** The coordinates where the context menu should open. */
6160
export type ContextMenuCoordinates = {x: number; y: number};
6261

6362
/**
64-
* A directive which when placed on some element opens a the Menu it is bound to when a user
65-
* right-clicks within that element. It is aware of nested Context Menus and the lowest level
66-
* non-disabled context menu will trigger.
63+
* A directive that opens a menu when a user right-clicks within its host element.
64+
* It is aware of nested context menus and will trigger only the lowest level non-disabled context menu.
6765
*/
6866
@Directive({
6967
selector: '[cdkContextMenuTriggerFor]',
7068
exportAs: 'cdkContextMenuTriggerFor',
7169
host: {
70+
'[attr.data-cdk-menu-stack-id]': 'null',
7271
'(contextmenu)': '_openOnContextMenu($event)',
7372
},
74-
inputs: ['_menuTemplateRef: cdkContextMenuTriggerFor', 'menuPosition: cdkContextMenuPosition'],
73+
inputs: ['menuTemplateRef: cdkContextMenuTriggerFor', 'menuPosition: cdkContextMenuPosition'],
7574
outputs: ['opened: cdkContextMenuOpened', 'closed: cdkContextMenuClosed'],
7675
providers: [
7776
{provide: MENU_TRIGGER, useExisting: CdkContextMenuTrigger},
7877
{provide: MENU_STACK, useClass: MenuStack},
7978
],
8079
})
81-
export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
82-
/** Whether the context menu should be disabled. */
80+
export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestroy {
81+
/** Whether the context menu is disabled. */
8382
@Input('cdkContextMenuDisabled')
8483
get disabled(): boolean {
8584
return this._disabled;
@@ -90,15 +89,21 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
9089
private _disabled = false;
9190

9291
constructor(
92+
/** The DI injector for this component */
9393
injector: Injector,
94-
protected readonly _viewContainerRef: ViewContainerRef,
94+
/** The view container ref for this component */
95+
viewContainerRef: ViewContainerRef,
96+
/** The CDK overlay service */
9597
private readonly _overlay: Overlay,
98+
/** The app's context menu tracking registry */
9699
private readonly _contextMenuTracker: ContextMenuTracker,
100+
/** The menu stack this menu is part of. */
97101
@Inject(MENU_STACK) menuStack: MenuStack,
102+
/** The directionality of the current page */
98103
@Optional() private readonly _directionality?: Directionality,
99104
) {
100-
super(injector, menuStack);
101-
this._setMenuStackListener();
105+
super(injector, viewContainerRef, menuStack);
106+
this._setMenuStackCloseListener();
102107
}
103108

104109
/**
@@ -109,42 +114,13 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
109114
this._open(coordinates, false);
110115
}
111116

112-
private _open(coordinates: ContextMenuCoordinates, ignoreFirstOutsideAuxClick: boolean) {
113-
if (this.disabled) {
114-
return;
115-
} else if (this.isOpen()) {
116-
// since we're moving this menu we need to close any submenus first otherwise they end up
117-
// disconnected from this one.
118-
this.menuStack.closeSubMenuOf(this.childMenu!);
119-
120-
(
121-
this._overlayRef!.getConfig().positionStrategy as FlexibleConnectedPositionStrategy
122-
).setOrigin(coordinates);
123-
this._overlayRef!.updatePosition();
124-
} else {
125-
this.opened.next();
126-
127-
if (this._overlayRef) {
128-
(
129-
this._overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy
130-
).setOrigin(coordinates);
131-
this._overlayRef.updatePosition();
132-
} else {
133-
this._overlayRef = this._overlay.create(this._getOverlayConfig(coordinates));
134-
}
135-
136-
this._overlayRef.attach(this._getMenuContent());
137-
this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick);
138-
}
139-
}
140-
141-
/** Close the opened menu. */
117+
/** Close the currently opened context menu. */
142118
close() {
143119
this.menuStack.closeAll();
144120
}
145121

146122
/**
147-
* Open the context menu and close any previously open menus.
123+
* Open the context menu and closes any previously open menus.
148124
* @param event the mouse event which opens the context menu.
149125
*/
150126
_openOnContextMenu(event: MouseEvent) {
@@ -184,7 +160,7 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
184160
}
185161

186162
/**
187-
* Build the position strategy for the overlay which specifies where to place the menu.
163+
* Get the position strategy for the overlay which specifies where to place the menu.
188164
* @param coordinates the location to place the opened menu
189165
*/
190166
private _getOverlayPositionStrategy(
@@ -196,52 +172,70 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
196172
.withPositions(this.menuPosition ?? CONTEXT_MENU_POSITIONS);
197173
}
198174

199-
/**
200-
* Get the portal to be attached to the overlay which contains the menu. Allows for the menu
201-
* content to change dynamically and be reflected in the application.
202-
*/
203-
private _getMenuContent(): Portal<unknown> {
204-
const hasMenuContentChanged = this._menuTemplateRef !== this._menuPortal?.templateRef;
205-
if (this._menuTemplateRef && (!this._menuPortal || hasMenuContentChanged)) {
206-
this._menuPortal = new TemplatePortal(
207-
this._menuTemplateRef,
208-
this._viewContainerRef,
209-
undefined,
210-
this.getChildMenuInjector(),
211-
);
212-
}
213-
214-
return this._menuPortal;
215-
}
216-
217175
/** Subscribe to the menu stack close events and close this menu when requested. */
218-
private _setMenuStackListener() {
219-
this.menuStack.closed.pipe(takeUntil(this._destroyed)).subscribe(({item}) => {
176+
private _setMenuStackCloseListener() {
177+
this.menuStack.closed.pipe(takeUntil(this.destroyed)).subscribe(({item}) => {
220178
if (item === this.childMenu && this.isOpen()) {
221179
this.closed.next();
222-
this._overlayRef!.detach();
180+
this.overlayRef!.detach();
223181
}
224182
});
225183
}
226184

227185
/**
228186
* Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
229187
* click occurs outside the menus.
188+
* @param ignoreFirstAuxClick Whether to ignore the first auxclick event outside the menu.
230189
*/
231190
private _subscribeToOutsideClicks(ignoreFirstAuxClick: boolean) {
232-
if (this._overlayRef) {
233-
let outsideClicks = this._overlayRef.outsidePointerEvents();
191+
if (this.overlayRef) {
192+
let outsideClicks = this.overlayRef.outsidePointerEvents();
234193
// If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
235194
// because it fires when the mouse is released on the same click that opened the menu.
236195
if (ignoreFirstAuxClick) {
237196
const [auxClicks, nonAuxClicks] = partition(outsideClicks, ({type}) => type === 'auxclick');
238197
outsideClicks = merge(nonAuxClicks, auxClicks.pipe(skip(1)));
239198
}
240-
outsideClicks.pipe(takeUntil(this._stopOutsideClicksListener)).subscribe(event => {
241-
if (!isClickInsideMenuOverlay(event.target as Element)) {
199+
outsideClicks.pipe(takeUntil(this.stopOutsideClicksListener)).subscribe(event => {
200+
if (!this.isElementInsideMenuStack(event.target as Element)) {
242201
this.menuStack.closeAll();
243202
}
244203
});
245204
}
246205
}
206+
207+
/**
208+
* Open the attached menu at the specified location.
209+
* @param coordinates where to open the context menu
210+
* @param ignoreFirstOutsideAuxClick Whether to ignore the first auxclick outside the menu after opening.
211+
*/
212+
private _open(coordinates: ContextMenuCoordinates, ignoreFirstOutsideAuxClick: boolean) {
213+
if (this.disabled) {
214+
return;
215+
}
216+
if (this.isOpen()) {
217+
// since we're moving this menu we need to close any submenus first otherwise they end up
218+
// disconnected from this one.
219+
this.menuStack.closeSubMenuOf(this.childMenu!);
220+
221+
(
222+
this.overlayRef!.getConfig().positionStrategy as FlexibleConnectedPositionStrategy
223+
).setOrigin(coordinates);
224+
this.overlayRef!.updatePosition();
225+
} else {
226+
this.opened.next();
227+
228+
if (this.overlayRef) {
229+
(
230+
this.overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy
231+
).setOrigin(coordinates);
232+
this.overlayRef.updatePosition();
233+
} else {
234+
this.overlayRef = this._overlay.create(this._getOverlayConfig(coordinates));
235+
}
236+
237+
this.overlayRef.attach(this.getMenuContentPortal());
238+
this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick);
239+
}
240+
}
247241
}

src/cdk-experimental/menu/menu-aim.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import {throwMissingPointerFocusTracker, throwMissingMenuReference} from './menu
1919
* order to determine if it may perform its close actions.
2020
*/
2121
export interface MenuAim {
22-
/** Set the Menu and its PointerFocusTracker. */
22+
/**
23+
* Set the Menu and its PointerFocusTracker.
24+
* @param menu The menu that this menu aim service controls.
25+
* @param pointerTracker The `PointerFocusTracker` for the given menu.
26+
*/
2327
initialize(menu: Menu, pointerTracker: PointerFocusTracker<FocusableElement & Toggler>): void;
2428

2529
/**
@@ -45,11 +49,9 @@ const NUM_POINTS = 5;
4549
*/
4650
const CLOSE_DELAY = 300;
4751

48-
/**
49-
* An element which when hovered over may perform closing actions on the open submenu and
50-
* potentially open its own menu.
51-
*/
52+
/** An element which when hovered over may open or close a menu. */
5253
export interface Toggler {
54+
/** Gets the open menu, or undefined if no menu is open. */
5355
getMenu(): Menu | undefined;
5456
}
5557

@@ -72,7 +74,6 @@ type Point = {x: number; y: number};
7274
* @param submenuPoints the submenu DOMRect points.
7375
* @param m the slope of the trajectory line.
7476
* @param b the y intercept of the trajectory line.
75-
*
7677
* @return true if any point on the line falls within the submenu.
7778
*/
7879
function isWithinSubmenu(submenuPoints: DOMRect, m: number, b: number) {
@@ -88,6 +89,7 @@ function isWithinSubmenu(submenuPoints: DOMRect, m: number, b: number) {
8889
((bottom - b) / m >= left && (bottom - b) / m <= right)
8990
);
9091
}
92+
9193
/**
9294
* TargetMenuAim predicts if a user is moving into a submenu. It calculates the
9395
* trajectory of the user's mouse movement in the current menu to determine if the
@@ -115,9 +117,21 @@ export class TargetMenuAim implements MenuAim, OnDestroy {
115117
/** Emits when this service is destroyed. */
116118
private readonly _destroyed: Subject<void> = new Subject();
117119

118-
constructor(private readonly _ngZone: NgZone) {}
120+
constructor(
121+
/** The Angular zone. */
122+
private readonly _ngZone: NgZone,
123+
) {}
124+
125+
ngOnDestroy() {
126+
this._destroyed.next();
127+
this._destroyed.complete();
128+
}
119129

120-
/** Set the Menu and its PointerFocusTracker. */
130+
/**
131+
* Set the Menu and its PointerFocusTracker.
132+
* @param menu The menu that this menu aim service controls.
133+
* @param pointerTracker The `PointerFocusTracker` for the given menu.
134+
*/
121135
initialize(menu: Menu, pointerTracker: PointerFocusTracker<FocusableElement & Toggler>) {
122136
this._menu = menu;
123137
this._pointerTracker = pointerTracker;
@@ -157,6 +171,8 @@ export class TargetMenuAim implements MenuAim, OnDestroy {
157171
*
158172
* The delayed toggle handler executes the `doToggle` callback after some period of time iff the
159173
* users mouse is on an item in the current menu.
174+
*
175+
* @param doToggle the function called when the user is not moving towards the submenu.
160176
*/
161177
private _startTimeout(doToggle: () => void) {
162178
// If the users mouse is moving towards a submenu we don't want to immediately resolve.
@@ -197,9 +213,7 @@ export class TargetMenuAim implements MenuAim, OnDestroy {
197213

198214
/** Get the bounding DOMRect for the open submenu. */
199215
private _getSubmenuBounds(): DOMRect | undefined {
200-
return this._pointerTracker?.previousElement
201-
?.getMenu()
202-
?._elementRef.nativeElement.getBoundingClientRect();
216+
return this._pointerTracker?.previousElement?.getMenu()?.nativeElement.getBoundingClientRect();
203217
}
204218

205219
/**
@@ -220,7 +234,7 @@ export class TargetMenuAim implements MenuAim, OnDestroy {
220234
/** Subscribe to the root menus mouse move events and update the tracked mouse points. */
221235
private _subscribeToMouseMoves() {
222236
this._ngZone.runOutsideAngular(() => {
223-
fromEvent<MouseEvent>(this._menu._elementRef.nativeElement, 'mousemove')
237+
fromEvent<MouseEvent>(this._menu.nativeElement, 'mousemove')
224238
.pipe(
225239
filter((_: MouseEvent, index: number) => index % MOUSE_MOVE_SAMPLE_FREQUENCY === 0),
226240
takeUntil(this._destroyed),
@@ -233,15 +247,10 @@ export class TargetMenuAim implements MenuAim, OnDestroy {
233247
});
234248
});
235249
}
236-
237-
ngOnDestroy() {
238-
this._destroyed.next();
239-
this._destroyed.complete();
240-
}
241250
}
242251

243252
/**
244-
* CdkTargetMenuAim is a provider for the TargetMenuAim service. It should be added to an
253+
* CdkTargetMenuAim is a provider for the TargetMenuAim service. It can be added to an
245254
* element with either the `cdkMenu` or `cdkMenuBar` directive and child menu items.
246255
*/
247256
@Directive({

0 commit comments

Comments
 (0)