Skip to content

feat(dialog): add enter/exit animations #2825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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