Skip to content

Commit 5492225

Browse files
crisbetommalerba
authored andcommitted
feat(dialog): add enter/exit animations (#2825)
* 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 e3b2486 commit 5492225

File tree

5 files changed

+200
-87
lines changed

5 files changed

+200
-87
lines changed

Diff for: src/lib/core/overlay/overlay-ref.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class OverlayRef implements PortalHost {
5252
* @returns Resolves when the overlay has been detached.
5353
*/
5454
detach(): Promise<any> {
55-
this._detachBackdrop();
55+
this.detachBackdrop();
5656

5757
// When the overlay is detached, the pane element should disable pointer events.
5858
// This is necessary because otherwise the pane element will cover the page and disable
@@ -70,7 +70,7 @@ export class OverlayRef implements PortalHost {
7070
this._state.positionStrategy.dispose();
7171
}
7272

73-
this._detachBackdrop();
73+
this.detachBackdrop();
7474
this._portalHost.dispose();
7575
}
7676

@@ -154,7 +154,7 @@ export class OverlayRef implements PortalHost {
154154
}
155155

156156
/** Detaches the backdrop (if any) associated with the overlay. */
157-
private _detachBackdrop(): void {
157+
detachBackdrop(): void {
158158
let backdropToDetach = this._backdropElement;
159159

160160
if (backdropToDetach) {

Diff for: src/lib/dialog/dialog-container.ts

+59-13
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,50 @@ 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.mat-dialog-container]': 'true',
2948
'[attr.role]': 'dialogConfig?.role',
49+
'[@slideDialog]': '_state',
50+
'(@slideDialog.done)': '_onAnimationDone($event)',
3051
},
31-
encapsulation: ViewEncapsulation.None,
3252
})
3353
export class MdDialogContainer extends BasePortalHost implements OnDestroy {
3454
/** The portal host inside of this container into which the dialog content will be loaded. */
@@ -38,15 +58,18 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
3858
@ViewChild(FocusTrap) _focusTrap: FocusTrap;
3959

4060
/** Element that was focused before the dialog was opened. Save this to restore upon close. */
41-
private _elementFocusedBeforeDialogWasOpened: Element = null;
61+
private _elementFocusedBeforeDialogWasOpened: HTMLElement = null;
4262

4363
/** The dialog configuration. */
4464
dialogConfig: MdDialogConfig;
4565

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

49-
constructor(private _ngZone: NgZone, private _renderer: Renderer) {
72+
constructor(private _ngZone: NgZone) {
5073
super();
5174
}
5275

@@ -87,20 +110,43 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
87110
// ready in instances where change detection has to run first. To deal with this, we simply
88111
// wait for the microtask queue to be empty.
89112
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
90-
this._elementFocusedBeforeDialogWasOpened = document.activeElement;
113+
this._elementFocusedBeforeDialogWasOpened = document.activeElement as HTMLElement;
91114
this._focusTrap.focusFirstTabbableElement();
92115
});
93116
}
94117

118+
/**
119+
* Kicks off the leave animation.
120+
* @docs-private
121+
*/
122+
_exit(): void {
123+
this._state = 'exit';
124+
this._onAnimationStateChange.emit('exit-start');
125+
}
126+
127+
/**
128+
* Callback, invoked whenever an animation on the host completes.
129+
* @docs-private
130+
*/
131+
_onAnimationDone(event: AnimationTransitionEvent) {
132+
this._onAnimationStateChange.emit(event.toState as MdDialogContainerAnimationState);
133+
}
134+
95135
ngOnDestroy() {
96136
// When the dialog is destroyed, return focus to the element that originally had it before
97137
// the dialog was opened. Wait for the DOM to finish settling before changing the focus so
98138
// that it doesn't end up back on the <body>. Also note that we need the extra check, because
99139
// IE can set the `activeElement` to null in some cases.
100-
if (this._elementFocusedBeforeDialogWasOpened) {
101-
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
102-
this._renderer.invokeElementMethod(this._elementFocusedBeforeDialogWasOpened, 'focus');
103-
});
104-
}
140+
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
141+
let toFocus = this._elementFocusedBeforeDialogWasOpened as HTMLElement;
142+
143+
// We need to check whether the focus method exists at all, because IE seems to throw an
144+
// exception, even if the element is the document.body.
145+
if (toFocus && 'focus' in toFocus) {
146+
toFocus.focus();
147+
}
148+
149+
this._onAnimationStateChange.complete();
150+
});
105151
}
106152
}

Diff for: src/lib/dialog/dialog-ref.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {OverlayRef} from '../core';
2-
import {MdDialogConfig} from './dialog-config';
32
import {Observable} from 'rxjs/Observable';
43
import {Subject} from 'rxjs/Subject';
4+
import {MdDialogContainer, MdDialogContainerAnimationState} from './dialog-container';
55

66

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

21-
constructor(private _overlayRef: OverlayRef, public config: MdDialogConfig) { }
21+
/** Result to be passed to afterClosed. */
22+
private _result: any;
23+
24+
constructor(private _overlayRef: OverlayRef, public _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+
}
2237

2338
/**
2439
* Close the dialog.
2540
* @param dialogResult Optional result to return to the dialog opener.
2641
*/
2742
close(dialogResult?: any): void {
28-
this._overlayRef.dispose();
29-
this._afterClosed.next(dialogResult);
30-
this._afterClosed.complete();
43+
this._result = dialogResult;
44+
this._containerInstance._exit();
3145
}
3246

3347
/**

0 commit comments

Comments
 (0)