diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index 425e66547ec1..021131e3ae9c 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -47,6 +47,13 @@ export interface MdMenuDefaultOptions { export const MD_MENU_DEFAULT_OPTIONS = new InjectionToken('md-menu-default-options'); +/** + * Start elevation for the menu panel. + * @docs-private + */ +const MD_MENU_BASE_ELEVATION = 2; + + @Component({ moduleId: module.id, selector: 'md-menu, mat-menu', @@ -64,6 +71,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { private _keyManager: FocusKeyManager; private _xPosition: MenuPositionX = this._defaultOptions.xPosition; private _yPosition: MenuPositionY = this._defaultOptions.yPosition; + private _previousElevation: string; /** Subscription to tab events on the menu panel */ private _tabSubscription: Subscription; @@ -74,8 +82,8 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { /** Current state of the panel animation. */ _panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void'; - /** Whether the menu is a sub-menu or a top-level menu. */ - isSubmenu: boolean = false; + /** Parent menu of the current menu panel. */ + parentMenu: MdMenuPanel | undefined; /** Layout direction of the menu. */ direction: Direction; @@ -162,12 +170,12 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { this.close.emit('keydown'); break; case LEFT_ARROW: - if (this.isSubmenu && this.direction === 'ltr') { + if (this.parentMenu && this.direction === 'ltr') { this.close.emit('keydown'); } break; case RIGHT_ARROW: - if (this.isSubmenu && this.direction === 'rtl') { + if (this.parentMenu && this.direction === 'rtl') { this.close.emit('keydown'); } break; @@ -195,6 +203,25 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { this._classList['mat-menu-below'] = posY === 'below'; } + /** + * Sets the menu panel elevation. + * @param depth Number of parent menus that come before the menu. + */ + setElevation(depth: number): void { + // The elevation starts at the base and increases by one for each level. + const newElevation = `mat-elevation-z${MD_MENU_BASE_ELEVATION + depth}`; + const customElevation = Object.keys(this._classList).find(c => c.startsWith('mat-elevation-z')); + + if (!customElevation || customElevation === this._previousElevation) { + if (this._previousElevation) { + this._classList[this._previousElevation] = false; + } + + this._classList[newElevation] = true; + this._previousElevation = newElevation; + } + } + /** Starts the enter animation. */ _startAnimation() { this._panelAnimationState = 'enter-start'; diff --git a/src/lib/menu/menu-panel.ts b/src/lib/menu/menu-panel.ts index 18dd96c0f2b5..2eeb4fbbef7f 100644 --- a/src/lib/menu/menu-panel.ts +++ b/src/lib/menu/menu-panel.ts @@ -16,8 +16,9 @@ export interface MdMenuPanel { overlapTrigger: boolean; templateRef: TemplateRef; close: EventEmitter; - isSubmenu?: boolean; + parentMenu?: MdMenuPanel | undefined; direction?: Direction; focusFirstItem: () => void; setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void; + setElevation?(depth: number): void; } diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index 8433c9c8d38d..bbcfe673daaa 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -224,8 +224,9 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy { * the menu was opened via the keyboard. */ private _initMenu(): void { - this.menu.isSubmenu = this.triggersSubmenu(); + this.menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined; this.menu.direction = this.dir; + this._setMenuElevation(); this._setIsMenuOpen(true); // Should only set focus if opened via the keyboard, so keyboard users can @@ -236,6 +237,21 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy { } } + /** Updates the menu elevation based on the amount of parent menus that it has. */ + private _setMenuElevation(): void { + if (this.menu.setElevation) { + let depth = 0; + let parentMenu = this.menu.parentMenu; + + while (parentMenu) { + depth++; + parentMenu = parentMenu.parentMenu; + } + + this.menu.setElevation(depth); + } + } + /** * This method resets the menu when it's closed, most importantly restoring * focus to the menu trigger if the menu was opened via the keyboard. diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index 3ea7b72c9af2..c1e177b1ec7e 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -46,7 +46,8 @@ describe('MdMenu', () => { OverlapMenu, CustomMenuPanel, CustomMenu, - NestedMenu + NestedMenu, + NestedMenuCustomElevation ], providers: [ {provide: OverlayContainer, useFactory: () => { @@ -530,7 +531,7 @@ describe('MdMenu', () => { expect(instance.levelTwoTrigger.triggersSubmenu()).toBe(true); }); - it('should set the `isSubmenu` flag on the menu instances', () => { + it('should set the `parentMenu` on the sub-menu instances', () => { compileTestComponent(); instance.rootTriggerEl.nativeElement.click(); fixture.detectChanges(); @@ -541,9 +542,9 @@ describe('MdMenu', () => { instance.levelTwoTrigger.openMenu(); fixture.detectChanges(); - expect(instance.rootMenu.isSubmenu).toBe(false); - expect(instance.levelOneMenu.isSubmenu).toBe(true); - expect(instance.levelTwoMenu.isSubmenu).toBe(true); + expect(instance.rootMenu.parentMenu).toBeFalsy(); + expect(instance.levelOneMenu.parentMenu).toBe(instance.rootMenu); + expect(instance.levelTwoMenu.parentMenu).toBe(instance.levelOneMenu); }); it('should pass the layout direction the nested menus', () => { @@ -885,6 +886,77 @@ describe('MdMenu', () => { expect(menuItems[1].classList).not.toContain('mat-menu-item-submenu-trigger'); }); + it('should increase the sub-menu elevation based on its depth', () => { + compileTestComponent(); + instance.rootTrigger.openMenu(); + fixture.detectChanges(); + + instance.levelOneTrigger.openMenu(); + fixture.detectChanges(); + + instance.levelTwoTrigger.openMenu(); + fixture.detectChanges(); + + const menus = overlay.querySelectorAll('.mat-menu-panel'); + + expect(menus[0].classList) + .toContain('mat-elevation-z2', 'Expected root menu to have base elevation.'); + expect(menus[1].classList) + .toContain('mat-elevation-z3', 'Expected first sub-menu to have base elevation + 1.'); + expect(menus[2].classList) + .toContain('mat-elevation-z4', 'Expected second sub-menu to have base elevation + 2.'); + }); + + it('should update the elevation when the same menu is opened at a different depth', () => { + compileTestComponent(); + instance.rootTrigger.openMenu(); + fixture.detectChanges(); + + instance.levelOneTrigger.openMenu(); + fixture.detectChanges(); + + instance.levelTwoTrigger.openMenu(); + fixture.detectChanges(); + + let lastMenu = overlay.querySelectorAll('.mat-menu-panel')[2]; + + expect(lastMenu.classList) + .toContain('mat-elevation-z4', 'Expected menu to have the base elevation plus two.'); + + (overlay.querySelector('.cdk-overlay-backdrop')! as HTMLElement).click(); + fixture.detectChanges(); + + expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus'); + + instance.alternateTrigger.openMenu(); + fixture.detectChanges(); + + lastMenu = overlay.querySelector('.mat-menu-panel') as HTMLElement; + + expect(lastMenu.classList) + .not.toContain('mat-elevation-z4', 'Expected menu not to maintain old elevation.'); + expect(lastMenu.classList) + .toContain('mat-elevation-z2', 'Expected menu to have the proper updated elevation.'); + }); + + it('should not increase the elevation if the user specified a custom one', () => { + const elevationFixture = TestBed.createComponent(NestedMenuCustomElevation); + + elevationFixture.detectChanges(); + elevationFixture.componentInstance.rootTrigger.openMenu(); + elevationFixture.detectChanges(); + + elevationFixture.componentInstance.levelOneTrigger.openMenu(); + elevationFixture.detectChanges(); + + const menuClasses = overlayContainerElement.querySelectorAll('.mat-menu-panel')[1].classList; + + expect(menuClasses) + .toContain('mat-elevation-z24', 'Expected user elevation to be maintained'); + expect(menuClasses) + .not.toContain('mat-elevation-z3', 'Expected no stacked elevation.'); + }); + }); }); @@ -976,7 +1048,7 @@ class CustomMenuPanel implements MdMenuPanel { xPosition: MenuPositionX = 'after'; yPosition: MenuPositionY = 'below'; overlapTrigger = true; - isSubmenu = false; + parentMenu: MdMenuPanel; @ViewChild(TemplateRef) templateRef: TemplateRef; @Output() close = new EventEmitter(); @@ -1004,6 +1076,10 @@ class CustomMenu { #rootTrigger="mdMenuTrigger" #rootTriggerEl>Toggle menu + + + + + + + + + + + ` +}) +class NestedMenuCustomElevation { + @ViewChild('rootTrigger') rootTrigger: MdMenuTrigger; + @ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger; +}