mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(overlay): add connected overlay directive (#496)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
it,
|
||||
describe,
|
||||
expect,
|
||||
beforeEach,
|
||||
inject,
|
||||
async,
|
||||
fakeAsync,
|
||||
flushMicrotasks,
|
||||
beforeEachProviders
|
||||
} from '@angular/core/testing';
|
||||
import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
|
||||
import {Component, provide, ViewChild} from '@angular/core';
|
||||
import {ConnectedOverlayDirective, OverlayOrigin} from './overlay-directives';
|
||||
import {OVERLAY_CONTAINER_TOKEN, Overlay} from './overlay';
|
||||
import {ViewportRuler} from './position/viewport-ruler';
|
||||
import {OverlayPositionBuilder} from './position/overlay-position-builder';
|
||||
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
|
||||
|
||||
|
||||
describe('Overlay directives', () => {
|
||||
let builder: TestComponentBuilder;
|
||||
let overlayContainerElement: HTMLElement;
|
||||
let fixture: ComponentFixture<ConnectedOverlayDirectiveTest>;
|
||||
|
||||
beforeEachProviders(() => [
|
||||
Overlay,
|
||||
OverlayPositionBuilder,
|
||||
ViewportRuler,
|
||||
provide(OVERLAY_CONTAINER_TOKEN, {useFactory: () => {
|
||||
overlayContainerElement = document.createElement('div');
|
||||
return overlayContainerElement;
|
||||
}})
|
||||
]);
|
||||
|
||||
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
builder = tcb;
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
builder.createAsync(ConnectedOverlayDirectiveTest).then(f => {
|
||||
fixture = f;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
}));
|
||||
|
||||
it(`should create an overlay and attach the directive's template`, () => {
|
||||
expect(overlayContainerElement.textContent).toContain('Menu content');
|
||||
});
|
||||
|
||||
it('should destroy the overlay when the directive is destroyed', fakeAsync(() => {
|
||||
fixture.destroy();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(overlayContainerElement.textContent.trim()).toBe('');
|
||||
}));
|
||||
|
||||
it('should use a connected position strategy with a default set of positions', () => {
|
||||
let testComponent: ConnectedOverlayDirectiveTest =
|
||||
fixture.debugElement.componentInstance;
|
||||
let overlayDirective = testComponent.connectedOverlayDirective;
|
||||
|
||||
let strategy =
|
||||
<ConnectedPositionStrategy> overlayDirective.overlayRef.getState().positionStrategy;
|
||||
expect(strategy) .toEqual(jasmine.any(ConnectedPositionStrategy));
|
||||
|
||||
let positions = strategy.positions;
|
||||
expect(positions.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<button overlay-origin #trigger="overlayOrigin">Toggle menu</button>
|
||||
<template connected-overlay [origin]="trigger">
|
||||
<p>Menu content</p>
|
||||
</template>`,
|
||||
directives: [ConnectedOverlayDirective, OverlayOrigin],
|
||||
})
|
||||
class ConnectedOverlayDirectiveTest {
|
||||
@ViewChild(ConnectedOverlayDirective) connectedOverlayDirective: ConnectedOverlayDirective;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
Directive,
|
||||
TemplateRef,
|
||||
ViewContainerRef,
|
||||
OnInit,
|
||||
Input,
|
||||
OnDestroy,
|
||||
ElementRef
|
||||
} from '@angular/core';
|
||||
import {Overlay} from './overlay';
|
||||
import {OverlayRef} from './overlay-ref';
|
||||
import {TemplatePortal} from '../portal/portal';
|
||||
import {OverlayState} from './overlay-state';
|
||||
import {ConnectionPositionPair} from './position/connected-position';
|
||||
|
||||
/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
|
||||
let defaultPositionList = [
|
||||
new ConnectionPositionPair(
|
||||
{originX: 'start', originY: 'bottom'},
|
||||
{overlayX: 'start', overlayY: 'top'}),
|
||||
new ConnectionPositionPair(
|
||||
{originX: 'start', originY: 'top'},
|
||||
{overlayX: 'start', overlayY: 'bottom'}),
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Directive to facilitate declarative creation of an Overlay using a ConnectedPositionStrategy.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[connected-overlay]'
|
||||
})
|
||||
export class ConnectedOverlayDirective implements OnInit, OnDestroy {
|
||||
private _overlayRef: OverlayRef;
|
||||
private _templatePortal: TemplatePortal;
|
||||
|
||||
@Input() origin: OverlayOrigin;
|
||||
@Input() positions: ConnectionPositionPair[];
|
||||
|
||||
// TODO(jelbourn): inputs for size, scroll behavior, animation, etc.
|
||||
|
||||
constructor(
|
||||
private _overlay: Overlay,
|
||||
templateRef: TemplateRef<any>,
|
||||
viewContainerRef: ViewContainerRef) {
|
||||
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
|
||||
}
|
||||
|
||||
get overlayRef() {
|
||||
return this._overlayRef;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
ngOnInit() {
|
||||
this._createOverlay();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
ngOnDestroy() {
|
||||
this._destroyOverlay();
|
||||
}
|
||||
|
||||
/** Creates an overlay and attaches this directive's template to it. */
|
||||
private _createOverlay() {
|
||||
if (!this.positions || !this.positions.length) {
|
||||
this.positions = defaultPositionList;
|
||||
}
|
||||
|
||||
let overlayConfig = new OverlayState();
|
||||
overlayConfig.positionStrategy =
|
||||
this._overlay.position().connectedTo(
|
||||
this.origin.elementRef,
|
||||
{originX: this.positions[0].overlayX, originY: this.positions[0].originY},
|
||||
{overlayX: this.positions[0].overlayX, overlayY: this.positions[0].overlayY});
|
||||
|
||||
this._overlay.create(overlayConfig).then(ref => {
|
||||
this._overlayRef = ref;
|
||||
this._overlayRef.attach(this._templatePortal);
|
||||
});
|
||||
}
|
||||
|
||||
/** Destroys the overlay created by this directive. */
|
||||
private _destroyOverlay() {
|
||||
this._overlayRef.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Directive applied to an element to make it usable as an origin for an Overlay using a
|
||||
* ConnectedPositionStrategy.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[overlay-origin]',
|
||||
exportAs: 'overlayOrigin',
|
||||
})
|
||||
export class OverlayOrigin {
|
||||
constructor(private _elementRef: ElementRef) { }
|
||||
|
||||
get elementRef() {
|
||||
return this._elementRef;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const OVERLAY_DIRECTIVES = [ConnectedOverlayDirective, OverlayOrigin];
|
||||
@@ -29,6 +29,11 @@ export class OverlayRef implements PortalHost {
|
||||
return this._portalHost.hasAttached();
|
||||
}
|
||||
|
||||
/** Gets the current state config of the overlay. */
|
||||
getState() {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/** Updates the position of the overlay based on the position strategy. */
|
||||
private _updatePosition() {
|
||||
if (this._state.positionStrategy) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO(jelbourn): change from the `md` prefix to something else for everything in the toolkit.
|
||||
|
||||
/** The overlay-container is an invisible element which contains all individual overlays. */
|
||||
.md-overlay-container {
|
||||
position: absolute;
|
||||
|
||||
@@ -12,11 +12,6 @@ import {OverlayPositionBuilder} from './position/overlay-position-builder';
|
||||
import {ViewportRuler} from './position/viewport-ruler';
|
||||
|
||||
|
||||
// Re-export overlay-related modules so they can be imported directly from here.
|
||||
export {OverlayState} from './overlay-state';
|
||||
export {OverlayRef} from './overlay-ref';
|
||||
export {createOverlayContainer} from './overlay-container';
|
||||
|
||||
/** Token used to inject the DOM element that serves as the overlay container. */
|
||||
export const OVERLAY_CONTAINER_TOKEN = new OpaqueToken('overlayContainer');
|
||||
|
||||
@@ -103,3 +98,9 @@ export const OVERLAY_PROVIDERS = [
|
||||
OverlayPositionBuilder,
|
||||
Overlay,
|
||||
];
|
||||
|
||||
// Re-export overlay-related modules so they can be imported directly from here.
|
||||
export {OverlayState} from './overlay-state';
|
||||
export {OverlayRef} from './overlay-ref';
|
||||
export {createOverlayContainer} from './overlay-container';
|
||||
export {OVERLAY_DIRECTIVES, ConnectedOverlayDirective, OverlayOrigin} from './overlay-directives';
|
||||
|
||||
@@ -2,8 +2,11 @@ import {PositionStrategy} from './position-strategy';
|
||||
import {ElementRef} from '@angular/core';
|
||||
import {ViewportRuler} from './viewport-ruler';
|
||||
import {applyCssTransform} from '@angular2-material/core/style/apply-transform';
|
||||
import {ConnectionPair, OriginPos, OverlayPos} from './connected-position';
|
||||
|
||||
import {
|
||||
ConnectionPositionPair,
|
||||
OriginConnectionPosition,
|
||||
OverlayConnectionPosition
|
||||
} from './connected-position';
|
||||
|
||||
|
||||
/**
|
||||
@@ -19,7 +22,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
_isRtl: boolean = false;
|
||||
|
||||
/** Ordered list of preferred positions, from most to least desirable. */
|
||||
_preferredPositions: ConnectionPair[] = [];
|
||||
_preferredPositions: ConnectionPositionPair[] = [];
|
||||
|
||||
/** The origin element against which the overlay will be positioned. */
|
||||
private _origin: HTMLElement;
|
||||
@@ -27,13 +30,16 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
|
||||
constructor(
|
||||
private _connectedTo: ElementRef,
|
||||
private _originPos: OriginPos,
|
||||
private _overlayPos: OverlayPos,
|
||||
private _originPos: OriginConnectionPosition,
|
||||
private _overlayPos: OverlayConnectionPosition,
|
||||
private _viewportRuler: ViewportRuler) {
|
||||
this._origin = this._connectedTo.nativeElement;
|
||||
this.withFallbackPosition(_originPos, _overlayPos);
|
||||
}
|
||||
|
||||
get positions() {
|
||||
return this._preferredPositions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of the overlay element, using whichever preferred position relative
|
||||
@@ -74,12 +80,14 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
|
||||
|
||||
/** Adds a preferred position to the end of the ordered preferred position list. */
|
||||
addPreferredPosition(pos: ConnectionPair): void {
|
||||
addPreferredPosition(pos: ConnectionPositionPair): void {
|
||||
this._preferredPositions.push(pos);
|
||||
}
|
||||
|
||||
withFallbackPosition(originPos: OriginPos, overlayPos: OverlayPos): this {
|
||||
this._preferredPositions.push(new ConnectionPair(originPos, overlayPos));
|
||||
withFallbackPosition(
|
||||
originPos: OriginConnectionPosition,
|
||||
overlayPos: OverlayConnectionPosition): this {
|
||||
this._preferredPositions.push(new ConnectionPositionPair(originPos, overlayPos));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -106,7 +114,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
* @param originRect
|
||||
* @param pos
|
||||
*/
|
||||
private _getOriginConnectionPoint(originRect: ClientRect, pos: ConnectionPair): Point {
|
||||
private _getOriginConnectionPoint(originRect: ClientRect, pos: ConnectionPositionPair): Point {
|
||||
const originStartX = this._getStartX(originRect);
|
||||
const originEndX = this._getEndX(originRect);
|
||||
|
||||
@@ -138,7 +146,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
private _getOverlayPoint(
|
||||
originPoint: Point,
|
||||
overlayRect: ClientRect,
|
||||
pos: ConnectionPair): Point {
|
||||
pos: ConnectionPositionPair): Point {
|
||||
// Calculate the (overlayStartX, overlayStartY), the start of the potential overlay position
|
||||
// relative to the origin point.
|
||||
let overlayStartX: number;
|
||||
|
||||
@@ -6,28 +6,25 @@ export type VerticalConnectionPos = 'top' | 'center' | 'bottom';
|
||||
|
||||
|
||||
/** A connection point on the origin element. */
|
||||
export interface OriginPos {
|
||||
export interface OriginConnectionPosition {
|
||||
originX: HorizontalConnectionPos;
|
||||
originY: VerticalConnectionPos;
|
||||
}
|
||||
|
||||
/** A connection point on the overlay element. */
|
||||
export interface OverlayPos {
|
||||
export interface OverlayConnectionPosition {
|
||||
overlayX: HorizontalConnectionPos;
|
||||
overlayY: VerticalConnectionPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* The points of the origin element and the overlay element to connect.
|
||||
* @internal
|
||||
*/
|
||||
export class ConnectionPair {
|
||||
/** The points of the origin element and the overlay element to connect. */
|
||||
export class ConnectionPositionPair {
|
||||
originX: HorizontalConnectionPos;
|
||||
originY: VerticalConnectionPos;
|
||||
overlayX: HorizontalConnectionPos;
|
||||
overlayY: VerticalConnectionPos;
|
||||
|
||||
constructor(origin: OriginPos, overlay: OverlayPos) {
|
||||
constructor(origin: OriginConnectionPosition, overlay: OverlayConnectionPosition) {
|
||||
this.originX = origin.originX;
|
||||
this.originY = origin.originY;
|
||||
this.overlayX = overlay.overlayX;
|
||||
|
||||
@@ -2,7 +2,7 @@ import {ViewportRuler} from './viewport-ruler';
|
||||
import {ConnectedPositionStrategy} from './connected-position-strategy';
|
||||
import {ElementRef, Injectable} from '@angular/core';
|
||||
import {GlobalPositionStrategy} from './global-position-strategy';
|
||||
import {OverlayPos, OriginPos} from './connected-position';
|
||||
import {OverlayConnectionPosition, OriginConnectionPosition} from './connected-position';
|
||||
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@ export class OverlayPositionBuilder {
|
||||
}
|
||||
|
||||
/** Creates a relative position strategy. */
|
||||
connectedTo(elementRef: ElementRef, originPos: OriginPos, overlayPos: OverlayPos) {
|
||||
connectedTo(
|
||||
elementRef: ElementRef,
|
||||
originPos: OriginConnectionPosition,
|
||||
overlayPos: OverlayConnectionPosition) {
|
||||
return new ConnectedPositionStrategy(elementRef, originPos, overlayPos, this._viewportRuler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,3 +100,5 @@ export class PortalHostDirective extends BasePortalHost {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const PORTAL_DIRECTIVES = [TemplatePortalDirective, PortalHostDirective];
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from './portal-errors';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A `Portal` is something that you want to render somewhere else.
|
||||
* It can be attach to / detached from a `PortalHost`.
|
||||
@@ -202,3 +203,7 @@ export abstract class BasePortalHost implements PortalHost {
|
||||
this._disposeFn = fn;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {PORTAL_DIRECTIVES, TemplatePortalDirective, PortalHostDirective} from './portal-directives';
|
||||
export {DomPortalHost} from './dom-portal-host';
|
||||
|
||||
@@ -10,6 +10,17 @@
|
||||
Pasta 3
|
||||
</button>
|
||||
|
||||
|
||||
<button overlay-origin #trigger="overlayOrigin" (click)="isMenuOpen = !isMenuOpen">
|
||||
Open menu
|
||||
</button>
|
||||
|
||||
<template connected-overlay [origin]="trigger">
|
||||
<div style="background-color: mediumpurple" *ngIf="isMenuOpen">
|
||||
This is the menu panel.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Template to load into an overlay. -->
|
||||
<template portal>
|
||||
<p class="demo-fusilli"> Fusilli </p>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewChildren,
|
||||
QueryList,
|
||||
ViewEncapsulation,
|
||||
Directive,
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import {Overlay, OverlayState, OVERLAY_PROVIDERS} from '@angular2-material/core/overlay/overlay';
|
||||
import {ComponentPortal, Portal} from '@angular2-material/core/portal/portal';
|
||||
import {TemplatePortalDirective} from '@angular2-material/core/portal/portal-directives';
|
||||
|
||||
|
||||
|
||||
@Directive({selector: '[overlay-origin]'})
|
||||
class OverlayOrigin {
|
||||
constructor(public elementRef: ElementRef) { }
|
||||
}
|
||||
import {
|
||||
Overlay,
|
||||
OverlayState,
|
||||
OverlayOrigin,
|
||||
OVERLAY_PROVIDERS,
|
||||
OVERLAY_DIRECTIVES
|
||||
} from '@angular2-material/core/overlay/overlay';
|
||||
import {
|
||||
ComponentPortal,
|
||||
Portal,
|
||||
PORTAL_DIRECTIVES,
|
||||
TemplatePortalDirective
|
||||
} from '@angular2-material/core/portal/portal';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -25,12 +26,13 @@ class OverlayOrigin {
|
||||
selector: 'overlay-demo',
|
||||
templateUrl: 'overlay-demo.html',
|
||||
styleUrls: ['overlay-demo.css'],
|
||||
directives: [TemplatePortalDirective, OverlayOrigin],
|
||||
directives: [PORTAL_DIRECTIVES, OVERLAY_DIRECTIVES],
|
||||
providers: [OVERLAY_PROVIDERS],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class OverlayDemo {
|
||||
nextPosition: number = 0;
|
||||
isMenuOpen: boolean = false;
|
||||
|
||||
@ViewChildren(TemplatePortalDirective) templatePortals: QueryList<Portal<any>>;
|
||||
@ViewChild(OverlayOrigin) private _overlayOrigin: OverlayOrigin;
|
||||
|
||||
Reference in New Issue
Block a user