Skip to content

Commit 087ed2c

Browse files
committed
fix(material/dialog): improve screen reader support when opened
- notify screen reader users that they have entered a dialog - previously only the focused element would be read i.e. "Close Button Press Search plus Space to activate" - now the screen reader user gets the normal dialog behavior, which is to read the dialog title, role, content, and then tell the user about the focused element - this matches the guidance here: https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/dialog.html - Avoid opening multiple of the same dialog before animations complete by returning the previous `MatDialogRef` - update tests to use different dialog components when they need to open multiple dialogs quickly Fixes #21840
1 parent c660dac commit 087ed2c

File tree

8 files changed

+140
-51
lines changed

8 files changed

+140
-51
lines changed

src/components-examples/material/dialog/dialog-harness/dialog-harness-example.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
1212

1313
describe('DialogHarnessExample', () => {
1414
let fixture: ComponentFixture<DialogHarnessExample>;
15+
let fixtureTwo: ComponentFixture<DialogHarnessExample>;
1516
let loader: HarnessLoader;
1617

1718
beforeAll(() => {
@@ -27,6 +28,8 @@ describe('DialogHarnessExample', () => {
2728
}).compileComponents();
2829
fixture = TestBed.createComponent(DialogHarnessExample);
2930
fixture.detectChanges();
31+
fixtureTwo = TestBed.createComponent(DialogHarnessExample);
32+
fixtureTwo.detectChanges();
3033
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
3134
}));
3235

@@ -38,7 +41,7 @@ describe('DialogHarnessExample', () => {
3841

3942
it('should load harness for dialog with specific id', async () => {
4043
fixture.componentInstance.open({id: 'my-dialog'});
41-
fixture.componentInstance.open({id: 'other'});
44+
fixtureTwo.componentInstance.open({id: 'other'});
4245
let dialogs = await loader.getAllHarnesses(MatDialogHarness);
4346
expect(dialogs.length).toBe(2);
4447

@@ -48,7 +51,7 @@ describe('DialogHarnessExample', () => {
4851

4952
it('should be able to get role of dialog', async () => {
5053
fixture.componentInstance.open({role: 'alertdialog'});
51-
fixture.componentInstance.open({role: 'dialog'});
54+
fixtureTwo.componentInstance.open({role: 'dialog'});
5255
const dialogs = await loader.getAllHarnesses(MatDialogHarness);
5356
expect(await dialogs[0].getRole()).toBe('alertdialog');
5457
expect(await dialogs[1].getRole()).toBe('dialog');
@@ -57,7 +60,7 @@ describe('DialogHarnessExample', () => {
5760

5861
it('should be able to close dialog', async () => {
5962
fixture.componentInstance.open({disableClose: true});
60-
fixture.componentInstance.open();
63+
fixtureTwo.componentInstance.open();
6164
let dialogs = await loader.getAllHarnesses(MatDialogHarness);
6265

6366
expect(dialogs.length).toBe(2);

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

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -615,8 +615,8 @@ describe('MDC-based MatDialog', () => {
615615

616616
it('should close all of the dialogs', fakeAsync(() => {
617617
dialog.open(PizzaMsg);
618-
dialog.open(PizzaMsg);
619-
dialog.open(PizzaMsg);
618+
dialog.open(PizzaMsgTwo);
619+
dialog.open(PizzaMsgThree);
620620

621621
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(3);
622622

@@ -629,7 +629,7 @@ describe('MDC-based MatDialog', () => {
629629

630630
it('should close all dialogs when the user goes forwards/backwards in history', fakeAsync(() => {
631631
dialog.open(PizzaMsg);
632-
dialog.open(PizzaMsg);
632+
dialog.open(PizzaMsgTwo);
633633

634634
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
635635

@@ -642,7 +642,7 @@ describe('MDC-based MatDialog', () => {
642642

643643
it('should close all open dialogs when the location hash changes', fakeAsync(() => {
644644
dialog.open(PizzaMsg);
645-
dialog.open(PizzaMsg);
645+
dialog.open(PizzaMsgTwo);
646646

647647
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
648648

@@ -655,8 +655,8 @@ describe('MDC-based MatDialog', () => {
655655

656656
it('should close all of the dialogs when the injectable is destroyed', fakeAsync(() => {
657657
dialog.open(PizzaMsg);
658-
dialog.open(PizzaMsg);
659-
dialog.open(PizzaMsg);
658+
dialog.open(PizzaMsgTwo);
659+
dialog.open(PizzaMsgThree);
660660

661661
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(3);
662662

@@ -685,7 +685,7 @@ describe('MDC-based MatDialog', () => {
685685

686686
it('should allow the consumer to disable closing a dialog on navigation', fakeAsync(() => {
687687
dialog.open(PizzaMsg);
688-
dialog.open(PizzaMsg, {closeOnNavigation: false});
688+
dialog.open(PizzaMsgTwo, {closeOnNavigation: false});
689689

690690
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
691691

@@ -774,7 +774,7 @@ describe('MDC-based MatDialog', () => {
774774

775775
it('should assign a unique id to each dialog', () => {
776776
const one = dialog.open(PizzaMsg);
777-
const two = dialog.open(PizzaMsg);
777+
const two = dialog.open(PizzaMsgTwo);
778778

779779
expect(one.id).toBeTruthy();
780780
expect(two.id).toBeTruthy();
@@ -1188,7 +1188,7 @@ describe('MDC-based MatDialog', () => {
11881188
expect(document.activeElement!.id)
11891189
.not.toBe(
11901190
'dialog-trigger',
1191-
'Expcted the focus not to have changed before the animation finishes.');
1191+
'Expected the focus not to have changed before the animation finishes.');
11921192

11931193
flushMicrotasks();
11941194
viewContainerFixture.detectChanges();
@@ -1247,7 +1247,8 @@ describe('MDC-based MatDialog', () => {
12471247

12481248
tick(500);
12491249
viewContainerFixture.detectChanges();
1250-
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
1250+
expect(lastFocusOrigin!)
1251+
.withContext('Expected the trigger button to be blurred').toBeNull();
12511252

12521253
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
12531254

@@ -1256,7 +1257,8 @@ describe('MDC-based MatDialog', () => {
12561257
tick(500);
12571258

12581259
expect(lastFocusOrigin!)
1259-
.toBe('keyboard', 'Expected the trigger button to be focused via keyboard');
1260+
.withContext('Expected the trigger button to be focused via keyboard')
1261+
.toBe('keyboard');
12601262

12611263
focusMonitor.stopMonitoring(button);
12621264
document.body.removeChild(button);
@@ -1281,7 +1283,8 @@ describe('MDC-based MatDialog', () => {
12811283

12821284
tick(500);
12831285
viewContainerFixture.detectChanges();
1284-
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
1286+
expect(lastFocusOrigin!)
1287+
.withContext('Expected the trigger button to be blurred').toBeNull();
12851288

12861289
const backdrop =
12871290
overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
@@ -1291,7 +1294,8 @@ describe('MDC-based MatDialog', () => {
12911294
tick(500);
12921295

12931296
expect(lastFocusOrigin!)
1294-
.toBe('mouse', 'Expected the trigger button to be focused via mouse');
1297+
.withContext('Expected the trigger button to be focused via mouse')
1298+
.toBe('mouse');
12951299

12961300
focusMonitor.stopMonitoring(button);
12971301
document.body.removeChild(button);
@@ -1317,7 +1321,8 @@ describe('MDC-based MatDialog', () => {
13171321

13181322
tick(500);
13191323
viewContainerFixture.detectChanges();
1320-
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
1324+
expect(lastFocusOrigin!)
1325+
.withContext('Expected the trigger button to be blurred').toBeNull();
13211326

13221327
const closeButton =
13231328
overlayContainerElement.querySelector('button[mat-dialog-close]') as HTMLElement;
@@ -1330,7 +1335,8 @@ describe('MDC-based MatDialog', () => {
13301335
tick(500);
13311336

13321337
expect(lastFocusOrigin!)
1333-
.toBe('keyboard', 'Expected the trigger button to be focused via keyboard');
1338+
.withContext('Expected the trigger button to be focused via keyboard')
1339+
.toBe('keyboard');
13341340

13351341
focusMonitor.stopMonitoring(button);
13361342
document.body.removeChild(button);
@@ -1355,7 +1361,8 @@ describe('MDC-based MatDialog', () => {
13551361

13561362
tick(500);
13571363
viewContainerFixture.detectChanges();
1358-
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
1364+
expect(lastFocusOrigin!)
1365+
.withContext('Expected the trigger button to be blurred').toBeNull();
13591366

13601367
const closeButton =
13611368
overlayContainerElement.querySelector('button[mat-dialog-close]') as HTMLElement;
@@ -1369,7 +1376,8 @@ describe('MDC-based MatDialog', () => {
13691376
tick(500);
13701377

13711378
expect(lastFocusOrigin!)
1372-
.toBe('mouse', 'Expected the trigger button to be focused via mouse');
1379+
.withContext('Expected the trigger button to be focused via mouse')
1380+
.toBe('mouse');
13731381

13741382
focusMonitor.stopMonitoring(button);
13751383
document.body.removeChild(button);
@@ -1959,12 +1967,26 @@ class ComponentWithTemplateRef {
19591967
}
19601968
}
19611969

1962-
/** Simple component for testing ComponentPortal. */
1970+
/** Simple components for testing ComponentPortal and multiple dialogs. */
19631971
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
19641972
class PizzaMsg {
1965-
constructor(
1966-
public dialogRef: MatDialogRef<PizzaMsg>, public dialogInjector: Injector,
1967-
public directionality: Directionality) {}
1973+
constructor(public dialogRef: MatDialogRef<PizzaMsg>,
1974+
public dialogInjector: Injector,
1975+
public directionality: Directionality) {}
1976+
}
1977+
1978+
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
1979+
class PizzaMsgTwo {
1980+
constructor(public dialogRef: MatDialogRef<PizzaMsgTwo>,
1981+
public dialogInjector: Injector,
1982+
public directionality: Directionality) {}
1983+
}
1984+
1985+
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
1986+
class PizzaMsgThree {
1987+
constructor(public dialogRef: MatDialogRef<PizzaMsgThree>,
1988+
public dialogInjector: Injector,
1989+
public directionality: Directionality) {}
19681990
}
19691991

19701992
@Component({
@@ -2035,6 +2057,8 @@ const TEST_DIRECTIVES = [
20352057
ComponentWithChildViewContainer,
20362058
ComponentWithTemplateRef,
20372059
PizzaMsg,
2060+
PizzaMsgTwo,
2061+
PizzaMsgThree,
20382062
DirectiveWithViewContainer,
20392063
ComponentWithOnPushViewContainer,
20402064
ContentElementDialog,
@@ -2052,6 +2076,8 @@ const TEST_DIRECTIVES = [
20522076
ComponentWithChildViewContainer,
20532077
ComponentWithTemplateRef,
20542078
PizzaMsg,
2079+
PizzaMsgTwo,
2080+
PizzaMsgThree,
20552081
ContentElementDialog,
20562082
DialogWithInjectedData,
20572083
DialogWithoutFocusableElements,

src/material/dialog/dialog-animations.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import {
1414
AnimationTriggerMetadata,
1515
} from '@angular/animations';
1616

17+
/**
18+
* Animation transition time used by MatDialog.
19+
* @docs-private
20+
*/
21+
export const _transitionTime = 150;
22+
1723
/**
1824
* Animations used by MatDialog.
1925
* @docs-private
@@ -28,7 +34,7 @@ export const matDialogAnimations: {
2834
// decimate the animation performance. Leaving it as `none` solves both issues.
2935
state('void, exit', style({opacity: 0, transform: 'scale(0.7)'})),
3036
state('enter', style({transform: 'none'})),
31-
transition('* => enter', animate('150ms cubic-bezier(0, 0, 0.2, 1)',
37+
transition('* => enter', animate(`${_transitionTime}ms cubic-bezier(0, 0, 0.2, 1)`,
3238
style({transform: 'none', opacity: 1}))),
3339
transition('* => void, * => exit',
3440
animate('75ms cubic-bezier(0.4, 0.0, 0.2, 1)', style({opacity: 0}))),

src/material/dialog/dialog-container.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
114114
// Save the previously focused element. This element will be re-focused
115115
// when the dialog closes.
116116
this._capturePreviouslyFocusedElement();
117-
// Move focus onto the dialog immediately in order to prevent the user
118-
// from accidentally opening multiple dialogs at the same time.
119-
this._focusDialogContainer();
120117
}
121118

122119
/**
@@ -218,7 +215,13 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
218215
break;
219216
case true:
220217
case 'first-tabbable':
221-
this._focusTrap.focusInitialElementWhenReady();
218+
this._focusTrap.focusInitialElementWhenReady().then(focusedSuccessfully => {
219+
// If we weren't able to find a focusable element in the dialog, then focus the dialog
220+
// container instead.
221+
if (!focusedSuccessfully) {
222+
this._focusDialogContainer();
223+
}
224+
});
222225
break;
223226
case 'first-heading':
224227
this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]');

0 commit comments

Comments
 (0)