fix(menu): unable to bind to xPosition and yPosition (#4213)

Fixes not being able to use data bindings on the `xPosition` and `yPosition` properties.

**Note:** These changes could be considered breaking, if somebody was implementing the `MdMenuPanel` interface.

Fixes #4169.
This commit is contained in:
Kristiyan Kostadinov
2017-05-03 01:36:48 +02:00
committed by Andrew Seguin
parent b4e8c7db7d
commit 1fd50aaa44
5 changed files with 80 additions and 59 deletions
+31 -40
View File
@@ -2,7 +2,6 @@
import {
AfterContentInit,
Attribute,
Component,
ContentChildren,
EventEmitter,
@@ -37,6 +36,8 @@ import {transformMenu, fadeInItems} from './menu-animations';
})
export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
private _keyManager: FocusKeyManager;
private _xPosition: MenuPositionX = 'after';
private _yPosition: MenuPositionY = 'below';
/** Subscription to tab events on the menu panel */
private _tabSubscription: Subscription;
@@ -45,34 +46,38 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
_classList: any = {};
/** Position of the menu in the X axis. */
positionX: MenuPositionX = 'after';
@Input()
get xPosition() { return this._xPosition; }
set xPosition(value: MenuPositionX) {
if (value !== 'before' && value !== 'after') {
throw new MdMenuInvalidPositionX();
}
this._xPosition = value;
this.setPositionClasses();
}
/** Position of the menu in the Y axis. */
positionY: MenuPositionY = 'below';
@Input()
get yPosition() { return this._yPosition; }
set yPosition(value: MenuPositionY) {
if (value !== 'above' && value !== 'below') {
throw new MdMenuInvalidPositionY();
}
this._yPosition = value;
this.setPositionClasses();
}
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
/** List of the items inside of a menu. */
@ContentChildren(MdMenuItem) items: QueryList<MdMenuItem>;
/** Whether the menu should overlap its trigger. */
@Input() overlapTrigger = true;
constructor(@Attribute('xPosition') posX: MenuPositionX,
@Attribute('yPosition') posY: MenuPositionY,
@Attribute('x-position') deprecatedPosX: MenuPositionX,
@Attribute('y-position') deprecatedPosY: MenuPositionY) {
// TODO(kara): Remove kebab-case attributes after next release
if (deprecatedPosX) { this._setPositionX(deprecatedPosX); }
if (deprecatedPosY) { this._setPositionY(deprecatedPosY); }
if (posX) { this._setPositionX(posX); }
if (posY) { this._setPositionY(posY); }
this.setPositionClasses(this.positionX, this.positionY);
}
ngAfterContentInit() {
this._keyManager = new FocusKeyManager(this.items).withWrap();
this._tabSubscription = this._keyManager.tabOut.subscribe(() => {
this._emitCloseEvent();
});
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this._emitCloseEvent());
}
ngOnDestroy() {
@@ -93,7 +98,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
obj[className] = true;
return obj;
}, {});
this.setPositionClasses(this.positionX, this.positionY);
this.setPositionClasses();
}
/** Event emitted when the menu is closed. */
@@ -115,29 +120,15 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
this.close.emit();
}
private _setPositionX(pos: MenuPositionX): void {
if (pos !== 'before' && pos !== 'after') {
throw new MdMenuInvalidPositionX();
}
this.positionX = pos;
}
private _setPositionY(pos: MenuPositionY): void {
if (pos !== 'above' && pos !== 'below') {
throw new MdMenuInvalidPositionY();
}
this.positionY = pos;
}
/**
* It's necessary to set position-based classes to ensure the menu panel animation
* folds out from the correct direction.
*/
setPositionClasses(posX: MenuPositionX, posY: MenuPositionY): void {
this._classList['mat-menu-before'] = posX == 'before';
this._classList['mat-menu-after'] = posX == 'after';
this._classList['mat-menu-above'] = posY == 'above';
this._classList['mat-menu-below'] = posY == 'below';
setPositionClasses(posX = this.xPosition, posY = this.yPosition): void {
this._classList['mat-menu-before'] = posX === 'before';
this._classList['mat-menu-after'] = posX === 'after';
this._classList['mat-menu-above'] = posY === 'above';
this._classList['mat-menu-below'] = posY === 'below';
}
}
+6 -6
View File
@@ -16,27 +16,27 @@ export class MdMenuMissingError extends MdError {
}
/**
* Exception thrown when menu's x-position value isn't valid.
* Exception thrown when menu's xPosition value isn't valid.
* In other words, it doesn't match 'before' or 'after'.
* @docs-private
*/
export class MdMenuInvalidPositionX extends MdError {
constructor() {
super(`x-position value must be either 'before' or after'.
Example: <md-menu x-position="before" #menu="mdMenu"></md-menu>
super(`xPosition value must be either 'before' or after'.
Example: <md-menu xPosition="before" #menu="mdMenu"></md-menu>
`);
}
}
/**
* Exception thrown when menu's y-position value isn't valid.
* Exception thrown when menu's yPosition value isn't valid.
* In other words, it doesn't match 'above' or 'below'.
* @docs-private
*/
export class MdMenuInvalidPositionY extends MdError {
constructor() {
super(`y-position value must be either 'above' or below'.
Example: <md-menu y-position="above" #menu="mdMenu"></md-menu>
super(`yPosition value must be either 'above' or below'.
Example: <md-menu yPosition="above" #menu="mdMenu"></md-menu>
`);
}
}
+2 -2
View File
@@ -2,8 +2,8 @@ import {EventEmitter, TemplateRef} from '@angular/core';
import {MenuPositionX, MenuPositionY} from './menu-positions';
export interface MdMenuPanel {
positionX: MenuPositionX;
positionY: MenuPositionY;
xPosition: MenuPositionX;
yPosition: MenuPositionY;
overlapTrigger: boolean;
templateRef: TemplateRef<any>;
close: EventEmitter<void>;
+2 -2
View File
@@ -244,10 +244,10 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
*/
private _getPosition(): ConnectedPositionStrategy {
const [posX, fallbackX]: HorizontalConnectionPos[] =
this.menu.positionX === 'before' ? ['end', 'start'] : ['start', 'end'];
this.menu.xPosition === 'before' ? ['end', 'start'] : ['start', 'end'];
const [overlayY, fallbackOverlayY]: VerticalConnectionPos[] =
this.menu.positionY === 'above' ? ['bottom', 'top'] : ['top', 'bottom'];
this.menu.yPosition === 'above' ? ['bottom', 'top'] : ['top', 'bottom'];
let originY = overlayY;
let fallbackOriginY = fallbackOverlayY;
+39 -9
View File
@@ -108,10 +108,13 @@ describe('MdMenu', () => {
});
describe('positions', () => {
let fixture: ComponentFixture<PositionedMenu>;
let panel: HTMLElement;
beforeEach(() => {
const fixture = TestBed.createComponent(PositionedMenu);
fixture = TestBed.createComponent(PositionedMenu);
fixture.detectChanges();
const trigger = fixture.componentInstance.triggerEl.nativeElement;
// Push trigger to the bottom edge of viewport,so it has space to open "above"
@@ -123,18 +126,43 @@ describe('MdMenu', () => {
fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();
panel = overlayContainerElement.querySelector('.mat-menu-panel') as HTMLElement;
});
it('should append mat-menu-before if x position is changed', () => {
const panel = overlayContainerElement.querySelector('.mat-menu-panel');
it('should append mat-menu-before if the x position is changed', () => {
expect(panel.classList).toContain('mat-menu-before');
expect(panel.classList).not.toContain('mat-menu-after');
fixture.componentInstance.xPosition = 'after';
fixture.detectChanges();
expect(panel.classList).toContain('mat-menu-after');
expect(panel.classList).not.toContain('mat-menu-before');
});
it('should append mat-menu-above if y position is changed', () => {
const panel = overlayContainerElement.querySelector('.mat-menu-panel');
it('should append mat-menu-above if the y position is changed', () => {
expect(panel.classList).toContain('mat-menu-above');
expect(panel.classList).not.toContain('mat-menu-below');
fixture.componentInstance.yPosition = 'below';
fixture.detectChanges();
expect(panel.classList).toContain('mat-menu-below');
expect(panel.classList).not.toContain('mat-menu-above');
});
it('should default to the "below" and "after" positions', () => {
fixture.destroy();
let newFixture = TestBed.createComponent(SimpleMenu);
newFixture.detectChanges();
newFixture.componentInstance.trigger.openMenu();
newFixture.detectChanges();
panel = overlayContainerElement.querySelector('.mat-menu-panel') as HTMLElement;
expect(panel.classList).toContain('mat-menu-below');
expect(panel.classList).toContain('mat-menu-after');
});
});
@@ -193,7 +221,7 @@ describe('MdMenu', () => {
.toBe(Math.round(expectedTop),
`Expected menu to open in "above" position if "below" position wouldn't fit.`);
// The x-position of the overlay should be unaffected, as it can already fit horizontally
// The xPosition of the overlay should be unaffected, as it can already fit horizontally
expect(Math.round(overlayRect.left))
.toBe(Math.round(triggerRect.left),
`Expected menu x position to be unchanged if it can fit in the viewport.`);
@@ -437,7 +465,7 @@ class SimpleMenu {
@Component({
template: `
<button [mdMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
<md-menu x-position="before" y-position="above" #menu="mdMenu">
<md-menu [xPosition]="xPosition" [yPosition]="yPosition" #menu="mdMenu">
<button md-menu-item> Positioned Content </button>
</md-menu>
`
@@ -445,6 +473,8 @@ class SimpleMenu {
class PositionedMenu {
@ViewChild(MdMenuTrigger) trigger: MdMenuTrigger;
@ViewChild('triggerEl') triggerEl: ElementRef;
xPosition: MenuPositionX = 'before';
yPosition: MenuPositionY = 'above';
}
interface TestableMenu {
@@ -476,8 +506,8 @@ class OverlapMenu implements TestableMenu {
exportAs: 'mdCustomMenu'
})
class CustomMenuPanel implements MdMenuPanel {
positionX: MenuPositionX = 'after';
positionY: MenuPositionY = 'below';
xPosition: MenuPositionX = 'after';
yPosition: MenuPositionY = 'below';
overlapTrigger: true;
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;