1
+ import { FocusMonitor , FocusOrigin } from '@angular/cdk/a11y' ;
1
2
import { Directionality } from '@angular/cdk/bidi' ;
2
3
import { A , ESCAPE } from '@angular/cdk/keycodes' ;
3
4
import { Overlay , OverlayContainer , ScrollStrategy } from '@angular/cdk/overlay' ;
4
5
import { ScrollDispatcher } from '@angular/cdk/scrolling' ;
5
6
import {
6
7
createKeyboardEvent ,
7
8
dispatchEvent ,
8
- dispatchKeyboardEvent
9
+ dispatchKeyboardEvent ,
10
+ dispatchMouseEvent ,
11
+ patchElementFocus
9
12
} from '@angular/cdk/testing/private' ;
10
13
import { Location } from '@angular/common' ;
11
14
import { SpyLocation } from '@angular/common/testing' ;
@@ -52,6 +55,7 @@ describe('MDC-based dialog', () => {
52
55
let testViewContainerRef : ViewContainerRef ;
53
56
let viewContainerFixture : ComponentFixture < ComponentWithChildViewContainer > ;
54
57
let mockLocation : SpyLocation ;
58
+ let focusMonitor : FocusMonitor ;
55
59
56
60
beforeEach ( fakeAsync ( ( ) => {
57
61
TestBed . configureTestingModule ( {
@@ -69,12 +73,13 @@ describe('MDC-based dialog', () => {
69
73
} ) ) ;
70
74
71
75
beforeEach ( inject (
72
- [ MatDialog , Location , OverlayContainer ] ,
73
- ( d : MatDialog , l : Location , oc : OverlayContainer ) => {
76
+ [ MatDialog , Location , OverlayContainer , FocusMonitor ] ,
77
+ ( d : MatDialog , l : Location , oc : OverlayContainer , fm : FocusMonitor ) => {
74
78
dialog = d ;
75
79
mockLocation = l as SpyLocation ;
76
80
overlayContainer = oc ;
77
81
overlayContainerElement = oc . getContainerElement ( ) ;
82
+ focusMonitor = fm ;
78
83
} ) ) ;
79
84
80
85
afterEach ( ( ) => {
@@ -895,6 +900,34 @@ describe('MDC-based dialog', () => {
895
900
896
901
expect ( document . activeElement ) . toBe ( input , 'Expected input to stay focused after click' ) ;
897
902
} ) ) ;
903
+
904
+ it ( 'should recapture focus to the container when clicking on the backdrop with ' +
905
+ 'autoFocus disabled' , fakeAsync ( ( ) => {
906
+ dialog . open ( PizzaMsg , {
907
+ disableClose : true ,
908
+ viewContainerRef : testViewContainerRef ,
909
+ autoFocus : false
910
+ } ) ;
911
+
912
+ viewContainerFixture . detectChanges ( ) ;
913
+ flushMicrotasks ( ) ;
914
+
915
+ let backdrop =
916
+ overlayContainerElement . querySelector ( '.cdk-overlay-backdrop' ) as HTMLElement ;
917
+ let container =
918
+ overlayContainerElement . querySelector ( '.mat-mdc-dialog-container' ) as HTMLInputElement ;
919
+
920
+ expect ( document . activeElement ) . toBe ( container , 'Expected container to be focused on open' ) ;
921
+
922
+ container . blur ( ) ; // Programmatic clicks might not move focus so we simulate it.
923
+ backdrop . click ( ) ;
924
+ viewContainerFixture . detectChanges ( ) ;
925
+ flush ( ) ;
926
+
927
+ expect ( document . activeElement )
928
+ . toBe ( container , 'Expected container to stay focused after click' ) ;
929
+ } ) ) ;
930
+
898
931
} ) ;
899
932
900
933
describe ( 'hasBackdrop option' , ( ) => {
@@ -970,6 +1003,147 @@ describe('MDC-based dialog', () => {
970
1003
expect ( document . activeElement ! . tagName ) . not . toBe ( 'INPUT' ) ;
971
1004
} ) ) ;
972
1005
1006
+ it ( 'should re-focus the trigger via keyboard when closed via escape key' , fakeAsync ( ( ) => {
1007
+ const button = document . createElement ( 'button' ) ;
1008
+ let lastFocusOrigin : FocusOrigin = null ;
1009
+
1010
+ focusMonitor . monitor ( button , false )
1011
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1012
+
1013
+ document . body . appendChild ( button ) ;
1014
+ button . focus ( ) ;
1015
+
1016
+ // Patch the element focus after the initial and real focus, because otherwise the
1017
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1018
+ patchElementFocus ( button ) ;
1019
+
1020
+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
1021
+
1022
+ tick ( 500 ) ;
1023
+ viewContainerFixture . detectChanges ( ) ;
1024
+
1025
+ expect ( lastFocusOrigin ! ) . toBeNull ( 'Expected the trigger button to be blurred' ) ;
1026
+
1027
+ dispatchKeyboardEvent ( document . body , 'keydown' , ESCAPE ) ;
1028
+
1029
+ flushMicrotasks ( ) ;
1030
+ viewContainerFixture . detectChanges ( ) ;
1031
+ tick ( 500 ) ;
1032
+
1033
+ expect ( lastFocusOrigin ! )
1034
+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1035
+
1036
+ focusMonitor . stopMonitoring ( button ) ;
1037
+ document . body . removeChild ( button ) ;
1038
+ } ) ) ;
1039
+
1040
+ it ( 'should re-focus the trigger via mouse when backdrop has been clicked' , fakeAsync ( ( ) => {
1041
+ const button = document . createElement ( 'button' ) ;
1042
+ let lastFocusOrigin : FocusOrigin = null ;
1043
+
1044
+ focusMonitor . monitor ( button , false )
1045
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1046
+
1047
+ document . body . appendChild ( button ) ;
1048
+ button . focus ( ) ;
1049
+
1050
+ // Patch the element focus after the initial and real focus, because otherwise the
1051
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1052
+ patchElementFocus ( button ) ;
1053
+
1054
+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
1055
+
1056
+ tick ( 500 ) ;
1057
+ viewContainerFixture . detectChanges ( ) ;
1058
+
1059
+ const backdrop = overlayContainerElement
1060
+ . querySelector ( '.cdk-overlay-backdrop' ) as HTMLElement ;
1061
+
1062
+ backdrop . click ( ) ;
1063
+ viewContainerFixture . detectChanges ( ) ;
1064
+ tick ( 500 ) ;
1065
+
1066
+ expect ( lastFocusOrigin ! )
1067
+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1068
+
1069
+ focusMonitor . stopMonitoring ( button ) ;
1070
+ document . body . removeChild ( button ) ;
1071
+ } ) ) ;
1072
+
1073
+ it ( 'should re-focus via keyboard if the close button has been triggered through keyboard' ,
1074
+ fakeAsync ( ( ) => {
1075
+ const button = document . createElement ( 'button' ) ;
1076
+ let lastFocusOrigin : FocusOrigin = null ;
1077
+
1078
+ focusMonitor . monitor ( button , false )
1079
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1080
+
1081
+ document . body . appendChild ( button ) ;
1082
+ button . focus ( ) ;
1083
+
1084
+ // Patch the element focus after the initial and real focus, because otherwise the active
1085
+ // element won't be set, and the dialog won't be able to restore focus to an element.
1086
+ patchElementFocus ( button ) ;
1087
+
1088
+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1089
+
1090
+ tick ( 500 ) ;
1091
+ viewContainerFixture . detectChanges ( ) ;
1092
+
1093
+ const closeButton = overlayContainerElement
1094
+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1095
+
1096
+ // Fake the behavior of pressing the SPACE key on a button element. Browsers fire a `click`
1097
+ // event with a MouseEvent, which has coordinates that are out of the element boundaries.
1098
+ dispatchMouseEvent ( closeButton , 'click' , 0 , 0 ) ;
1099
+
1100
+ viewContainerFixture . detectChanges ( ) ;
1101
+ tick ( 500 ) ;
1102
+
1103
+ expect ( lastFocusOrigin ! )
1104
+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1105
+
1106
+ focusMonitor . stopMonitoring ( button ) ;
1107
+ document . body . removeChild ( button ) ;
1108
+ } ) ) ;
1109
+
1110
+ it ( 'should re-focus via mouse if the close button has been clicked' , fakeAsync ( ( ) => {
1111
+ const button = document . createElement ( 'button' ) ;
1112
+ let lastFocusOrigin : FocusOrigin = null ;
1113
+
1114
+ focusMonitor . monitor ( button , false )
1115
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1116
+
1117
+ document . body . appendChild ( button ) ;
1118
+ button . focus ( ) ;
1119
+
1120
+ // Patch the element focus after the initial and real focus, because otherwise the
1121
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1122
+ patchElementFocus ( button ) ;
1123
+
1124
+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1125
+
1126
+ tick ( 500 ) ;
1127
+ viewContainerFixture . detectChanges ( ) ;
1128
+
1129
+ const closeButton = overlayContainerElement
1130
+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1131
+
1132
+ // The dialog close button detects the focus origin by inspecting the click event. If
1133
+ // coordinates of the click are not present, it assumes that the click has been triggered
1134
+ // by keyboard.
1135
+ dispatchMouseEvent ( closeButton , 'click' , 10 , 10 ) ;
1136
+
1137
+ viewContainerFixture . detectChanges ( ) ;
1138
+ tick ( 500 ) ;
1139
+
1140
+ expect ( lastFocusOrigin ! )
1141
+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1142
+
1143
+ focusMonitor . stopMonitoring ( button ) ;
1144
+ document . body . removeChild ( button ) ;
1145
+ } ) ) ;
1146
+
973
1147
it ( 'should allow the consumer to shift focus in afterClosed' , fakeAsync ( ( ) => {
974
1148
// Create a element that has focus before the dialog is opened.
975
1149
let button = document . createElement ( 'button' ) ;
0 commit comments