@@ -27,6 +27,10 @@ import {
27
27
OnInit ,
28
28
ChangeDetectorRef ,
29
29
booleanAttribute ,
30
+ afterNextRender ,
31
+ AfterRenderRef ,
32
+ inject ,
33
+ Injector ,
30
34
} from '@angular/core' ;
31
35
import { AnimationEvent } from '@angular/animations' ;
32
36
import { FocusKeyManager , FocusOrigin } from '@angular/cdk/a11y' ;
@@ -39,8 +43,8 @@ import {
39
43
UP_ARROW ,
40
44
hasModifierKey ,
41
45
} from '@angular/cdk/keycodes' ;
42
- import { merge , Observable , Subject , Subscription } from 'rxjs' ;
43
- import { startWith , switchMap , take } from 'rxjs/operators' ;
46
+ import { merge , Observable , Subject } from 'rxjs' ;
47
+ import { startWith , switchMap } from 'rxjs/operators' ;
44
48
import { MatMenuItem } from './menu-item' ;
45
49
import { MatMenuPanel , MAT_MENU_PANEL } from './menu-panel' ;
46
50
import { MenuPositionX , MenuPositionY } from './menu-positions' ;
@@ -115,7 +119,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
115
119
private _keyManager : FocusKeyManager < MatMenuItem > ;
116
120
private _xPosition : MenuPositionX ;
117
121
private _yPosition : MenuPositionY ;
118
- private _firstItemFocusSubscription ?: Subscription ;
122
+ private _firstItemFocusRef ?: AfterRenderRef ;
119
123
private _previousElevation : string ;
120
124
private _elevationPrefix = 'mat-elevation-z' ;
121
125
private _baseElevation = 8 ;
@@ -267,6 +271,8 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
267
271
268
272
readonly panelId = `mat-menu-panel-${ menuPanelUid ++ } ` ;
269
273
274
+ private _injector = inject ( Injector ) ;
275
+
270
276
constructor (
271
277
elementRef : ElementRef < HTMLElement > ,
272
278
ngZone : NgZone ,
@@ -287,7 +293,11 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
287
293
288
294
constructor (
289
295
private _elementRef : ElementRef < HTMLElement > ,
290
- private _ngZone : NgZone ,
296
+ /**
297
+ * @deprecated Unused param, will be removed.
298
+ * @breaking -change 19.0.0
299
+ */
300
+ _unusedNgZone : NgZone ,
291
301
@Inject ( MAT_MENU_DEFAULT_OPTIONS ) defaultOptions : MatMenuDefaultOptions ,
292
302
// @breaking -change 15.0.0 `_changeDetectorRef` to become a required parameter.
293
303
private _changeDetectorRef ?: ChangeDetectorRef ,
@@ -345,7 +355,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
345
355
this . _keyManager ?. destroy ( ) ;
346
356
this . _directDescendantItems . destroy ( ) ;
347
357
this . closed . complete ( ) ;
348
- this . _firstItemFocusSubscription ?. unsubscribe ( ) ;
358
+ this . _firstItemFocusRef ?. destroy ( ) ;
349
359
}
350
360
351
361
/** Stream that emits whenever the hovered menu item changes. */
@@ -415,32 +425,35 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
415
425
* @param origin Action from which the focus originated. Used to set the correct styling.
416
426
*/
417
427
focusFirstItem ( origin : FocusOrigin = 'program' ) : void {
418
- // Wait for `onStable` to ensure iOS VoiceOver screen reader focuses the first item (#24735).
419
- this . _firstItemFocusSubscription ?. unsubscribe ( ) ;
420
- this . _firstItemFocusSubscription = this . _ngZone . onStable . pipe ( take ( 1 ) ) . subscribe ( ( ) => {
421
- let menuPanel : HTMLElement | null = null ;
422
-
423
- if ( this . _directDescendantItems . length ) {
424
- // Because the `mat-menuPanel` is at the DOM insertion point, not inside the overlay, we don't
425
- // have a nice way of getting a hold of the menuPanel panel. We can't use a `ViewChild` either
426
- // because the panel is inside an `ng-template`. We work around it by starting from one of
427
- // the items and walking up the DOM.
428
- menuPanel = this . _directDescendantItems . first ! . _getHostElement ( ) . closest ( '[role="menu"]' ) ;
429
- }
430
-
431
- // If an item in the menuPanel is already focused, avoid overriding the focus.
432
- if ( ! menuPanel || ! menuPanel . contains ( document . activeElement ) ) {
433
- const manager = this . _keyManager ;
434
- manager . setFocusOrigin ( origin ) . setFirstItemActive ( ) ;
428
+ // Wait for `afterNextRender` to ensure iOS VoiceOver screen reader focuses the first item (#24735).
429
+ this . _firstItemFocusRef ?. destroy ( ) ;
430
+ this . _firstItemFocusRef = afterNextRender (
431
+ ( ) => {
432
+ let menuPanel : HTMLElement | null = null ;
433
+
434
+ if ( this . _directDescendantItems . length ) {
435
+ // Because the `mat-menuPanel` is at the DOM insertion point, not inside the overlay, we don't
436
+ // have a nice way of getting a hold of the menuPanel panel. We can't use a `ViewChild` either
437
+ // because the panel is inside an `ng-template`. We work around it by starting from one of
438
+ // the items and walking up the DOM.
439
+ menuPanel = this . _directDescendantItems . first ! . _getHostElement ( ) . closest ( '[role="menu"]' ) ;
440
+ }
435
441
436
- // If there's no active item at this point, it means that all the items are disabled.
437
- // Move focus to the menuPanel panel so keyboard events like Escape still work. Also this will
438
- // give _some_ feedback to screen readers.
439
- if ( ! manager . activeItem && menuPanel ) {
440
- menuPanel . focus ( ) ;
442
+ // If an item in the menuPanel is already focused, avoid overriding the focus.
443
+ if ( ! menuPanel || ! menuPanel . contains ( document . activeElement ) ) {
444
+ const manager = this . _keyManager ;
445
+ manager . setFocusOrigin ( origin ) . setFirstItemActive ( ) ;
446
+
447
+ // If there's no active item at this point, it means that all the items are disabled.
448
+ // Move focus to the menuPanel panel so keyboard events like Escape still work. Also this will
449
+ // give _some_ feedback to screen readers.
450
+ if ( ! manager . activeItem && menuPanel ) {
451
+ menuPanel . focus ( ) ;
452
+ }
441
453
}
442
- }
443
- } ) ;
454
+ } ,
455
+ { injector : this . _injector } ,
456
+ ) ;
444
457
}
445
458
446
459
/**
0 commit comments