mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(menu): increase nested menu elevation based on depth (#5937)
* 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) * refactor: address feedback
This commit is contained in:
committed by
Andrew Seguin
parent
f52c7f4aee
commit
91f7bf7c91
@@ -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',
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+103
-6
@@ -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<any>;
|
||||
@Output() close = new EventEmitter<void | 'click' | 'keydown'>();
|
||||
@@ -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"
|
||||
@@ -1033,6 +1109,7 @@ 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;
|
||||
@@ -1040,3 +1117,23 @@ class NestedMenu {
|
||||
@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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user