1
+ import { FocusMonitor , FocusOrigin } from '@angular/cdk/a11y' ;
2
+ import { Directionality } from '@angular/cdk/bidi' ;
3
+ import { A , ESCAPE } from '@angular/cdk/keycodes' ;
4
+ import { Overlay , OverlayContainer , ScrollStrategy } from '@angular/cdk/overlay' ;
5
+ import { ScrollDispatcher } from '@angular/cdk/scrolling' ;
1
6
import {
2
- ComponentFixture ,
3
- fakeAsync ,
4
- flushMicrotasks ,
5
- inject ,
6
- TestBed ,
7
- tick ,
8
- flush ,
9
- } from '@angular/core/testing' ;
7
+ createKeyboardEvent ,
8
+ dispatchKeyboardEvent ,
9
+ dispatchMouseEvent ,
10
+ patchElementFocus
11
+ } from '@angular/cdk/testing' ;
12
+ import { Location } from '@angular/common' ;
13
+ import { SpyLocation } from '@angular/common/testing' ;
10
14
import {
11
15
ChangeDetectionStrategy ,
12
16
Component ,
@@ -18,31 +22,34 @@ import {
18
22
ViewChild ,
19
23
ViewContainerRef
20
24
} from '@angular/core' ;
25
+ import {
26
+ ComponentFixture ,
27
+ fakeAsync ,
28
+ flush ,
29
+ flushMicrotasks ,
30
+ inject ,
31
+ TestBed ,
32
+ tick ,
33
+ } from '@angular/core/testing' ;
21
34
import { By } from '@angular/platform-browser' ;
22
35
import { NoopAnimationsModule } from '@angular/platform-browser/animations' ;
23
- import { Location } from '@angular/common' ;
24
- import { SpyLocation } from '@angular/common/testing' ;
25
- import { Directionality } from '@angular/cdk/bidi' ;
36
+ import { Subject } from 'rxjs' ;
26
37
import { MatDialogContainer } from './dialog-container' ;
27
- import { OverlayContainer , ScrollStrategy , Overlay } from '@angular/cdk/overlay' ;
28
- import { ScrollDispatcher } from '@angular/cdk/scrolling' ;
29
- import { A , ESCAPE } from '@angular/cdk/keycodes' ;
30
- import { dispatchKeyboardEvent , createKeyboardEvent } from '@angular/cdk/testing' ;
31
38
import {
32
39
MAT_DIALOG_DATA ,
40
+ MAT_DIALOG_DEFAULT_OPTIONS ,
33
41
MatDialog ,
34
42
MatDialogModule ,
35
- MatDialogRef ,
36
- MAT_DIALOG_DEFAULT_OPTIONS
43
+ MatDialogRef
37
44
} from './index' ;
38
- import { Subject } from 'rxjs' ;
39
45
40
46
41
47
describe ( 'MatDialog' , ( ) => {
42
48
let dialog : MatDialog ;
43
49
let overlayContainer : OverlayContainer ;
44
50
let overlayContainerElement : HTMLElement ;
45
51
let scrolledSubject = new Subject ( ) ;
52
+ let focusMonitor : FocusMonitor ;
46
53
47
54
let testViewContainerRef : ViewContainerRef ;
48
55
let viewContainerFixture : ComponentFixture < ComponentWithChildViewContainer > ;
@@ -62,13 +69,14 @@ describe('MatDialog', () => {
62
69
TestBed . compileComponents ( ) ;
63
70
} ) ) ;
64
71
65
- beforeEach ( inject ( [ MatDialog , Location , OverlayContainer ] ,
66
- ( d : MatDialog , l : Location , oc : OverlayContainer ) => {
72
+ beforeEach ( inject ( [ MatDialog , Location , OverlayContainer , FocusMonitor ] ,
73
+ ( d : MatDialog , l : Location , oc : OverlayContainer , fm : FocusMonitor ) => {
67
74
dialog = d ;
68
75
mockLocation = l as SpyLocation ;
69
76
overlayContainer = oc ;
70
77
overlayContainerElement = oc . getContainerElement ( ) ;
71
- } ) ) ;
78
+ focusMonitor = fm ;
79
+ } ) ) ;
72
80
73
81
afterEach ( ( ) => {
74
82
overlayContainer . ngOnDestroy ( ) ;
@@ -1035,6 +1043,148 @@ describe('MatDialog', () => {
1035
1043
document . body . removeChild ( button ) ;
1036
1044
} ) ) ;
1037
1045
1046
+ it ( 'should re-focus the trigger via keyboard when closed via escape key' , fakeAsync ( ( ) => {
1047
+ const button = document . createElement ( 'button' ) ;
1048
+ let lastFocusOrigin : FocusOrigin = null ;
1049
+
1050
+ focusMonitor . monitor ( button , false )
1051
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1052
+
1053
+ document . body . appendChild ( button ) ;
1054
+ button . focus ( ) ;
1055
+
1056
+ // Patch the element focus after the initial and real focus, because otherwise the
1057
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1058
+ patchElementFocus ( button ) ;
1059
+
1060
+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
1061
+
1062
+ tick ( 500 ) ;
1063
+ viewContainerFixture . detectChanges ( ) ;
1064
+
1065
+ expect ( lastFocusOrigin ! ) . toBeNull ( 'Expected the trigger button to be blurred' ) ;
1066
+
1067
+ dispatchKeyboardEvent ( document . body , 'keydown' , ESCAPE ) ;
1068
+
1069
+ flushMicrotasks ( ) ;
1070
+ viewContainerFixture . detectChanges ( ) ;
1071
+ tick ( 500 ) ;
1072
+
1073
+ expect ( lastFocusOrigin ! )
1074
+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1075
+
1076
+ focusMonitor . stopMonitoring ( button ) ;
1077
+ document . body . removeChild ( button ) ;
1078
+ } ) ) ;
1079
+
1080
+ it ( 'should re-focus the trigger via mouse when backdrop has been clicked' , fakeAsync ( ( ) => {
1081
+ const button = document . createElement ( 'button' ) ;
1082
+ let lastFocusOrigin : FocusOrigin = null ;
1083
+
1084
+ focusMonitor . monitor ( button , false )
1085
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1086
+
1087
+ document . body . appendChild ( button ) ;
1088
+ button . focus ( ) ;
1089
+
1090
+ // Patch the element focus after the initial and real focus, because otherwise the
1091
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1092
+ patchElementFocus ( button ) ;
1093
+
1094
+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
1095
+
1096
+ tick ( 500 ) ;
1097
+ viewContainerFixture . detectChanges ( ) ;
1098
+
1099
+ const backdrop = overlayContainerElement
1100
+ . querySelector ( '.cdk-overlay-backdrop' ) as HTMLElement ;
1101
+
1102
+ backdrop . click ( ) ;
1103
+ viewContainerFixture . detectChanges ( ) ;
1104
+ tick ( 500 ) ;
1105
+
1106
+ expect ( lastFocusOrigin ! )
1107
+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1108
+
1109
+ focusMonitor . stopMonitoring ( button ) ;
1110
+ document . body . removeChild ( button ) ;
1111
+ } ) ) ;
1112
+
1113
+ it ( 'should re-focus via keyboard if the close button has been triggered through keyboard' ,
1114
+ fakeAsync ( ( ) => {
1115
+
1116
+ const button = document . createElement ( 'button' ) ;
1117
+ let lastFocusOrigin : FocusOrigin = null ;
1118
+
1119
+ focusMonitor . monitor ( button , false )
1120
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1121
+
1122
+ document . body . appendChild ( button ) ;
1123
+ button . focus ( ) ;
1124
+
1125
+ // Patch the element focus after the initial and real focus, because otherwise the
1126
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1127
+ patchElementFocus ( button ) ;
1128
+
1129
+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1130
+
1131
+ tick ( 500 ) ;
1132
+ viewContainerFixture . detectChanges ( ) ;
1133
+
1134
+ const closeButton = overlayContainerElement
1135
+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1136
+
1137
+ // Fake the behavior of pressing the SPACE key on a button element. Browsers fire a `click`
1138
+ // event with a MouseEvent, which has coordinates that are out of the element boundaries.
1139
+ dispatchMouseEvent ( closeButton , 'click' , 0 , 0 ) ;
1140
+
1141
+ viewContainerFixture . detectChanges ( ) ;
1142
+ tick ( 500 ) ;
1143
+
1144
+ expect ( lastFocusOrigin ! )
1145
+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1146
+
1147
+ focusMonitor . stopMonitoring ( button ) ;
1148
+ document . body . removeChild ( button ) ;
1149
+ } ) ) ;
1150
+
1151
+ it ( 'should re-focus via mouse if the close button has been clicked' , fakeAsync ( ( ) => {
1152
+ const button = document . createElement ( 'button' ) ;
1153
+ let lastFocusOrigin : FocusOrigin = null ;
1154
+
1155
+ focusMonitor . monitor ( button , false )
1156
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1157
+
1158
+ document . body . appendChild ( button ) ;
1159
+ button . focus ( ) ;
1160
+
1161
+ // Patch the element focus after the initial and real focus, because otherwise the
1162
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1163
+ patchElementFocus ( button ) ;
1164
+
1165
+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1166
+
1167
+ tick ( 500 ) ;
1168
+ viewContainerFixture . detectChanges ( ) ;
1169
+
1170
+ const closeButton = overlayContainerElement
1171
+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1172
+
1173
+ // The dialog close button detects the focus origin by inspecting the click event. If
1174
+ // coordinates of the click are not present, it assumes that the click has been triggered
1175
+ // by keyboard.
1176
+ dispatchMouseEvent ( closeButton , 'click' , 10 , 10 ) ;
1177
+
1178
+ viewContainerFixture . detectChanges ( ) ;
1179
+ tick ( 500 ) ;
1180
+
1181
+ expect ( lastFocusOrigin ! )
1182
+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1183
+
1184
+ focusMonitor . stopMonitoring ( button ) ;
1185
+ document . body . removeChild ( button ) ;
1186
+ } ) ) ;
1187
+
1038
1188
it ( 'should allow the consumer to shift focus in afterClosed' , fakeAsync ( ( ) => {
1039
1189
// Create a element that has focus before the dialog is opened.
1040
1190
let button = document . createElement ( 'button' ) ;
@@ -1057,7 +1207,7 @@ describe('MatDialog', () => {
1057
1207
1058
1208
tick ( 500 ) ;
1059
1209
viewContainerFixture . detectChanges ( ) ;
1060
- flushMicrotasks ( ) ;
1210
+ flush ( ) ;
1061
1211
1062
1212
expect ( document . activeElement ! . id ) . toBe ( 'input-to-be-focused' ,
1063
1213
'Expected that the trigger was refocused after the dialog is closed.' ) ;
0 commit comments