Skip to content

Commit 801d45a

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 e783494 commit 801d45a

File tree

5 files changed

+187
-83
lines changed

5 files changed

+187
-83
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: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,51 @@ import {
55
ViewEncapsulation,
66
NgZone,
77
OnDestroy,
8-
Renderer,
8+
animate,
9+
state,
10+
style,
11+
transition,
12+
trigger,
13+
AnimationTransitionEvent,
14+
EventEmitter,
915
} from '@angular/core';
1016
import {BasePortalHost, ComponentPortal, PortalHostDirective, TemplatePortal} from '../core';
1117
import {MdDialogConfig} from './dialog-config';
12-
import {MdDialogRef} from './dialog-ref';
1318
import {MdDialogContentAlreadyAttachedError} from './dialog-errors';
1419
import {FocusTrap} from '../core/a11y/focus-trap';
1520
import 'rxjs/add/operator/first';
1621

1722

23+
/** Possible states for the dialog container animation. */
24+
export type MdDialogContainerAnimationState = 'void' | 'enter' | 'exit' | 'exit-start';
25+
26+
1827
/**
1928
* Internal component that wraps user-provided dialog content.
29+
* Animation is based on https://material.io/guidelines/motion/choreography.html.
2030
* @docs-private
2131
*/
2232
@Component({
2333
moduleId: module.id,
2434
selector: 'md-dialog-container, mat-dialog-container',
2535
templateUrl: 'dialog-container.html',
2636
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+
],
2746
host: {
2847
'class': 'md-dialog-container',
2948
'[attr.role]': 'dialogConfig?.role',
30-
'(keydown.escape)': 'handleEscapeKey()',
49+
'(keydown.escape)': '_handleEscapeKey()',
50+
'[@slideDialog]': '_state',
51+
'(@slideDialog.done)': '_onAnimationDone($event)',
3152
},
32-
encapsulation: ViewEncapsulation.None,
3353
})
3454
export class MdDialogContainer extends BasePortalHost implements OnDestroy {
3555
/** The portal host inside of this container into which the dialog content will be loaded. */
@@ -44,10 +64,13 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
4464
/** The dialog configuration. */
4565
dialogConfig: MdDialogConfig;
4666

47-
/** Reference to the open dialog. */
48-
dialogRef: MdDialogRef<any>;
67+
/** State of the dialog animation. */
68+
_state: MdDialogContainerAnimationState = 'enter';
4969

50-
constructor(private _ngZone: NgZone, private _renderer: Renderer) {
70+
/** Emits the current animation state whenever it changes. */
71+
_onAnimationStateChange = new EventEmitter<MdDialogContainerAnimationState>();
72+
73+
constructor(private _ngZone: NgZone) {
5174
super();
5275
}
5376

@@ -78,25 +101,46 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
78101
throw Error('Not yet implemented');
79102
}
80103

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+
let toFocus = this._elementFocusedBeforeDialogWasOpened as HTMLElement;
110+
111+
// We need to check whether the focus method exists at all, because IE seems to throw an
112+
// exception, even if the element is the document.body.
113+
if (toFocus && 'focus' in toFocus) {
114+
toFocus.focus();
115+
}
116+
this._onAnimationStateChange.complete();
117+
});
118+
}
119+
81120
/**
82121
* Handles the user pressing the Escape key.
83122
* @docs-private
84123
*/
85-
handleEscapeKey() {
124+
_handleEscapeKey() {
86125
if (!this.dialogConfig.disableClose) {
87-
this.dialogRef.close();
126+
this._exit();
88127
}
89128
}
90129

91-
ngOnDestroy() {
92-
// When the dialog is destroyed, return focus to the element that originally had it before
93-
// the dialog was opened. Wait for the DOM to finish settling before changing the focus so
94-
// that it doesn't end up back on the <body>. Also note that we need the extra check, because
95-
// IE can set the `activeElement` to null in some cases.
96-
if (this._elementFocusedBeforeDialogWasOpened) {
97-
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
98-
this._renderer.invokeElementMethod(this._elementFocusedBeforeDialogWasOpened, 'focus');
99-
});
100-
}
130+
/**
131+
* Kicks off the leave animation.
132+
* @docs-private
133+
*/
134+
_exit(): void {
135+
this._state = 'exit';
136+
this._onAnimationStateChange.emit('exit-start');
137+
}
138+
139+
/**
140+
* Callback, invoked whenever an animation on the host completes.
141+
* @docs-private
142+
*/
143+
_onAnimationDone(event: AnimationTransitionEvent) {
144+
this._onAnimationStateChange.emit(event.toState as MdDialogContainerAnimationState);
101145
}
102146
}

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)