Skip to content

Commit 8ef4a77

Browse files
authored
fix(material-experimental/mdc-menu): implement increasing elevation (#22506)
Previously we didn't implement the feature where nested menus increase their elevation for each level, because we didn't have elevation classes based on MDC. Now that we do, we can implement the feature. Also adds an extra elevation class that allows us to increase the specificity over the default one from MDC.
1 parent f9cc564 commit 8ef4a77

File tree

7 files changed

+149
-28
lines changed

7 files changed

+149
-28
lines changed

scripts/check-mdc-tests-config.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,6 @@ export const config = {
113113
'should dispatch the selectionChange event when selecting via ctrl + a'
114114

115115
],
116-
'mdc-menu': [
117-
// Disabled since we don't have equivalents to our elevation classes in the MDC packages.
118-
'should not remove mat-elevation class from overlay when panelClass is changed',
119-
'should increase the sub-menu elevation based on its depth',
120-
'should update the elevation when the same menu is opened at a different depth',
121-
'should not increase the elevation if the user specified a custom one'
122-
],
123116
'mdc-progress-bar': [
124117
// These tests are verifying implementation details that are not relevant for MDC.
125118
'should return the transform attribute for bufferValue and mode',

src/material-experimental/mdc-core/_core-theme.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
// `mat-mdc-elevation-z$zValue` where `$zValue` corresponds to the z-space to which the
2222
// element is elevated.
2323
@for $zValue from 0 through 24 {
24-
.#{elevation.$prefix}#{$zValue} {
24+
$selector: elevation.$prefix + $zValue;
25+
// We need the `mat-mdc-elevation-specific`, because some MDC mixins
26+
// come with elevation baked in and we don't have a way of removing it.
27+
.#{$selector}, .mat-mdc-elevation-specific.#{$selector} {
2528
@include elevation.private-theme-elevation($zValue, $color);
2629
}
2730
}

src/material-experimental/mdc-menu/menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<ng-template>
22
<div
3-
class="mat-mdc-menu-panel mdc-menu-surface mdc-menu-surface--open"
3+
class="mat-mdc-menu-panel mdc-menu-surface mdc-menu-surface--open mat-mdc-elevation-specific"
44
[id]="panelId"
55
[ngClass]="_classList"
66
(keydown)="_handleKeydown($event)"

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

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,29 @@ describe('MDC-based MatMenu', () => {
532532
expect(panel.classList).toContain('custom-two');
533533
});
534534

535+
it('should not remove mat-elevation class from overlay when panelClass is changed', () => {
536+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
537+
538+
fixture.componentInstance.panelClass = 'custom-one';
539+
fixture.detectChanges();
540+
fixture.componentInstance.trigger.openMenu();
541+
fixture.detectChanges();
542+
543+
const panel = overlayContainerElement.querySelector('.mat-mdc-menu-panel')!;
544+
545+
expect(panel.classList).toContain('custom-one');
546+
expect(panel.classList).toContain('mat-mdc-elevation-z8');
547+
548+
fixture.componentInstance.panelClass = 'custom-two';
549+
fixture.detectChanges();
550+
551+
expect(panel.classList).not.toContain('custom-one');
552+
expect(panel.classList).toContain('custom-two');
553+
expect(panel.classList).toContain('mat-mdc-elevation-specific');
554+
expect(panel.classList)
555+
.toContain('mat-mdc-elevation-z8', 'Expected mat-mdc-elevation-z8 not to be removed');
556+
});
557+
535558
it('should set the "menu" role on the overlay panel', () => {
536559
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
537560
fixture.detectChanges();
@@ -2050,6 +2073,70 @@ describe('MDC-based MatMenu', () => {
20502073
expect(menuItems[1].classList).not.toContain('mat-mdc-menu-item-submenu-trigger');
20512074
});
20522075

2076+
it('should increase the sub-menu elevation based on its depth', () => {
2077+
compileTestComponent();
2078+
instance.rootTrigger.openMenu();
2079+
fixture.detectChanges();
2080+
2081+
instance.levelOneTrigger.openMenu();
2082+
fixture.detectChanges();
2083+
2084+
instance.levelTwoTrigger.openMenu();
2085+
fixture.detectChanges();
2086+
2087+
const menus = overlay.querySelectorAll('.mat-mdc-menu-panel');
2088+
2089+
expect(menus[0].classList).toContain('mat-mdc-elevation-specific');
2090+
expect(menus[0].classList)
2091+
.toContain('mat-mdc-elevation-z8', 'Expected root menu to have base elevation.');
2092+
2093+
expect(menus[1].classList).toContain('mat-mdc-elevation-specific');
2094+
expect(menus[1].classList)
2095+
.toContain('mat-mdc-elevation-z9', 'Expected first sub-menu to have base elevation + 1.');
2096+
2097+
expect(menus[2].classList).toContain('mat-mdc-elevation-specific');
2098+
expect(menus[2].classList)
2099+
.toContain('mat-mdc-elevation-z10', 'Expected second sub-menu to have base elevation + 2.');
2100+
});
2101+
2102+
it('should update the elevation when the same menu is opened at a different depth',
2103+
fakeAsync(() => {
2104+
compileTestComponent();
2105+
instance.rootTrigger.openMenu();
2106+
fixture.detectChanges();
2107+
2108+
instance.levelOneTrigger.openMenu();
2109+
fixture.detectChanges();
2110+
2111+
instance.levelTwoTrigger.openMenu();
2112+
fixture.detectChanges();
2113+
2114+
let lastMenu = overlay.querySelectorAll('.mat-mdc-menu-panel')[2];
2115+
2116+
expect(lastMenu.classList).toContain('mat-mdc-elevation-specific');
2117+
expect(lastMenu.classList)
2118+
.toContain('mat-mdc-elevation-z10', 'Expected menu to have the base elevation plus two.');
2119+
2120+
(overlay.querySelector('.cdk-overlay-backdrop')! as HTMLElement).click();
2121+
fixture.detectChanges();
2122+
tick(500);
2123+
2124+
expect(overlay.querySelectorAll('.mat-mdc-menu-panel').length)
2125+
.toBe(0, 'Expected no open menus');
2126+
2127+
instance.alternateTrigger.openMenu();
2128+
fixture.detectChanges();
2129+
tick(500);
2130+
2131+
lastMenu = overlay.querySelector('.mat-mdc-menu-panel') as HTMLElement;
2132+
2133+
expect(lastMenu.classList).toContain('mat-mdc-elevation-specific');
2134+
expect(lastMenu.classList).not.toContain('mat-mdc-elevation-z10',
2135+
'Expected menu not to maintain old elevation.');
2136+
expect(lastMenu.classList).toContain('mat-mdc-elevation-z8',
2137+
'Expected menu to have the proper updated elevation.');
2138+
}));
2139+
20532140
it('should not change focus origin if origin not specified for trigger', fakeAsync(() => {
20542141
compileTestComponent();
20552142

@@ -2067,6 +2154,26 @@ describe('MDC-based MatMenu', () => {
20672154
expect(levelTwoTrigger.classList).toContain('cdk-mouse-focused');
20682155
}));
20692156

2157+
it('should not increase the elevation if the user specified a custom one', () => {
2158+
const elevationFixture = createComponent(NestedMenuCustomElevation);
2159+
2160+
elevationFixture.detectChanges();
2161+
elevationFixture.componentInstance.rootTrigger.openMenu();
2162+
elevationFixture.detectChanges();
2163+
2164+
elevationFixture.componentInstance.levelOneTrigger.openMenu();
2165+
elevationFixture.detectChanges();
2166+
2167+
const menuClasses =
2168+
overlayContainerElement.querySelectorAll('.mat-mdc-menu-panel')[1].classList;
2169+
2170+
expect(menuClasses).toContain('mat-mdc-elevation-specific');
2171+
expect(menuClasses)
2172+
.toContain('mat-mdc-elevation-z24', 'Expected user elevation to be maintained');
2173+
expect(menuClasses)
2174+
.not.toContain('mat-mdc-elevation-z8', 'Expected no stacked elevation.');
2175+
});
2176+
20702177
it('should close all of the menus when the root is closed programmatically', fakeAsync(() => {
20712178
compileTestComponent();
20722179
instance.rootTrigger.openMenu();
@@ -2487,6 +2594,26 @@ class NestedMenu {
24872594
showLazy = false;
24882595
}
24892596

2597+
@Component({
2598+
template: `
2599+
<button [matMenuTriggerFor]="root" #rootTrigger="matMenuTrigger">Toggle menu</button>
2600+
2601+
<mat-menu #root="matMenu">
2602+
<button mat-menu-item
2603+
[matMenuTriggerFor]="levelOne"
2604+
#levelOneTrigger="matMenuTrigger">One</button>
2605+
</mat-menu>
2606+
2607+
<mat-menu #levelOne="matMenu" class="mat-mdc-elevation-z24">
2608+
<button mat-menu-item>Two</button>
2609+
</mat-menu>
2610+
`
2611+
})
2612+
class NestedMenuCustomElevation {
2613+
@ViewChild('rootTrigger') rootTrigger: MatMenuTrigger;
2614+
@ViewChild('levelOneTrigger') levelOneTrigger: MatMenuTrigger;
2615+
}
2616+
24902617

24912618
@Component({
24922619
template: `

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,12 @@ export const MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER: Provider = {
5858
]
5959
})
6060
export class MatMenu extends _MatMenuBase {
61+
protected _elevationPrefix = 'mat-mdc-elevation-z';
62+
protected _baseElevation = 8;
63+
6164
constructor(_elementRef: ElementRef<HTMLElement>,
6265
_ngZone: NgZone,
6366
@Inject(MAT_MENU_DEFAULT_OPTIONS) _defaultOptions: MatMenuDefaultOptions) {
6467
super(_elementRef, _ngZone, _defaultOptions);
6568
}
66-
67-
setElevation(_depth: number) {
68-
// TODO(crisbeto): MDC's styles come with elevation already and we haven't mapped our mixins
69-
// to theirs. Disable the elevation stacking for now until everything has been mapped.
70-
// The following unit tests should be re-enabled:
71-
// - should not remove mat-elevation class from overlay when panelClass is changed
72-
// - should increase the sub-menu elevation based on its depth
73-
// - should update the elevation when the same menu is opened at a different depth
74-
// - should not increase the elevation if the user specified a custom one
75-
}
7669
}

src/material/menu/menu.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,9 @@ export function MAT_MENU_DEFAULT_OPTIONS_FACTORY(): MatMenuDefaultOptions {
8585
backdropClass: 'cdk-overlay-transparent-backdrop',
8686
};
8787
}
88-
/**
89-
* Start elevation for the menu panel.
90-
* @docs-private
91-
*/
92-
const MAT_MENU_BASE_ELEVATION = 4;
9388

9489
let menuPanelUid = 0;
9590

96-
9791
/** Reason why the menu was closed. */
9892
export type MenuCloseReason = void | 'click' | 'keydown' | 'tab';
9993

@@ -106,6 +100,8 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
106100
private _xPosition: MenuPositionX = this._defaultOptions.xPosition;
107101
private _yPosition: MenuPositionY = this._defaultOptions.yPosition;
108102
private _previousElevation: string;
103+
protected _elevationPrefix: string;
104+
protected _baseElevation: number;
109105

110106
/** All items inside the menu. Includes items nested inside another menu. */
111107
@ContentChildren(MatMenuItem, {descendants: true}) _allItems: QueryList<MatMenuItem>;
@@ -404,9 +400,11 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
404400
setElevation(depth: number): void {
405401
// The elevation starts at the base and increases by one for each level.
406402
// Capped at 24 because that's the maximum elevation defined in the Material design spec.
407-
const elevation = Math.min(MAT_MENU_BASE_ELEVATION + depth, 24);
408-
const newElevation = `mat-elevation-z${elevation}`;
409-
const customElevation = Object.keys(this._classList).find(c => c.startsWith('mat-elevation-z'));
403+
const elevation = Math.min(this._baseElevation + depth, 24);
404+
const newElevation = `${this._elevationPrefix}${elevation}`;
405+
const customElevation = Object.keys(this._classList).find(className => {
406+
return className.startsWith(this._elevationPrefix);
407+
});
410408

411409
if (!customElevation || customElevation === this._previousElevation) {
412410
if (this._previousElevation) {
@@ -506,6 +504,9 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
506504
]
507505
})
508506
export class MatMenu extends _MatMenuBase {
507+
protected _elevationPrefix = 'mat-elevation-z';
508+
protected _baseElevation = 4;
509+
509510
constructor(elementRef: ElementRef<HTMLElement>, ngZone: NgZone,
510511
@Inject(MAT_MENU_DEFAULT_OPTIONS) defaultOptions: MatMenuDefaultOptions) {
511512
super(elementRef, ngZone, defaultOptions);

tools/public_api_guard/material/menu.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnInit, OnDestroy {
22
_allItems: QueryList<MatMenuItem>;
33
readonly _animationDone: Subject<AnimationEvent>;
4+
protected _baseElevation: number;
45
_classList: {
56
[key: string]: boolean;
67
};
8+
protected _elevationPrefix: string;
79
_isAnimating: boolean;
810
_panelAnimationState: 'void' | 'enter';
911
ariaDescribedby: string;
@@ -69,6 +71,8 @@ export declare const MAT_MENU_PANEL: InjectionToken<MatMenuPanel<any>>;
6971
export declare const MAT_MENU_SCROLL_STRATEGY: InjectionToken<() => ScrollStrategy>;
7072

7173
export declare class MatMenu extends _MatMenuBase {
74+
protected _baseElevation: number;
75+
protected _elevationPrefix: string;
7276
constructor(elementRef: ElementRef<HTMLElement>, ngZone: NgZone, defaultOptions: MatMenuDefaultOptions);
7377
static ɵcmp: i0.ɵɵComponentDeclaration<MatMenu, "mat-menu", ["matMenu"], {}, {}, never, ["*"]>;
7478
static ɵfac: i0.ɵɵFactoryDeclaration<MatMenu, never>;

0 commit comments

Comments
 (0)