@@ -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 } 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 ,
@@ -40,6 +41,7 @@ describe('MatDialog', () => {
40
41
let dialog : MatDialog ;
41
42
let overlayContainer : OverlayContainer ;
42
43
let overlayContainerElement : HTMLElement ;
44
+ let focusMonitor : FocusMonitor ;
43
45
44
46
let testViewContainerRef : ViewContainerRef ;
45
47
let viewContainerFixture : ComponentFixture < ComponentWithChildViewContainer > ;
@@ -56,13 +58,14 @@ describe('MatDialog', () => {
56
58
TestBed . compileComponents ( ) ;
57
59
} ) ) ;
58
60
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 ) => {
61
63
dialog = d ;
62
64
mockLocation = l as SpyLocation ;
63
65
overlayContainer = oc ;
64
66
overlayContainerElement = oc . getContainerElement ( ) ;
65
- } ) ) ;
67
+ focusMonitor = fm ;
68
+ } ) ) ;
66
69
67
70
afterEach ( ( ) => {
68
71
overlayContainer . ngOnDestroy ( ) ;
@@ -913,6 +916,148 @@ describe('MatDialog', () => {
913
916
document . body . removeChild ( button ) ;
914
917
} ) ) ;
915
918
919
+ it ( 'should re-focus the trigger via keyboard when closed via escape key' , fakeAsync ( ( ) => {
920
+ const button = document . createElement ( 'button' ) ;
921
+ let lastFocusOrigin : FocusOrigin = null ;
922
+
923
+ focusMonitor . monitor ( button , false )
924
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
925
+
926
+ document . body . appendChild ( button ) ;
927
+ button . focus ( ) ;
928
+
929
+ // Patch the element focus after the initial and real focus, because otherwise the
930
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
931
+ patchElementFocus ( button ) ;
932
+
933
+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
934
+
935
+ tick ( 500 ) ;
936
+ viewContainerFixture . detectChanges ( ) ;
937
+
938
+ expect ( lastFocusOrigin ! ) . toBeNull ( 'Expected the trigger button to be blurred' ) ;
939
+
940
+ dispatchKeyboardEvent ( document . body , 'keydown' , ESCAPE ) ;
941
+
942
+ flushMicrotasks ( ) ;
943
+ viewContainerFixture . detectChanges ( ) ;
944
+ tick ( 500 ) ;
945
+
946
+ expect ( lastFocusOrigin ! )
947
+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
948
+
949
+ focusMonitor . stopMonitoring ( button ) ;
950
+ document . body . removeChild ( button ) ;
951
+ } ) ) ;
952
+
953
+ it ( 'should re-focus the trigger via mouse when backdrop has been clicked' , fakeAsync ( ( ) => {
954
+ const button = document . createElement ( 'button' ) ;
955
+ let lastFocusOrigin : FocusOrigin = null ;
956
+
957
+ focusMonitor . monitor ( button , false )
958
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
959
+
960
+ document . body . appendChild ( button ) ;
961
+ button . focus ( ) ;
962
+
963
+ // Patch the element focus after the initial and real focus, because otherwise the
964
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
965
+ patchElementFocus ( button ) ;
966
+
967
+ dialog . open ( PizzaMsg , { viewContainerRef : testViewContainerRef } ) ;
968
+
969
+ tick ( 500 ) ;
970
+ viewContainerFixture . detectChanges ( ) ;
971
+
972
+ const backdrop = overlayContainerElement
973
+ . querySelector ( '.cdk-overlay-backdrop' ) as HTMLElement ;
974
+
975
+ backdrop . click ( ) ;
976
+ viewContainerFixture . detectChanges ( ) ;
977
+ tick ( 500 ) ;
978
+
979
+ expect ( lastFocusOrigin ! )
980
+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
981
+
982
+ focusMonitor . stopMonitoring ( button ) ;
983
+ document . body . removeChild ( button ) ;
984
+ } ) ) ;
985
+
986
+ it ( 'should re-focus via keyboard if the close button has been triggered through keyboard' ,
987
+ fakeAsync ( ( ) => {
988
+
989
+ const button = document . createElement ( 'button' ) ;
990
+ let lastFocusOrigin : FocusOrigin = null ;
991
+
992
+ focusMonitor . monitor ( button , false )
993
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
994
+
995
+ document . body . appendChild ( button ) ;
996
+ button . focus ( ) ;
997
+
998
+ // Patch the element focus after the initial and real focus, because otherwise the
999
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1000
+ patchElementFocus ( button ) ;
1001
+
1002
+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1003
+
1004
+ tick ( 500 ) ;
1005
+ viewContainerFixture . detectChanges ( ) ;
1006
+
1007
+ const closeButton = overlayContainerElement
1008
+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1009
+
1010
+ // Fake the behavior of pressing the SPACE key on a button element. Browsers fire a `click`
1011
+ // event with a MouseEvent, which has coordinates that are out of the element boundaries.
1012
+ dispatchMouseEvent ( closeButton , 'click' , 0 , 0 ) ;
1013
+
1014
+ viewContainerFixture . detectChanges ( ) ;
1015
+ tick ( 500 ) ;
1016
+
1017
+ expect ( lastFocusOrigin ! )
1018
+ . toBe ( 'keyboard' , 'Expected the trigger button to be focused via keyboard' ) ;
1019
+
1020
+ focusMonitor . stopMonitoring ( button ) ;
1021
+ document . body . removeChild ( button ) ;
1022
+ } ) ) ;
1023
+
1024
+ it ( 'should re-focus via mouse if the close button has been clicked' , fakeAsync ( ( ) => {
1025
+ const button = document . createElement ( 'button' ) ;
1026
+ let lastFocusOrigin : FocusOrigin = null ;
1027
+
1028
+ focusMonitor . monitor ( button , false )
1029
+ . subscribe ( focusOrigin => lastFocusOrigin = focusOrigin ) ;
1030
+
1031
+ document . body . appendChild ( button ) ;
1032
+ button . focus ( ) ;
1033
+
1034
+ // Patch the element focus after the initial and real focus, because otherwise the
1035
+ // `activeElement` won't be set, and the dialog won't be able to restore focus to an element.
1036
+ patchElementFocus ( button ) ;
1037
+
1038
+ dialog . open ( ContentElementDialog , { viewContainerRef : testViewContainerRef } ) ;
1039
+
1040
+ tick ( 500 ) ;
1041
+ viewContainerFixture . detectChanges ( ) ;
1042
+
1043
+ const closeButton = overlayContainerElement
1044
+ . querySelector ( 'button[mat-dialog-close]' ) as HTMLElement ;
1045
+
1046
+ // The dialog close button detects the focus origin by inspecting the click event. If
1047
+ // coordinates of the click are not present, it assumes that the click has been triggered
1048
+ // by keyboard.
1049
+ dispatchMouseEvent ( closeButton , 'click' , 10 , 10 ) ;
1050
+
1051
+ viewContainerFixture . detectChanges ( ) ;
1052
+ tick ( 500 ) ;
1053
+
1054
+ expect ( lastFocusOrigin ! )
1055
+ . toBe ( 'mouse' , 'Expected the trigger button to be focused via mouse' ) ;
1056
+
1057
+ focusMonitor . stopMonitoring ( button ) ;
1058
+ document . body . removeChild ( button ) ;
1059
+ } ) ) ;
1060
+
916
1061
it ( 'should allow the consumer to shift focus in afterClosed' , fakeAsync ( ( ) => {
917
1062
// Create a element that has focus before the dialog is opened.
918
1063
let button = document . createElement ( 'button' ) ;
@@ -935,7 +1080,7 @@ describe('MatDialog', () => {
935
1080
936
1081
tick ( 500 ) ;
937
1082
viewContainerFixture . detectChanges ( ) ;
938
- flushMicrotasks ( ) ;
1083
+ flush ( ) ;
939
1084
940
1085
expect ( document . activeElement . id ) . toBe ( 'input-to-be-focused' ,
941
1086
'Expected that the trigger was refocused after the dialog is closed.' ) ;
0 commit comments