@@ -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' ;
1016import { BasePortalHost , ComponentPortal , PortalHostDirective , TemplatePortal } from '../core' ;
1117import { MdDialogConfig } from './dialog-config' ;
12- import { MdDialogRef } from './dialog-ref' ;
1318import { MdDialogContentAlreadyAttachedError } from './dialog-errors' ;
1419import { FocusTrap } from '../core/a11y/focus-trap' ;
1520import '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} )
3353export 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}
0 commit comments