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() {