Skip to content

Commit 78018dc

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 3cb3945 commit 78018dc

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
@@ -64,6 +64,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
6464
private _keyManager: FocusKeyManager;
6565
private _xPosition: MenuPositionX = this._defaultOptions.xPosition;
6666
private _yPosition: MenuPositionY = this._defaultOptions.yPosition;
67+
private _previousElevation: string;
6768

6869
/** Subscription to tab events on the menu panel */
6970
private _tabSubscription: Subscription;
@@ -74,8 +75,8 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
7475
/** Current state of the panel animation. */
7576
_panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void';
7677

77-
/** Whether the menu is a sub-menu or a top-level menu. */
78-
isSubmenu: boolean = false;
78+
/** Parent menu os the current menu panel. */
79+
parentMenu: MdMenuPanel | undefined;
7980

8081
/** Layout direction of the menu. */
8182
direction: Direction;
@@ -162,12 +163,12 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
162163
this.close.emit('keydown');
163164
break;
164165
case LEFT_ARROW:
165-
if (this.isSubmenu && this.direction === 'ltr') {
166+
if (this.parentMenu && this.direction === 'ltr') {
166167
this.close.emit('keydown');
167168
}
168169
break;
169170
case RIGHT_ARROW:
170-
if (this.isSubmenu && this.direction === 'rtl') {
171+
if (this.parentMenu && this.direction === 'rtl') {
171172
this.close.emit('keydown');
172173
}
173174
break;
@@ -195,6 +196,25 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
195196
this._classList['mat-menu-below'] = posY === 'below';
196197
}
197198

199+
/**
200+
* Sets the menu panel elevation.
201+
* @param depth Amount of parent menus that come before the menu.
202+
*/
203+
setElevation(depth: number): void {
204+
// The elevation starts at 2 and increases by one for each level.
205+
const newElevation = `mat-elevation-z${depth + 2}`;
206+
const customElevation = Object.keys(this._classList).find(c => c.startsWith('mat-elevation-z'));
207+
208+
if (!customElevation || customElevation === this._previousElevation) {
209+
if (this._previousElevation) {
210+
this._classList[this._previousElevation] = false;
211+
}
212+
213+
this._classList[newElevation] = true;
214+
this._previousElevation = newElevation;
215+
}
216+
}
217+
198218
/** Starts the enter animation. */
199219
_startAnimation() {
200220
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
@@ -46,7 +46,8 @@ describe('MdMenu', () => {
4646
OverlapMenu,
4747
CustomMenuPanel,
4848
CustomMenu,
49-
NestedMenu
49+
NestedMenu,
50+
NestedMenuCustomElevation
5051
],
5152
providers: [
5253
{provide: OverlayContainer, useFactory: () => {
@@ -528,7 +529,7 @@ describe('MdMenu', () => {
528529
expect(instance.levelTwoTrigger.triggersSubmenu()).toBe(true);
529530
});
530531

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

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

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

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

877949
});
@@ -963,7 +1035,7 @@ class CustomMenuPanel implements MdMenuPanel {
9631035
xPosition: MenuPositionX = 'after';
9641036
yPosition: MenuPositionY = 'below';
9651037
overlapTrigger = true;
966-
isSubmenu = false;
1038+
parentMenu: MdMenuPanel;
9671039

9681040
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
9691041
@Output() close = new EventEmitter<void | 'click' | 'keydown'>();
@@ -991,6 +1063,10 @@ class CustomMenu {
9911063
#rootTrigger="mdMenuTrigger"
9921064
#rootTriggerEl>Toggle menu</button>
9931065
1066+
<button
1067+
[mdMenuTriggerFor]="levelTwo"
1068+
#alternateTrigger="mdMenuTrigger">Toggle alternate menu</button>
1069+
9941070
<md-menu #root="mdMenu">
9951071
<button md-menu-item
9961072
id="level-one-trigger"
@@ -1020,10 +1096,31 @@ class NestedMenu {
10201096
@ViewChild('root') rootMenu: MdMenu;
10211097
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
10221098
@ViewChild('rootTriggerEl') rootTriggerEl: ElementRef;
1099+
@ViewChild('alternateTrigger') alternateTrigger: MdMenuTrigger;
10231100

10241101
@ViewChild('levelOne') levelOneMenu: MdMenu;
10251102
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
10261103

10271104
@ViewChild('levelTwo') levelTwoMenu: MdMenu;
10281105
@ViewChild('levelTwoTrigger') levelTwoTrigger: MdMenuTrigger;
10291106
}
1107+
1108+
@Component({
1109+
template: `
1110+
<button [mdMenuTriggerFor]="root" #rootTrigger="mdMenuTrigger">Toggle menu</button>
1111+
1112+
<md-menu #root="mdMenu">
1113+
<button md-menu-item
1114+
[mdMenuTriggerFor]="levelOne"
1115+
#levelOneTrigger="mdMenuTrigger">One</button>
1116+
</md-menu>
1117+
1118+
<md-menu #levelOne="mdMenu" class="mat-elevation-z24">
1119+
<button md-menu-item>Two</button>
1120+
</md-menu>
1121+
`
1122+
})
1123+
class NestedMenuCustomElevation {
1124+
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
1125+
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
1126+
}

0 commit comments

Comments
 (0)