Skip to content

Commit 389d174

Browse files
committed
feat(menu): increase nested menu elevation based on depth
Increases the sub-menu elevation, based on its depth. [Spec for reference](https://material.io/guidelines/material-design/elevation-shadows.html)
1 parent 3b41c0c commit 389d174

File tree

4 files changed

+146
-12
lines changed

4 files changed

+146
-12
lines changed

src/lib/menu/menu-directive.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
5252
private _keyManager: FocusKeyManager;
5353
private _xPosition: MenuPositionX = 'after';
5454
private _yPosition: MenuPositionY = 'below';
55+
private _previousElevation: string;
5556

5657
/** Subscription to tab events on the menu panel */
5758
private _tabSubscription: Subscription;
@@ -62,8 +63,8 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
6263
/** Current state of the panel animation. */
6364
_panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void';
6465

65-
/** Whether the menu is a sub-menu or a top-level menu. */
66-
isSubmenu: boolean = false;
66+
/** Parent menu os the current menu panel. */
67+
parentMenu: MdMenuPanel | undefined;
6768

6869
/** Layout direction of the menu. */
6970
direction: Direction;
@@ -148,12 +149,12 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
148149
this.close.emit('keydown');
149150
break;
150151
case LEFT_ARROW:
151-
if (this.isSubmenu && this.direction === 'ltr') {
152+
if (this.parentMenu && this.direction === 'ltr') {
152153
this.close.emit('keydown');
153154
}
154155
break;
155156
case RIGHT_ARROW:
156-
if (this.isSubmenu && this.direction === 'rtl') {
157+
if (this.parentMenu && this.direction === 'rtl') {
157158
this.close.emit('keydown');
158159
}
159160
break;
@@ -181,6 +182,25 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
181182
this._classList['mat-menu-below'] = posY === 'below';
182183
}
183184

185+
/**
186+
* Sets the menu panel elevation.
187+
* @param depth Amount of parent menus that come before the menu.
188+
*/
189+
setElevation(depth: number): void {
190+
// The elevation starts at 2 and increases by one for each level.
191+
const newElevation = `mat-elevation-z${depth + 2}`;
192+
const customElevation = Object.keys(this._classList).find(c => c.startsWith('mat-elevation-z'));
193+
194+
if (!customElevation || customElevation === this._previousElevation) {
195+
if (this._previousElevation) {
196+
this._classList[this._previousElevation] = false;
197+
}
198+
199+
this._classList[newElevation] = true;
200+
this._previousElevation = newElevation;
201+
}
202+
}
203+
184204
/** Starts the enter animation. */
185205
_startAnimation() {
186206
this._panelAnimationState = 'enter-start';

src/lib/menu/menu-panel.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ export interface MdMenuPanel {
1616
overlapTrigger: boolean;
1717
templateRef: TemplateRef<any>;
1818
close: EventEmitter<void | 'click' | 'keydown'>;
19-
isSubmenu?: boolean;
19+
parentMenu?: MdMenuPanel | undefined;
2020
direction?: Direction;
2121
focusFirstItem: () => void;
2222
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
23+
setElevation?(depth: number): void;
2324
}

src/lib/menu/menu-trigger.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,9 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
218218
* the menu was opened via the keyboard.
219219
*/
220220
private _initMenu(): void {
221-
this.menu.isSubmenu = this.triggersSubmenu();
221+
this.menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
222222
this.menu.direction = this.dir;
223+
this._setMenuElevation();
223224
this._setIsMenuOpen(true);
224225

225226
// Should only set focus if opened via the keyboard, so keyboard users can
@@ -230,6 +231,21 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
230231
}
231232
}
232233

234+
/** Updates the menu elevation based on the amount of parent menus that it has. */
235+
private _setMenuElevation(): void {
236+
if (this.menu.setElevation) {
237+
let depth = 0;
238+
let parentMenu = this.menu.parentMenu;
239+
240+
while (parentMenu) {
241+
depth++;
242+
parentMenu = parentMenu.parentMenu;
243+
}
244+
245+
this.menu.setElevation(depth);
246+
}
247+
}
248+
233249
/**
234250
* This method resets the menu when it's closed, most importantly restoring
235251
* focus to the menu trigger if the menu was opened via the keyboard.

src/lib/menu/menu.spec.ts

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ describe('MdMenu', () => {
4545
OverlapMenu,
4646
CustomMenuPanel,
4747
CustomMenu,
48-
NestedMenu
48+
NestedMenu,
49+
NestedMenuCustomElevation
4950
],
5051
providers: [
5152
{provide: OverlayContainer, useFactory: () => {
@@ -527,7 +528,7 @@ describe('MdMenu', () => {
527528
expect(instance.levelTwoTrigger.triggersSubmenu()).toBe(true);
528529
});
529530

530-
it('should set the `isSubmenu` flag on the menu instances', () => {
531+
it('should set the `parentMenu` on the sub-menu instances', () => {
531532
compileTestComponent();
532533
instance.rootTriggerEl.nativeElement.click();
533534
fixture.detectChanges();
@@ -538,9 +539,9 @@ describe('MdMenu', () => {
538539
instance.levelTwoTrigger.openMenu();
539540
fixture.detectChanges();
540541

541-
expect(instance.rootMenu.isSubmenu).toBe(false);
542-
expect(instance.levelOneMenu.isSubmenu).toBe(true);
543-
expect(instance.levelTwoMenu.isSubmenu).toBe(true);
542+
expect(instance.rootMenu.parentMenu).toBeFalsy();
543+
expect(instance.levelOneMenu.parentMenu).toBe(instance.rootMenu);
544+
expect(instance.levelTwoMenu.parentMenu).toBe(instance.levelOneMenu);
544545
});
545546

546547
it('should pass the layout direction the nested menus', () => {
@@ -871,6 +872,77 @@ describe('MdMenu', () => {
871872
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus');
872873
});
873874

875+
it('should increase the sub-menu elevation based on its depth', () => {
876+
compileTestComponent();
877+
instance.rootTrigger.openMenu();
878+
fixture.detectChanges();
879+
880+
instance.levelOneTrigger.openMenu();
881+
fixture.detectChanges();
882+
883+
instance.levelTwoTrigger.openMenu();
884+
fixture.detectChanges();
885+
886+
const menus = overlay.querySelectorAll('.mat-menu-panel');
887+
888+
expect(menus[0].classList)
889+
.toContain('mat-elevation-z2', 'Expected root menu to have base elevation.');
890+
expect(menus[1].classList)
891+
.toContain('mat-elevation-z3', 'Expected first sub-menu to have base elevation + 1.');
892+
expect(menus[2].classList)
893+
.toContain('mat-elevation-z4', 'Expected second sub-menu to have base elevation + 2.');
894+
});
895+
896+
it('should update the elevation when the same menu is opened at a different depth', () => {
897+
compileTestComponent();
898+
instance.rootTrigger.openMenu();
899+
fixture.detectChanges();
900+
901+
instance.levelOneTrigger.openMenu();
902+
fixture.detectChanges();
903+
904+
instance.levelTwoTrigger.openMenu();
905+
fixture.detectChanges();
906+
907+
let lastMenu = overlay.querySelectorAll('.mat-menu-panel')[2];
908+
909+
expect(lastMenu.classList)
910+
.toContain('mat-elevation-z4', 'Expected menu to have the base elevation plus two.');
911+
912+
(overlay.querySelector('.cdk-overlay-backdrop')! as HTMLElement).click();
913+
fixture.detectChanges();
914+
915+
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus');
916+
917+
instance.alternateTrigger.openMenu();
918+
fixture.detectChanges();
919+
920+
lastMenu = overlay.querySelector('.mat-menu-panel') as HTMLElement;
921+
922+
expect(lastMenu.classList)
923+
.not.toContain('mat-elevation-z4', 'Expected menu not to maintain old elevation.');
924+
expect(lastMenu.classList)
925+
.toContain('mat-elevation-z2', 'Expected menu to have the proper updated elevation.');
926+
});
927+
928+
it('should not increase the elevation if the user specified a custom one', () => {
929+
const elevationFixture = TestBed.createComponent(NestedMenuCustomElevation);
930+
931+
elevationFixture.detectChanges();
932+
elevationFixture.componentInstance.rootTrigger.openMenu();
933+
elevationFixture.detectChanges();
934+
935+
elevationFixture.componentInstance.levelOneTrigger.openMenu();
936+
elevationFixture.detectChanges();
937+
938+
const menuClasses = overlayContainerElement.querySelectorAll('.mat-menu-panel')[1].classList;
939+
940+
expect(menuClasses)
941+
.toContain('mat-elevation-z24', 'Expected user elevation to be maintained');
942+
expect(menuClasses)
943+
.not.toContain('mat-elevation-z3', 'Expected no stacked elevation.');
944+
});
945+
874946
});
875947

876948
});
@@ -939,7 +1011,7 @@ class CustomMenuPanel implements MdMenuPanel {
9391011
xPosition: MenuPositionX = 'after';
9401012
yPosition: MenuPositionY = 'below';
9411013
overlapTrigger = true;
942-
isSubmenu = false;
1014+
parentMenu: MdMenuPanel;
9431015

9441016
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
9451017
@Output() close = new EventEmitter<void | 'click' | 'keydown'>();
@@ -967,6 +1039,10 @@ class CustomMenu {
9671039
#rootTrigger="mdMenuTrigger"
9681040
#rootTriggerEl>Toggle menu</button>
9691041
1042+
<button
1043+
[mdMenuTriggerFor]="levelTwo"
1044+
#alternateTrigger="mdMenuTrigger">Toggle alternate menu</button>
1045+
9701046
<md-menu #root="mdMenu">
9711047
<button md-menu-item
9721048
id="level-one-trigger"
@@ -996,10 +1072,31 @@ class NestedMenu {
9961072
@ViewChild('root') rootMenu: MdMenu;
9971073
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
9981074
@ViewChild('rootTriggerEl') rootTriggerEl: ElementRef;
1075+
@ViewChild('alternateTrigger') alternateTrigger: MdMenuTrigger;
9991076

10001077
@ViewChild('levelOne') levelOneMenu: MdMenu;
10011078
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
10021079

10031080
@ViewChild('levelTwo') levelTwoMenu: MdMenu;
10041081
@ViewChild('levelTwoTrigger') levelTwoTrigger: MdMenuTrigger;
10051082
}
1083+
1084+
@Component({
1085+
template: `
1086+
<button [mdMenuTriggerFor]="root" #rootTrigger="mdMenuTrigger">Toggle menu</button>
1087+
1088+
<md-menu #root="mdMenu">
1089+
<button md-menu-item
1090+
[mdMenuTriggerFor]="levelOne"
1091+
#levelOneTrigger="mdMenuTrigger">One</button>
1092+
</md-menu>
1093+
1094+
<md-menu #levelOne="mdMenu" class="mat-elevation-z24">
1095+
<button md-menu-item>Two</button>
1096+
</md-menu>
1097+
`
1098+
})
1099+
class NestedMenuCustomElevation {
1100+
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
1101+
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
1102+
}

0 commit comments

Comments
 (0)