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 Feb 21, 2017
1 parent b939cd8 commit e857460
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 87 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 @@ -52,7 +52,7 @@ export class OverlayRef implements PortalHost {
* @returns Resolves when the overlay has been detached.
*/
detach(): Promise<any> {
this._detachBackdrop();
this.detachBackdrop();

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

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

Expand Down Expand Up @@ -154,7 +154,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
72 changes: 59 additions & 13 deletions src/lib/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,50 @@ import {
ViewEncapsulation,
NgZone,
OnDestroy,
Renderer,
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.mat-dialog-container]': 'true',
'[attr.role]': 'dialogConfig?.role',
'[@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 @@ -38,15 +58,18 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
@ViewChild(FocusTrap) _focusTrap: FocusTrap;

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

/** 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, private _renderer: Renderer) {
constructor(private _ngZone: NgZone) {
super();
}

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

/**
* 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);
}

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>. Also note that we need the extra check, because
// IE can set the `activeElement` to null in some cases.
if (this._elementFocusedBeforeDialogWasOpened) {
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
this._renderer.invokeElementMethod(this._elementFocusedBeforeDialogWasOpened, 'focus');
});
}
this._ngZone.onMicrotaskEmpty.first().subscribe(() => {
let toFocus = this._elementFocusedBeforeDialogWasOpened as HTMLElement;

// We need to check whether the focus method exists at all, because IE seems to throw an
// exception, even if the element is the document.body.
if (toFocus && 'focus' in toFocus) {
toFocus.focus();
}

this._onAnimationStateChange.complete();
});
}
}
24 changes: 19 additions & 5 deletions src/lib/dialog/dialog-ref.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {OverlayRef} from '../core';
import {MdDialogConfig} from './dialog-config';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {MdDialogContainer, MdDialogContainerAnimationState} from './dialog-container';


// TODO(jelbourn): resizing
Expand All @@ -18,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, public config: MdDialogConfig) { }
/** Result to be passed to afterClosed. */
private _result: any;

constructor(private _overlayRef: OverlayRef, public _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 e857460

Please sign in to comment.