Skip to content

Commit 6f322cf

Browse files
authored
feat(overlay): support all overlay config properties (#1591)
1 parent 333b11e commit 6f322cf

File tree

3 files changed

+200
-25
lines changed

3 files changed

+200
-25
lines changed

src/demo-app/overlay/overlay-demo.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
Open menu
1616
</button>
1717

18-
<template connected-overlay [origin]="trigger">
19-
<div style="background-color: mediumpurple" *ngIf="isMenuOpen">
18+
<template connected-overlay [origin]="trigger" [width]="500" hasBackdrop [open]="isMenuOpen"
19+
(backdropClick)="isMenuOpen=false">
20+
<div style="background-color: mediumpurple" >
2021
This is the menu panel.
2122
</div>
2223
</template>

src/lib/core/overlay/overlay-directives.spec.ts

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,38 +27,120 @@ describe('Overlay directives', () => {
2727
fixture.detectChanges();
2828
});
2929

30-
it(`should create an overlay and attach the directive's template`, () => {
30+
it(`should attach the overlay based on the open property`, () => {
31+
fixture.componentInstance.isOpen = true;
32+
fixture.detectChanges();
33+
3134
expect(overlayContainerElement.textContent).toContain('Menu content');
35+
36+
fixture.componentInstance.isOpen = false;
37+
fixture.detectChanges();
38+
39+
expect(overlayContainerElement.textContent).toBe('');
3240
});
3341

3442
it('should destroy the overlay when the directive is destroyed', () => {
43+
fixture.componentInstance.isOpen = true;
44+
fixture.detectChanges();
3545
fixture.destroy();
3646

3747
expect(overlayContainerElement.textContent.trim()).toBe('');
3848
});
3949

4050
it('should use a connected position strategy with a default set of positions', () => {
51+
fixture.componentInstance.isOpen = true;
52+
fixture.detectChanges();
53+
4154
let testComponent: ConnectedOverlayDirectiveTest =
4255
fixture.debugElement.componentInstance;
4356
let overlayDirective = testComponent.connectedOverlayDirective;
4457

4558
let strategy =
4659
<ConnectedPositionStrategy> overlayDirective.overlayRef.getState().positionStrategy;
47-
expect(strategy) .toEqual(jasmine.any(ConnectedPositionStrategy));
60+
expect(strategy).toEqual(jasmine.any(ConnectedPositionStrategy));
4861

4962
let positions = strategy.positions;
5063
expect(positions.length).toBeGreaterThan(0);
5164
});
65+
66+
describe('inputs', () => {
67+
68+
it('should set the width', () => {
69+
fixture.componentInstance.width = 250;
70+
fixture.componentInstance.isOpen = true;
71+
fixture.detectChanges();
72+
73+
const pane = overlayContainerElement.children[0] as HTMLElement;
74+
expect(pane.style.width).toEqual('250px');
75+
});
76+
77+
it('should set the height', () => {
78+
fixture.componentInstance.height = '100vh';
79+
fixture.componentInstance.isOpen = true;
80+
fixture.detectChanges();
81+
82+
const pane = overlayContainerElement.children[0] as HTMLElement;
83+
expect(pane.style.height).toEqual('100vh');
84+
});
85+
86+
it('should create the backdrop if designated', () => {
87+
fixture.componentInstance.hasBackdrop = true;
88+
fixture.componentInstance.isOpen = true;
89+
fixture.detectChanges();
90+
91+
let backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop');
92+
expect(backdrop).toBeTruthy();
93+
});
94+
95+
it('should not create the backdrop by default', () => {
96+
fixture.componentInstance.isOpen = true;
97+
fixture.detectChanges();
98+
99+
let backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop');
100+
expect(backdrop).toBeNull();
101+
});
102+
103+
it('should set the custom backdrop class', () => {
104+
fixture.componentInstance.hasBackdrop = true;
105+
fixture.componentInstance.isOpen = true;
106+
fixture.detectChanges();
107+
108+
const backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
109+
expect(backdrop.classList).toContain('md-test-class');
110+
});
111+
112+
it('should emit backdropClick appropriately', () => {
113+
fixture.componentInstance.hasBackdrop = true;
114+
fixture.componentInstance.isOpen = true;
115+
fixture.detectChanges();
116+
117+
const backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
118+
backdrop.click();
119+
fixture.detectChanges();
120+
121+
expect(fixture.componentInstance.backdropClicked).toBe(true);
122+
});
123+
124+
});
125+
52126
});
53127

54128

55129
@Component({
56130
template: `
57131
<button overlay-origin #trigger="overlayOrigin">Toggle menu</button>
58-
<template connected-overlay [origin]="trigger">
132+
<template connected-overlay [origin]="trigger" [open]="isOpen" [width]="width" [height]="height"
133+
[hasBackdrop]="hasBackdrop" backdropClass="md-test-class"
134+
(backdropClick)="backdropClicked=true">
59135
<p>Menu content</p>
60136
</template>`,
61137
})
62138
class ConnectedOverlayDirectiveTest {
139+
isOpen = false;
140+
width: number | string;
141+
height: number | string;
142+
hasBackdrop: boolean;
143+
backdropClicked = false;
144+
63145
@ViewChild(ConnectedOverlayDirective) connectedOverlayDirective: ConnectedOverlayDirective;
64146
}

src/lib/core/overlay/overlay-directives.ts

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import {
22
NgModule,
33
ModuleWithProviders,
44
Directive,
5+
EventEmitter,
56
TemplateRef,
67
ViewContainerRef,
7-
OnInit,
88
Input,
99
OnDestroy,
10+
Output,
1011
ElementRef
1112
} from '@angular/core';
1213
import {Overlay, OVERLAY_PROVIDERS} from './overlay';
@@ -15,7 +16,8 @@ import {TemplatePortal} from '../portal/portal';
1516
import {OverlayState} from './overlay-state';
1617
import {ConnectionPositionPair} from './position/connected-position';
1718
import {PortalModule} from '../portal/portal-directives';
18-
19+
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
20+
import {Subscription} from 'rxjs/Subscription';
1921

2022
/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
2123
let defaultPositionList = [
@@ -50,15 +52,52 @@ export class OverlayOrigin {
5052
* Directive to facilitate declarative creation of an Overlay using a ConnectedPositionStrategy.
5153
*/
5254
@Directive({
53-
selector: '[connected-overlay]'
55+
selector: '[connected-overlay]',
56+
exportAs: 'connectedOverlay'
5457
})
55-
export class ConnectedOverlayDirective implements OnInit, OnDestroy {
58+
export class ConnectedOverlayDirective implements OnDestroy {
5659
private _overlayRef: OverlayRef;
5760
private _templatePortal: TemplatePortal;
61+
private _open = false;
62+
private _hasBackdrop = false;
63+
private _backdropSubscription: Subscription;
5864

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

68+
/** The width of the overlay panel. */
69+
@Input() width: number | string;
70+
71+
/** The height of the overlay panel. */
72+
@Input() height: number | string;
73+
74+
/** The custom class to be set on the backdrop element. */
75+
@Input() backdropClass: string;
76+
77+
/** Whether or not the overlay should attach a backdrop. */
78+
@Input()
79+
get hasBackdrop() {
80+
return this._hasBackdrop;
81+
}
82+
83+
// TODO: move the boolean coercion logic to a shared function in core
84+
set hasBackdrop(value: any) {
85+
this._hasBackdrop = value != null && `${value}` !== 'false';
86+
}
87+
88+
@Input()
89+
get open() {
90+
return this._open;
91+
}
92+
93+
set open(value: boolean) {
94+
value ? this._attachOverlay() : this._detachOverlay();
95+
this._open = value;
96+
}
97+
98+
/** Event emitted when the backdrop is clicked. */
99+
@Output() backdropClick: EventEmitter<null> = new EventEmitter();
100+
62101
// TODO(jelbourn): inputs for size, scroll behavior, animation, etc.
63102

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

71-
get overlayRef() {
110+
get overlayRef(): OverlayRef {
72111
return this._overlayRef;
73112
}
74113

75-
/** TODO: internal */
76-
ngOnInit() {
77-
this._createOverlay();
78-
}
79-
80114
/** TODO: internal */
81115
ngOnDestroy() {
82116
this._destroyOverlay();
83117
}
84118

85-
/** Creates an overlay and attaches this directive's template to it. */
119+
/** Creates an overlay */
86120
private _createOverlay() {
87121
if (!this.positions || !this.positions.length) {
88122
this.positions = defaultPositionList;
89123
}
90124

125+
this._overlayRef = this._overlay.create(this._buildConfig());
126+
}
127+
128+
/** Builds the overlay config based on the directive's inputs */
129+
private _buildConfig(): OverlayState {
91130
let overlayConfig = new OverlayState();
92-
overlayConfig.positionStrategy =
93-
this._overlay.position().connectedTo(
94-
this.origin.elementRef,
95-
{originX: this.positions[0].overlayX, originY: this.positions[0].originY},
96-
{overlayX: this.positions[0].overlayX, overlayY: this.positions[0].overlayY});
97-
98-
this._overlayRef = this._overlay.create(overlayConfig);
99-
this._overlayRef.attach(this._templatePortal);
131+
132+
if (this.width || this.width === 0) {
133+
overlayConfig.width = this.width;
134+
}
135+
136+
if (this.height || this.height === 0) {
137+
overlayConfig.height = this.height;
138+
}
139+
140+
overlayConfig.hasBackdrop = this.hasBackdrop;
141+
142+
if (this.backdropClass) {
143+
overlayConfig.backdropClass = this.backdropClass;
144+
}
145+
146+
overlayConfig.positionStrategy = this._getPosition();
147+
148+
return overlayConfig;
149+
}
150+
151+
/** Returns the position of the overlay to be set on the overlay config */
152+
private _getPosition(): ConnectedPositionStrategy {
153+
return this._overlay.position().connectedTo(
154+
this.origin.elementRef,
155+
{originX: this.positions[0].overlayX, originY: this.positions[0].originY},
156+
{overlayX: this.positions[0].overlayX, overlayY: this.positions[0].overlayY});
157+
}
158+
159+
/** Attaches the overlay and subscribes to backdrop clicks if backdrop exists */
160+
private _attachOverlay() {
161+
if (!this._overlayRef) {
162+
this._createOverlay();
163+
}
164+
165+
if (!this._overlayRef.hasAttached()) {
166+
this._overlayRef.attach(this._templatePortal);
167+
}
168+
169+
if (this.hasBackdrop) {
170+
this._backdropSubscription = this._overlayRef.backdropClick().subscribe(() => {
171+
this.backdropClick.emit(null);
172+
});
173+
}
174+
}
175+
176+
/** Detaches the overlay and unsubscribes to backdrop clicks if backdrop exists */
177+
private _detachOverlay() {
178+
if (this._overlayRef) {
179+
this._overlayRef.detach();
180+
}
181+
182+
if (this._backdropSubscription) {
183+
this._backdropSubscription.unsubscribe();
184+
this._backdropSubscription = null;
185+
}
100186
}
101187

102188
/** Destroys the overlay created by this directive. */
103189
private _destroyOverlay() {
104-
this._overlayRef.dispose();
190+
if (this._overlayRef) {
191+
this._overlayRef.dispose();
192+
}
193+
194+
if (this._backdropSubscription) {
195+
this._backdropSubscription.unsubscribe();
196+
}
105197
}
106198
}
107199

0 commit comments

Comments
 (0)