Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion goldens/material/menu/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
constructor();
_checkDisabled(event: Event): void;
disabled: boolean;
disabledInteractive: boolean;
disableRipple: boolean;
focus(origin?: FocusOrigin, options?: FocusOptions): void;
readonly _focused: Subject<MatMenuItem>;
_getAriaDisabled(): boolean | null;
_getDisabled(): boolean | null;
_getHostElement(): HTMLElement;
getLabel(): string;
_getTabIndex(): string;
Expand All @@ -184,8 +187,12 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
_highlighted: boolean;
readonly _hovered: Subject<MatMenuItem>;
// (undocumented)
_isAnchor: boolean;
// (undocumented)
static ngAcceptInputType_disabled: unknown;
// (undocumented)
static ngAcceptInputType_disabledInteractive: unknown;
// (undocumented)
static ngAcceptInputType_disableRipple: unknown;
// (undocumented)
ngAfterViewInit(): void;
Expand All @@ -200,7 +207,7 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
_setTriggersSubmenu(triggersSubmenu: boolean): void;
_triggersSubmenu: boolean;
// (undocumented)
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>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatMenuItem, "[mat-menu-item]", ["matMenuItem"], { "role": { "alias": "role"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "disabledInteractive": { "alias": "disabledInteractive"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; }, {}, never, ["mat-icon, [matMenuItemIcon]", "*"], true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatMenuItem, never>;
}
Expand Down
31 changes: 28 additions & 3 deletions src/material/menu/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
'[class.mat-mdc-menu-item-highlighted]': '_highlighted',
'[class.mat-mdc-menu-item-submenu-trigger]': '_triggersSubmenu',
'[attr.tabindex]': '_getTabIndex()',
'[attr.aria-disabled]': 'disabled',
'[attr.disabled]': 'disabled || null',
'[attr.aria-disabled]': '_getAriaDisabled()',
'[attr.disabled]': '_getDisabled()',
'[class.mat-mdc-menu-item-disabled]': 'disabled',
'[class.mat-mdc-menu-item-disabled-interactive]': 'disabledInteractive',
'(click)': '_checkDisabled($event)',
'(mouseenter)': '_handleMouseEnter()',
},
Expand All @@ -47,6 +49,7 @@ import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
imports: [MatRipple],
})
export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
_isAnchor: boolean;
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private _document = inject(DOCUMENT);
private _focusMonitor = inject(FocusMonitor);
Expand All @@ -59,6 +62,13 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
/** Whether the menu item is disabled. */
@Input({transform: booleanAttribute}) disabled: boolean = false;

/**
* Natively disabled menu items prevent focus and any pointer events from reaching the item.
* In some scenarios this might not be desirable, because it can prevent users from finding out
* why the item is disabled (e.g. via tooltip).
*/
@Input({transform: booleanAttribute}) disabledInteractive: boolean = false;

/** Whether ripples are disabled on the menu item. */
@Input({transform: booleanAttribute}) disableRipple: boolean = false;

Expand All @@ -77,6 +87,7 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {
constructor() {
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
this._parentMenu?.addItem?.(this);
this._isAnchor = this._elementRef.nativeElement.tagName === 'A';
}

/** Focuses the menu item. */
Expand Down Expand Up @@ -114,7 +125,21 @@ export class MatMenuItem implements FocusableOption, AfterViewInit, OnDestroy {

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

/** Gets the `aria-disabled` value for the menu item. */
_getAriaDisabled(): boolean | null {
if (this._isAnchor) {
return this.disabled || null;
}

return this.disabled && this.disabledInteractive ? true : null;
}

/** Gets the `disabled` attribute value for the menu item. */
_getDisabled(): boolean | null {
return this.disabledInteractive || !this.disabled ? null : true;
}

/** Returns the host DOM element. */
Expand Down
3 changes: 2 additions & 1 deletion src/material/menu/menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ mat-menu {
color: token-utils.slot(menu-item-icon-color, $fallbacks);
}

&[disabled] {
&[disabled],
&.mat-mdc-menu-item-disabled {
cursor: default;
opacity: 0.38;

Expand Down
38 changes: 38 additions & 0 deletions src/material/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,28 @@ describe('MatMenu', () => {

expect(ripple.disabled).toBe(true);
});

it('should set disabled interactive classes and attributes', () => {
const fixture = TestBed.createComponent(DisabledInteractiveMenuItem);
fixture.detectChanges();

fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();

const items = fixture.debugElement.queryAll(By.css('.mat-mdc-menu-item'));
const disabledNativeItem = items[0].nativeElement;
const disabledInteractiveItem = items[1].nativeElement;

expect(disabledNativeItem.hasAttribute('disabled')).toBe(true);
expect(disabledNativeItem.getAttribute('aria-disabled')).toBeNull();
expect(disabledNativeItem.classList).toContain('mat-mdc-menu-item-disabled');
expect(disabledNativeItem.classList).not.toContain('mat-mdc-menu-item-disabled-interactive');

expect(disabledInteractiveItem.hasAttribute('disabled')).toBe(false);
expect(disabledInteractiveItem.getAttribute('aria-disabled')).toBe('true');
expect(disabledInteractiveItem.classList).toContain('mat-mdc-menu-item-disabled');
expect(disabledInteractiveItem.classList).toContain('mat-mdc-menu-item-disabled-interactive');
});
});

describe('close event', () => {
Expand Down Expand Up @@ -2554,6 +2576,22 @@ class DisabledMenu {
@ViewChild('triggerEl', {read: ElementRef}) triggerEl!: ElementRef<HTMLElement>;
}

@Component({
template: `
<button [matMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
<mat-menu #menu="matMenu">
<button mat-menu-item disabled> Native disabled </button>
<button mat-menu-item disabled [disabledInteractive]="true"> Disabled Interactive </button>
</mat-menu>
`,
imports: [MatMenuTrigger, MatMenu, MatMenuItem],
changeDetection: ChangeDetectionStrategy.Eager,
})
class DisabledInteractiveMenuItem {
@ViewChild(MatMenuTrigger) trigger!: MatMenuTrigger;
@ViewChild('triggerEl', {read: ElementRef}) triggerEl!: ElementRef<HTMLElement>;
}

@Component({
template: `
<button [matMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
Expand Down
7 changes: 5 additions & 2 deletions src/material/menu/testing/menu-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,11 @@ export class MatMenuItemHarness extends ContentContainerComponentHarness<string>

/** Whether the menu is disabled. */
async isDisabled(): Promise<boolean> {
const disabled = (await this.host()).getAttribute('disabled');
return coerceBooleanProperty(await disabled);
const host = await this.host();
return (
coerceBooleanProperty(await host.getAttribute('disabled')) ||
(await host.hasClass('mat-mdc-menu-item-disabled'))
);
}

/** Gets the text of the menu item. */
Expand Down
Loading