feat(overlay): support all overlay config properties (#1591)

This commit is contained in:
Kara
2016-10-25 10:21:25 -07:00
committed by GitHub
parent 333b11eaa7
commit 6f322cf2fe
3 changed files with 200 additions and 25 deletions
+3 -2
View File
@@ -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>
@@ -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;
}
+112 -20
View File
@@ -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';
@@ -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 = [
@@ -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(
@@ -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;
}
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(this._buildConfig());
}
this._overlayRef = this._overlay.create(overlayConfig);
this._overlayRef.attach(this._templatePortal);
/** Builds the overlay config based on the directive's inputs */
private _buildConfig(): OverlayState {
let overlayConfig = new OverlayState();
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();
}
}
}