Skip to content

Commit d634a9b

Browse files
committed
feat(dialog): add enter/exit animations
* Adds enter/exit animations to the dialog. * Refactors the `MdDialogContainer` and `MdDialogRef` to accommodate the animations. * Fixes some test failures due to the animations. * Allows for the backdrop to be detached before the rest of the overlay, in order to allow for it to be transitioned in parallel. Fixes #2665.
1 parent 3b6cab0 commit d634a9b

File tree

5 files changed

+180
-78
lines changed

5 files changed

+180
-78
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class OverlayRef implements PortalHost {
4747
* @returns Resolves when the overlay has been detached.
4848
*/
4949
detach(): Promise<any> {
50-
this._detachBackdrop();
50+
this.detachBackdrop();
5151
return this._portalHost.detach();
5252
}
5353

@@ -59,7 +59,7 @@ export class OverlayRef implements PortalHost {
5959
this._state.positionStrategy.dispose();
6060
}
6161

62-
this._detachBackdrop();
62+
this.detachBackdrop();
6363
this._portalHost.dispose();
6464
}
6565

@@ -138,7 +138,7 @@ export class OverlayRef implements PortalHost {
138138
}
139139

140140
/** Detaches the backdrop (if any) associated with the overlay. */
141-
private _detachBackdrop(): void {
141+
detachBackdrop(): void {
142142
let backdropToDetach = this._backdropElement;
143143

144144
if (backdropToDetach) {

src/lib/dialog/dialog-container.ts

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,51 @@ import {
55
ViewEncapsulation,
66
NgZone,
77
OnDestroy,
8+
animate,
9+
state,
10+
style,
11+
transition,
12+
trigger,
13+
AnimationTransitionEvent,
14+
EventEmitter,
815
} from '@angular/core';
916
import {BasePortalHost, ComponentPortal, PortalHostDirective, TemplatePortal} from '../core';
1017
import {MdDialogConfig} from './dialog-config';
11-
import {MdDialogRef} from './dialog-ref';
1218
import {MdDialogContentAlreadyAttachedError} from './dialog-errors';
1319
import {FocusTrap} from '../core/a11y/focus-trap';
1420
import 'rxjs/add/operator/first';
1521

1622

23+
/** Possible states for the dialog container animation. */
24+
export type MdDialogContainerAnimationState = 'void' | 'enter' | 'exit' | 'exit-start';
25+
26+
1727
/**
1828
* Internal component that wraps user-provided dialog content.
29+
* Animation is based on https://material.io/guidelines/motion/choreography.html.
1930
* @docs-private
2031
*/
2132
@Component({
2233
moduleId: module.id,
2334
selector: 'md-dialog-container, mat-dialog-container',
2435
templateUrl: 'dialog-container.html',
2536
styleUrls: ['dialog.css'],
37+
encapsulation: ViewEncapsulation.None,
38+
animations: [
39+
trigger('slideDialog', [
40+
state('void', style({ transform: 'translateY(25%) scale(0.9)', opacity: 0 })),
41+
state('enter', style({ transform: 'translateY(0%) scale(1)', opacity: 1 })),
42+
state('exit', style({ transform: 'translateY(25%)', opacity: 0 })),
43+
transition('* => *', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')),
44+
])
45+
],
2646
host: {
2747
'class': 'md-dialog-container',
2848
'[attr.role]': 'dialogConfig?.role',
29-
'(keydown.escape)': 'handleEscapeKey()',
49+
'(keydown.escape)': '_handleEscapeKey()',
50+
'[@slideDialog]': '_state',
51+
'(@slideDialog.done)': '_onAnimationDone($event)',
3052
},
31-
encapsulation: ViewEncapsulation.None,
3253
})
3354
export class MdDialogContainer extends BasePortalHost implements OnDestroy {
3455
/** The portal host inside of this container into which the dialog content will be loaded. */
@@ -43,8 +64,11 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
4364
/** The dialog configuration. */
4465
dialogConfig: MdDialogConfig;
4566

46-
/** Reference to the open dialog. */
47-
dialogRef: MdDialogRef<any>;
67+
/** State of the dialog animation. */
68+
_state: MdDialogContainerAnimationState = 'enter';
69+
70+
/** Emits the current animation state whenever it changes. */
71+
_onAnimationStateChange = new EventEmitter<MdDialogContainerAnimationState>();
4872

4973
constructor(private _ngZone: NgZone) {
5074
super();
@@ -77,22 +101,40 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
77101
throw Error('Not yet implemented');
78102
}
79103

104+
ngOnDestroy() {
105+
// When the dialog is destroyed, return focus to the element that originally had it before
106+
// the dialog was opened. Wait for the DOM to finish settling before changing the focus so
107+
// that it doesn't end up back on the <body>.
108+
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
109+
(this._elementFocusedBeforeDialogWasOpened as HTMLElement).focus();
110+
this._onAnimationStateChange.complete();
111+
});
112+
}
113+
80114
/**
81115
* Handles the user pressing the Escape key.
82116
* @docs-private
83117
*/
84-
handleEscapeKey() {
118+
_handleEscapeKey() {
85119
if (!this.dialogConfig.disableClose) {
86-
this.dialogRef.close();
120+
this._exit();
87121
}
88122
}
89123

90-
ngOnDestroy() {
91-
// When the dialog is destroyed, return focus to the element that originally had it before
92-
// the dialog was opened. Wait for the DOM to finish settling before changing the focus so
93-
// that it doesn't end up back on the <body>.
94-
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
95-
(this._elementFocusedBeforeDialogWasOpened as HTMLElement).focus();
96-
});
124+
/**
125+
* Kicks off the leave animation.
126+
* @docs-private
127+
*/
128+
_exit(): void {
129+
this._state = 'exit';
130+
this._onAnimationStateChange.emit('exit-start');
131+
}
132+
133+
/**
134+
* Callback, invoked whenever an animation on the host completes.
135+
* @docs-private
136+
*/
137+
_onAnimationDone(event: AnimationTransitionEvent) {
138+
this._onAnimationStateChange.emit(event.toState as MdDialogContainerAnimationState);
97139
}
98140
}

src/lib/dialog/dialog-ref.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {OverlayRef} from '../core';
22
import {Observable} from 'rxjs/Observable';
33
import {Subject} from 'rxjs/Subject';
4+
import {MdDialogContainer, MdDialogContainerAnimationState} from './dialog-container';
45

56

67
// TODO(jelbourn): resizing
@@ -17,16 +18,30 @@ export class MdDialogRef<T> {
1718
/** Subject for notifying the user that the dialog has finished closing. */
1819
private _afterClosed: Subject<any> = new Subject();
1920

20-
constructor(private _overlayRef: OverlayRef) { }
21+
/** Result to be passed to afterClosed. */
22+
private _result: any;
23+
24+
constructor(private _overlayRef: OverlayRef, private _containerInstance: MdDialogContainer) {
25+
_containerInstance._onAnimationStateChange.subscribe(
26+
(state: MdDialogContainerAnimationState) => {
27+
if (state === 'exit-start') {
28+
// Transition the backdrop in parallel with the dialog.
29+
this._overlayRef.detachBackdrop();
30+
} else if (state === 'exit') {
31+
this._overlayRef.dispose();
32+
this._afterClosed.next(this._result);
33+
this._afterClosed.complete();
34+
}
35+
});
36+
}
2137

2238
/**
2339
* Close the dialog.
2440
* @param dialogResult Optional result to return to the dialog opener.
2541
*/
2642
close(dialogResult?: any): void {
27-
this._overlayRef.dispose();
28-
this._afterClosed.next(dialogResult);
29-
this._afterClosed.complete();
43+
this._result = dialogResult;
44+
this._containerInstance._exit();
3045
}
3146

3247
/**

0 commit comments

Comments
 (0)