@@ -25,8 +25,9 @@ import {SpyLocation} from '@angular/common/testing';
25
25
import { Directionality } from '@angular/cdk/bidi' ;
26
26
import { MatDialogContainer } from './dialog-container' ;
27
27
import { OverlayContainer , ScrollStrategy , ScrollDispatcher , Overlay } from '@angular/cdk/overlay' ;
28
+ import { FocusOrigin , FocusMonitor } from '@angular/cdk/a11y' ;
28
29
import { A , ESCAPE } from '@angular/cdk/keycodes' ;
29
- import { dispatchKeyboardEvent } from '@angular/cdk/testing' ;
30
+ import { dispatchKeyboardEvent , dispatchMouseEvent , patchElementFocus } from '@angular/cdk/testing' ;
30
31
import {
31
32
MAT_DIALOG_DATA ,
32
33
MatDialog ,
@@ -42,6 +43,7 @@ describe('MatDialog', () => {
42
43
let overlayContainer : OverlayContainer ;
43
44
let overlayContainerElement : HTMLElement ;
44
45
let scrolledSubject = new Subject ( ) ;
46
+ let focusMonitor : FocusMonitor ;
45
47
46
48
let testViewContainerRef : ViewContainerRef ;
47
49
let viewContainerFixture : ComponentFixture < ComponentWithChildViewContainer > ;
@@ -61,13 +63,14 @@ describe('MatDialog', () => {
61
63
TestBed . compileComponents ( ) ;
62
64
} ) ) ;
63
65
64
- beforeEach ( inject ( [ MatDialog , Location , OverlayContainer ] ,
65
- ( d : MatDialog , l : Location , oc : OverlayContainer ) => {
66
+ beforeEach ( inject ( [ MatDialog , Location , OverlayContainer , FocusMonitor ] ,
67
+ ( d : MatDialog , l : Location , oc : OverlayContainer , fm : FocusMonitor ) => {
66
68
dialog = d ;
67
69
mockLocation = l as SpyLocation ;
68
70
overlayContainer = oc ;
69
71
overlayContainerElement = oc . getContainerElement ( ) ;
70
- } ) ) ;
72
+ focusMonitor = fm ;
73
+ } ) ) ;
71
74
72
75
afterEach ( ( ) => {
73
76
overlayContainer . ngOnDestroy ( ) ;
@@ -967,6 +970,148 @@ describe('MatDialog', () => {
967
970
document . body . removeChild ( button ) ;
968
971
} ) ) ;
969
972
973
+ it ( 'should re-focus the trigger via keyboard when closed via escape key' , fakeAsync ( ( ) => {
974
+ const button = document . createElement ( 'button' ) ;
975
+ let lastFocusOrigin : FocusOrigin = null ;
976
+
977
+ focusMonitor . monitor ( button , false )
978
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
979
+
980
+ document . body . appendChild ( button ) ;
981
+ button . focus ( ) ;
982
+
983
+ // Patch the element focus after the initial and real focus, because otherwise the
984
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
985
+ patchElementFocus ( button ) ;
986
+
987
+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
988
+
989
+ tick ( 500 ) ;
990
+ viewContainerFixture . detectChanges ( ) ;
991
+
992
+ expect ( lastFocusOrigin ! ) . toBeNull ( 'Expected the trigger button to be blurred' ) ;
993
+
994
+ dispatchKeyboardEvent ( document . body , 'keydown' , ESCAPE ) ;
995
+
996
+ flushMicrotasks ( ) ;
997
+ viewContainerFixture . detectChanges ( ) ;
998
+ tick ( 500 ) ;
999
+
1000
+ expect ( lastFocusOrigin ! )
1001
+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1002
+
1003
+ focusMonitor . stopMonitoring ( button ) ;
1004
+ document . body . removeChild ( button ) ;
1005
+ } ) ) ;
1006
+
1007
+ it ( 'should re-focus the trigger via mouse when backdrop has been clicked' , fakeAsync ( ( ) => {
1008
+ const button = document . createElement ( 'button' ) ;
1009
+ let lastFocusOrigin : FocusOrigin = null ;
1010
+
1011
+ focusMonitor . monitor ( button , false )
1012
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1013
+
1014
+ document . body . appendChild ( button ) ;
1015
+ button . focus ( ) ;
1016
+
1017
+ // Patch the element focus after the initial and real focus, because otherwise the
1018
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1019
+ patchElementFocus ( button ) ;
1020
+
1021
+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
1022
+
1023
+ tick ( 500 ) ;
1024
+ viewContainerFixture . detectChanges ( ) ;
1025
+
1026
+ const backdrop = overlayContainerElement
1027
+ . querySelector ( '.cdk-overlay-backdrop' ) as HTMLElement ;
1028
+
1029
+ backdrop . click ( ) ;
1030
+ viewContainerFixture . detectChanges ( ) ;
1031
+ tick ( 500 ) ;
1032
+
1033
+ expect ( lastFocusOrigin ! )
1034
+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1035
+
1036
+ focusMonitor . stopMonitoring ( button ) ;
1037
+ document . body . removeChild ( button ) ;
1038
+ } ) ) ;
1039
+
1040
+ it ( 'should re-focus via keyboard if the close button has been triggered through keyboard' ,
1041
+ fakeAsync ( ( ) => {
1042
+
1043
+ const button = document . createElement ( 'button' ) ;
1044
+ let lastFocusOrigin : FocusOrigin = null ;
1045
+
1046
+ focusMonitor . monitor ( button , false )
1047
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1048
+
1049
+ document . body . appendChild ( button ) ;
1050
+ button . focus ( ) ;
1051
+
1052
+ // Patch the element focus after the initial and real focus, because otherwise the
1053
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1054
+ patchElementFocus ( button ) ;
1055
+
1056
+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1057
+
1058
+ tick ( 500 ) ;
1059
+ viewContainerFixture . detectChanges ( ) ;
1060
+
1061
+ const closeButton = overlayContainerElement
1062
+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1063
+
1064
+ // Fake the behavior of pressing the SPACE key on a button element. Browsers fire a `click`
1065
+ // event with a MouseEvent, which has coordinates that are out of the element boundaries.
1066
+ dispatchMouseEvent ( closeButton , 'click' , 0 , 0 ) ;
1067
+
1068
+ viewContainerFixture . detectChanges ( ) ;
1069
+ tick ( 500 ) ;
1070
+
1071
+ expect ( lastFocusOrigin ! )
1072
+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1073
+
1074
+ focusMonitor . stopMonitoring ( button ) ;
1075
+ document . body . removeChild ( button ) ;
1076
+ } ) ) ;
1077
+
1078
+ it ( 'should re-focus via mouse if the close button has been clicked' , fakeAsync ( ( ) => {
1079
+ const button = document . createElement ( 'button' ) ;
1080
+ let lastFocusOrigin : FocusOrigin = null ;
1081
+
1082
+ focusMonitor . monitor ( button , false )
1083
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1084
+
1085
+ document . body . appendChild ( button ) ;
1086
+ button . focus ( ) ;
1087
+
1088
+ // Patch the element focus after the initial and real focus, because otherwise the
1089
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1090
+ patchElementFocus ( button ) ;
1091
+
1092
+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1093
+
1094
+ tick ( 500 ) ;
1095
+ viewContainerFixture . detectChanges ( ) ;
1096
+
1097
+ const closeButton = overlayContainerElement
1098
+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1099
+
1100
+ // The dialog close button detects the focus origin by inspecting the click event. If
1101
+ // coordinates of the click are not present, it assumes that the click has been triggered
1102
+ // by keyboard.
1103
+ dispatchMouseEvent ( closeButton , 'click' , 10 , 10 ) ;
1104
+
1105
+ viewContainerFixture . detectChanges ( ) ;
1106
+ tick ( 500 ) ;
1107
+
1108
+ expect ( lastFocusOrigin ! )
1109
+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1110
+
1111
+ focusMonitor . stopMonitoring ( button ) ;
1112
+ document . body . removeChild ( button ) ;
1113
+ } ) ) ;
1114
+
970
1115
it ( 'should allow the consumer to shift focus in afterClosed' , fakeAsync ( ( ) => {
971
1116
// Create a element that has focus before the dialog is opened.
972
1117
let button = document . createElement ( 'button' ) ;
@@ -989,7 +1134,7 @@ describe('MatDialog', () => {
989
1134
990
1135
tick ( 500 ) ;
991
1136
viewContainerFixture . detectChanges ( ) ;
992
- flushMicrotasks ( ) ;
1137
+ flush ( ) ;
993
1138
994
1139
expect ( document . activeElement . id ) . toBe ( 'input-to-be-focused' ,
995
1140
'Expected that the trigger was refocused after the dialog is closed.' ) ;
0 commit comments