Skip to content

Commit f8578df

Browse files
committed
fix(dialog): restore focus with the proper focus origin
* Restores the trigger focus upon dialog close with the proper focus origin that caused the dialog closing. For example, a backdrop click leads to a focus restore via `mouse`. Pressing `escape` leads to a focus restore via `keyboard`. References #8420
1 parent 813cdcd commit f8578df

File tree

4 files changed

+79
-10
lines changed

4 files changed

+79
-10
lines changed

src/lib/dialog/dialog-container.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
CdkPortalOutlet,
2929
TemplatePortal
3030
} from '@angular/cdk/portal';
31-
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
31+
import {FocusTrap, FocusMonitor, FocusOrigin, FocusTrapFactory} from '@angular/cdk/a11y';
3232
import {MatDialogConfig} from './dialog-config';
3333

3434

@@ -79,6 +79,9 @@ export class MatDialogContainer extends BasePortalOutlet {
7979
/** Element that was focused before the dialog was opened. Save this to restore upon close. */
8080
private _elementFocusedBeforeDialogWasOpened: HTMLElement | null = null;
8181

82+
/** Focus origin type that describes how the dialog got closed. */
83+
_closeFocusOrigin: FocusOrigin = 'program';
84+
8285
/** The dialog configuration. */
8386
_config: MatDialogConfig;
8487

@@ -95,6 +98,7 @@ export class MatDialogContainer extends BasePortalOutlet {
9598
private _elementRef: ElementRef,
9699
private _focusTrapFactory: FocusTrapFactory,
97100
private _changeDetectorRef: ChangeDetectorRef,
101+
private _focusMonitor: FocusMonitor,
98102
@Optional() @Inject(DOCUMENT) private _document: any) {
99103

100104
super();
@@ -146,7 +150,7 @@ export class MatDialogContainer extends BasePortalOutlet {
146150

147151
// We need the extra check, because IE can set the `activeElement` to null in some cases.
148152
if (toFocus && typeof toFocus.focus === 'function') {
149-
toFocus.focus();
153+
this._focusMonitor.focusVia(toFocus, this._closeFocusOrigin);
150154
}
151155

152156
if (this._focusTrap) {

src/lib/dialog/dialog-ref.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {OverlayRef, GlobalPositionStrategy} from '@angular/cdk/overlay';
1010
import {ESCAPE} from '@angular/cdk/keycodes';
1111
import {filter} from 'rxjs/operators/filter';
1212
import {take} from 'rxjs/operators/take';
13+
import {FocusOrigin} from '@angular/cdk/a11y';
1314
import {DialogPosition} from './dialog-config';
1415
import {Observable} from 'rxjs/Observable';
1516
import {Subject} from 'rxjs/Subject';
@@ -72,14 +73,15 @@ export class MatDialogRef<T, R = any> {
7273

7374
_overlayRef.keydownEvents()
7475
.pipe(filter(event => event.keyCode === ESCAPE && !this.disableClose))
75-
.subscribe(() => this.close());
76+
.subscribe(() => this.close(undefined, 'keyboard'));
7677
}
7778

7879
/**
7980
* Close the dialog.
8081
* @param dialogResult Optional result to return to the dialog opener.
82+
* @param closeFocusOrigin Focus origin that describes how the dialog got closed
8183
*/
82-
close(dialogResult?: R): void {
84+
close(dialogResult?: R, closeFocusOrigin: FocusOrigin = 'program'): void {
8385
this._result = dialogResult;
8486

8587
// Transition the backdrop in parallel to the dialog.
@@ -93,6 +95,7 @@ export class MatDialogRef<T, R = any> {
9395
this._overlayRef.detachBackdrop();
9496
});
9597

98+
this._containerInstance._closeFocusOrigin = closeFocusOrigin;
9699
this._containerInstance._startExitAnimation();
97100
}
98101

src/lib/dialog/dialog.spec.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {SpyLocation} from '@angular/common/testing';
2525
import {Directionality} from '@angular/cdk/bidi';
2626
import {MatDialogContainer} from './dialog-container';
2727
import {OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay';
28+
import {A11yModule, FocusOrigin, FocusMonitor} from '@angular/cdk/a11y';
2829
import {A, ESCAPE} from '@angular/cdk/keycodes';
2930
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
3031
import {
@@ -40,14 +41,15 @@ describe('MatDialog', () => {
4041
let dialog: MatDialog;
4142
let overlayContainer: OverlayContainer;
4243
let overlayContainerElement: HTMLElement;
44+
let focusMonitor: FocusMonitor;
4345

4446
let testViewContainerRef: ViewContainerRef;
4547
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
4648
let mockLocation: SpyLocation;
4749

4850
beforeEach(fakeAsync(() => {
4951
TestBed.configureTestingModule({
50-
imports: [MatDialogModule, DialogTestModule],
52+
imports: [MatDialogModule, DialogTestModule, A11yModule],
5153
providers: [
5254
{provide: Location, useClass: SpyLocation}
5355
],
@@ -56,13 +58,14 @@ describe('MatDialog', () => {
5658
TestBed.compileComponents();
5759
}));
5860

59-
beforeEach(inject([MatDialog, Location, OverlayContainer],
60-
(d: MatDialog, l: Location, oc: OverlayContainer) => {
61+
beforeEach(inject([MatDialog, Location, OverlayContainer, FocusMonitor],
62+
(d: MatDialog, l: Location, oc: OverlayContainer, fm: FocusMonitor) => {
6163
dialog = d;
6264
mockLocation = l as SpyLocation;
6365
overlayContainer = oc;
6466
overlayContainerElement = oc.getContainerElement();
65-
}));
67+
focusMonitor = fm;
68+
}));
6669

6770
afterEach(() => {
6871
overlayContainer.ngOnDestroy();
@@ -900,6 +903,65 @@ describe('MatDialog', () => {
900903
document.body.removeChild(button);
901904
}));
902905

906+
it('should re-focus the trigger via mouse when dialog closes through escape', fakeAsync(() => {
907+
const button = document.createElement('button');
908+
let lastFocusOrigin: FocusOrigin = null;
909+
910+
focusMonitor.monitor(button, false).subscribe(focusOrigin => lastFocusOrigin = focusOrigin);
911+
document.body.appendChild(button);
912+
button.focus();
913+
914+
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
915+
916+
flushMicrotasks();
917+
viewContainerFixture.detectChanges();
918+
flushMicrotasks();
919+
920+
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
921+
922+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
923+
924+
flushMicrotasks();
925+
viewContainerFixture.detectChanges();
926+
tick(500);
927+
928+
expect(lastFocusOrigin!)
929+
.toBe('keyboard', 'Expected the trigger button to be focused via keyboard');
930+
931+
focusMonitor.stopMonitoring(button);
932+
document.body.removeChild(button);
933+
}));
934+
935+
it('should re-focus the trigger via keyboard when backdrop has been clicked', fakeAsync(() => {
936+
const button = document.createElement('button');
937+
let lastFocusOrigin: FocusOrigin = null;
938+
939+
focusMonitor.monitor(button, false).subscribe(focusOrigin => lastFocusOrigin = focusOrigin);
940+
document.body.appendChild(button);
941+
button.focus();
942+
943+
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
944+
945+
flushMicrotasks();
946+
viewContainerFixture.detectChanges();
947+
flushMicrotasks();
948+
949+
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
950+
951+
const backdrop = overlayContainerElement
952+
.querySelector('.cdk-overlay-backdrop') as HTMLElement;
953+
954+
backdrop.click();
955+
viewContainerFixture.detectChanges();
956+
tick(500);
957+
958+
expect(lastFocusOrigin!)
959+
.toBe('mouse', 'Expected the trigger button to be focused via mouse');
960+
961+
focusMonitor.stopMonitoring(button);
962+
document.body.removeChild(button);
963+
}));
964+
903965
it('should allow the consumer to shift focus in afterClosed', fakeAsync(() => {
904966
// Create a element that has focus before the dialog is opened.
905967
let button = document.createElement('button');
@@ -922,7 +984,7 @@ describe('MatDialog', () => {
922984

923985
tick(500);
924986
viewContainerFixture.detectChanges();
925-
flushMicrotasks();
987+
flush();
926988

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

src/lib/dialog/dialog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export class MatDialog {
238238
if (config.hasBackdrop) {
239239
overlayRef.backdropClick().subscribe(() => {
240240
if (!dialogRef.disableClose) {
241-
dialogRef.close();
241+
dialogRef.close(undefined, 'mouse');
242242
}
243243
});
244244
}

0 commit comments

Comments
 (0)