Skip to content

Commit 83d363e

Browse files
committed
docs: improve accessibility for custom form-field control guide
Improves the accessibility for the control that is built as part of the custom form-field control guide.
1 parent 617ca87 commit 83d363e

File tree

5 files changed

+68
-21
lines changed

5 files changed

+68
-21
lines changed

guides/creating-a-custom-form-field-control.md

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class MyTel {
2323
@Component({
2424
selector: 'example-tel-input',
2525
template: `
26-
<div [formGroup]="parts">
26+
<div role="group" [formGroup]="parts">
2727
<input class="area" formControlName="area" maxlength="3">
2828
<span>&ndash;</span>
2929
<input class="exchange" formControlName="exchange" maxlength="3">
@@ -45,7 +45,7 @@ class MyTel {
4545
}
4646
`],
4747
})
48-
class MyTelInput {
48+
export class MyTelInput {
4949
parts: FormGroup;
5050

5151
@Input()
@@ -85,7 +85,7 @@ a provider to our component so that the form field will be able to inject it as
8585
...
8686
providers: [{provide: MatFormFieldControl, useExisting: MyTelInput}],
8787
})
88-
class MyTelInput implements MatFormFieldControl<MyTel> {
88+
export class MyTelInput implements MatFormFieldControl<MyTel> {
8989
...
9090
}
9191
```
@@ -201,7 +201,7 @@ To resolve this, remove the `NG_VALUE_ACCESSOR` provider and instead set the val
201201
// },
202202
],
203203
})
204-
class MyTelInput implements MatFormFieldControl<MyTel> {
204+
export class MyTelInput implements MatFormFieldControl<MyTel> {
205205
constructor(
206206
...,
207207
@Optional() @Self() public ngControl: NgControl,
@@ -341,16 +341,20 @@ controlType = 'example-tel-input';
341341

342342
This method is used by the `<mat-form-field>` to specify the IDs that should be used for the
343343
`aria-describedby` attribute of your component. The method has one parameter, the list of IDs, we
344-
just need to apply the given IDs to our host element.
344+
just need to apply the given IDs to the element that represents our control.
345345

346-
```ts
347-
@HostBinding('attr.aria-describedby') describedBy = '';
346+
In our concrete example, these IDs would need to be applied to the group element.
348347

348+
```ts
349349
setDescribedByIds(ids: string[]) {
350350
this.describedBy = ids.join(' ');
351351
}
352352
```
353353

354+
```html
355+
<div role="group" [formGroup]="parts" [attr.aria-describedby]="describedBy">
356+
```
357+
354358
#### `onContainerClick(event: MouseEvent)`
355359

356360
This method will be called when the form field is clicked on. It allows your component to hook in
@@ -366,6 +370,41 @@ onContainerClick(event: MouseEvent) {
366370
}
367371
```
368372

373+
### Improving accessibility
374+
375+
Our custom form field control consists of multiple inputs that describe segments of a phone
376+
number. For accessibility purposes, we put those inputs as part of a `div` element with
377+
`role="group"`. This ensures that screen reader users can tell that all those inputs belong
378+
together.
379+
380+
One significant piece of information is missing for screen reader users though. They won't be able
381+
to tell what this input group represents. To improve this, we should add a label for the group
382+
element using either `aria-label` or `aria-labelledby`.
383+
384+
It's recommended to link the group to the label that is displayed as part of the parent
385+
`<mat-form-field>`. This ensures that explicitly specified labels (using `<mat-label>`) are
386+
actually used for labelling the control.
387+
388+
In our concrete example, we add an attribute binding for `aria-labelledby` and bind it
389+
to the label element id provided by the parent `<mat-form-field>`.
390+
391+
```typescript
392+
export class MyTelInput implements MatFormFieldControl<MyTel> {
393+
...
394+
395+
constructor(...
396+
@Optional() public parentFormField: MatFormField) {
397+
```
398+
399+
```html
400+
@Component({
401+
selector: 'example-tel-input',
402+
template: `
403+
<div role="group" [formGroup]="parts"
404+
[attr.aria-describedby]="describedBy"
405+
[attr.aria-labelledby]="parentFormField?.getLabelId()">
406+
```
407+
369408
### Trying it out
370409

371410
Now that we've fully implemented the interface, we're ready to try our component out! All we need to

src/components-examples/material-experimental/mdc-form-field/mdc-form-field-custom-control/example-tel-input-example.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
<div [formGroup]="parts" class="example-tel-input-container">
1+
<div role="group" class="example-tel-input-container"
2+
[formGroup]="parts"
3+
[attr.aria-labelledby]="_formField?.getLabelId()"
4+
[attr.aria-describedby]="describedBy">
25
<input
36
class="example-tel-input-element"
47
formControlName="area" size="3"

src/components-examples/material-experimental/mdc-form-field/mdc-form-field-custom-control/form-field-custom-control-example.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {FocusMonitor} from '@angular/cdk/a11y';
22
import {coerceBooleanProperty} from '@angular/cdk/coercion';
3-
import {Component, ElementRef, Input, OnDestroy, Optional, Self} from '@angular/core';
4-
import {FormBuilder, FormGroup, ControlValueAccessor, NgControl, Validators} from '@angular/forms';
5-
import {MatFormFieldControl} from '@angular/material-experimental/mdc-form-field';
3+
import {Component, ElementRef, Inject, Input, OnDestroy, Optional, Self} from '@angular/core';
4+
import {ControlValueAccessor, FormBuilder, FormGroup, NgControl, Validators} from '@angular/forms';
5+
import {MatFormField, MatFormFieldControl} from '@angular/material-experimental/mdc-form-field';
6+
import {MAT_FORM_FIELD} from '@angular/material/form-field';
67
import {Subject} from 'rxjs';
78

89
/** @title Form field with custom telephone number input control. */
@@ -27,7 +28,6 @@ export class MyTel {
2728
host: {
2829
'[class.example-floating]': 'shouldLabelFloat',
2930
'[id]': 'id',
30-
'[attr.aria-describedby]': 'describedBy',
3131
}
3232
})
3333
export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
@@ -94,6 +94,7 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
9494
formBuilder: FormBuilder,
9595
private _focusMonitor: FocusMonitor,
9696
private _elementRef: ElementRef<HTMLElement>,
97+
@Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField,
9798
@Optional() @Self() public ngControl: NgControl) {
9899

99100
this.parts = formBuilder.group({

src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
<div [formGroup]="parts" class="example-tel-input-container">
1+
<div role="group" class="example-tel-input-container"
2+
[formGroup]="parts"
3+
[attr.aria-labelledby]="_formField?.getLabelId()"
4+
[attr.aria-describedby]="describedBy">
25
<input class="example-tel-input-element"
36
formControlName="area" size="3"
47
maxLength="3"

src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,23 @@ import {coerceBooleanProperty} from '@angular/cdk/coercion';
33
import {
44
Component,
55
ElementRef,
6+
Inject,
67
Input,
78
OnDestroy,
89
Optional,
910
Self,
1011
ViewChild
1112
} from '@angular/core';
1213
import {
14+
AbstractControl,
15+
ControlValueAccessor,
1316
FormBuilder,
17+
FormControl,
1418
FormGroup,
15-
ControlValueAccessor,
1619
NgControl,
17-
Validators,
18-
FormControl,
19-
AbstractControl
20+
Validators
2021
} from '@angular/forms';
21-
import {MatFormFieldControl} from '@angular/material/form-field';
22+
import {MAT_FORM_FIELD, MatFormField, MatFormFieldControl} from '@angular/material/form-field';
2223
import {Subject} from 'rxjs';
2324

2425
/** @title Form field with custom telephone number input control. */
@@ -51,7 +52,6 @@ export class MyTel {
5152
host: {
5253
'[class.example-floating]': 'shouldLabelFloat',
5354
'[id]': 'id',
54-
'[attr.aria-describedby]': 'describedBy'
5555
}
5656
})
5757
export class MyTelInput
@@ -134,8 +134,9 @@ export class MyTelInput
134134
formBuilder: FormBuilder,
135135
private _focusMonitor: FocusMonitor,
136136
private _elementRef: ElementRef<HTMLElement>,
137-
@Optional() @Self() public ngControl: NgControl
138-
) {
137+
@Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField,
138+
@Optional() @Self() public ngControl: NgControl) {
139+
139140
this.parts = formBuilder.group({
140141
area: [
141142
null,

0 commit comments

Comments
 (0)