Skip to content

Commit

Permalink
feat(overlay): support all overlay config properties (#1591)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara authored Oct 25, 2016
1 parent 333b11e commit 6f322cf
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 25 deletions.
5 changes: 3 additions & 2 deletions src/demo-app/overlay/overlay-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
Open menu
</button>

<template connected-overlay [origin]="trigger">
<div style="background-color: mediumpurple" *ngIf="isMenuOpen">
<template connected-overlay [origin]="trigger" [width]="500" hasBackdrop [open]="isMenuOpen"
(backdropClick)="isMenuOpen=false">
<div style="background-color: mediumpurple" >
This is the menu panel.
</div>
</template>
Expand Down
88 changes: 85 additions & 3 deletions src/lib/core/overlay/overlay-directives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,120 @@ describe('Overlay directives', () => {
fixture.detectChanges();
});

it(`should create an overlay and attach the directive's template`, () => {
it(`should attach the overlay based on the open property`, () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

expect(overlayContainerElement.textContent).toContain('Menu content');

fixture.componentInstance.isOpen = false;
fixture.detectChanges();

expect(overlayContainerElement.textContent).toBe('');
});

it('should destroy the overlay when the directive is destroyed', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();
fixture.destroy();

expect(overlayContainerElement.textContent.trim()).toBe('');
});

it('should use a connected position strategy with a default set of positions', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

let testComponent: ConnectedOverlayDirectiveTest =
fixture.debugElement.componentInstance;
let overlayDirective = testComponent.connectedOverlayDirective;

let strategy =
<ConnectedPositionStrategy> overlayDirective.overlayRef.getState().positionStrategy;
expect(strategy) .toEqual(jasmine.any(ConnectedPositionStrategy));
expect(strategy).toEqual(jasmine.any(ConnectedPositionStrategy));

let positions = strategy.positions;
expect(positions.length).toBeGreaterThan(0);
});

describe('inputs', () => {

it('should set the width', () => {
fixture.componentInstance.width = 250;
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const pane = overlayContainerElement.children[0] as HTMLElement;
expect(pane.style.width).toEqual('250px');
});

it('should set the height', () => {
fixture.componentInstance.height = '100vh';
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const pane = overlayContainerElement.children[0] as HTMLElement;
expect(pane.style.height).toEqual('100vh');
});

it('should create the backdrop if designated', () => {
fixture.componentInstance.hasBackdrop = true;
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

let backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop');
expect(backdrop).toBeTruthy();
});

it('should not create the backdrop by default', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

let backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop');
expect(backdrop).toBeNull();
});

it('should set the custom backdrop class', () => {
fixture.componentInstance.hasBackdrop = true;
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
expect(backdrop.classList).toContain('md-test-class');
});

it('should emit backdropClick appropriately', () => {
fixture.componentInstance.hasBackdrop = true;
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
backdrop.click();
fixture.detectChanges();

expect(fixture.componentInstance.backdropClicked).toBe(true);
});

});

});


@Component({
template: `
<button overlay-origin #trigger="overlayOrigin">Toggle menu</button>
<template connected-overlay [origin]="trigger">
<template connected-overlay [origin]="trigger" [open]="isOpen" [width]="width" [height]="height"
[hasBackdrop]="hasBackdrop" backdropClass="md-test-class"
(backdropClick)="backdropClicked=true">
<p>Menu content</p>
</template>`,
})
class ConnectedOverlayDirectiveTest {
isOpen = false;
width: number | string;
height: number | string;
hasBackdrop: boolean;
backdropClicked = false;

@ViewChild(ConnectedOverlayDirective) connectedOverlayDirective: ConnectedOverlayDirective;
}
132 changes: 112 additions & 20 deletions src/lib/core/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import {
NgModule,
ModuleWithProviders,
Directive,
EventEmitter,
TemplateRef,
ViewContainerRef,
OnInit,
Input,
OnDestroy,
Output,
ElementRef
} from '@angular/core';
import {Overlay, OVERLAY_PROVIDERS} from './overlay';
Expand All @@ -15,7 +16,8 @@ import {TemplatePortal} from '../portal/portal';
import {OverlayState} from './overlay-state';
import {ConnectionPositionPair} from './position/connected-position';
import {PortalModule} from '../portal/portal-directives';

import {ConnectedPositionStrategy} from './position/connected-position-strategy';
import {Subscription} from 'rxjs/Subscription';

/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
let defaultPositionList = [
Expand Down Expand Up @@ -50,15 +52,52 @@ export class OverlayOrigin {
* Directive to facilitate declarative creation of an Overlay using a ConnectedPositionStrategy.
*/
@Directive({
selector: '[connected-overlay]'
selector: '[connected-overlay]',
exportAs: 'connectedOverlay'
})
export class ConnectedOverlayDirective implements OnInit, OnDestroy {
export class ConnectedOverlayDirective implements OnDestroy {
private _overlayRef: OverlayRef;
private _templatePortal: TemplatePortal;
private _open = false;
private _hasBackdrop = false;
private _backdropSubscription: Subscription;

@Input() origin: OverlayOrigin;
@Input() positions: ConnectionPositionPair[];

/** The width of the overlay panel. */
@Input() width: number | string;

/** The height of the overlay panel. */
@Input() height: number | string;

/** The custom class to be set on the backdrop element. */
@Input() backdropClass: string;

/** Whether or not the overlay should attach a backdrop. */
@Input()
get hasBackdrop() {
return this._hasBackdrop;
}

// TODO: move the boolean coercion logic to a shared function in core
set hasBackdrop(value: any) {
this._hasBackdrop = value != null && `${value}` !== 'false';
}

@Input()
get open() {
return this._open;
}

set open(value: boolean) {
value ? this._attachOverlay() : this._detachOverlay();
this._open = value;
}

/** Event emitted when the backdrop is clicked. */
@Output() backdropClick: EventEmitter<null> = new EventEmitter();

// TODO(jelbourn): inputs for size, scroll behavior, animation, etc.

constructor(
Expand All @@ -68,40 +107,93 @@ export class ConnectedOverlayDirective implements OnInit, OnDestroy {
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
}

get overlayRef() {
get overlayRef(): OverlayRef {
return this._overlayRef;
}

/** TODO: internal */
ngOnInit() {
this._createOverlay();
}

/** TODO: internal */
ngOnDestroy() {
this._destroyOverlay();
}

/** Creates an overlay and attaches this directive's template to it. */
/** Creates an overlay */
private _createOverlay() {
if (!this.positions || !this.positions.length) {
this.positions = defaultPositionList;
}

this._overlayRef = this._overlay.create(this._buildConfig());
}

/** Builds the overlay config based on the directive's inputs */
private _buildConfig(): OverlayState {
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._overlayRef = this._overlay.create(overlayConfig);
this._overlayRef.attach(this._templatePortal);

if (this.width || this.width === 0) {
overlayConfig.width = this.width;
}

if (this.height || this.height === 0) {
overlayConfig.height = this.height;
}

overlayConfig.hasBackdrop = this.hasBackdrop;

if (this.backdropClass) {
overlayConfig.backdropClass = this.backdropClass;
}

overlayConfig.positionStrategy = this._getPosition();

return overlayConfig;
}

/** Returns the position of the overlay to be set on the overlay config */
private _getPosition(): ConnectedPositionStrategy {
return 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});
}

/** Attaches the overlay and subscribes to backdrop clicks if backdrop exists */
private _attachOverlay() {
if (!this._overlayRef) {
this._createOverlay();
}

if (!this._overlayRef.hasAttached()) {
this._overlayRef.attach(this._templatePortal);
}

if (this.hasBackdrop) {
this._backdropSubscription = this._overlayRef.backdropClick().subscribe(() => {
this.backdropClick.emit(null);
});
}
}

/** Detaches the overlay and unsubscribes to backdrop clicks if backdrop exists */
private _detachOverlay() {
if (this._overlayRef) {
this._overlayRef.detach();
}

if (this._backdropSubscription) {
this._backdropSubscription.unsubscribe();
this._backdropSubscription = null;
}
}

/** Destroys the overlay created by this directive. */
private _destroyOverlay() {
this._overlayRef.dispose();
if (this._overlayRef) {
this._overlayRef.dispose();
}

if (this._backdropSubscription) {
this._backdropSubscription.unsubscribe();
}
}
}

Expand Down

0 comments on commit 6f322cf

Please sign in to comment.