diff --git a/src/demo-app/menu/menu-demo.html b/src/demo-app/menu/menu-demo.html
index 1d818adf3e7f..d43fa31347ad 100644
--- a/src/demo-app/menu/menu-demo.html
+++ b/src/demo-app/menu/menu-demo.html
@@ -62,3 +62,54 @@
+
+
diff --git a/src/lib/menu/OVERVIEW.md b/src/lib/menu/OVERVIEW.md
index 44650a524bbc..d78a0fba903e 100644
--- a/src/lib/menu/OVERVIEW.md
+++ b/src/lib/menu/OVERVIEW.md
@@ -52,8 +52,9 @@ Menus support displaying `md-icon` elements before the menu item text.
### Customizing menu position
-By default, the menu will display after and below its trigger. The position can be changed
+By default, the menu will display below (y-axis), after (x-axis), and overlapping its trigger. The position can be changed
using the `x-position` (`before | after`) and `y-position` (`above | below`) attributes.
+The menu can be be forced to not overlap the trigger using `[overlapTrigger]="false"` attribute.
### Keyboard interaction
diff --git a/src/lib/menu/README.md b/src/lib/menu/README.md
index f6d8f210395e..64080625654e 100644
--- a/src/lib/menu/README.md
+++ b/src/lib/menu/README.md
@@ -115,7 +115,8 @@ Output:
### Customizing menu position
By default, the menu will display after and below its trigger. You can change this display position
-using the `x-position` (`before | after`) and `y-position` (`above | below`) attributes.
+using the `x-position` (`before | after`) and `y-position` (`above | below`) attributes. The menu
+can be positioned over the menu button or outside using `overlapTrigger` (`true | false`).
*my-comp.html*
```html
@@ -148,6 +149,7 @@ also adds `aria-hasPopup="true"` to the trigger element.
| --- | --- | --- |
| `x-position` | `before | after` | The horizontal position of the menu in relation to the trigger. Defaults to `after`. |
| `y-position` | `above | below` | The vertical position of the menu in relation to the trigger. Defaults to `below`. |
+| `overlapTrigger` | `true | false` | Whether to have the menu show on top of the menu trigger or outside. Defaults to `true`. |
### Trigger Programmatic API
diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts
index 765fb56030b0..e5369dbf821b 100644
--- a/src/lib/menu/menu-directive.ts
+++ b/src/lib/menu/menu-directive.ts
@@ -52,6 +52,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
@ViewChild(TemplateRef) templateRef: TemplateRef;
@ContentChildren(MdMenuItem) items: QueryList;
+ @Input() overlapTrigger = true;
constructor(@Attribute('x-position') posX: MenuPositionX,
@Attribute('y-position') posY: MenuPositionY) {
diff --git a/src/lib/menu/menu-panel.ts b/src/lib/menu/menu-panel.ts
index ac92973e99c1..dd3728cd2e7b 100644
--- a/src/lib/menu/menu-panel.ts
+++ b/src/lib/menu/menu-panel.ts
@@ -4,6 +4,7 @@ import {MenuPositionX, MenuPositionY} from './menu-positions';
export interface MdMenuPanel {
positionX: MenuPositionX;
positionY: MenuPositionY;
+ overlapTrigger: boolean;
templateRef: TemplateRef;
close: EventEmitter;
focusFirstItem: () => void;
diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts
index 1b0a80216192..d295603a2a58 100644
--- a/src/lib/menu/menu-trigger.ts
+++ b/src/lib/menu/menu-trigger.ts
@@ -216,7 +216,12 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
private _subscribeToPositions(position: ConnectedPositionStrategy): void {
this._positionSubscription = position.onPositionChange.subscribe((change) => {
const posX: MenuPositionX = change.connectionPair.originX === 'start' ? 'after' : 'before';
- const posY: MenuPositionY = change.connectionPair.originY === 'top' ? 'below' : 'above';
+ let posY: MenuPositionY = change.connectionPair.originY === 'top' ? 'below' : 'above';
+
+ if (!this.menu.overlapTrigger) {
+ posY = posY === 'below' ? 'above' : 'below';
+ }
+
this.menu.setPositionClasses(posX, posY);
});
}
@@ -230,21 +235,29 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
const [posX, fallbackX]: HorizontalConnectionPos[] =
this.menu.positionX === 'before' ? ['end', 'start'] : ['start', 'end'];
- const [posY, fallbackY]: VerticalConnectionPos[] =
+ const [overlayY, fallbackOverlayY]: VerticalConnectionPos[] =
this.menu.positionY === 'above' ? ['bottom', 'top'] : ['top', 'bottom'];
+ let originY = overlayY;
+ let fallbackOriginY = fallbackOverlayY;
+
+ if (!this.menu.overlapTrigger) {
+ originY = overlayY === 'top' ? 'bottom' : 'top';
+ fallbackOriginY = fallbackOverlayY === 'top' ? 'bottom' : 'top';
+ }
+
return this._overlay.position()
.connectedTo(this._element,
- {originX: posX, originY: posY}, {overlayX: posX, overlayY: posY})
+ {originX: posX, originY: originY}, {overlayX: posX, overlayY: overlayY})
.withFallbackPosition(
- {originX: fallbackX, originY: posY},
- {overlayX: fallbackX, overlayY: posY})
+ {originX: fallbackX, originY: originY},
+ {overlayX: fallbackX, overlayY: overlayY})
.withFallbackPosition(
- {originX: posX, originY: fallbackY},
- {overlayX: posX, overlayY: fallbackY})
+ {originX: posX, originY: fallbackOriginY},
+ {overlayX: posX, overlayY: fallbackOverlayY})
.withFallbackPosition(
- {originX: fallbackX, originY: fallbackY},
- {overlayX: fallbackX, overlayY: fallbackY});
+ {originX: fallbackX, originY: fallbackOriginY},
+ {overlayX: fallbackX, overlayY: fallbackOverlayY});
}
private _cleanUpSubscriptions(): void {
diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts
index e75f5321c834..7dbecd19129a 100644
--- a/src/lib/menu/menu.spec.ts
+++ b/src/lib/menu/menu.spec.ts
@@ -1,9 +1,10 @@
-import {TestBed, async} from '@angular/core/testing';
+import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {
Component,
ElementRef,
EventEmitter,
+ Input,
Output,
TemplateRef,
ViewChild
@@ -18,6 +19,7 @@ import {
import {OverlayContainer} from '../core/overlay/overlay-container';
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
import {Dir, LayoutDirection} from '../core/rtl/dir';
+import {extendObject} from '../core/util/object-extend';
describe('MdMenu', () => {
let overlayContainerElement: HTMLElement;
@@ -27,7 +29,7 @@ describe('MdMenu', () => {
dir = 'ltr';
TestBed.configureTestingModule({
imports: [MdMenuModule.forRoot()],
- declarations: [SimpleMenu, PositionedMenu, CustomMenuPanel, CustomMenu],
+ declarations: [SimpleMenu, PositionedMenu, OverlapMenu, CustomMenuPanel, CustomMenu],
providers: [
{provide: OverlayContainer, useFactory: () => {
overlayContainerElement = document.createElement('div');
@@ -256,6 +258,106 @@ describe('MdMenu', () => {
}
});
+ describe('overlapping trigger', () => {
+ /**
+ * This test class is used to create components containing a menu.
+ * It provides helpers to reposition the trigger, open the menu,
+ * and access the trigger and overlay positions.
+ * Additionally it can take any inputs for the menu wrapper component.
+ *
+ * Basic usage:
+ * const subject = new OverlapSubject(MyComponent);
+ * subject.openMenu();
+ */
+ class OverlapSubject {
+ private readonly fixture: ComponentFixture;
+ private readonly trigger: any;
+
+ constructor(ctor: {new(): T; }, inputs: {[key: string]: any} = {}) {
+ this.fixture = TestBed.createComponent(ctor);
+ extendObject(this.fixture.componentInstance, inputs);
+ this.fixture.detectChanges();
+ this.trigger = this.fixture.componentInstance.triggerEl.nativeElement;
+ }
+
+ openMenu() {
+ this.fixture.componentInstance.trigger.openMenu();
+ this.fixture.detectChanges();
+ }
+
+ updateTriggerStyle(style: any) {
+ return extendObject(this.trigger.style, style);
+ }
+
+ get overlayRect() {
+ return this.overlayPane.getBoundingClientRect();
+ }
+
+ get triggerRect() {
+ return this.trigger.getBoundingClientRect();
+ }
+
+ get menuPanel() {
+ return overlayContainerElement.querySelector('.md-menu-panel');
+ }
+
+ private get overlayPane() {
+ return overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
+ }
+ }
+
+ let subject: OverlapSubject;
+ describe('explicitly overlapping', () => {
+ beforeEach(() => {
+ subject = new OverlapSubject(OverlapMenu, {overlapTrigger: true});
+ });
+
+ it('positions the overlay below the trigger', () => {
+ subject.openMenu();
+
+ // Since the menu is overlaying the trigger, the overlay top should be the trigger top.
+ expect(Math.round(subject.overlayRect.top))
+ .toBe(Math.round(subject.triggerRect.top),
+ `Expected menu to open in default "below" position.`);
+ });
+ });
+
+ describe('not overlapping', () => {
+ beforeEach(() => {
+ subject = new OverlapSubject(OverlapMenu, {overlapTrigger: false});
+ });
+
+ it('positions the overlay below the trigger', () => {
+ subject.openMenu();
+
+ // Since the menu is below the trigger, the overlay top should be the trigger bottom.
+ expect(Math.round(subject.overlayRect.top))
+ .toBe(Math.round(subject.triggerRect.bottom),
+ `Expected menu to open directly below the trigger.`);
+ });
+
+ it('supports above position fall back', () => {
+ // Push trigger to the bottom part of viewport, so it doesn't have space to open
+ // in its default "below" position below the trigger.
+ subject.updateTriggerStyle({position: 'relative', top: '650px'});
+ subject.openMenu();
+
+ // Since the menu is above the trigger, the overlay bottom should be the trigger top.
+ expect(Math.round(subject.overlayRect.bottom))
+ .toBe(Math.round(subject.triggerRect.top),
+ `Expected menu to open in "above" position if "below" position wouldn't fit.`);
+ });
+
+ it('repositions the origin to be below, so the menu opens from the trigger', () => {
+ subject.openMenu();
+
+ expect(subject.menuPanel.classList).toContain('md-menu-below');
+ expect(subject.menuPanel.classList).not.toContain('md-menu-above');
+ });
+
+ });
+ });
+
describe('animations', () => {
it('should include the ripple on items by default', () => {
const fixture = TestBed.createComponent(SimpleMenu);
@@ -311,6 +413,23 @@ class PositionedMenu {
@ViewChild('triggerEl') triggerEl: ElementRef;
}
+interface TestableMenu {
+ trigger: MdMenuTrigger;
+ triggerEl: ElementRef;
+}
+@Component({
+ template: `
+
+
+
+
+ `
+})
+class OverlapMenu implements TestableMenu {
+ @Input() overlapTrigger: boolean;
+ @ViewChild(MdMenuTrigger) trigger: MdMenuTrigger;
+ @ViewChild('triggerEl') triggerEl: ElementRef;
+}
@Component({
selector: 'custom-menu',
@@ -325,6 +444,7 @@ class PositionedMenu {
class CustomMenuPanel implements MdMenuPanel {
positionX: MenuPositionX = 'after';
positionY: MenuPositionY = 'below';
+ overlapTrigger: true;
@ViewChild(TemplateRef) templateRef: TemplateRef;
@Output() close = new EventEmitter();