Skip to content

Commit

Permalink
fix(dialog): restoring focus too early
Browse files Browse the repository at this point in the history
Fixes the dialog restoring focus before the close animation is done.

Fixes angular#4287.
  • Loading branch information
crisbeto committed Apr 29, 2017
1 parent b4e8c7d commit 0055c76
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 42 deletions.
47 changes: 18 additions & 29 deletions src/lib/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,7 @@ export class MdDialogContainer extends BasePortalHost {
return this._portalHost.attachTemplatePortal(portal);
}

/**
* Moves the focus inside the focus trap.
*/
/** Moves the focus inside the focus trap. */
private _trapFocus() {
if (!this._focusTrap) {
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
Expand All @@ -123,45 +121,36 @@ export class MdDialogContainer extends BasePortalHost {
this._focusTrap.focusFirstTabbableElementWhenReady();
}

/**
* Saves a reference to the element that was focused before the dialog was opened.
*/
/** Restores focus to the element that was focused before the dialog opened. */
private _restoreFocus() {
const toFocus = this._elementFocusedBeforeDialogWasOpened;

// We need the extra check, because IE can set the `activeElement` to null in some cases.
if (toFocus && 'focus' in toFocus) {
toFocus.focus();
}

if (this._focusTrap) {
this._focusTrap.destroy();
}
}

/** Saves a reference to the element that was focused before the dialog was opened. */
private _savePreviouslyFocusedElement() {
if (this._document) {
this._elementFocusedBeforeDialogWasOpened = this._document.activeElement as HTMLElement;
}
}

/**
* Callback, invoked whenever an animation on the host completes.
* @docs-private
*/
/** Callback, invoked whenever an animation on the host completes. */
_onAnimationDone(event: AnimationEvent) {
this._onAnimationStateChange.emit(event);

if (event.toState === 'enter') {
this._trapFocus();
} else if (event.toState === 'exit') {
this._restoreFocus();
this._onAnimationStateChange.complete();
}
}

/**
* Kicks off the leave animation and restores focus to the previously-focused element.
* @docs-private
*/
_exit(): void {
// We need the extra check, because IE can set the `activeElement` to null in some cases.
let toFocus = this._elementFocusedBeforeDialogWasOpened;

if (toFocus && 'focus' in toFocus) {
toFocus.focus();
}

if (this._focusTrap) {
this._focusTrap.destroy();
}

this._state = 'exit';
}
}
2 changes: 1 addition & 1 deletion src/lib/dialog/dialog-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class MdDialogRef<T> {
*/
close(dialogResult?: any): void {
this._result = dialogResult;
this._containerInstance._exit();
this._containerInstance._state = 'exit';
this._overlayRef.detachBackdrop(); // Transition the backdrop in parallel with the dialog.
}

Expand Down
20 changes: 8 additions & 12 deletions src/lib/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,15 +480,9 @@ describe('MdDialog', () => {
});

describe('focus management', () => {

// When testing focus, all of the elements must be in the DOM.
beforeEach(() => {
document.body.appendChild(overlayContainerElement);
});

afterEach(() => {
document.body.removeChild(overlayContainerElement);
});
beforeEach(() => document.body.appendChild(overlayContainerElement));
afterEach(() => document.body.removeChild(overlayContainerElement));

it('should focus the first tabbable element of the dialog on open', fakeAsync(() => {
dialog.open(PizzaMsg, {
Expand All @@ -515,17 +509,19 @@ describe('MdDialog', () => {

viewContainerFixture.detectChanges();
flushMicrotasks();

expect(document.activeElement.id)
.not.toBe('dialog-trigger', 'Expected the focus to change when dialog was opened.');

dialogRef.close();
expect(document.activeElement.id).not.toBe('dialog-trigger',
'Expcted the focus not to have changed before the animation finishes.');

tick(500);
viewContainerFixture.detectChanges();
flushMicrotasks();

expect(document.activeElement.id)
.toBe('dialog-trigger', 'Expected that the trigger was refocused after dialog close');
expect(document.activeElement.id).toBe('dialog-trigger',
'Expected that the trigger was refocused after the dialog is closed.');

document.body.removeChild(button);
}));
Expand All @@ -552,7 +548,7 @@ describe('MdDialog', () => {
flushMicrotasks();

expect(document.activeElement.id).toBe('input-to-be-focused',
'Expected that the trigger was refocused after dialog close');
'Expected that the trigger was refocused after the dialog is closed.');

document.body.removeChild(button);
document.body.removeChild(input);
Expand Down

0 comments on commit 0055c76

Please sign in to comment.