@@ -25,8 +25,9 @@ import {SpyLocation} from '@angular/common/testing';
2525import { Directionality } from '@angular/cdk/bidi' ;
2626import { MatDialogContainer } from './dialog-container' ;
2727import { OverlayContainer , ScrollStrategy } from '@angular/cdk/overlay' ;
28+ import { FocusOrigin , FocusMonitor } from '@angular/cdk/a11y' ;
2829import { A , ESCAPE } from '@angular/cdk/keycodes' ;
29- import { dispatchKeyboardEvent } from '@angular/cdk/testing' ;
30+ import { dispatchKeyboardEvent , dispatchMouseEvent , patchElementFocus } from '@angular/cdk/testing' ;
3031import {
3132 MAT_DIALOG_DATA ,
3233 MatDialog ,
@@ -40,6 +41,7 @@ describe('MatDialog', () => {
4041 let dialog : MatDialog ;
4142 let overlayContainer : OverlayContainer ;
4243 let overlayContainerElement : HTMLElement ;
44+ let focusMonitor : FocusMonitor ;
4345
4446 let testViewContainerRef : ViewContainerRef ;
4547 let viewContainerFixture : ComponentFixture < ComponentWithChildViewContainer > ;
@@ -56,13 +58,14 @@ describe('MatDialog', () => {
5658 TestBed . compileComponents ( ) ;
5759 } ) ) ;
5860
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 ) => {
6163 dialog = d ;
6264 mockLocation = l as SpyLocation ;
6365 overlayContainer = oc ;
6466 overlayContainerElement = oc . getContainerElement ( ) ;
65- } ) ) ;
67+ focusMonitor = fm ;
68+ } ) ) ;
6669
6770 afterEach ( ( ) => {
6871 overlayContainer . ngOnDestroy ( ) ;
@@ -913,6 +916,148 @@ describe('MatDialog', () => {
913916 document . body . removeChild ( button ) ;
914917 } ) ) ;
915918
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+
9161061 it ( 'should allow the consumer to shift focus in afterClosed' , fakeAsync ( ( ) => {
9171062 // Create a element that has focus before the dialog is opened.
9181063 let button = document . createElement ( 'button' ) ;
@@ -935,7 +1080,7 @@ describe('MatDialog', () => {
9351080
9361081 tick ( 500 ) ;
9371082 viewContainerFixture . detectChanges ( ) ;
938- flushMicrotasks ( ) ;
1083+ flush ( ) ;
9391084
9401085 expect ( document . activeElement . id ) . toBe ( 'input-to-be-focused' ,
9411086 'Expected that the trigger was refocused after the dialog is closed.' ) ;
0 commit comments