Skip to content

Commit

Permalink
docs(focus-monitor): add documentation (#10547)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba authored and jelbourn committed Apr 6, 2018
1 parent 0272d0b commit c407cc8
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 12 deletions.
76 changes: 64 additions & 12 deletions src/cdk/a11y/a11y.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
The `a11y` package provides a number of tools to improve accessibility, described below.

### ListKeyManager
## ListKeyManager
`ListKeyManager` manages the active option in a list of items based on keyboard interaction.
Intended to be used with components that correspond to a `role="menu"` or `role="listbox"` pattern.

#### Basic usage
### Basic usage
Any component that uses a `ListKeyManager` will generally do three things:
* Create a `@ViewChildren` query for the options being managed.
* Initialize the `ListKeyManager`, passing in the options.
Expand All @@ -18,16 +18,16 @@ interface ListKeyManagerOption {
}
```

#### Wrapping
### Wrapping
Navigation through options can be made to wrap via the `withWrap` method
```ts
this.keyManager = new FocusKeyManager(...).withWrap();
```

#### Types of key managers
### Types of key managers
There are two varieties of `ListKeyManager`, `FocusKeyManager` and `ActiveDescendantKeyManager`.

##### FocusKeyManager
#### FocusKeyManager
Used when options will directly receive browser focus. Each item managed must implement the
`FocusableOption` interface:
```ts
Expand All @@ -36,7 +36,7 @@ interface FocusableOption extends ListKeyManagerOption {
}
```

##### ActiveDescendantKeyManager
#### ActiveDescendantKeyManager
Used when options will be marked as active via `aria-activedescendant`.
Each item managed must implement the
`Highlightable` interface:
Expand All @@ -50,15 +50,15 @@ interface Highlightable extends ListKeyManagerOption {
Each item must also have an ID bound to the listbox's or menu's `aria-activedescendant`.


### FocusTrap
## FocusTrap
The `cdkTrapFocus` directive traps <kbd>Tab</kbd> key focus within an element. This is intended to
be used to create accessible experience for components like
[modal dialogs](https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal), where focus must be
constrained.

This directive is declared in `A11yModule`.

#### Example
### Example
```html
<div class="my-inner-dialog-content" cdkTrapFocus>
<!-- Tab and Shift + Tab will not leave this element. -->
Expand All @@ -68,7 +68,7 @@ This directive is declared in `A11yModule`.
This directive will not prevent focus from moving out of the trapped region due to mouse
interaction.

#### Regions
### Regions
Regions can be declared explicitly with an initial focus element by using
the `cdkFocusRegionStart`, `cdkFocusRegionEnd` and `cdkFocusInitial` DOM attributes.
`cdkFocusInitial` specifies the element that will receive focus upon initialization of the region.
Expand All @@ -85,18 +85,18 @@ For example:
```


### InteractivityChecker
## InteractivityChecker
`InteractivityChecker` is used to check the interactivity of an element, capturing disabled,
visible, tabbable, and focusable states for accessibility purposes. See the API docs for more
details.


### LiveAnnouncer
## LiveAnnouncer
`LiveAnnouncer` is used to announce messages for screen-reader users using an `aria-live` region.
See [the W3C's WAI-ARIA](https://www.w3.org/TR/wai-aria/states_and_properties#aria-live)
for more information on aria-live regions.

#### Example
### Example
```ts
@Component({...})
export class MyComponent {
Expand All @@ -106,3 +106,55 @@ export class MyComponent {
}
}
```

## FocusMonitor
The `FocusMonitor` is an injectable service that can be used to listen for changes in the focus
state of an element. It's more powerful than just listening for `focus` or `blur` events because it
tells you how the element was focused (via mouse, keyboard, touch, or programmatically). It also
allows listening for focus on descendant elements if desired.

To listen for focus changes on an element, use the `monitor` method which takes an element to
monitor and an optional boolean flag `checkChildren`. Passing true for `checkChildren` will tell the
`FocusMonitor` to consider the element focused if any of its descendants are focused. This option
defaults to `false` if not specified. The `monitor` method will return an Observable that emits the
`FocusOrigin` whenever the focus state changes. The `FocusOrigin` will be one of the following:

* `'mouse'` indicates the element was focused with the mouse
* `'keyboard'` indicates the element was focused with the keyboard
* `'touch'` indicates the element was focused by touching on a touchscreen
* `'program'` indicates the element was focused programmatically
* `null` indicates the element was blurred

In addition to emitting on the observable, the `FocusMonitor` will automatically apply CSS classes
to the element when focused. It will add `.cdk-focused` if the element is focused and will further
add `.cdk-${origin}-focused` (with `${origin}` being `mouse`, `keyboard`, `touch`, or `program`) to
indicate how the element was focused.

Note: currently the `FocusMonitor` emits on the observable _outside_ of the Angular zone. Therefore
if you `markForCheck` in the subscription you must put yourself back in the Angular zone.

```ts
focusMonitor.monitor(el).subscribe(origin => this.ngZone.run(() => /* ... */ ));
```

Any element that is monitored by calling `monitor` should eventually be unmonitored by calling
`stopMonitoring` with the same element.

<!-- example(focus-monitor-overview) -->

It is possible to falsify the `FocusOrigin` when setting the focus programmatically by using the
`focusVia` method of `FocusMonitor`. This method accepts an element to focus and the `FocusOrigin`
to use. If the element being focused is currently being monitored by the `FocusMonitor` it will
report the `FocusOrigin` that was passed in. If the element is not currently being monitored it will
just be focused like normal.

<!-- example(focus-monitor-focus-via) -->

### cdkMonitorElementFocus and cdkMonitorSubtreeFocus
For convenience, the CDK also provides two directives that allow for easily monitoring an element.
`cdkMonitorElementFocus` is the equivalent of calling `monitor` on the host element with
`checkChildren` set to `false`. `cdkMonitorSubtreeFocus` is the equivalent of calling `monitor` on
the host element with `checkChildren` set to `true`. Each of these directives has an `@Output()`
`cdkFocusChange` that will emit the new `FocusOrigin` whenever it changes.

<!-- example(focus-monitor-directives) -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.example-focus-monitor {
padding: 20px;
}

.example-focus-monitor .cdk-mouse-focused {
background: rgba(255, 0, 0, 0.5);
}

.example-focus-monitor .cdk-keyboard-focused {
background: rgba(0, 255, 0, 0.5);
}

.example-focus-monitor .cdk-touch-focused {
background: rgba(0, 0, 255, 0.5);
}

.example-focus-monitor .cdk-program-focused {
background: rgba(255, 0, 255, 0.5);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="example-focus-monitor">
<button cdkMonitorSubtreeFocus
(cdkFocusChange)="elementOrigin = formatOrigin($event); markForCheck()">
Focus Monitored Element ({{elementOrigin}})
</button>
</div>

<div class="example-focus-monitor">
<div cdkMonitorSubtreeFocus
(cdkFocusChange)="subtreeOrigin = formatOrigin($event); markForCheck()">
<p>Focus Monitored Subtree ({{subtreeOrigin}})</p>
<button>Child Button 1</button>
<button>Child Button 2</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {FocusOrigin} from '@angular/cdk/a11y';
import {ChangeDetectorRef, Component, NgZone} from '@angular/core';

/** @title Monitoring focus with FocusMonitor */
@Component({
selector: 'focus-monitor-directives-example',
templateUrl: 'focus-monitor-directives-example.html',
styleUrls: ['focus-monitor-directives-example.css']
})
export class FocusMonitorDirectivesExample {
elementOrigin: string = this.formatOrigin(null);
subtreeOrigin: string = this.formatOrigin(null);

constructor(private ngZone: NgZone, private cdr: ChangeDetectorRef) {}


formatOrigin(origin: FocusOrigin): string {
return origin ? origin + ' focused' : 'blurred';
}

// Workaround for the fact that (cdkFocusChange) emits outside NgZone.
markForCheck() {
this.ngZone.run(() => this.cdr.markForCheck());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.example-focus-monitor {
padding: 20px;
}

.example-focus-monitor .cdk-mouse-focused {
background: rgba(255, 0, 0, 0.5);
}

.example-focus-monitor .cdk-keyboard-focused {
background: rgba(0, 255, 0, 0.5);
}

.example-focus-monitor .cdk-touch-focused {
background: rgba(0, 0, 255, 0.5);
}

.example-focus-monitor .cdk-program-focused {
background: rgba(255, 0, 255, 0.5);
}

.example-focus-monitor button:focus {
box-shadow: 0 0 30px cyan;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div class="example-focus-monitor">
<button #monitored>1. Focus Monitored Element ({{origin}})</button>
<button #unmonitored>2. Not Monitored</button>
</div>

<mat-form-field>
<mat-label>Simulated focus origin</mat-label>
<mat-select #simulatedOrigin value="mouse">
<mat-option value="mouse">Mouse</mat-option>
<mat-option value="keyboard">Keyboard</mat-option>
<mat-option value="touch">Touch</mat-option>
<mat-option value="program">Programmatic</mat-option>
</mat-select>
</mat-form-field>

<button (click)="focusMonitor.focusVia(monitored, simulatedOrigin.value)">
Focus button #1
</button>
<button (click)="focusMonitor.focusVia(unmonitored, simulatedOrigin.value)">
Focus button #2
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
import {
ChangeDetectorRef,
Component,
ElementRef,
NgZone,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';

/** @title Focusing with a specific FocusOrigin */
@Component({
selector: 'focus-monitor-focus-via-example',
templateUrl: 'focus-monitor-focus-via-example.html',
styleUrls: ['focus-monitor-focus-via-example.css']
})
export class FocusMonitorFocusViaExample implements OnDestroy, OnInit {
@ViewChild('monitored') monitoredEl: ElementRef;

origin: string = this.formatOrigin(null);

constructor(public focusMonitor: FocusMonitor,
private cdr: ChangeDetectorRef,
private ngZone: NgZone) {}

ngOnInit() {
this.focusMonitor.monitor(this.monitoredEl.nativeElement)
.subscribe(origin => this.ngZone.run(() => {
this.origin = this.formatOrigin(origin);
this.cdr.markForCheck();
}));
}

ngOnDestroy() {
this.focusMonitor.stopMonitoring(this.monitoredEl.nativeElement);
}

formatOrigin(origin: FocusOrigin): string {
return origin ? origin + ' focused' : 'blurred';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.example-focus-monitor {
padding: 20px;
}

.example-focus-monitor .cdk-mouse-focused {
background: rgba(255, 0, 0, 0.5);
}

.example-focus-monitor .cdk-keyboard-focused {
background: rgba(0, 255, 0, 0.5);
}

.example-focus-monitor .cdk-touch-focused {
background: rgba(0, 0, 255, 0.5);
}

.example-focus-monitor .cdk-program-focused {
background: rgba(255, 0, 255, 0.5);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class="example-focus-monitor">
<button #element>Focus Monitored Element ({{elementOrigin}})</button>
</div>

<div class="example-focus-monitor">
<div #subtree>
<p>Focus Monitored Subtree ({{subtreeOrigin}})</p>
<button>Child Button 1</button>
<button>Child Button 2</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
import {
ChangeDetectorRef,
Component,
ElementRef,
NgZone,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';

/** @title Monitoring focus with FocusMonitor */
@Component({
selector: 'focus-monitor-overview-example',
templateUrl: 'focus-monitor-overview-example.html',
styleUrls: ['focus-monitor-overview-example.css']
})
export class FocusMonitorOverviewExample implements OnDestroy, OnInit {
@ViewChild('element') element: ElementRef;
@ViewChild('subtree') subtree: ElementRef;

elementOrigin: string = this.formatOrigin(null);
subtreeOrigin: string = this.formatOrigin(null);

constructor(private focusMonitor: FocusMonitor,
private cdr: ChangeDetectorRef,
private ngZone: NgZone) {}

ngOnInit() {
this.focusMonitor.monitor(this.element.nativeElement)
.subscribe(origin => this.ngZone.run(() => {
this.elementOrigin = this.formatOrigin(origin);
this.cdr.markForCheck();
}));
this.focusMonitor.monitor(this.subtree.nativeElement, true)
.subscribe(origin => this.ngZone.run(() => {
this.subtreeOrigin = this.formatOrigin(origin);
this.cdr.markForCheck();
}));
}

ngOnDestroy() {
this.focusMonitor.stopMonitoring(this.element.nativeElement);
this.focusMonitor.stopMonitoring(this.subtree.nativeElement);
}

formatOrigin(origin: FocusOrigin): string {
return origin ? origin + ' focused' : 'blurred';
}
}

0 comments on commit c407cc8

Please sign in to comment.