1+ import { FocusMonitor , FocusOrigin } from '@angular/cdk/a11y' ;
12import {
23 ComponentFixture ,
34 fakeAsync ,
@@ -31,7 +32,9 @@ import {A, ESCAPE} from '@angular/cdk/keycodes';
3132import {
3233 dispatchKeyboardEvent ,
3334 createKeyboardEvent ,
34- dispatchEvent
35+ dispatchEvent ,
36+ patchElementFocus ,
37+ dispatchMouseEvent
3538} from '@angular/cdk/testing/private' ;
3639import {
3740 MAT_DIALOG_DATA ,
@@ -43,12 +46,12 @@ import {
4346} from './index' ;
4447import { Subject } from 'rxjs' ;
4548
46-
4749describe ( 'MatDialog' , ( ) => {
4850 let dialog : MatDialog ;
4951 let overlayContainer : OverlayContainer ;
5052 let overlayContainerElement : HTMLElement ;
5153 let scrolledSubject = new Subject ( ) ;
54+ let focusMonitor : FocusMonitor ;
5255
5356 let testViewContainerRef : ViewContainerRef ;
5457 let viewContainerFixture : ComponentFixture < ComponentWithChildViewContainer > ;
@@ -68,13 +71,14 @@ describe('MatDialog', () => {
6871 TestBed . compileComponents ( ) ;
6972 } ) ) ;
7073
71- beforeEach ( inject ( [ MatDialog , Location , OverlayContainer ] ,
72- ( d : MatDialog , l : Location , oc : OverlayContainer ) => {
74+ beforeEach ( inject ( [ MatDialog , Location , OverlayContainer , FocusMonitor ] ,
75+ ( d : MatDialog , l : Location , oc : OverlayContainer , fm : FocusMonitor ) => {
7376 dialog = d ;
7477 mockLocation = l as SpyLocation ;
7578 overlayContainer = oc ;
7679 overlayContainerElement = oc . getContainerElement ( ) ;
77- } ) ) ;
80+ focusMonitor = fm ;
81+ } ) ) ;
7882
7983 afterEach ( ( ) => {
8084 overlayContainer . ngOnDestroy ( ) ;
@@ -1145,6 +1149,148 @@ describe('MatDialog', () => {
11451149 document . body . removeChild ( button ) ;
11461150 } ) ) ;
11471151
1152+ it ( 'should re-focus the trigger via keyboard when closed via escape key' , fakeAsync ( ( ) => {
1153+ const button = document . createElement ( 'button' ) ;
1154+ let lastFocusOrigin : FocusOrigin = null ;
1155+
1156+ focusMonitor . monitor ( button , false )
1157+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1158+
1159+ document . body . appendChild ( button ) ;
1160+ button . focus ( ) ;
1161+
1162+ // Patch the element focus after the initial and real focus, because otherwise the
1163+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1164+ patchElementFocus ( button ) ;
1165+
1166+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
1167+
1168+ tick ( 500 ) ;
1169+ viewContainerFixture . detectChanges ( ) ;
1170+
1171+ expect ( lastFocusOrigin ! ) . toBeNull ( 'Expected the trigger button to be blurred' ) ;
1172+
1173+ dispatchKeyboardEvent ( document . body , 'keydown' , ESCAPE ) ;
1174+
1175+ flushMicrotasks ( ) ;
1176+ viewContainerFixture . detectChanges ( ) ;
1177+ tick ( 500 ) ;
1178+
1179+ expect ( lastFocusOrigin ! )
1180+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1181+
1182+ focusMonitor . stopMonitoring ( button ) ;
1183+ document . body . removeChild ( button ) ;
1184+ } ) ) ;
1185+
1186+ it ( 'should re-focus the trigger via mouse when backdrop has been clicked' , fakeAsync ( ( ) => {
1187+ const button = document . createElement ( 'button' ) ;
1188+ let lastFocusOrigin : FocusOrigin = null ;
1189+
1190+ focusMonitor . monitor ( button , false )
1191+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1192+
1193+ document . body . appendChild ( button ) ;
1194+ button . focus ( ) ;
1195+
1196+ // Patch the element focus after the initial and real focus, because otherwise the
1197+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1198+ patchElementFocus ( button ) ;
1199+
1200+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
1201+
1202+ tick ( 500 ) ;
1203+ viewContainerFixture . detectChanges ( ) ;
1204+
1205+ const backdrop = overlayContainerElement
1206+ . querySelector ( '.cdk-overlay-backdrop' ) as HTMLElement ;
1207+
1208+ backdrop . click ( ) ;
1209+ viewContainerFixture . detectChanges ( ) ;
1210+ tick ( 500 ) ;
1211+
1212+ expect ( lastFocusOrigin ! )
1213+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1214+
1215+ focusMonitor . stopMonitoring ( button ) ;
1216+ document . body . removeChild ( button ) ;
1217+ } ) ) ;
1218+
1219+ it ( 'should re-focus via keyboard if the close button has been triggered through keyboard' ,
1220+ fakeAsync ( ( ) => {
1221+
1222+ const button = document . createElement ( 'button' ) ;
1223+ let lastFocusOrigin : FocusOrigin = null ;
1224+
1225+ focusMonitor . monitor ( button , false )
1226+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1227+
1228+ document . body . appendChild ( button ) ;
1229+ button . focus ( ) ;
1230+
1231+ // Patch the element focus after the initial and real focus, because otherwise the
1232+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1233+ patchElementFocus ( button ) ;
1234+
1235+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1236+
1237+ tick ( 500 ) ;
1238+ viewContainerFixture . detectChanges ( ) ;
1239+
1240+ const closeButton = overlayContainerElement
1241+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1242+
1243+ // Fake the behavior of pressing the SPACE key on a button element. Browsers fire a `click`
1244+ // event with a MouseEvent, which has coordinates that are out of the element boundaries.
1245+ dispatchMouseEvent ( closeButton , 'click' , 0 , 0 ) ;
1246+
1247+ viewContainerFixture . detectChanges ( ) ;
1248+ tick ( 500 ) ;
1249+
1250+ expect ( lastFocusOrigin ! )
1251+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1252+
1253+ focusMonitor . stopMonitoring ( button ) ;
1254+ document . body . removeChild ( button ) ;
1255+ } ) ) ;
1256+
1257+ it ( 'should re-focus via mouse if the close button has been clicked' , fakeAsync ( ( ) => {
1258+ const button = document . createElement ( 'button' ) ;
1259+ let lastFocusOrigin : FocusOrigin = null ;
1260+
1261+ focusMonitor . monitor ( button , false )
1262+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1263+
1264+ document . body . appendChild ( button ) ;
1265+ button . focus ( ) ;
1266+
1267+ // Patch the element focus after the initial and real focus, because otherwise the
1268+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1269+ patchElementFocus ( button ) ;
1270+
1271+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1272+
1273+ tick ( 500 ) ;
1274+ viewContainerFixture . detectChanges ( ) ;
1275+
1276+ const closeButton = overlayContainerElement
1277+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1278+
1279+ // The dialog close button detects the focus origin by inspecting the click event. If
1280+ // coordinates of the click are not present, it assumes that the click has been triggered
1281+ // by keyboard.
1282+ dispatchMouseEvent ( closeButton , 'click' , 10 , 10 ) ;
1283+
1284+ viewContainerFixture . detectChanges ( ) ;
1285+ tick ( 500 ) ;
1286+
1287+ expect ( lastFocusOrigin ! )
1288+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1289+
1290+ focusMonitor . stopMonitoring ( button ) ;
1291+ document . body . removeChild ( button ) ;
1292+ } ) ) ;
1293+
11481294 it ( 'should allow the consumer to shift focus in afterClosed' , fakeAsync ( ( ) => {
11491295 // Create a element that has focus before the dialog is opened.
11501296 let button = document . createElement ( 'button' ) ;
@@ -1167,7 +1313,7 @@ describe('MatDialog', () => {
11671313
11681314 tick ( 500 ) ;
11691315 viewContainerFixture . detectChanges ( ) ;
1170- flushMicrotasks ( ) ;
1316+ flush ( ) ;
11711317
11721318 expect ( document . activeElement ! . id ) . toBe ( 'input-to-be-focused' ,
11731319 'Expected that the trigger was refocused after the dialog is closed.' ) ;
0 commit comments