Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(overlay): add connected overlay directive #496

Merged
merged 1 commit into from
May 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/core/overlay/overlay-directives.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
}
106 changes: 106 additions & 0 deletions src/core/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
@@ -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];
5 changes: 5 additions & 0 deletions src/core/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions src/core/overlay/overlay.scss
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
11 changes: 6 additions & 5 deletions src/core/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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';
28 changes: 18 additions & 10 deletions src/core/overlay/position/connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


/**
Expand All @@ -19,21 +22,24 @@ 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;


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
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand Down
13 changes: 5 additions & 8 deletions src/core/overlay/position/connected-position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 5 additions & 3 deletions src/core/overlay/position/overlay-position-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';



Expand All @@ -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);
}
}

2 changes: 2 additions & 0 deletions src/core/portal/portal-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,5 @@ export class PortalHostDirective extends BasePortalHost {
});
}
}

export const PORTAL_DIRECTIVES = [TemplatePortalDirective, PortalHostDirective];
5 changes: 5 additions & 0 deletions src/core/portal/portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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';
Loading