diff --git a/src/material-experimental/mdc-menu/menu.spec.ts b/src/material-experimental/mdc-menu/menu.spec.ts index 43ba01ee11b8..ba899093a421 100644 --- a/src/material-experimental/mdc-menu/menu.spec.ts +++ b/src/material-experimental/mdc-menu/menu.spec.ts @@ -1151,6 +1151,7 @@ describe('MDC-based MatMenu', () => { fixture.detectChanges(); fixture.componentInstance.trigger.menuOpened.subscribe(() => { + flush(); (document.querySelectorAll('.mat-mdc-menu-panel [mat-menu-item]')[3] as HTMLElement).focus(); }); fixture.componentInstance.trigger.openMenu(); diff --git a/src/material/menu/menu.spec.ts b/src/material/menu/menu.spec.ts index 8dc06ec502f4..1f312c6dd04d 100644 --- a/src/material/menu/menu.spec.ts +++ b/src/material/menu/menu.spec.ts @@ -1147,6 +1147,7 @@ describe('MatMenu', () => { fixture.detectChanges(); fixture.componentInstance.trigger.menuOpened.subscribe(() => { + flush(); (document.querySelectorAll('.mat-menu-panel [mat-menu-item]')[3] as HTMLElement).focus(); }); fixture.componentInstance.trigger.openMenu(); diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts index fcf0d2fa54d5..baf9d2a34ade 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -391,42 +391,32 @@ export class _MatMenuBase * @param origin Action from which the focus originated. Used to set the correct styling. */ focusFirstItem(origin: FocusOrigin = 'program'): void { - // When the content is rendered lazily, it takes a bit before the items are inside the DOM. - if (this.lazyContent) { - this._ngZone.onStable.pipe(take(1)).subscribe(() => this._focusFirstItem(origin)); - } else { - this._focusFirstItem(origin); - } - } - - /** - * Actual implementation that focuses the first item. Needs to be separated - * out so we don't repeat the same logic in the public `focusFirstItem` method. - */ - private _focusFirstItem(origin: FocusOrigin) { - const manager = this._keyManager; + // Wait for `onStable` to ensure iOS VoiceOver screen reader focuses the first item (#24735). + this._ngZone.onStable.pipe(take(1)).subscribe(() => { + const manager = this._keyManager; - manager.setFocusOrigin(origin).setFirstItemActive(); - - // If there's no active item at this point, it means that all the items are disabled. - // Move focus to the menu panel so keyboard events like Escape still work. Also this will - // give _some_ feedback to screen readers. - if (!manager.activeItem && this._directDescendantItems.length) { - let element = this._directDescendantItems.first!._getHostElement().parentElement; - - // Because the `mat-menu` is at the DOM insertion point, not inside the overlay, we don't - // have a nice way of getting a hold of the menu panel. We can't use a `ViewChild` either - // because the panel is inside an `ng-template`. We work around it by starting from one of - // the items and walking up the DOM. - while (element) { - if (element.getAttribute('role') === 'menu') { - element.focus(); - break; - } else { - element = element.parentElement; + manager.setFocusOrigin(origin).setFirstItemActive(); + + // If there's no active item at this point, it means that all the items are disabled. + // Move focus to the menu panel so keyboard events like Escape still work. Also this will + // give _some_ feedback to screen readers. + if (!manager.activeItem && this._directDescendantItems.length) { + let element = this._directDescendantItems.first!._getHostElement().parentElement; + + // Because the `mat-menu` is at the DOM insertion point, not inside the overlay, we don't + // have a nice way of getting a hold of the menu panel. We can't use a `ViewChild` either + // because the panel is inside an `ng-template`. We work around it by starting from one of + // the items and walking up the DOM. + while (element) { + if (element.getAttribute('role') === 'menu') { + element.focus(); + break; + } else { + element = element.parentElement; + } } } - } + }); } /**