Skip to content

Commit 6612523

Browse files
committed
fixup! feat(material-experimental): MDC-based version of dialog
Sync with changes in non-MDC dialog
1 parent 08d24a1 commit 6612523

File tree

4 files changed

+193
-7
lines changed

4 files changed

+193
-7
lines changed

src/material-experimental/mdc-dialog/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ ng_test_library(
6161
),
6262
deps = [
6363
":mdc-dialog",
64+
"//src/cdk/a11y",
6465
"//src/cdk/bidi",
6566
"//src/cdk/keycodes",
6667
"//src/cdk/overlay",

src/material-experimental/mdc-dialog/dialog-container.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {FocusTrapFactory} from '@angular/cdk/a11y';
9+
import {FocusMonitor, FocusTrapFactory} from '@angular/cdk/a11y';
1010
import {DOCUMENT} from '@angular/common';
1111
import {
1212
AfterViewInit,
@@ -70,8 +70,9 @@ export class MatDialogContainer extends _MatDialogContainerBase implements
7070
@Optional() @Inject(DOCUMENT) document: any,
7171
config: MatDialogConfig,
7272
private _ngZone: NgZone,
73-
@Optional() @Inject(ANIMATION_MODULE_TYPE) private _animationMode?: string) {
74-
super(elementRef, focusTrapFactory, changeDetectorRef, document, config);
73+
@Optional() @Inject(ANIMATION_MODULE_TYPE) private _animationMode?: string,
74+
focusMonitor?: FocusMonitor) {
75+
super(elementRef, focusTrapFactory, changeDetectorRef, document, config, focusMonitor);
7576
}
7677

7778
ngAfterViewInit() {

src/material-experimental/mdc-dialog/dialog-content-directives.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Optional,
1616
SimpleChanges,
1717
} from '@angular/core';
18+
import {_closeDialogVia} from '@angular/material/dialog';
1819

1920
import {MatDialog} from './dialog';
2021
import {MatDialogRef} from './dialog-ref';
@@ -29,7 +30,7 @@ let dialogElementUid = 0;
2930
selector: '[mat-dialog-close], [matDialogClose]',
3031
exportAs: 'matDialogClose',
3132
host: {
32-
'(click)': 'dialogRef.close(dialogResult)',
33+
'(click)': '_onButtonClick($event)',
3334
'[attr.aria-label]': 'ariaLabel || null',
3435
'[attr.type]': 'type',
3536
}
@@ -68,6 +69,15 @@ export class MatDialogClose implements OnInit, OnChanges {
6869
this.dialogResult = proxiedChange.currentValue;
6970
}
7071
}
72+
73+
_onButtonClick(event: MouseEvent) {
74+
// Determinate the focus origin using the click event, because using the FocusMonitor will
75+
// result in incorrect origins. Most of the time, close buttons will be auto focused in the
76+
// dialog, and therefore clicking the button won't result in a focus change. This means that
77+
// the FocusMonitor won't detect any origin change, and will always output `program`.
78+
_closeDialogVia(this.dialogRef,
79+
event.screenX === 0 && event.screenY === 0 ? 'keyboard' : 'mouse', this.dialogResult);
80+
}
7181
}
7282

7383
/**

src/material-experimental/mdc-dialog/dialog.spec.ts

+177-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
12
import {Directionality} from '@angular/cdk/bidi';
23
import {A, ESCAPE} from '@angular/cdk/keycodes';
34
import {Overlay, OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay';
45
import {ScrollDispatcher} from '@angular/cdk/scrolling';
56
import {
67
createKeyboardEvent,
78
dispatchEvent,
8-
dispatchKeyboardEvent
9+
dispatchKeyboardEvent,
10+
dispatchMouseEvent,
11+
patchElementFocus
912
} from '@angular/cdk/testing/private';
1013
import {Location} from '@angular/common';
1114
import {SpyLocation} from '@angular/common/testing';
@@ -52,6 +55,7 @@ describe('MDC-based dialog', () => {
5255
let testViewContainerRef: ViewContainerRef;
5356
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
5457
let mockLocation: SpyLocation;
58+
let focusMonitor: FocusMonitor;
5559

5660
beforeEach(fakeAsync(() => {
5761
TestBed.configureTestingModule({
@@ -69,12 +73,13 @@ describe('MDC-based dialog', () => {
6973
}));
7074

7175
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) => {
7478
dialog = d;
7579
mockLocation = l as SpyLocation;
7680
overlayContainer = oc;
7781
overlayContainerElement = oc.getContainerElement();
82+
focusMonitor = fm;
7883
}));
7984

8085
afterEach(() => {
@@ -895,6 +900,34 @@ describe('MDC-based dialog', () => {
895900

896901
expect(document.activeElement).toBe(input, 'Expected input to stay focused after click');
897902
}));
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+
898931
});
899932

900933
describe('hasBackdrop option', () => {
@@ -970,6 +1003,147 @@ describe('MDC-based dialog', () => {
9701003
expect(document.activeElement!.tagName).not.toBe('INPUT');
9711004
}));
9721005

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+
9731147
it('should allow the consumer to shift focus in afterClosed', fakeAsync(() => {
9741148
// Create a element that has focus before the dialog is opened.
9751149
let button = document.createElement('button');

0 commit comments

Comments
 (0)