@@ -23,17 +23,16 @@ import {
23
23
OverlayConfig ,
24
24
STANDARD_DROPDOWN_BELOW_POSITIONS ,
25
25
} from '@angular/cdk/overlay' ;
26
- import { Portal , TemplatePortal } from '@angular/cdk/portal' ;
27
26
import { BooleanInput , coerceBooleanProperty } from '@angular/cdk/coercion' ;
28
27
import { merge , partition } from 'rxjs' ;
29
28
import { skip , takeUntil } from 'rxjs/operators' ;
30
29
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' ;
33
31
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. */
36
33
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.
37
36
const offsetX = position . overlayX === 'start' ? 2 : - 2 ;
38
37
const offsetY = position . overlayY === 'top' ? 2 : - 2 ;
39
38
return { ...position , offsetX, offsetY} ;
@@ -47,7 +46,7 @@ export class ContextMenuTracker {
47
46
48
47
/**
49
48
* 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.
51
50
*/
52
51
update ( trigger : CdkContextMenuTrigger ) {
53
52
if ( ContextMenuTracker . _openContextMenuTrigger !== trigger ) {
@@ -57,29 +56,29 @@ export class ContextMenuTracker {
57
56
}
58
57
}
59
58
60
- /** The coordinates of where the context menu should open. */
59
+ /** The coordinates where the context menu should open. */
61
60
export type ContextMenuCoordinates = { x : number ; y : number } ;
62
61
63
62
/**
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.
67
65
*/
68
66
@Directive ( {
69
67
selector : '[cdkContextMenuTriggerFor]' ,
70
68
exportAs : 'cdkContextMenuTriggerFor' ,
71
69
host : {
70
+ '[attr.data-cdk-menu-stack-id]' : 'null' ,
72
71
'(contextmenu)' : '_openOnContextMenu($event)' ,
73
72
} ,
74
- inputs : [ '_menuTemplateRef : cdkContextMenuTriggerFor' , 'menuPosition: cdkContextMenuPosition' ] ,
73
+ inputs : [ 'menuTemplateRef : cdkContextMenuTriggerFor' , 'menuPosition: cdkContextMenuPosition' ] ,
75
74
outputs : [ 'opened: cdkContextMenuOpened' , 'closed: cdkContextMenuClosed' ] ,
76
75
providers : [
77
76
{ provide : MENU_TRIGGER , useExisting : CdkContextMenuTrigger } ,
78
77
{ provide : MENU_STACK , useClass : MenuStack } ,
79
78
] ,
80
79
} )
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. */
83
82
@Input ( 'cdkContextMenuDisabled' )
84
83
get disabled ( ) : boolean {
85
84
return this . _disabled ;
@@ -90,15 +89,21 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
90
89
private _disabled = false ;
91
90
92
91
constructor (
92
+ /** The DI injector for this component */
93
93
injector : Injector ,
94
- protected readonly _viewContainerRef : ViewContainerRef ,
94
+ /** The view container ref for this component */
95
+ viewContainerRef : ViewContainerRef ,
96
+ /** The CDK overlay service */
95
97
private readonly _overlay : Overlay ,
98
+ /** The app's context menu tracking registry */
96
99
private readonly _contextMenuTracker : ContextMenuTracker ,
100
+ /** The menu stack this menu is part of. */
97
101
@Inject ( MENU_STACK ) menuStack : MenuStack ,
102
+ /** The directionality of the current page */
98
103
@Optional ( ) private readonly _directionality ?: Directionality ,
99
104
) {
100
- super ( injector , menuStack ) ;
101
- this . _setMenuStackListener ( ) ;
105
+ super ( injector , viewContainerRef , menuStack ) ;
106
+ this . _setMenuStackCloseListener ( ) ;
102
107
}
103
108
104
109
/**
@@ -109,42 +114,13 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
109
114
this . _open ( coordinates , false ) ;
110
115
}
111
116
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. */
142
118
close ( ) {
143
119
this . menuStack . closeAll ( ) ;
144
120
}
145
121
146
122
/**
147
- * Open the context menu and close any previously open menus.
123
+ * Open the context menu and closes any previously open menus.
148
124
* @param event the mouse event which opens the context menu.
149
125
*/
150
126
_openOnContextMenu ( event : MouseEvent ) {
@@ -184,7 +160,7 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
184
160
}
185
161
186
162
/**
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.
188
164
* @param coordinates the location to place the opened menu
189
165
*/
190
166
private _getOverlayPositionStrategy (
@@ -196,52 +172,70 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
196
172
. withPositions ( this . menuPosition ?? CONTEXT_MENU_POSITIONS ) ;
197
173
}
198
174
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
-
217
175
/** 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} ) => {
220
178
if ( item === this . childMenu && this . isOpen ( ) ) {
221
179
this . closed . next ( ) ;
222
- this . _overlayRef ! . detach ( ) ;
180
+ this . overlayRef ! . detach ( ) ;
223
181
}
224
182
} ) ;
225
183
}
226
184
227
185
/**
228
186
* Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
229
187
* click occurs outside the menus.
188
+ * @param ignoreFirstAuxClick Whether to ignore the first auxclick event outside the menu.
230
189
*/
231
190
private _subscribeToOutsideClicks ( ignoreFirstAuxClick : boolean ) {
232
- if ( this . _overlayRef ) {
233
- let outsideClicks = this . _overlayRef . outsidePointerEvents ( ) ;
191
+ if ( this . overlayRef ) {
192
+ let outsideClicks = this . overlayRef . outsidePointerEvents ( ) ;
234
193
// If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
235
194
// because it fires when the mouse is released on the same click that opened the menu.
236
195
if ( ignoreFirstAuxClick ) {
237
196
const [ auxClicks , nonAuxClicks ] = partition ( outsideClicks , ( { type} ) => type === 'auxclick' ) ;
238
197
outsideClicks = merge ( nonAuxClicks , auxClicks . pipe ( skip ( 1 ) ) ) ;
239
198
}
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 ) ) {
242
201
this . menuStack . closeAll ( ) ;
243
202
}
244
203
} ) ;
245
204
}
246
205
}
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
+ }
247
241
}
0 commit comments