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(connected-overlay): support all overlay config properties #1591

Merged
merged 1 commit into from
Oct 25, 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
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