Skip to content

Commit 7571403

Browse files
committed
fix(material/menu): Do not open the menu when trigger is aria-disabled (properly handle disabledInteractive)
1 parent ef53e73 commit 7571403

4 files changed

Lines changed: 84 additions & 13 deletions

File tree

src/dev-app/menu/menu-demo.html

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,7 @@
105105
</mat-menu>
106106
</div>
107107
<div class="demo-menu-section">
108-
<p>
109-
Position x: before
110-
</p>
108+
<p>Position x: before</p>
111109
<mat-toolbar class="demo-end-icon">
112110
<button matIconButton [matMenuTriggerFor]="posXMenu" aria-label="Open x-positioned menu">
113111
<mat-icon>more_vert</mat-icon>
@@ -124,9 +122,7 @@
124122
</mat-menu>
125123
</div>
126124
<div class="demo-menu-section">
127-
<p>
128-
Position y: above
129-
</p>
125+
<p>Position y: above</p>
130126
<mat-toolbar>
131127
<button matIconButton [matMenuTriggerFor]="posYMenu" aria-label="Open y-positioned menu">
132128
<mat-icon>more_vert</mat-icon>
@@ -158,9 +154,7 @@
158154
</mat-menu>
159155
</div>
160156
<div class="demo-menu-section">
161-
<p>
162-
Position x: before, overlapTrigger: true
163-
</p>
157+
<p>Position x: before, overlapTrigger: true</p>
164158
<mat-toolbar class="demo-end-icon">
165159
<button matIconButton [mat-menu-trigger-for]="posXMenuOverlay">
166160
<mat-icon>more_vert</mat-icon>
@@ -177,9 +171,7 @@
177171
</mat-menu>
178172
</div>
179173
<div class="demo-menu-section">
180-
<p>
181-
Position y: above, overlapTrigger: true
182-
</p>
174+
<p>Position y: above, overlapTrigger: true</p>
183175
<mat-toolbar>
184176
<button matIconButton [mat-menu-trigger-for]="posYMenuOverlay">
185177
<mat-icon>more_vert</mat-icon>
@@ -192,6 +184,39 @@
192184
}
193185
</mat-menu>
194186
</div>
187+
<div class="demo-menu-section">
188+
<p>disabledInteractive (should not open)</p>
189+
<mat-toolbar>
190+
<button
191+
disabled
192+
[disabledInteractive]="true"
193+
matIconButton
194+
[mat-menu-trigger-for]="disabledInteractiveMenu"
195+
>
196+
<mat-icon>more_vert</mat-icon>
197+
</button>
198+
</mat-toolbar>
199+
200+
<mat-menu #disabledInteractiveMenu="matMenu">
201+
@for (item of items; track item) {
202+
<button mat-menu-item [disabled]="item.disabled">{{ item.text }}</button>
203+
}
204+
</mat-menu>
205+
</div>
206+
<div class="demo-menu-section">
207+
<p>vanilla button disabled binding</p>
208+
<mat-toolbar>
209+
<button [disabled]="true" [mat-menu-trigger-for]="disabledInteractiveMenu">
210+
<mat-icon>more_vert</mat-icon>
211+
</button>
212+
</mat-toolbar>
213+
214+
<mat-menu #disabledInteractiveMenu="matMenu">
215+
@for (item of items; track item) {
216+
<button mat-menu-item [disabled]="item.disabled">{{ item.text }}</button>
217+
}
218+
</mat-menu>
219+
</div>
195220
</div>
196221

197222
<div class="demo-context-menu-area" [matContextMenuTriggerFor]="contextMenu">

src/material/menu/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ ng_project(
111111
"//src/cdk/overlay",
112112
"//src/cdk/scrolling",
113113
"//src/cdk/testing/private",
114+
"//src/material/button",
114115
"//src/material/core",
115116
],
116117
)

src/material/menu/menu-trigger-base.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '@angular/cdk/overlay';
2323
import {TemplatePortal} from '@angular/cdk/portal';
2424
import {
25+
booleanAttribute,
2526
ChangeDetectorRef,
2627
Directive,
2728
ElementRef,
@@ -191,6 +192,10 @@ export abstract class MatMenuTriggerBase implements OnDestroy {
191192

192193
/** Internal method to open menu providing option to auto focus on first item. */
193194
protected _openMenu(autoFocus: boolean): void {
195+
if (this._triggerIsAriaDisabled()) {
196+
return;
197+
}
198+
194199
const menu = this._menu;
195200

196201
if (this._menuOpen || !menu) {
@@ -477,4 +482,12 @@ export abstract class MatMenuTriggerBase implements OnDestroy {
477482
private _ownsMenu(menu: MatMenuPanel): boolean {
478483
return PANELS_TO_TRIGGERS.get(menu) === this;
479484
}
485+
486+
/**
487+
* Detect if the trigger element is aria-disabled, indicating it should behave as
488+
* disabled and not open the menu.
489+
*/
490+
private _triggerIsAriaDisabled() {
491+
return booleanAttribute(this._element.nativeElement.getAttribute('aria-disabled'));
492+
}
480493
}

src/material/menu/menu.spec.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
provideFakeDirectionality,
4343
} from '../../cdk/testing/private';
4444
import {MATERIAL_ANIMATIONS, MatRipple} from '../core';
45+
import {MatButton} from '@angular/material/button';
4546
import {MatMenu, MatMenuItem} from './index';
4647
import {
4748
MAT_MENU_DEFAULT_OPTIONS,
@@ -1279,6 +1280,23 @@ describe('MatMenu', () => {
12791280
}));
12801281
});
12811282

1283+
it('does not open if the trigger element is disabled (including disabledInteractive)', fakeAsync(() => {
1284+
const fixture = TestBed.createComponent(DisabledMenu);
1285+
fixture.detectChanges();
1286+
1287+
const trigger = fixture.componentInstance.triggerEl.nativeElement;
1288+
trigger.click();
1289+
fixture.detectChanges();
1290+
tick(500);
1291+
expect(overlayContainerElement.querySelector('.mat-mdc-menu-panel [mat-menu-item]')).toBeNull();
1292+
1293+
dispatchKeyboardEvent(trigger, 'keydown', ENTER);
1294+
trigger.click();
1295+
fixture.detectChanges();
1296+
tick(500);
1297+
expect(overlayContainerElement.querySelector('.mat-mdc-menu-panel [mat-menu-item]')).toBeNull();
1298+
}));
1299+
12821300
describe('positions', () => {
12831301
let fixture: ComponentFixture<PositionedMenu>;
12841302
let trigger: HTMLElement;
@@ -2634,6 +2652,20 @@ class SimpleMenu {
26342652
})
26352653
class SimpleMenuOnPush extends SimpleMenu {}
26362654

2655+
@Component({
2656+
template: `
2657+
<button mat-button disabled [disabledInteractive]="true"
2658+
[matMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
2659+
<mat-menu #menu="matMenu">
2660+
<button mat-menu-item> Action! </button>
2661+
</mat-menu>
2662+
`,
2663+
imports: [MatButton, MatMenuTrigger, MatMenu, MatMenuItem],
2664+
})
2665+
class DisabledMenu {
2666+
@ViewChild('triggerEl', {read: ElementRef}) triggerEl!: ElementRef<HTMLElement>;
2667+
}
2668+
26372669
@Component({
26382670
template: `
26392671
<button [matMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
@@ -2666,7 +2698,7 @@ interface TestableMenu {
26662698
class OverlapMenu implements TestableMenu {
26672699
@Input() overlapTrigger: boolean = false;
26682700
@ViewChild(MatMenuTrigger) trigger!: MatMenuTrigger;
2669-
@ViewChild('triggerEl') triggerEl!: ElementRef<HTMLElement>;
2701+
@ViewChild('triggerEl', {read: ElementRef}) triggerEl!: ElementRef<HTMLElement>;
26702702
}
26712703

26722704
@Component({

0 commit comments

Comments
 (0)