Skip to content

Commit 56c5c0f

Browse files
committed
fix(material/select): make VoiceOver read options for selects in dialogs
Fixes #20694 @zelliot noted that this issue was caused by `aria-modal` preventing VoiceOver from accessing the select's listbox overlay. He suggested using `aria-owns` to re-parent the overlay element to the select trigger. I tried this and it works great.
1 parent 50d3f29 commit 56c5c0f

File tree

8 files changed

+80
-5
lines changed

8 files changed

+80
-5
lines changed

src/dev-app/select/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ng_module(
1212
deps = [
1313
"//src/material/button",
1414
"//src/material/card",
15+
"//src/material/dialog",
1516
"//src/material/form-field",
1617
"//src/material/icon",
1718
"//src/material/input",

src/dev-app/select/select-demo-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {NgModule} from '@angular/core';
1111
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
1212
import {MatButtonModule} from '@angular/material/button';
1313
import {MatCardModule} from '@angular/material/card';
14+
import {MatDialogModule} from '@angular/material/dialog';
1415
import {MatFormFieldModule} from '@angular/material/form-field';
1516
import {MatIconModule} from '@angular/material/icon';
1617
import {MatInputModule} from '@angular/material/input';
@@ -24,6 +25,7 @@ import {SelectDemo} from './select-demo';
2425
FormsModule,
2526
MatButtonModule,
2627
MatCardModule,
28+
MatDialogModule,
2729
MatFormFieldModule,
2830
MatIconModule,
2931
MatInputModule,

src/dev-app/select/select-demo.html

+27
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,33 @@
246246
</mat-card>
247247
</div>
248248

249+
<mat-card>
250+
<mat-card-subtitle>MatSelect inside a dialog</mat-card-subtitle>
251+
<mat-card-content>
252+
253+
<button (click)="openDialogWithSelectInside(dialogTemplate)">Open dialog</button>
254+
255+
<ng-template #dialogTemplate>
256+
<mat-form-field>
257+
<mat-label>Your name</mat-label>
258+
<input matInput>
259+
</mat-form-field>
260+
261+
<mat-form-field>
262+
<mat-label>Select a topping</mat-label>
263+
<mat-select>
264+
<mat-option>Cheese</mat-option>
265+
<mat-option>Onion</mat-option>
266+
<mat-option>Pepper</mat-option>
267+
</mat-select>
268+
</mat-form-field>
269+
270+
<button>Done</button>
271+
</ng-template>
272+
273+
</mat-card-content>
274+
</mat-card>
275+
249276
</div>
250277

251278
<mat-card class="demo-card demo-basic">

src/dev-app/select/select-demo.ts

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

9-
import {Component} from '@angular/core';
9+
import {Component, TemplateRef} from '@angular/core';
1010
import {FormControl, Validators} from '@angular/forms';
1111
import {ErrorStateMatcher, ThemePalette} from '@angular/material/core';
12+
import {MatDialog} from '@angular/material/dialog';
1213
import {FloatLabelType} from '@angular/material/form-field';
1314
import {MatSelectChange} from '@angular/material/select';
1415

@@ -23,9 +24,9 @@ export class MyErrorStateMatcher implements ErrorStateMatcher {
2324
}
2425

2526
@Component({
26-
selector: 'select-demo',
27-
templateUrl: 'select-demo.html',
28-
styleUrls: ['select-demo.css'],
27+
selector: 'select-demo',
28+
templateUrl: 'select-demo.html',
29+
styleUrls: ['select-demo.css'],
2930
})
3031
export class SelectDemo {
3132
drinksRequired = false;
@@ -133,6 +134,8 @@ export class SelectDemo {
133134
{value: 'indramon-5', viewValue: 'Indramon'}
134135
];
135136

137+
constructor(private _dialog: MatDialog) {}
138+
136139
toggleDisabled() {
137140
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
138141
}
@@ -158,4 +161,8 @@ export class SelectDemo {
158161
toggleSelected() {
159162
this.currentAppearanceValue = this.currentAppearanceValue ? null : this.digimon[0].value;
160163
}
164+
165+
openDialogWithSelectInside(dialogTemplate: TemplateRef<unknown>) {
166+
this._dialog.open(dialogTemplate);
167+
}
161168
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@ describe('MDC-based MatSelect', () => {
196196
expect(ariaControls).toBe(document.querySelector('.mat-mdc-select-panel')!.id);
197197
}));
198198

199+
it('should point the aria-owns attribute to the listbox', fakeAsync(() => {
200+
expect(select.hasAttribute('aria-owns')).toBe(false);
201+
202+
fixture.componentInstance.select.open();
203+
fixture.detectChanges();
204+
flush();
205+
206+
const ariaControls = select.getAttribute('aria-owns');
207+
expect(ariaControls).toBeTruthy();
208+
expect(ariaControls).toBe(document.querySelector('.mat-mdc-select-panel')!.id);
209+
}));
210+
199211
it('should set aria-expanded based on the select open state', fakeAsync(() => {
200212
expect(select.getAttribute('aria-expanded')).toBe('false');
201213

src/material-experimental/mdc-select/select.ts

+7
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ export class MatSelectTrigger {}
6262
'class': 'mat-mdc-select',
6363
'[attr.id]': 'id',
6464
'[attr.tabindex]': 'tabIndex',
65+
// While aria-owns is not required for the `role="combobox"` interaction pattern,
66+
// it fixes an issue with VoiceOver when the select appears inside of an `aria-model="true"`
67+
// element (e.g. a dialog). Without `aria-owns`, the `aria-modal` on a dialog would prevent
68+
// VoiceOver from "seeing" the select's listbox overlay for aria-activedescendant.
69+
// Using `aria-owns` re-parents the select overlay so that it works again.
70+
// See https://github.com/angular/components/issues/20694
71+
'[attr.aria-owns]': 'panelOpen ? id + "-panel" : null',
6572
'[attr.aria-controls]': 'panelOpen ? id + "-panel" : null',
6673
'[attr.aria-expanded]': 'panelOpen',
6774
'[attr.aria-label]': 'ariaLabel || null',

src/material/select/select.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ describe('MatSelect', () => {
165165
expect(ariaControls).toBe(document.querySelector('.mat-select-panel')!.id);
166166
}));
167167

168+
it('should point the aria-controls attribute to the listbox', fakeAsync(() => {
169+
expect(select.hasAttribute('aria-owns')).toBe(false);
170+
171+
fixture.componentInstance.select.open();
172+
fixture.detectChanges();
173+
flush();
174+
175+
const ariaControls = select.getAttribute('aria-owns');
176+
expect(ariaControls).toBeTruthy();
177+
expect(ariaControls).toBe(document.querySelector('.mat-select-panel')!.id);
178+
}));
179+
168180
it('should set aria-expanded based on the select open state', fakeAsync(() => {
169181
expect(select.getAttribute('aria-expanded')).toBe('false');
170182

src/material/select/select.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1105,12 +1105,19 @@ export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements A
11051105
'role': 'combobox',
11061106
'aria-autocomplete': 'none',
11071107
// TODO(crisbeto): the value for aria-haspopup should be `listbox`, but currently it's difficult
1108-
// to sync into g3, because of an outdated automated a11y check which flags it as an invalid
1108+
// to sync into Google, because of an outdated automated a11y check which flags it as an invalid
11091109
// value. At some point we should try to switch it back to being `listbox`.
11101110
'aria-haspopup': 'true',
11111111
'class': 'mat-select',
11121112
'[attr.id]': 'id',
11131113
'[attr.tabindex]': 'tabIndex',
1114+
// While aria-owns is not required for the `role="combobox"` interaction pattern,
1115+
// it fixes an issue with VoiceOver when the select appears inside of an `aria-model="true"`
1116+
// element (e.g. a dialog). Without `aria-owns`, the `aria-modal` on a dialog would prevent
1117+
// VoiceOver from "seeing" the select's listbox overlay for aria-activedescendant.
1118+
// Using `aria-owns` re-parents the select overlay so that it works again.
1119+
// See https://github.com/angular/components/issues/20694
1120+
'[attr.aria-owns]': 'panelOpen ? id + "-panel" : null',
11141121
'[attr.aria-controls]': 'panelOpen ? id + "-panel" : null',
11151122
'[attr.aria-expanded]': 'panelOpen',
11161123
'[attr.aria-label]': 'ariaLabel || null',

0 commit comments

Comments
 (0)