Skip to content

Commit db1fafe

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 e9797be commit db1fafe

File tree

5 files changed

+61
-17
lines changed

5 files changed

+61
-17
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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<div [formGroup]="parts" class="example-tel-input-container">
1+
<div role="group" class="example-tel-input-container" [formGroup]="parts"
2+
[attr.aria-labelledby]="_formField?.getLabelId()"
3+
[attr.aria-describedby]="describedBy">
24
<input class="example-tel-input-element" formControlName="area" size="3"
35
aria-label="Area code" (input)="_handleInput()">
46
<span class="example-tel-input-spacer">&ndash;</span>

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<div [formGroup]="parts" class="example-tel-input-container">
1+
<div role="group" class="example-tel-input-container" [formGroup]="parts"
2+
[attr.aria-labelledby]="_formField?.getLabelId()"
3+
[attr.aria-describedby]="describedBy">
24
<input class="example-tel-input-element" formControlName="area" size="3" aria-label="Area code" (input)="_handleInput()">
35
<span class="example-tel-input-spacer">&ndash;</span>
46
<input class="example-tel-input-element" formControlName="exchange" size="3" aria-label="Exchange code" (input)="_handleInput()">

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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/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 {MAT_FORM_FIELD, MatFormField, MatFormFieldControl} from '@angular/material/form-field';
66
import {Subject} from 'rxjs';
77

88
/** @title Form field with custom telephone number input control. */
@@ -27,7 +27,6 @@ export class MyTel {
2727
host: {
2828
'[class.example-floating]': 'shouldLabelFloat',
2929
'[id]': 'id',
30-
'[attr.aria-describedby]': 'describedBy',
3130
}
3231
})
3332
export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
@@ -94,6 +93,7 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
9493
formBuilder: FormBuilder,
9594
private _focusMonitor: FocusMonitor,
9695
private _elementRef: ElementRef<HTMLElement>,
96+
@Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField,
9797
@Optional() @Self() public ngControl: NgControl) {
9898

9999
this.parts = formBuilder.group({

0 commit comments

Comments
 (0)