Skip to content

Commit

Permalink
docs: improve accessibility for custom form-field control guide
Browse files Browse the repository at this point in the history
Improves the accessibility for the control that is built as part
of the custom form-field control guide.
  • Loading branch information
devversion committed Feb 26, 2020
1 parent cd71944 commit c5fa00e
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 13 deletions.
53 changes: 46 additions & 7 deletions guides/creating-a-custom-form-field-control.md
Expand Up @@ -23,7 +23,7 @@ class MyTel {
@Component({
selector: 'example-tel-input',
template: `
<div [formGroup]="parts">
<div role="group" [formGroup]="parts">
<input class="area" formControlName="area" maxlength="3">
<span>&ndash;</span>
<input class="exchange" formControlName="exchange" maxlength="3">
Expand All @@ -45,7 +45,7 @@ class MyTel {
}
`],
})
class MyTelInput {
export class MyTelInput {
parts: FormGroup;

@Input()
Expand Down Expand Up @@ -85,7 +85,7 @@ a provider to our component so that the form field will be able to inject it as
...
providers: [{provide: MatFormFieldControl, useExisting: MyTelInput}],
})
class MyTelInput implements MatFormFieldControl<MyTel> {
export class MyTelInput implements MatFormFieldControl<MyTel> {
...
}
```
Expand Down Expand Up @@ -201,7 +201,7 @@ To resolve this, remove the `NG_VALUE_ACCESSOR` provider and instead set the val
// },
],
})
class MyTelInput implements MatFormFieldControl<MyTel> {
export class MyTelInput implements MatFormFieldControl<MyTel> {
constructor(
...,
@Optional() @Self() public ngControl: NgControl,
Expand Down Expand Up @@ -341,16 +341,20 @@ controlType = 'example-tel-input';

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

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

```ts
setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}
```

```html
<div role="group" [formGroup]="parts" [attr.aria-describedby]="describedBy">
```

#### `onContainerClick(event: MouseEvent)`

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

### Improving accessibility

Our custom form field control consists of multiple inputs that describe segments of a phone
number. For accessibility purposes, we put those inputs as part of a `div` element with
`role="group"`. This ensures that screen reader users can tell that all those inputs belong
together.

One significant information is missing for screen reader users though. They won't be able
to tell what this input group represents. To improve this, we should add a label for the group
element using either `aria-label` or `aria-labelledby`.

It's recommended to link the group to the label that is displayed as part of the parent
`<mat-form-field>`. This ensures that explicitly specified labels (using `<mat-label>`) are
actually used for labelling the control.

In our concrete example, we add an attribute binding for `aria-labelledby` and bind it
to the label element id provided by the parent `<mat-form-field>`.

```typescript
export class MyTelInput implements MatFormFieldControl<MyTel> {
...

constructor(...
@Optional() public parentFormField: MatFormField) {
```
```html
@Component({
selector: 'example-tel-input',
template: `
<div role="group" [formGroup]="parts"
[attr.aria-describedby]="describedBy"
[attr.aria-labelledby]="parentFormField?.getLabelId()">
```

### Trying it out

Now that we've fully implemented the interface, we're ready to try our component out! All we need to
Expand Down
@@ -1,4 +1,6 @@
<div [formGroup]="parts" class="example-tel-input-container">
<div role="group" class="example-tel-input-container" [formGroup]="parts"
[attr.aria-labelledby]="_formField?.getLabelId()"
[attr.aria-describedby]="describedBy">
<input class="example-tel-input-element" formControlName="area" size="3"
aria-label="Area code" (input)="_handleInput()">
<span class="example-tel-input-spacer">&ndash;</span>
Expand Down
Expand Up @@ -2,7 +2,7 @@ import {FocusMonitor} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Component, ElementRef, Input, OnDestroy, Optional, Self} from '@angular/core';
import {FormBuilder, FormGroup, ControlValueAccessor, NgControl} from '@angular/forms';
import {MatFormFieldControl} from '@angular/material-experimental/mdc-form-field';
import {MatFormField, MatFormFieldControl} from '@angular/material-experimental/mdc-form-field';
import {Subject} from 'rxjs';

/** @title Form field with custom telephone number input control. */
Expand All @@ -27,7 +27,6 @@ export class MyTel {
host: {
'[class.example-floating]': 'shouldLabelFloat',
'[id]': 'id',
'[attr.aria-describedby]': 'describedBy',
}
})
export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
Expand Down Expand Up @@ -94,6 +93,7 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
formBuilder: FormBuilder,
private _focusMonitor: FocusMonitor,
private _elementRef: ElementRef<HTMLElement>,
@Optional() public _formField: MatFormField,
@Optional() @Self() public ngControl: NgControl) {

this.parts = formBuilder.group({
Expand Down
@@ -1,4 +1,6 @@
<div [formGroup]="parts" class="example-tel-input-container">
<div role="group" class="example-tel-input-container" [formGroup]="parts"
[attr.aria-labelledby]="_formField?.getLabelId()"
[attr.aria-describedby]="describedBy">
<input class="example-tel-input-element" formControlName="area" size="3" aria-label="Area code" (input)="_handleInput()">
<span class="example-tel-input-spacer">&ndash;</span>
<input class="example-tel-input-element" formControlName="exchange" size="3" aria-label="Exchange code" (input)="_handleInput()">
Expand Down
Expand Up @@ -2,7 +2,7 @@ import {FocusMonitor} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Component, ElementRef, Input, OnDestroy, Optional, Self} from '@angular/core';
import {FormBuilder, FormGroup, ControlValueAccessor, NgControl} from '@angular/forms';
import {MatFormFieldControl} from '@angular/material/form-field';
import {MatFormField, MatFormFieldControl} from '@angular/material/form-field';
import {Subject} from 'rxjs';

/** @title Form field with custom telephone number input control. */
Expand All @@ -27,7 +27,6 @@ export class MyTel {
host: {
'[class.example-floating]': 'shouldLabelFloat',
'[id]': 'id',
'[attr.aria-describedby]': 'describedBy',
}
})
export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
Expand Down Expand Up @@ -94,6 +93,7 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
formBuilder: FormBuilder,
private _focusMonitor: FocusMonitor,
private _elementRef: ElementRef<HTMLElement>,
@Optional() public _formField: MatFormField,
@Optional() @Self() public ngControl: NgControl) {

this.parts = formBuilder.group({
Expand Down

0 comments on commit c5fa00e

Please sign in to comment.