Skip to content

Commit

Permalink
feat(menu): Added ability to show the menu overlay around the menu tr…
Browse files Browse the repository at this point in the history
…igger
  • Loading branch information
trshafer committed Jan 9, 2017
1 parent d4ab3d3 commit 659775a
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 12 deletions.
51 changes: 51 additions & 0 deletions src/demo-app/menu/menu-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,54 @@
</md-menu>
</div>
</div>

<div class="demo-menu">
<div class="menu-section">
<p>overlap-trigger: false</p>

<md-toolbar>
<button md-icon-button [md-menu-trigger-for]="menuOverlay">
<md-icon>more_vert</md-icon>
</button>
</md-toolbar>

<md-menu overlap-trigger="false" #menuOverlay="mdMenu">
<button md-menu-item *ngFor="let item of items" [disabled]="item.disabled">
{{ item.text }}
</button>
</md-menu>
</div>
<div class="menu-section">
<p>
Position x: before, overlap-trigger: false
</p>
<md-toolbar class="end-icon">
<button md-icon-button [md-menu-trigger-for]="posXMenuOverlay">
<md-icon>more_vert</md-icon>
</button>
</md-toolbar>

<md-menu x-position="before" overlap-trigger="false" #posXMenuOverlay="mdMenu" class="before">
<button md-menu-item *ngFor="let item of iconItems" [disabled]="item.disabled">
<md-icon>{{ item.icon }}</md-icon>
{{ item.text }}
</button>
</md-menu>
</div>
<div class="menu-section">
<p>
Position y: above, overlap-trigger: false
</p>
<md-toolbar>
<button md-icon-button [md-menu-trigger-for]="posYMenuOverlay">
<md-icon>more_vert</md-icon>
</button>
</md-toolbar>

<md-menu y-position="above" overlap-trigger="false" #posYMenuOverlay="mdMenu">
<button md-menu-item *ngFor="let item of items" [disabled]="item.disabled">
{{ item.text }}
</button>
</md-menu>
</div>
</div>
4 changes: 3 additions & 1 deletion src/lib/menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `overlap-trigger` (`true | false`).

*my-comp.html*
```html
Expand Down Expand Up @@ -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`. |
| `overlap-trigger` | `true | false` | Whether to have the menu show on top of the menu trigger or outside. Defaults to `true`. |

### Trigger Programmatic API

Expand Down
3 changes: 2 additions & 1 deletion src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
@ContentChildren(MdMenuItem) items: QueryList<MdMenuItem>;

constructor(@Attribute('x-position') posX: MenuPositionX,
@Attribute('y-position') posY: MenuPositionY) {
@Attribute('y-position') posY: MenuPositionY,
@Attribute('overlap-trigger') public overlapTrigger = true) {
if (posX) { this._setPositionX(posX); }
if (posY) { this._setPositionY(posY); }
this.setPositionClasses(this.positionX, this.positionY);
Expand Down
1 change: 1 addition & 0 deletions src/lib/menu/menu-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {MenuPositionX, MenuPositionY} from './menu-positions';
export interface MdMenuPanel {
positionX: MenuPositionX;
positionY: MenuPositionY;
overlapTrigger: boolean;
templateRef: TemplateRef<any>;
close: EventEmitter<void>;
focusFirstItem: () => void;
Expand Down
24 changes: 16 additions & 8 deletions src/lib/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,21 +230,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 {
Expand Down
81 changes: 79 additions & 2 deletions src/lib/menu/menu.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {TestBed, async} from '@angular/core/testing';
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {
Component,
Expand Down Expand Up @@ -27,7 +27,7 @@ describe('MdMenu', () => {
dir = 'ltr';
TestBed.configureTestingModule({
imports: [MdMenuModule.forRoot()],
declarations: [SimpleMenu, PositionedMenu, CustomMenuPanel, CustomMenu],
declarations: [SimpleMenu, PositionedMenu, NonOverlapMenu, CustomMenuPanel, CustomMenu],
providers: [
{provide: OverlayContainer, useFactory: () => {
overlayContainerElement = document.createElement('div');
Expand Down Expand Up @@ -256,6 +256,66 @@ describe('MdMenu', () => {
}
});

describe('not overlayed', () => {
class OverlaySubject<T extends TestableMenu> {
private readonly fixture: ComponentFixture<T>;
private readonly trigger: any;

constructor(ctor: {new(): T; }) {
this.fixture = TestBed.createComponent(ctor);
this.fixture.detectChanges();
this.trigger = this.fixture.componentInstance.triggerEl.nativeElement;
}

openMenu() {
this.fixture.componentInstance.trigger.openMenu();
this.fixture.detectChanges();
}

updateTriggerStyle(style: any) {
return Object.assign(this.trigger.style, style);
}

get overlayRect() {
return this.overlayPane.getBoundingClientRect();
}

get triggerRect() {
return this.trigger.getBoundingClientRect();
}

private get overlayPane() {
return overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
}
}

let subject: OverlaySubject<NonOverlapMenu>;
beforeEach(() => {
subject = new OverlaySubject(NonOverlapMenu);
});

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 in "above" position if "below" position wouldn't fit.`);
});

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.`);
});
});

describe('animations', () => {
it('should include the ripple on items by default', () => {
const fixture = TestBed.createComponent(SimpleMenu);
Expand Down Expand Up @@ -311,6 +371,22 @@ class PositionedMenu {
@ViewChild('triggerEl') triggerEl: ElementRef;
}

interface TestableMenu {
trigger: MdMenuTrigger;
triggerEl: ElementRef;
}
@Component({
template: `
<button [mdMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
<md-menu overlap-trigger="false" #menu="mdMenu">
<button md-menu-item> Not overlapped Content </button>
</md-menu>
`
})
class NonOverlapMenu implements TestableMenu {
@ViewChild(MdMenuTrigger) trigger: MdMenuTrigger;
@ViewChild('triggerEl') triggerEl: ElementRef;
}

@Component({
selector: 'custom-menu',
Expand All @@ -325,6 +401,7 @@ class PositionedMenu {
class CustomMenuPanel implements MdMenuPanel {
positionX: MenuPositionX = 'after';
positionY: MenuPositionY = 'below';
overlapTrigger: true;

@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
@Output() close = new EventEmitter<void>();
Expand Down

0 comments on commit 659775a

Please sign in to comment.