Skip to content

Commit

Permalink
fix(material/select): make VoiceOver read options for selects in dial…
Browse files Browse the repository at this point in the history
…ogs (#20695)

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.
  • Loading branch information
jelbourn committed Oct 7, 2020
1 parent d6e6f44 commit 33a43f7
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 5 deletions.
1 change: 1 addition & 0 deletions src/dev-app/select/BUILD.bazel
Expand Up @@ -12,6 +12,7 @@ ng_module(
deps = [
"//src/material/button",
"//src/material/card",
"//src/material/dialog",
"//src/material/form-field",
"//src/material/icon",
"//src/material/input",
Expand Down
2 changes: 2 additions & 0 deletions src/dev-app/select/select-demo-module.ts
Expand Up @@ -11,6 +11,7 @@ import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatDialogModule} from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
Expand All @@ -24,6 +25,7 @@ import {SelectDemo} from './select-demo';
FormsModule,
MatButtonModule,
MatCardModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
Expand Down
27 changes: 27 additions & 0 deletions src/dev-app/select/select-demo.html
Expand Up @@ -246,6 +246,33 @@
</mat-card>
</div>

<mat-card>
<mat-card-subtitle>MatSelect inside a dialog</mat-card-subtitle>
<mat-card-content>

<button (click)="openDialogWithSelectInside(dialogTemplate)">Open dialog</button>

<ng-template #dialogTemplate>
<mat-form-field>
<mat-label>Your name</mat-label>
<input matInput>
</mat-form-field>

<mat-form-field>
<mat-label>Select a topping</mat-label>
<mat-select>
<mat-option>Cheese</mat-option>
<mat-option>Onion</mat-option>
<mat-option>Pepper</mat-option>
</mat-select>
</mat-form-field>

<button>Done</button>
</ng-template>

</mat-card-content>
</mat-card>

</div>

<mat-card class="demo-card demo-basic">
Expand Down
15 changes: 11 additions & 4 deletions src/dev-app/select/select-demo.ts
Expand Up @@ -6,9 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Component} from '@angular/core';
import {Component, TemplateRef} from '@angular/core';
import {FormControl, Validators} from '@angular/forms';
import {ErrorStateMatcher, ThemePalette} from '@angular/material/core';
import {MatDialog} from '@angular/material/dialog';
import {FloatLabelType} from '@angular/material/form-field';
import {MatSelectChange} from '@angular/material/select';

Expand All @@ -23,9 +24,9 @@ export class MyErrorStateMatcher implements ErrorStateMatcher {
}

@Component({
selector: 'select-demo',
templateUrl: 'select-demo.html',
styleUrls: ['select-demo.css'],
selector: 'select-demo',
templateUrl: 'select-demo.html',
styleUrls: ['select-demo.css'],
})
export class SelectDemo {
drinksRequired = false;
Expand Down Expand Up @@ -133,6 +134,8 @@ export class SelectDemo {
{value: 'indramon-5', viewValue: 'Indramon'}
];

constructor(private _dialog: MatDialog) {}

toggleDisabled() {
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
}
Expand All @@ -158,4 +161,8 @@ export class SelectDemo {
toggleSelected() {
this.currentAppearanceValue = this.currentAppearanceValue ? null : this.digimon[0].value;
}

openDialogWithSelectInside(dialogTemplate: TemplateRef<unknown>) {
this._dialog.open(dialogTemplate);
}
}
10 changes: 10 additions & 0 deletions src/material-experimental/mdc-select/select.html
@@ -1,4 +1,14 @@
<!--
Note that the select trigger element specifies `aria-owns` pointing to the listbox overlay.
While aria-owns is not required for the ARIA 1.2 `role="combobox"` interaction pattern,
it fixes an issue with VoiceOver when the select appears inside of an `aria-model="true"`
element (e.g. a dialog). Without this `aria-owns`, the `aria-modal` on a dialog prevents
VoiceOver from "seeing" the select's listbox overlay for aria-activedescendant.
Using `aria-owns` re-parents the select overlay so that it works again.
See https://github.com/angular/components/issues/20694
-->
<div cdk-overlay-origin
[attr.aria-owns]="panelOpen ? id + '-panel' : null"
class="mat-mdc-select-trigger"
(click)="toggle()"
#fallbackOverlayOrigin="cdkOverlayOrigin"
Expand Down
13 changes: 13 additions & 0 deletions src/material-experimental/mdc-select/select.spec.ts
Expand Up @@ -196,6 +196,19 @@ describe('MDC-based MatSelect', () => {
expect(ariaControls).toBe(document.querySelector('.mat-mdc-select-panel')!.id);
}));

it('should point the aria-owns attribute to the listbox on the trigger', fakeAsync(() => {
const trigger = select.querySelector('.mat-mdc-select-trigger')!;
expect(trigger.hasAttribute('aria-owns')).toBe(false);

fixture.componentInstance.select.open();
fixture.detectChanges();
flush();

const ariaOwns = trigger.getAttribute('aria-owns');
expect(ariaOwns).toBeTruthy();
expect(ariaOwns).toBe(document.querySelector('.mat-mdc-select-panel')!.id);
}));

it('should set aria-expanded based on the select open state', fakeAsync(() => {
expect(select.getAttribute('aria-expanded')).toBe('false');

Expand Down
10 changes: 10 additions & 0 deletions src/material/select/select.html
@@ -1,4 +1,14 @@
<!--
Note that the select trigger element specifies `aria-owns` pointing to the listbox overlay.
While aria-owns is not required for the ARIA 1.2 `role="combobox"` interaction pattern,
it fixes an issue with VoiceOver when the select appears inside of an `aria-model="true"`
element (e.g. a dialog). Without this `aria-owns`, the `aria-modal` on a dialog prevents
VoiceOver from "seeing" the select's listbox overlay for aria-activedescendant.
Using `aria-owns` re-parents the select overlay so that it works again.
See https://github.com/angular/components/issues/20694
-->
<div cdk-overlay-origin
[attr.aria-owns]="panelOpen ? id + '-panel' : null"
class="mat-select-trigger"
(click)="toggle()"
#origin="cdkOverlayOrigin"
Expand Down
13 changes: 13 additions & 0 deletions src/material/select/select.spec.ts
Expand Up @@ -165,6 +165,19 @@ describe('MatSelect', () => {
expect(ariaControls).toBe(document.querySelector('.mat-select-panel')!.id);
}));

it('should point the aria-owns attribute to the listbox on the trigger', fakeAsync(() => {
const trigger = select.querySelector('.mat-select-trigger')!;
expect(trigger.hasAttribute('aria-owns')).toBe(false);

fixture.componentInstance.select.open();
fixture.detectChanges();
flush();

const ariaOwns = trigger.getAttribute('aria-owns');
expect(ariaOwns).toBeTruthy();
expect(ariaOwns).toBe(document.querySelector('.mat-select-panel')!.id);
}));

it('should set aria-expanded based on the select open state', fakeAsync(() => {
expect(select.getAttribute('aria-expanded')).toBe('false');

Expand Down
2 changes: 1 addition & 1 deletion src/material/select/select.ts
Expand Up @@ -1101,7 +1101,7 @@ export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements A
'role': 'combobox',
'aria-autocomplete': 'none',
// TODO(crisbeto): the value for aria-haspopup should be `listbox`, but currently it's difficult
// to sync into g3, because of an outdated automated a11y check which flags it as an invalid
// to sync into Google, because of an outdated automated a11y check which flags it as an invalid
// value. At some point we should try to switch it back to being `listbox`.
'aria-haspopup': 'true',
'class': 'mat-select',
Expand Down

0 comments on commit 33a43f7

Please sign in to comment.