Skip to content

feat(menu): increase nested menu elevation based on depth #5937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 28, 2017
Merged
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
35 changes: 31 additions & 4 deletions src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ export interface MdMenuDefaultOptions {
export const MD_MENU_DEFAULT_OPTIONS =
new InjectionToken<MdMenuDefaultOptions>('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',
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion src/lib/menu/menu-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export interface MdMenuPanel {
overlapTrigger: boolean;
templateRef: TemplateRef<any>;
close: EventEmitter<void | 'click' | 'keydown'>;
isSubmenu?: boolean;
parentMenu?: MdMenuPanel | undefined;
direction?: Direction;
focusFirstItem: () => void;
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
setElevation?(depth: number): void;
}
18 changes: 17 additions & 1 deletion src/lib/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
109 changes: 103 additions & 6 deletions src/lib/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ describe('MdMenu', () => {
OverlapMenu,
CustomMenuPanel,
CustomMenu,
NestedMenu
NestedMenu,
NestedMenuCustomElevation
],
providers: [
{provide: OverlayContainer, useFactory: () => {
Expand Down Expand Up @@ -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();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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.');
});

});

});
Expand Down Expand Up @@ -976,7 +1048,7 @@ class CustomMenuPanel implements MdMenuPanel {
xPosition: MenuPositionX = 'after';
yPosition: MenuPositionY = 'below';
overlapTrigger = true;
isSubmenu = false;
parentMenu: MdMenuPanel;

@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
@Output() close = new EventEmitter<void | 'click' | 'keydown'>();
Expand Down Expand Up @@ -1004,6 +1076,10 @@ class CustomMenu {
#rootTrigger="mdMenuTrigger"
#rootTriggerEl>Toggle menu</button>

<button
[mdMenuTriggerFor]="levelTwo"
#alternateTrigger="mdMenuTrigger">Toggle alternate menu</button>

<md-menu #root="mdMenu">
<button md-menu-item
id="level-one-trigger"
Expand Down Expand Up @@ -1033,10 +1109,31 @@ class NestedMenu {
@ViewChild('root') rootMenu: MdMenu;
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
@ViewChild('rootTriggerEl') rootTriggerEl: ElementRef;
@ViewChild('alternateTrigger') alternateTrigger: MdMenuTrigger;

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

@ViewChild('levelTwo') levelTwoMenu: MdMenu;
@ViewChild('levelTwoTrigger') levelTwoTrigger: MdMenuTrigger;
}

@Component({
template: `
<button [mdMenuTriggerFor]="root" #rootTrigger="mdMenuTrigger">Toggle menu</button>

<md-menu #root="mdMenu">
<button md-menu-item
[mdMenuTriggerFor]="levelOne"
#levelOneTrigger="mdMenuTrigger">One</button>
</md-menu>

<md-menu #levelOne="mdMenu" class="mat-elevation-z24">
<button md-menu-item>Two</button>
</md-menu>
`
})
class NestedMenuCustomElevation {
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
}