Skip to content

Commit 78ea4b4

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 78ea4b4

File tree

8 files changed

+120
-41
lines changed

8 files changed

+120
-41
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: 31 additions & 13 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();
@@ -1959,12 +1959,26 @@ class ComponentWithTemplateRef {
19591959
}
19601960
}
19611961

1962-
/** Simple component for testing ComponentPortal. */
1962+
/** Simple components for testing ComponentPortal and multiple dialogs. */
19631963
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
19641964
class PizzaMsg {
1965-
constructor(
1966-
public dialogRef: MatDialogRef<PizzaMsg>, public dialogInjector: Injector,
1967-
public directionality: Directionality) {}
1965+
constructor(public dialogRef: MatDialogRef<PizzaMsg>,
1966+
public dialogInjector: Injector,
1967+
public directionality: Directionality) {}
1968+
}
1969+
1970+
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
1971+
class PizzaMsgTwo {
1972+
constructor(public dialogRef: MatDialogRef<PizzaMsgTwo>,
1973+
public dialogInjector: Injector,
1974+
public directionality: Directionality) {}
1975+
}
1976+
1977+
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
1978+
class PizzaMsgThree {
1979+
constructor(public dialogRef: MatDialogRef<PizzaMsgThree>,
1980+
public dialogInjector: Injector,
1981+
public directionality: Directionality) {}
19681982
}
19691983

19701984
@Component({
@@ -2035,6 +2049,8 @@ const TEST_DIRECTIVES = [
20352049
ComponentWithChildViewContainer,
20362050
ComponentWithTemplateRef,
20372051
PizzaMsg,
2052+
PizzaMsgTwo,
2053+
PizzaMsgThree,
20382054
DirectiveWithViewContainer,
20392055
ComponentWithOnPushViewContainer,
20402056
ContentElementDialog,
@@ -2052,6 +2068,8 @@ const TEST_DIRECTIVES = [
20522068
ComponentWithChildViewContainer,
20532069
ComponentWithTemplateRef,
20542070
PizzaMsg,
2071+
PizzaMsgTwo,
2072+
PizzaMsgThree,
20552073
ContentElementDialog,
20562074
DialogWithInjectedData,
20572075
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"]');

src/material/dialog/dialog.spec.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -670,8 +670,8 @@ describe('MatDialog', () => {
670670

671671
it('should close all of the dialogs', fakeAsync(() => {
672672
dialog.open(PizzaMsg);
673-
dialog.open(PizzaMsg);
674-
dialog.open(PizzaMsg);
673+
dialog.open(PizzaMsgTwo);
674+
dialog.open(PizzaMsgThree);
675675

676676
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(3);
677677

@@ -697,7 +697,7 @@ describe('MatDialog', () => {
697697

698698
it('should close all dialogs when the user goes forwards/backwards in history', fakeAsync(() => {
699699
dialog.open(PizzaMsg);
700-
dialog.open(PizzaMsg);
700+
dialog.open(PizzaMsgTwo);
701701

702702
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
703703

@@ -710,7 +710,7 @@ describe('MatDialog', () => {
710710

711711
it('should close all open dialogs when the location hash changes', fakeAsync(() => {
712712
dialog.open(PizzaMsg);
713-
dialog.open(PizzaMsg);
713+
dialog.open(PizzaMsgTwo);
714714

715715
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
716716

@@ -723,8 +723,8 @@ describe('MatDialog', () => {
723723

724724
it('should close all of the dialogs when the injectable is destroyed', fakeAsync(() => {
725725
dialog.open(PizzaMsg);
726-
dialog.open(PizzaMsg);
727-
dialog.open(PizzaMsg);
726+
dialog.open(PizzaMsgTwo);
727+
dialog.open(PizzaMsgThree);
728728

729729
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(3);
730730

@@ -754,7 +754,7 @@ describe('MatDialog', () => {
754754

755755
it('should allow the consumer to disable closing a dialog on navigation', fakeAsync(() => {
756756
dialog.open(PizzaMsg);
757-
dialog.open(PizzaMsg, {closeOnNavigation: false});
757+
dialog.open(PizzaMsgTwo, {closeOnNavigation: false});
758758

759759
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
760760

@@ -849,7 +849,7 @@ describe('MatDialog', () => {
849849

850850
it('should assign a unique id to each dialog', () => {
851851
const one = dialog.open(PizzaMsg);
852-
const two = dialog.open(PizzaMsg);
852+
const two = dialog.open(PizzaMsgTwo);
853853

854854
expect(one.id).toBeTruthy();
855855
expect(two.id).toBeTruthy();
@@ -1320,7 +1320,7 @@ describe('MatDialog', () => {
13201320

13211321
tick(500);
13221322
viewContainerFixture.detectChanges();
1323-
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
1323+
expect(lastFocusOrigin!).toBe('program');
13241324

13251325
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
13261326

@@ -1353,7 +1353,7 @@ describe('MatDialog', () => {
13531353

13541354
tick(500);
13551355
viewContainerFixture.detectChanges();
1356-
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
1356+
expect(lastFocusOrigin!).toBe('program');
13571357

13581358
const backdrop = overlayContainerElement
13591359
.querySelector('.cdk-overlay-backdrop') as HTMLElement;
@@ -1389,7 +1389,7 @@ describe('MatDialog', () => {
13891389

13901390
tick(500);
13911391
viewContainerFixture.detectChanges();
1392-
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
1392+
expect(lastFocusOrigin!).toBe('program');
13931393

13941394
const closeButton = overlayContainerElement
13951395
.querySelector('button[mat-dialog-close]') as HTMLElement;
@@ -1426,7 +1426,7 @@ describe('MatDialog', () => {
14261426

14271427
tick(500);
14281428
viewContainerFixture.detectChanges();
1429-
expect(lastFocusOrigin!).toBeNull('Expected the trigger button to be blurred');
1429+
expect(lastFocusOrigin!).toBe('program');
14301430

14311431
const closeButton = overlayContainerElement
14321432
.querySelector('button[mat-dialog-close]') as HTMLElement;
@@ -2020,14 +2020,28 @@ class ComponentWithTemplateRef {
20202020
}
20212021
}
20222022

2023-
/** Simple component for testing ComponentPortal. */
2023+
/** Simple components for testing ComponentPortal and multiple dialogs. */
20242024
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
20252025
class PizzaMsg {
20262026
constructor(public dialogRef: MatDialogRef<PizzaMsg>,
20272027
public dialogInjector: Injector,
20282028
public directionality: Directionality) {}
20292029
}
20302030

2031+
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
2032+
class PizzaMsgTwo {
2033+
constructor(public dialogRef: MatDialogRef<PizzaMsgTwo>,
2034+
public dialogInjector: Injector,
2035+
public directionality: Directionality) {}
2036+
}
2037+
2038+
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
2039+
class PizzaMsgThree {
2040+
constructor(public dialogRef: MatDialogRef<PizzaMsgThree>,
2041+
public dialogInjector: Injector,
2042+
public directionality: Directionality) {}
2043+
}
2044+
20312045
@Component({
20322046
template: `
20332047
<h1 mat-dialog-title>This is the title</h1>
@@ -2097,6 +2111,8 @@ const TEST_DIRECTIVES = [
20972111
ComponentWithChildViewContainer,
20982112
ComponentWithTemplateRef,
20992113
PizzaMsg,
2114+
PizzaMsgTwo,
2115+
PizzaMsgThree,
21002116
DirectiveWithViewContainer,
21012117
ComponentWithOnPushViewContainer,
21022118
ContentElementDialog,
@@ -2114,6 +2130,8 @@ const TEST_DIRECTIVES = [
21142130
ComponentWithChildViewContainer,
21152131
ComponentWithTemplateRef,
21162132
PizzaMsg,
2133+
PizzaMsgTwo,
2134+
PizzaMsgThree,
21172135
ContentElementDialog,
21182136
DialogWithInjectedData,
21192137
DialogWithoutFocusableElements,

0 commit comments

Comments
 (0)