@@ -18,6 +18,9 @@ import {
1818 Optional ,
1919 Input ,
2020 HostListener ,
21+ NgZone ,
22+ AfterViewInit ,
23+ ViewChild ,
2124} from '@angular/core' ;
2225import {
2326 CanDisable , CanDisableCtor ,
@@ -57,11 +60,14 @@ const _MatMenuItemMixinBase: CanDisableRippleCtor & CanDisableCtor & typeof MatM
5760 templateUrl : 'menu-item.html' ,
5861} )
5962export class MatMenuItem extends _MatMenuItemMixinBase
60- implements FocusableOption , CanDisable , CanDisableRipple , OnDestroy {
63+ implements FocusableOption , CanDisable , CanDisableRipple , AfterViewInit , OnDestroy {
6164
6265 /** ARIA role for the menu item. */
6366 @Input ( ) role : 'menuitem' | 'menuitemradio' | 'menuitemcheckbox' = 'menuitem' ;
6467
68+ /** Reference to the element wrapping the projected content. */
69+ @ViewChild ( 'content' ) _content : ElementRef < HTMLElement > | undefined ;
70+
6571 private _document : Document ;
6672
6773 /** Stream that emits when the menu item is hovered. */
@@ -80,9 +86,11 @@ export class MatMenuItem extends _MatMenuItemMixinBase
8086 private _elementRef : ElementRef < HTMLElement > ,
8187 @Inject ( DOCUMENT ) document ?: any ,
8288 private _focusMonitor ?: FocusMonitor ,
83- @Inject ( MAT_MENU_PANEL ) @Optional ( ) public _parentMenu ?: MatMenuPanel < MatMenuItem > ) {
89+ @Inject ( MAT_MENU_PANEL ) @Optional ( ) public _parentMenu ?: MatMenuPanel < MatMenuItem > ,
90+ private _ngZone ?: NgZone ) {
8491
8592 // @breaking -change 8.0.0 make `_focusMonitor` and `document` required params.
93+ // @breaking -change 11.0.0 make `_ngZone` a required parameter.
8694 super ( ) ;
8795
8896 if ( _focusMonitor ) {
@@ -110,6 +118,15 @@ export class MatMenuItem extends _MatMenuItemMixinBase
110118 this . _focused . next ( this ) ;
111119 }
112120
121+ ngAfterViewInit ( ) {
122+ // @breaking -change 11.0.0 Remove null check for `_ngZone`.
123+ if ( this . _ngZone ) {
124+ this . _ngZone . runOutsideAngular ( ( ) => this . _bindDisabledClickEvents ( ) ) ;
125+ } else {
126+ this . _bindDisabledClickEvents ( ) ;
127+ }
128+ }
129+
113130 ngOnDestroy ( ) {
114131 if ( this . _focusMonitor ) {
115132 this . _focusMonitor . stopMonitoring ( this . _elementRef ) ;
@@ -119,6 +136,11 @@ export class MatMenuItem extends _MatMenuItemMixinBase
119136 this . _parentMenu . removeItem ( this ) ;
120137 }
121138
139+ this . _elementRef . nativeElement . removeEventListener ( 'click' , this . _preventDisabledClicks ) ;
140+ if ( this . _content ) {
141+ this . _content . nativeElement . removeEventListener ( 'click' , this . _preventDisabledClicks ) ;
142+ }
143+
122144 this . _hovered . complete ( ) ;
123145 this . _focused . complete ( ) ;
124146 }
@@ -133,20 +155,6 @@ export class MatMenuItem extends _MatMenuItemMixinBase
133155 return this . _elementRef . nativeElement ;
134156 }
135157
136- /** Prevents the default element actions if it is disabled. */
137- // We have to use a `HostListener` here in order to support both Ivy and ViewEngine.
138- // In Ivy the `host` bindings will be merged when this class is extended, whereas in
139- // ViewEngine they're overwritten.
140- // TODO(crisbeto): we move this back into `host` once Ivy is turned on by default.
141- // tslint:disable-next-line:no-host-decorator-in-concrete
142- @HostListener ( 'click' , [ '$event' ] )
143- _checkDisabled ( event : Event ) : void {
144- if ( this . disabled ) {
145- event . preventDefault ( ) ;
146- event . stopPropagation ( ) ;
147- }
148- }
149-
150158 /** Emits to the hover stream. */
151159 // We have to use a `HostListener` here in order to support both Ivy and ViewEngine.
152160 // In Ivy the `host` bindings will be merged when this class is extended, whereas in
@@ -160,7 +168,8 @@ export class MatMenuItem extends _MatMenuItemMixinBase
160168
161169 /** Gets the label to be used when determining whether the option should be focused. */
162170 getLabel ( ) : string {
163- const element : HTMLElement = this . _elementRef . nativeElement ;
171+ const element : HTMLElement = this . _content ?
172+ this . _content . nativeElement : this . _elementRef . nativeElement ;
164173 const textNodeType = this . _document ? this . _document . TEXT_NODE : 3 ;
165174 let output = '' ;
166175
@@ -180,6 +189,26 @@ export class MatMenuItem extends _MatMenuItemMixinBase
180189 return output . trim ( ) ;
181190 }
182191
192+ /** Binds the click events that prevent the default actions while disabled. */
193+ private _bindDisabledClickEvents ( ) {
194+ // We need to bind this event both on the root node and the content wrapper, because browsers
195+ // won't dispatch events on disabled `button` nodes, but they'll still be dispatched if the
196+ // user interacts with a non-disabled child of the button. This means that can get regions
197+ // inside a disabled menu item where clicks land and others where they don't.
198+ this . _elementRef . nativeElement . addEventListener ( 'click' , this . _preventDisabledClicks ) ;
199+ if ( this . _content ) {
200+ this . _content . nativeElement . addEventListener ( 'click' , this . _preventDisabledClicks ) ;
201+ }
202+ }
203+
204+ /** Prevents the default click action if the menu item is disabled. */
205+ private _preventDisabledClicks = ( event : Event ) => {
206+ if ( this . disabled ) {
207+ event . preventDefault ( ) ;
208+ event . stopPropagation ( ) ;
209+ }
210+ }
211+
183212 static ngAcceptInputType_disabled : BooleanInput ;
184213 static ngAcceptInputType_disableRipple : BooleanInput ;
185214}
0 commit comments