Skip to content

Commit 80b9481

Browse files
committed
feat(material/menu): enhance menu item component with interactive disabled state
In menu-item component add an option `disabledInteractive` to interact with menu item in disabled state similar to `mat-button`, `mat-radio`, etc Fixes #29984
1 parent 87f0621 commit 80b9481

File tree

9 files changed

+178
-45
lines changed

9 files changed

+178
-45
lines changed

Diff for: goldens/material/menu/index.api.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
151151
constructor(...args: unknown[]);
152152
_checkDisabled(event: Event): void;
153153
disabled: boolean;
154+
disabledInteractive: boolean;
154155
disableRipple: boolean;
155156
focus(origin?: FocusOrigin, options?: FocusOptions): void;
156157
readonly _focused: Subject<MatMenuItem>;
@@ -165,6 +166,8 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
165166
// (undocumented)
166167
static ngAcceptInputType_disabled: unknown;
167168
// (undocumented)
169+
static ngAcceptInputType_disabledInteractive: unknown;
170+
// (undocumented)
168171
static ngAcceptInputType_disableRipple: unknown;
169172
// (undocumented)
170173
ngAfterViewInit(): void;
@@ -179,7 +182,7 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
179182
_setTriggersSubmenu(triggersSubmenu: boolean): void;
180183
_triggersSubmenu: boolean;
181184
// (undocumented)
182-
static ɵcmp: i0.ɵɵComponentDeclaration<MatMenuItem, "[mat-menu-item]", ["matMenuItem"], { "role": { "alias": "role"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; }, {}, never, ["mat-icon, [matMenuItemIcon]", "*"], true, never>;
185+
static ɵcmp: i0.ɵɵComponentDeclaration<MatMenuItem, "[mat-menu-item]", ["matMenuItem"], { "role": { "alias": "role"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "disabledInteractive": { "alias": "disabledInteractive"; "required": false; }; }, {}, never, ["mat-icon, [matMenuItemIcon]", "*"], true, never>;
183186
// (undocumented)
184187
static ɵfac: i0.ɵɵFactoryDeclaration<MatMenuItem, never>;
185188
}

Diff for: src/dev-app/menu/BUILD.bazel

+2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ ng_project(
1313
deps = [
1414
"//:node_modules/@angular/core",
1515
"//src/material/button",
16+
"//src/material/checkbox",
1617
"//src/material/divider",
1718
"//src/material/icon",
1819
"//src/material/menu",
1920
"//src/material/toolbar",
21+
"//src/material/tooltip",
2022
],
2123
)
2224

Diff for: src/dev-app/menu/menu-demo.html

+50-20
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010

1111
<mat-menu #menu="matMenu">
1212
@for (item of items; track item) {
13-
<button mat-menu-item (click)="select(item.text)" [disabled]="item.disabled">
13+
<button
14+
mat-menu-item
15+
(click)="select(item.text)"
16+
[disabled]="item.disabled"
17+
[disabledInteractive]="disabledInteractive"
18+
[matTooltip]="item.tooltipText">
1419
{{ item.text }}
1520
</button>
1621
}
@@ -27,7 +32,12 @@
2732

2833
<mat-menu #divider="matMenu">
2934
@for (item of items; track item) {
30-
<button mat-menu-item [disabled]="item.disabled">{{ item.text }}</button>
35+
<button
36+
mat-menu-item
37+
[disabled]="item.disabled"
38+
[disabledInteractive]="disabledInteractive">
39+
{{ item.text }}
40+
</button>
3141
@if (!$last) {
3242
<mat-divider></mat-divider>
3343
}
@@ -98,16 +108,18 @@
98108

99109
<mat-menu #anchorMenu="matMenu">
100110
@for (item of items; track item) {
101-
<a mat-menu-item href="https://www.google.com" [disabled]="item.disabled">
111+
<a
112+
mat-menu-item
113+
href="https://www.google.com"
114+
[disabled]="item.disabled"
115+
[disabledInteractive]="disabledInteractive">
102116
{{ item.text }}
103117
</a>
104118
}
105119
</mat-menu>
106120
</div>
107121
<div class="demo-menu-section">
108-
<p>
109-
Position x: before
110-
</p>
122+
<p>Position x: before</p>
111123
<mat-toolbar class="demo-end-icon">
112124
<button matIconButton [matMenuTriggerFor]="posXMenu" aria-label="Open x-positioned menu">
113125
<mat-icon>more_vert</mat-icon>
@@ -116,17 +128,18 @@
116128

117129
<mat-menu xPosition="before" #posXMenu="matMenu">
118130
@for (item of iconItems; track item) {
119-
<button mat-menu-item [disabled]="item.disabled">
131+
<button
132+
mat-menu-item
133+
[disabled]="item.disabled"
134+
[disabledInteractive]="disabledInteractive">
120135
<mat-icon>{{ item.icon }}</mat-icon>
121136
{{ item.text }}
122137
</button>
123138
}
124139
</mat-menu>
125140
</div>
126141
<div class="demo-menu-section">
127-
<p>
128-
Position y: above
129-
</p>
142+
<p>Position y: above</p>
130143
<mat-toolbar>
131144
<button matIconButton [matMenuTriggerFor]="posYMenu" aria-label="Open y-positioned menu">
132145
<mat-icon>more_vert</mat-icon>
@@ -135,7 +148,12 @@
135148

136149
<mat-menu yPosition="above" #posYMenu="matMenu">
137150
@for (item of items; track item) {
138-
<button mat-menu-item [disabled]="item.disabled">{{ item.text }}</button>
151+
<button
152+
mat-menu-item
153+
[disabled]="item.disabled"
154+
[disabledInteractive]="disabledInteractive">
155+
{{ item.text }}
156+
</button>
139157
}
140158
</mat-menu>
141159
</div>
@@ -153,14 +171,17 @@
153171

154172
<mat-menu [overlapTrigger]="true" #menuOverlay="matMenu">
155173
@for (item of items; track item) {
156-
<button mat-menu-item [disabled]="item.disabled">{{ item.text }}</button>
174+
<button
175+
mat-menu-item
176+
[disabled]="item.disabled"
177+
[disabledInteractive]="disabledInteractive">
178+
{{ item.text }}
179+
</button>
157180
}
158181
</mat-menu>
159182
</div>
160183
<div class="demo-menu-section">
161-
<p>
162-
Position x: before, overlapTrigger: true
163-
</p>
184+
<p>Position x: before, overlapTrigger: true</p>
164185
<mat-toolbar class="demo-end-icon">
165186
<button matIconButton [mat-menu-trigger-for]="posXMenuOverlay">
166187
<mat-icon>more_vert</mat-icon>
@@ -169,17 +190,18 @@
169190

170191
<mat-menu xPosition="before" [overlapTrigger]="true" #posXMenuOverlay="matMenu">
171192
@for (item of iconItems; track item) {
172-
<button mat-menu-item [disabled]="item.disabled">
193+
<button
194+
mat-menu-item
195+
[disabled]="item.disabled"
196+
[disabledInteractive]="disabledInteractive">
173197
<mat-icon>{{ item.icon }}</mat-icon>
174198
{{ item.text }}
175199
</button>
176200
}
177201
</mat-menu>
178202
</div>
179203
<div class="demo-menu-section">
180-
<p>
181-
Position y: above, overlapTrigger: true
182-
</p>
204+
<p>Position y: above, overlapTrigger: true</p>
183205
<mat-toolbar>
184206
<button matIconButton [mat-menu-trigger-for]="posYMenuOverlay">
185207
<mat-icon>more_vert</mat-icon>
@@ -188,10 +210,18 @@
188210

189211
<mat-menu yPosition="above" [overlapTrigger]="true" #posYMenuOverlay="matMenu">
190212
@for (item of items; track item) {
191-
<button mat-menu-item [disabled]="item.disabled">{{ item.text }}</button>
213+
<button
214+
mat-menu-item
215+
[disabled]="item.disabled"
216+
[disabledInteractive]="disabledInteractive">
217+
{{ item.text }}
218+
</button>
192219
}
193220
</mat-menu>
194221
</div>
195222
</div>
223+
<div>
224+
<mat-checkbox [(ngModel)]="disabledInteractive">Disabled interactive</mat-checkbox>
225+
</div>
196226

197227
<div style="height: 500px">This div is for testing scrolled menus.</div>

Diff for: src/dev-app/menu/menu-demo.ts

+25-3
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,53 @@
77
*/
88

99
import {ChangeDetectionStrategy, Component} from '@angular/core';
10+
import {FormsModule} from '@angular/forms';
1011
import {MatButtonModule} from '@angular/material/button';
12+
import {MatCheckboxModule} from '@angular/material/checkbox';
1113
import {MatDividerModule} from '@angular/material/divider';
1214
import {MatIconModule} from '@angular/material/icon';
1315
import {MatMenuModule} from '@angular/material/menu';
1416
import {MatToolbarModule} from '@angular/material/toolbar';
17+
import {MatTooltip} from '@angular/material/tooltip';
1518

1619
@Component({
1720
selector: 'menu-demo',
1821
templateUrl: 'menu-demo.html',
1922
styleUrl: 'menu-demo.css',
20-
imports: [MatMenuModule, MatButtonModule, MatToolbarModule, MatIconModule, MatDividerModule],
23+
imports: [
24+
FormsModule,
25+
MatButtonModule,
26+
MatCheckboxModule,
27+
MatDividerModule,
28+
MatIconModule,
29+
MatMenuModule,
30+
MatToolbarModule,
31+
MatTooltip,
32+
],
2133
changeDetection: ChangeDetectionStrategy.OnPush,
2234
})
2335
export class MenuDemo {
2436
selected = '';
37+
disabledInteractive = true;
38+
2539
items = [
2640
{text: 'Refresh'},
2741
{text: 'Settings'},
28-
{text: 'Help', disabled: true},
42+
{
43+
text: 'Help',
44+
disabled: true,
45+
tooltipText: 'This is a menu item tooltip!',
46+
},
2947
{text: 'Sign Out'},
3048
];
3149

3250
iconItems = [
3351
{text: 'Redial', icon: 'dialpad'},
34-
{text: 'Check voicemail', icon: 'voicemail', disabled: true},
52+
{
53+
text: 'Check voicemail',
54+
icon: 'voicemail',
55+
disabled: true,
56+
},
3557
{text: 'Disable alerts', icon: 'notifications_off'},
3658
];
3759

Diff for: src/material/menu/_m2-menu.scss

+15-10
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,31 @@ $prefix: (mat, menu);
2020
item-with-icon-leading-spacing: 16px,
2121
item-with-icon-trailing-spacing: 16px,
2222
container-elevation-shadow: elevation.get-box-shadow(8),
23-
2423
// Unused
25-
base-elevation-level: null,
24+
base-elevation-level: null
2625
);
2726
}
2827

2928
// Tokens that can be configured through Angular Material's color theming API.
3029
@function get-color-tokens($theme) {
3130
$is-dark: inspection.get-theme-type($theme) == dark;
32-
$active-state-layer-color: inspection.get-theme-color($theme, foreground, base,
33-
if($is-dark, 0.08, 0.04));
31+
$active-state-layer-color: inspection.get-theme-color(
32+
$theme,
33+
foreground,
34+
base,
35+
if($is-dark, 0.08, 0.04)
36+
);
37+
$disabled-background: inspection.get-theme-color($theme, foreground, disabled-button);
3438
$text-color: inspection.get-theme-color($theme, foreground, text);
3539

3640
@return (
3741
item-label-text-color: $text-color,
3842
item-icon-color: $text-color,
3943
item-hover-state-layer-color: $active-state-layer-color,
44+
item-disabled-hover-state-layer-color: $disabled-background,
4045
item-focus-state-layer-color: $active-state-layer-color,
4146
container-color: inspection.get-theme-color($theme, background, card),
42-
divider-color: inspection.get-theme-color($theme, foreground, divider),
47+
divider-color: inspection.get-theme-color($theme, foreground, divider)
4348
);
4449
}
4550

@@ -50,7 +55,7 @@ $prefix: (mat, menu);
5055
item-label-text-size: inspection.get-theme-typography($theme, body-1, font-size),
5156
item-label-text-tracking: inspection.get-theme-typography($theme, body-1, letter-spacing),
5257
item-label-text-line-height: inspection.get-theme-typography($theme, body-1, line-height),
53-
item-label-text-weight: inspection.get-theme-typography($theme, body-1, font-weight),
58+
item-label-text-weight: inspection.get-theme-typography($theme, body-1, font-weight)
5459
);
5560
}
5661

@@ -63,9 +68,9 @@ $prefix: (mat, menu);
6368
// This is used to create token slots.
6469
@function get-token-slots() {
6570
@return sass-utils.deep-merge-all(
66-
get-unthemable-tokens(),
67-
get-color-tokens(m2-utils.$placeholder-color-config),
68-
get-typography-tokens(m2-utils.$placeholder-typography-config),
69-
get-density-tokens(m2-utils.$placeholder-density-config)
71+
get-unthemable-tokens(),
72+
get-color-tokens(m2-utils.$placeholder-color-config),
73+
get-typography-tokens(m2-utils.$placeholder-typography-config),
74+
get-density-tokens(m2-utils.$placeholder-density-config)
7075
);
7176
}

Diff for: src/material/menu/_m3-menu.scss

+13-7
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,27 @@ $prefix: (mat, menu);
2323
item-icon-color: map.get($systems, md-sys-color, on-surface-variant),
2424
item-icon-size: m3-utils.hardcode(24px, $exclude-hardcoded),
2525
item-hover-state-layer-color: sass-utils.safe-color-change(
26-
map.get($systems, md-sys-color, on-surface),
27-
$alpha: map.get($systems, md-sys-state, hover-state-layer-opacity)
28-
),
26+
map.get($systems, md-sys-color, on-surface),
27+
$alpha: map.get($systems, md-sys-state, hover-state-layer-opacity)
28+
),
29+
item-disabled-hover-state-layer-color: sass-utils.safe-color-change(
30+
map.get($systems, md-sys-color, n-surface),
31+
$alpha: 0.38
32+
),
2933
item-focus-state-layer-color: sass-utils.safe-color-change(
30-
map.get($systems, md-sys-color, on-surface),
31-
$alpha: map.get($systems, md-sys-state, focus-state-layer-opacity)
32-
),
34+
map.get($systems, md-sys-color, on-surface),
35+
$alpha: map.get($systems, md-sys-state, focus-state-layer-opacity)
36+
),
3337
item-spacing: m3-utils.hardcode(12px, $exclude-hardcoded),
3438
item-leading-spacing: m3-utils.hardcode(12px, $exclude-hardcoded),
3539
item-trailing-spacing: m3-utils.hardcode(12px, $exclude-hardcoded),
3640
item-with-icon-leading-spacing: m3-utils.hardcode(12px, $exclude-hardcoded),
3741
item-with-icon-trailing-spacing: m3-utils.hardcode(12px, $exclude-hardcoded),
3842
container-color: map.get($systems, md-sys-color, surface-container),
3943
container-elevation-shadow: m3-utils.hardcode(
40-
elevation.get-box-shadow(2), $exclude-hardcoded),
44+
elevation.get-box-shadow(2),
45+
$exclude-hardcoded
46+
),
4147
)
4248
);
4349

Diff for: src/material/menu/menu-item.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
3737
'[class.mat-mdc-menu-item-highlighted]': '_highlighted',
3838
'[class.mat-mdc-menu-item-submenu-trigger]': '_triggersSubmenu',
3939
'[attr.tabindex]': '_getTabIndex()',
40-
'[attr.aria-disabled]': 'disabled',
40+
'[attr.aria-disabled]': 'disabled && disabledInteractive ? "true" : null',
4141
'[attr.disabled]': 'disabled || null',
42+
'[class.mat-mdc-menu-item-disabled-interactive]': 'disabledInteractive',
4243
'(click)': '_checkDisabled($event)',
4344
'(mouseenter)': '_handleMouseEnter()',
4445
},
@@ -63,6 +64,10 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
6364
/** Whether ripples are disabled on the menu item. */
6465
@Input({transform: booleanAttribute}) disableRipple: boolean = false;
6566

67+
/** Whether the menu item should remain interactive when it is disabled. */
68+
@Input({transform: booleanAttribute})
69+
disabledInteractive: boolean = false;
70+
6671
/** Stream that emits when the menu item is hovered. */
6772
readonly _hovered: Subject<MatMenuItem> = new Subject<MatMenuItem>();
6873

@@ -117,7 +122,7 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
117122

118123
/** Used to set the `tabindex`. */
119124
_getTabIndex(): string {
120-
return this.disabled ? '-1' : '0';
125+
return this.disabled && !this.disabledInteractive ? '-1' : '0';
121126
}
122127

123128
/** Returns the host DOM element. */
@@ -130,6 +135,7 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
130135
if (this.disabled) {
131136
event.preventDefault();
132137
event.stopPropagation();
138+
return;
133139
}
134140
}
135141

0 commit comments

Comments
 (0)