@@ -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' ;
2726import { BooleanInput , coerceBooleanProperty } from '@angular/cdk/coercion' ;
2827import { merge , partition } from 'rxjs' ;
2928import { skip , takeUntil } from 'rxjs/operators' ;
3029import { 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. */
3633const 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. */
6160export 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}
0 commit comments