diff --git a/src/material/menu/menu.spec.ts b/src/material/menu/menu.spec.ts
index 67c4bb1908dd..ad7e4e9ac92d 100644
--- a/src/material/menu/menu.spec.ts
+++ b/src/material/menu/menu.spec.ts
@@ -200,6 +200,26 @@ describe('MatMenu', () => {
expect(document.activeElement).toBe(triggerEl);
});
+ it('should move focus to another item if the active item is destroyed', fakeAsync(() => {
+ const fixture = createComponent(MenuWithRepeatedItems, [], [FakeIcon]);
+ fixture.detectChanges();
+ const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
+
+ triggerEl.click();
+ fixture.detectChanges();
+ tick(500);
+
+ const items = overlayContainerElement.querySelectorAll('.mat-menu-panel .mat-menu-item');
+
+ expect(document.activeElement).toBe(items[0]);
+
+ fixture.componentInstance.items.shift();
+ fixture.detectChanges();
+ tick(500);
+
+ expect(document.activeElement).toBe(items[1]);
+ }));
+
it('should be able to set a custom class on the backdrop', fakeAsync(() => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
@@ -257,6 +277,7 @@ describe('MatMenu', () => {
// Add 50 items to make the menu scrollable
fixture.componentInstance.extraItems = new Array(50).fill('Hello there');
fixture.detectChanges();
+ tick(50);
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
dispatchFakeEvent(triggerEl, 'mousedown');
@@ -2392,7 +2413,6 @@ class DynamicPanelMenu {
@ViewChild('two', {static: false}) secondMenu: MatMenu;
}
-
@Component({
template: `
@@ -2447,3 +2467,18 @@ class LazyMenuWithOnPush {
@ViewChild('triggerEl', {static: false, read: ElementRef}) rootTrigger: ElementRef;
@ViewChild('menuItem', {static: false, read: ElementRef}) menuItemWithSubmenu: ElementRef;
}
+
+@Component({
+ template: `
+
+
+
+
+ `
+})
+class MenuWithRepeatedItems {
+ @ViewChild(MatMenuTrigger, {static: false}) trigger: MatMenuTrigger;
+ @ViewChild('triggerEl', {static: false}) triggerEl: ElementRef;
+ @ViewChild(MatMenu, {static: false}) menu: MatMenu;
+ items = ['One', 'Two', 'Three'];
+}
diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts
index e1b5c17b9cd2..5aa2b6997909 100644
--- a/src/material/menu/menu.ts
+++ b/src/material/menu/menu.ts
@@ -41,7 +41,7 @@ import {
OnInit,
} from '@angular/core';
import {merge, Observable, Subject, Subscription} from 'rxjs';
-import {startWith, switchMap, take} from 'rxjs/operators';
+import {startWith, switchMap, take, debounceTime} from 'rxjs/operators';
import {matMenuAnimations} from './menu-animations';
import {MatMenuContent} from './menu-content';
import {MenuPositionX, MenuPositionY} from './menu-positions';
@@ -250,6 +250,28 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel
this._updateDirectDescendants();
this._keyManager = new FocusKeyManager(this._directDescendantItems).withWrap().withTypeAhead();
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.closed.emit('tab'));
+
+ // TODO(crisbeto): the `debounce` here should be removed since it's something
+ // that people have to flush in their tests. It'll be possible once we switch back
+ // to using a QueryList in #11720.
+ this._ngZone.runOutsideAngular(() => {
+ // Move focus to another item, if the active item is removed from the list.
+ // We need to debounce the callback, because multiple items might be removed
+ // in quick succession.
+ this._directDescendantItems.changes.pipe(debounceTime(50)).subscribe(items => {
+ const manager = this._keyManager;
+
+ if (manager.activeItem && items.indexOf(manager.activeItem) === -1) {
+ const index = Math.max(0, Math.min(items.length - 1, manager.activeItemIndex || 0));
+
+ if (items[index] && !items[index].disabled) {
+ manager.setActiveItem(index);
+ } else {
+ manager.setNextItemActive();
+ }
+ }
+ });
+ });
}
ngOnDestroy() {