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(dialog): add backdrop #1041

Merged
merged 4 commits into from
Sep 9, 2016
Merged
Show file tree
Hide file tree
Changes from 3 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
53 changes: 52 additions & 1 deletion src/lib/core/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
import {PortalHost, Portal} from '../portal/portal';
import {OverlayState} from './overlay-state';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';


/**
* Reference to an overlay that has been created with the Overlay service.
* Used to manipulate or dispose of said overlay.
*/
export class OverlayRef implements PortalHost {
private _backdropElement: HTMLElement = null;
private _backdropClick: Subject<any> = new Subject();

constructor(
private _portalHost: PortalHost,
private _pane: HTMLElement,
private _state: OverlayState) { }

attach(portal: Portal<any>): any {
if (this._state.hasBackdrop) {
this._attachBackdrop();
}

let attachResult = this._portalHost.attach(portal);
this.updatePosition();

return attachResult;
}

detach(): Promise<any> {
this._detatchBackdrop();
return this._portalHost.detach();
}

dispose(): void {
this._detatchBackdrop();
this._portalHost.dispose();
}

hasAttached(): boolean {
return this._portalHost.hasAttached();
}

backdropClick(): Observable<void> {
return this._backdropClick.asObservable();
}

/** Gets the current state config of the overlay. */
getState() {
return this._state;
Expand All @@ -42,5 +58,40 @@ export class OverlayRef implements PortalHost {
}
}

// TODO(jelbourn): add additional methods for manipulating the overlay.
/** Attaches a backdrop for this overlay. */
private _attachBackdrop() {
this._backdropElement = document.createElement('div');
this._backdropElement.classList.add('md-overlay-backdrop');
this._pane.parentElement.appendChild(this._backdropElement);

// Forward backdrop clicks that that the consumer of the overlay can perform whatever
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that that -> such that

// action desired when such a click occurs (usually closing the overlay).
this._backdropElement.addEventListener('click', () => {
this._backdropClick.next(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentionally to not pass the DOM event to the Subject? It is not necessary but would be a good improvement though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to keep any DOM stuff out of public-facing APIs for now (keeping potential running on web-workers in mind).

});

// Add class to fade-in the backdrop after one frame.
requestAnimationFrame(() => {
this._backdropElement.classList.add('md-overlay-backdrop-showing');
});
}

/** Detaches the backdrop (if any) associated with the overlay. */
private _detatchBackdrop(): void {
let backdropToDetach = this._backdropElement;

if (backdropToDetach) {
backdropToDetach.classList.remove('md-overlay-backdrop-showing');
backdropToDetach.addEventListener('transitionend', () => {
backdropToDetach.parentNode.removeChild(backdropToDetach);

// It is possible that a new portal has been attached to this overlay since we started
// removing the backdrop. If that is the case, only clear our the backdrop reference if it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

our the -> the

// is still the same instance that we started to remove.
if (this._backdropElement == backdropToDetach) {
this._backdropElement = null;
}
});
}
}
}
3 changes: 3 additions & 0 deletions src/lib/core/overlay/overlay-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export class OverlayState {
/** Strategy with which to position the overlay. */
positionStrategy: PositionStrategy;

/** Whether the overlay has a backdrop. */
hasBackdrop: boolean = false;

// TODO(jelbourn): configuration still to add
// - overlay size
// - focus trap
Expand Down
30 changes: 29 additions & 1 deletion src/lib/core/overlay/overlay.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
@import 'variables';
@import 'palette';

$md-backdrop-color: md-color($md-grey, 900);

// TODO(jelbourn): change from the `md` prefix to something else for everything in the toolkit.

@import 'variables';
Expand All @@ -14,12 +19,35 @@
left: 0;
height: 100%;
width: 100%;

z-index: 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a sass variable too?

}

/** A single overlay pane. */
.md-overlay-pane {
position: absolute;
pointer-events: auto;
box-sizing: border-box;
z-index: $z-index-overlay;
z-index: $md-z-index-overlay;
}

.md-overlay-backdrop {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the md-fullscreen mixin here?


z-index: $md-z-index-overlay-backdrop;
pointer-events: auto;

// TODO(jelbourn): figure out if there are actually spec'ed colors for both light and dark
// themes here. Currently using the values from Angular Material 1.
transition: opacity $swift-ease-out-duration $swift-ease-out-timing-function;
background: $md-backdrop-color;
opacity: 0;
}

.md-overlay-backdrop.md-overlay-backdrop-showing {
opacity: 0.48;
}
27 changes: 25 additions & 2 deletions src/lib/core/overlay/overlay.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {inject, TestBed, async} from '@angular/core/testing';
import {inject, TestBed, async, ComponentFixture} from '@angular/core/testing';
import {NgModule, Component, ViewChild, ViewContainerRef} from '@angular/core';
import {TemplatePortalDirective, PortalModule} from '../portal/portal-directives';
import {TemplatePortal, ComponentPortal} from '../portal/portal';
Expand All @@ -14,6 +14,7 @@ describe('Overlay', () => {
let componentPortal: ComponentPortal<PizzaMsg>;
let templatePortal: TemplatePortal;
let overlayContainerElement: HTMLElement;
let viewContainerFixture: ComponentFixture<TestComponentWithTemplatePortals>;

beforeEach(async(() => {
TestBed.configureTestingModule({
Expand All @@ -36,6 +37,7 @@ describe('Overlay', () => {
fixture.detectChanges();
templatePortal = fixture.componentInstance.templatePortal;
componentPortal = new ComponentPortal(PizzaMsg, fixture.componentInstance.viewContainerRef);
viewContainerFixture = fixture;
}));

it('should load a component into an overlay', () => {
Expand Down Expand Up @@ -80,7 +82,7 @@ describe('Overlay', () => {
expect(overlayContainerElement.textContent).toBe('');
});

describe('applyState', () => {
describe('positioning', () => {
let state: OverlayState;

beforeEach(() => {
Expand All @@ -95,6 +97,27 @@ describe('Overlay', () => {
expect(overlayContainerElement.querySelectorAll('.fake-positioned').length).toBe(1);
});
});

describe('backdrop', () => {
it('should create and destroy an overlay backdrop', () => {
let config = new OverlayState();
config.hasBackdrop = true;

let overlayRef = overlay.create(config);
overlayRef.attach(componentPortal);

viewContainerFixture.detectChanges();
let backdrop = <HTMLElement> overlayContainerElement.querySelector('.md-overlay-backdrop');
expect(backdrop).toBeTruthy();
expect(backdrop.classList).not.toContain('.md-overlay-backdrop-showing');

let backdropClickHandler = jasmine.createSpy('backdropClickHander');
overlayRef.backdropClick().subscribe(backdropClickHandler);

backdrop.click();
expect(backdropClickHandler).toHaveBeenCalled();
});
});
});


Expand Down
7 changes: 6 additions & 1 deletion src/lib/core/style/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ $md-xsmall: 'max-width: 600px';

// TODO: Revisit all z-indices before beta
// z-index master list

$z-index-fab: 20 !default;
$z-index-drawer: 100 !default;
$z-index-overlay: 1000 !default;

// Overlay z indices.
$md-z-index-overlay: 1000;
$md-z-index-overlay-backdrop: 1;


// Global constants
$pi: 3.14159265;
Expand Down
16 changes: 14 additions & 2 deletions src/lib/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ describe('MdDialog', () => {

viewContainerFixture.detectChanges();

viewContainerFixture.detectChanges();

let afterCloseResult: string;
dialogRef.afterClosed().subscribe(result => {
afterCloseResult = result;
Expand All @@ -88,6 +86,20 @@ describe('MdDialog', () => {
expect(afterCloseResult).toBe('Charmander');
expect(overlayContainerElement.querySelector('md-dialog-container')).toBeNull();
});

it('should close when clicking on the overlay backdrop', () => {
let config = new MdDialogConfig();
config.viewContainerRef = testViewContainerRef;

dialog.open(PizzaMsg, config);

viewContainerFixture.detectChanges();

let backdrop = <HTMLElement> overlayContainerElement.querySelector('.md-overlay-backdrop');
backdrop.click();

expect(overlayContainerElement.querySelector('md-dialog-container')).toBeFalsy();
});
});


Expand Down
4 changes: 4 additions & 0 deletions src/lib/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export class MdDialog {
// to modify and close it.
let dialogRef = <MdDialogRef<T>> new MdDialogRef(overlayRef);

// When the dialog backdrop is clicked, we want to close it.
overlayRef.backdropClick().subscribe(() => dialogRef.close());

// We create an injector specifically for the component we're instantiating so that it can
// inject the MdDialogRef. This allows a component loaded inside of a dialog to close itself
// and, optionally, to return a value.
Expand All @@ -108,6 +111,7 @@ export class MdDialog {
private _getOverlayState(dialogConfig: MdDialogConfig): OverlayState {
let state = new OverlayState();

state.hasBackdrop = true;
state.positionStrategy = this._overlay.position()
.global()
.centerHorizontally()
Expand Down