@@ -27,6 +27,10 @@ import {
2727 OnInit ,
2828 ChangeDetectorRef ,
2929 booleanAttribute ,
30+ afterNextRender ,
31+ AfterRenderRef ,
32+ inject ,
33+ Injector ,
3034} from '@angular/core' ;
3135import { AnimationEvent } from '@angular/animations' ;
3236import { FocusKeyManager , FocusOrigin } from '@angular/cdk/a11y' ;
@@ -39,8 +43,8 @@ import {
3943 UP_ARROW ,
4044 hasModifierKey ,
4145} 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' ;
4448import { MatMenuItem } from './menu-item' ;
4549import { MatMenuPanel , MAT_MENU_PANEL } from './menu-panel' ;
4650import { MenuPositionX , MenuPositionY } from './menu-positions' ;
@@ -115,7 +119,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
115119 private _keyManager : FocusKeyManager < MatMenuItem > ;
116120 private _xPosition : MenuPositionX ;
117121 private _yPosition : MenuPositionY ;
118- private _firstItemFocusSubscription ?: Subscription ;
122+ private _firstItemFocusRef ?: AfterRenderRef ;
119123 private _previousElevation : string ;
120124 private _elevationPrefix = 'mat-elevation-z' ;
121125 private _baseElevation = 8 ;
@@ -267,6 +271,8 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
267271
268272 readonly panelId = `mat-menu-panel-${ menuPanelUid ++ } ` ;
269273
274+ private _injector = inject ( Injector ) ;
275+
270276 constructor (
271277 elementRef : ElementRef < HTMLElement > ,
272278 ngZone : NgZone ,
@@ -287,7 +293,11 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
287293
288294 constructor (
289295 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 ,
291301 @Inject ( MAT_MENU_DEFAULT_OPTIONS ) defaultOptions : MatMenuDefaultOptions ,
292302 // @breaking -change 15.0.0 `_changeDetectorRef` to become a required parameter.
293303 private _changeDetectorRef ?: ChangeDetectorRef ,
@@ -345,7 +355,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
345355 this . _keyManager ?. destroy ( ) ;
346356 this . _directDescendantItems . destroy ( ) ;
347357 this . closed . complete ( ) ;
348- this . _firstItemFocusSubscription ?. unsubscribe ( ) ;
358+ this . _firstItemFocusRef ?. destroy ( ) ;
349359 }
350360
351361 /** Stream that emits whenever the hovered menu item changes. */
@@ -415,32 +425,35 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
415425 * @param origin Action from which the focus originated. Used to set the correct styling.
416426 */
417427 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+ }
435441
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+ }
441453 }
442- }
443- } ) ;
454+ } ,
455+ { injector : this . _injector } ,
456+ ) ;
444457 }
445458
446459 /**
0 commit comments