Skip to content

Commit 017728a

Browse files
crisbetojelbourn
authored andcommitted
fix(menu): keyboard controls not working if all items are disabled inside lazy content (#17407)
Fixes the menu's keyboard controls not working if it uses lazy content and all of the items are disabled. We did a similar fix in #16572, but I hadn't accounted for the fact that the items come in asynchronously when lazy content is used. Fixes #17400.
1 parent c69a727 commit 017728a

File tree

3 files changed

+72
-4
lines changed

3 files changed

+72
-4
lines changed

src/material-experimental/mdc-menu/menu.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,17 @@ describe('MDC-based MatMenu', () => {
828828
.toBe(overlayContainerElement.querySelector('.mat-mdc-menu-panel'));
829829
}));
830830

831+
it('should focus the menu panel if all items are disabled inside lazy content', fakeAsync(() => {
832+
const fixture = createComponent(SimpleMenuWithRepeaterInLazyContent, [], [FakeIcon]);
833+
fixture.componentInstance.items.forEach(item => item.disabled = true);
834+
fixture.detectChanges();
835+
fixture.componentInstance.trigger.openMenu();
836+
fixture.detectChanges();
837+
838+
expect(document.activeElement)
839+
.toBe(overlayContainerElement.querySelector('.mat-mdc-menu-panel'));
840+
}));
841+
831842
describe('lazy rendering', () => {
832843
it('should be able to render the menu content lazily', fakeAsync(() => {
833844
const fixture = createComponent(SimpleLazyMenu);
@@ -2445,3 +2456,23 @@ class SimpleMenuWithRepeater {
24452456
@ViewChild(MatMenu) menu: MatMenu;
24462457
items = [{label: 'Pizza', disabled: false}, {label: 'Pasta', disabled: false}];
24472458
}
2459+
2460+
2461+
@Component({
2462+
template: `
2463+
<button [matMenuTriggerFor]="menu">Toggle menu</button>
2464+
<mat-menu #menu="matMenu">
2465+
<ng-template matMenuContent>
2466+
<button
2467+
*ngFor="let item of items"
2468+
[disabled]="item.disabled"
2469+
mat-menu-item>{{item.label}}</button>
2470+
</ng-template>
2471+
</mat-menu>
2472+
`
2473+
})
2474+
class SimpleMenuWithRepeaterInLazyContent {
2475+
@ViewChild(MatMenuTrigger, {static: false}) trigger: MatMenuTrigger;
2476+
@ViewChild(MatMenu, {static: false}) menu: MatMenu;
2477+
items = [{label: 'Pizza', disabled: false}, {label: 'Pasta', disabled: false}];
2478+
}

src/material/menu/menu.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,16 @@ describe('MatMenu', () => {
801801
expect(document.activeElement).toBe(overlayContainerElement.querySelector('.mat-menu-panel'));
802802
}));
803803

804+
it('should focus the menu panel if all items are disabled inside lazy content', fakeAsync(() => {
805+
const fixture = createComponent(SimpleMenuWithRepeaterInLazyContent, [], [FakeIcon]);
806+
fixture.componentInstance.items.forEach(item => item.disabled = true);
807+
fixture.detectChanges();
808+
fixture.componentInstance.trigger.openMenu();
809+
fixture.detectChanges();
810+
811+
expect(document.activeElement).toBe(overlayContainerElement.querySelector('.mat-menu-panel'));
812+
}));
813+
804814
describe('lazy rendering', () => {
805815
it('should be able to render the menu content lazily', fakeAsync(() => {
806816
const fixture = createComponent(SimpleLazyMenu);
@@ -2435,6 +2445,25 @@ class SimpleMenuWithRepeater {
24352445
items = [{label: 'Pizza', disabled: false}, {label: 'Pasta', disabled: false}];
24362446
}
24372447

2448+
@Component({
2449+
template: `
2450+
<button [matMenuTriggerFor]="menu">Toggle menu</button>
2451+
<mat-menu #menu="matMenu">
2452+
<ng-template matMenuContent>
2453+
<button
2454+
*ngFor="let item of items"
2455+
[disabled]="item.disabled"
2456+
mat-menu-item>{{item.label}}</button>
2457+
</ng-template>
2458+
</mat-menu>
2459+
`
2460+
})
2461+
class SimpleMenuWithRepeaterInLazyContent {
2462+
@ViewChild(MatMenuTrigger, {static: false}) trigger: MatMenuTrigger;
2463+
@ViewChild(MatMenu, {static: false}) menu: MatMenu;
2464+
items = [{label: 'Pizza', disabled: false}, {label: 'Pasta', disabled: false}];
2465+
}
2466+
24382467
@Component({
24392468
template: `
24402469
<button [matMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>

src/material/menu/menu.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,16 +328,24 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
328328
* @param origin Action from which the focus originated. Used to set the correct styling.
329329
*/
330330
focusFirstItem(origin: FocusOrigin = 'program'): void {
331-
const manager = this._keyManager;
332-
333331
// When the content is rendered lazily, it takes a bit before the items are inside the DOM.
334332
if (this.lazyContent) {
335333
this._ngZone.onStable.asObservable()
336334
.pipe(take(1))
337-
.subscribe(() => manager.setFocusOrigin(origin).setFirstItemActive());
335+
.subscribe(() => this._focusFirstItem(origin));
338336
} else {
339-
manager.setFocusOrigin(origin).setFirstItemActive();
337+
this._focusFirstItem(origin);
340338
}
339+
}
340+
341+
/**
342+
* Actual implementation that focuses the first item. Needs to be separated
343+
* out so we don't repeat the same logic in the public `focusFirstItem` method.
344+
*/
345+
private _focusFirstItem(origin: FocusOrigin) {
346+
const manager = this._keyManager;
347+
348+
manager.setFocusOrigin(origin).setFirstItemActive();
341349

342350
// If there's no active item at this point, it means that all the items are disabled.
343351
// Move focus to the menu panel so keyboard events like Escape still work. Also this will

0 commit comments

Comments
 (0)