mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(menu): Added ability to show the menu overlay around the menu trigger (#1771)
This commit is contained in:
@@ -62,3 +62,54 @@
|
||||
</md-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-menu">
|
||||
<div class="menu-section">
|
||||
<p>overlapTrigger: false</p>
|
||||
|
||||
<md-toolbar>
|
||||
<button md-icon-button [md-menu-trigger-for]="menuOverlay">
|
||||
<md-icon>more_vert</md-icon>
|
||||
</button>
|
||||
</md-toolbar>
|
||||
|
||||
<md-menu [overlapTrigger]="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, overlapTrigger: 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" [overlapTrigger]="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, overlapTrigger: 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" [overlapTrigger]="false" #posYMenuOverlay="mdMenu">
|
||||
<button md-menu-item *ngFor="let item of items" [disabled]="item.disabled">
|
||||
{{ item.text }}
|
||||
</button>
|
||||
</md-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
|
||||
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
@ContentChildren(MdMenuItem) items: QueryList<MdMenuItem>;
|
||||
@Input() overlapTrigger = true;
|
||||
|
||||
constructor(@Attribute('x-position') posX: MenuPositionX,
|
||||
@Attribute('y-position') posY: MenuPositionY) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+122
-2
@@ -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<T extends TestableMenu> {
|
||||
private readonly fixture: ComponentFixture<T>;
|
||||
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<OverlapMenu>;
|
||||
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: `
|
||||
<button [mdMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
|
||||
<md-menu [overlapTrigger]="overlapTrigger" #menu="mdMenu">
|
||||
<button md-menu-item> Not overlapped Content </button>
|
||||
</md-menu>
|
||||
`
|
||||
})
|
||||
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<any>;
|
||||
@Output() close = new EventEmitter<void>();
|
||||
|
||||
Reference in New Issue
Block a user