Skip to content

Commit

Permalink
feat(dialog): add enter/exit animations
Browse files Browse the repository at this point in the history
* 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 angular#2665.
  • Loading branch information
crisbeto committed Jan 27, 2017
1 parent 3b6cab0 commit d634a9b
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 78 deletions.
6 changes: 3 additions & 3 deletions src/lib/core/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class OverlayRef implements PortalHost {
* @returns Resolves when the overlay has been detached.
*/
detach(): Promise<any> {
this._detachBackdrop();
this.detachBackdrop();
return this._portalHost.detach();
}

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

this._detachBackdrop();
this.detachBackdrop();
this._portalHost.dispose();
}

Expand Down Expand Up @@ -138,7 +138,7 @@ export class OverlayRef implements PortalHost {
}

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

if (backdropToDetach) {
Expand Down
70 changes: 56 additions & 14 deletions src/lib/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,51 @@ import {
ViewEncapsulation,
NgZone,
OnDestroy,
animate,
state,
style,
transition,
trigger,
AnimationTransitionEvent,
EventEmitter,
} from '@angular/core';
import {BasePortalHost, ComponentPortal, PortalHostDirective, TemplatePortal} from '../core';
import {MdDialogConfig} from './dialog-config';
import {MdDialogRef} from './dialog-ref';
import {MdDialogContentAlreadyAttachedError} from './dialog-errors';
import {FocusTrap} from '../core/a11y/focus-trap';
import 'rxjs/add/operator/first';


/** Possible states for the dialog container animation. */
export type MdDialogContainerAnimationState = 'void' | 'enter' | 'exit' | 'exit-start';


/**
* Internal component that wraps user-provided dialog content.
* Animation is based on https://material.io/guidelines/motion/choreography.html.
* @docs-private
*/
@Component({
moduleId: module.id,
selector: 'md-dialog-container, mat-dialog-container',
templateUrl: 'dialog-container.html',
styleUrls: ['dialog.css'],
encapsulation: ViewEncapsulation.None,
animations: [
trigger('slideDialog', [
state('void', style({ transform: 'translateY(25%) scale(0.9)', opacity: 0 })),
state('enter', style({ transform: 'translateY(0%) scale(1)', opacity: 1 })),
state('exit', style({ transform: 'translateY(25%)', opacity: 0 })),
transition('* => *', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')),
])
],
host: {
'class': 'md-dialog-container',
'[attr.role]': 'dialogConfig?.role',
'(keydown.escape)': 'handleEscapeKey()',
'(keydown.escape)': '_handleEscapeKey()',
'[@slideDialog]': '_state',
'(@slideDialog.done)': '_onAnimationDone($event)',
},
encapsulation: ViewEncapsulation.None,
})
export class MdDialogContainer extends BasePortalHost implements OnDestroy {
/** The portal host inside of this container into which the dialog content will be loaded. */
Expand All @@ -43,8 +64,11 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
/** The dialog configuration. */
dialogConfig: MdDialogConfig;

/** Reference to the open dialog. */
dialogRef: MdDialogRef<any>;
/** State of the dialog animation. */
_state: MdDialogContainerAnimationState = 'enter';

/** Emits the current animation state whenever it changes. */
_onAnimationStateChange = new EventEmitter<MdDialogContainerAnimationState>();

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

ngOnDestroy() {
// When the dialog is destroyed, return focus to the element that originally had it before
// the dialog was opened. Wait for the DOM to finish settling before changing the focus so
// that it doesn't end up back on the <body>.
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
(this._elementFocusedBeforeDialogWasOpened as HTMLElement).focus();
this._onAnimationStateChange.complete();
});
}

/**
* Handles the user pressing the Escape key.
* @docs-private
*/
handleEscapeKey() {
_handleEscapeKey() {
if (!this.dialogConfig.disableClose) {
this.dialogRef.close();
this._exit();
}
}

ngOnDestroy() {
// When the dialog is destroyed, return focus to the element that originally had it before
// the dialog was opened. Wait for the DOM to finish settling before changing the focus so
// that it doesn't end up back on the <body>.
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
(this._elementFocusedBeforeDialogWasOpened as HTMLElement).focus();
});
/**
* Kicks off the leave animation.
* @docs-private
*/
_exit(): void {
this._state = 'exit';
this._onAnimationStateChange.emit('exit-start');
}

/**
* Callback, invoked whenever an animation on the host completes.
* @docs-private
*/
_onAnimationDone(event: AnimationTransitionEvent) {
this._onAnimationStateChange.emit(event.toState as MdDialogContainerAnimationState);
}
}
23 changes: 19 additions & 4 deletions src/lib/dialog/dialog-ref.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {OverlayRef} from '../core';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {MdDialogContainer, MdDialogContainerAnimationState} from './dialog-container';


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

constructor(private _overlayRef: OverlayRef) { }
/** Result to be passed to afterClosed. */
private _result: any;

constructor(private _overlayRef: OverlayRef, private _containerInstance: MdDialogContainer) {
_containerInstance._onAnimationStateChange.subscribe(
(state: MdDialogContainerAnimationState) => {
if (state === 'exit-start') {
// Transition the backdrop in parallel with the dialog.
this._overlayRef.detachBackdrop();
} else if (state === 'exit') {
this._overlayRef.dispose();
this._afterClosed.next(this._result);
this._afterClosed.complete();
}
});
}

/**
* Close the dialog.
* @param dialogResult Optional result to return to the dialog opener.
*/
close(dialogResult?: any): void {
this._overlayRef.dispose();
this._afterClosed.next(dialogResult);
this._afterClosed.complete();
this._result = dialogResult;
this._containerInstance._exit();
}

/**
Expand Down
Loading

0 comments on commit d634a9b

Please sign in to comment.