Skip to content

Commit c407cc8

Browse files
mmalerbajelbourn
authored andcommitted
docs(focus-monitor): add documentation (#10547)
1 parent 0272d0b commit c407cc8

10 files changed

+289
-12
lines changed

src/cdk/a11y/a11y.md

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
The `a11y` package provides a number of tools to improve accessibility, described below.
22

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

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

21-
#### Wrapping
21+
### Wrapping
2222
Navigation through options can be made to wrap via the `withWrap` method
2323
```ts
2424
this.keyManager = new FocusKeyManager(...).withWrap();
2525
```
2626

27-
#### Types of key managers
27+
### Types of key managers
2828
There are two varieties of `ListKeyManager`, `FocusKeyManager` and `ActiveDescendantKeyManager`.
2929

30-
##### FocusKeyManager
30+
#### FocusKeyManager
3131
Used when options will directly receive browser focus. Each item managed must implement the
3232
`FocusableOption` interface:
3333
```ts
@@ -36,7 +36,7 @@ interface FocusableOption extends ListKeyManagerOption {
3636
}
3737
```
3838

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

5252

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

5959
This directive is declared in `A11yModule`.
6060

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

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

8787

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

9393

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

99-
#### Example
99+
### Example
100100
```ts
101101
@Component({...})
102102
export class MyComponent {
@@ -106,3 +106,55 @@ export class MyComponent {
106106
}
107107
}
108108
```
109+
110+
## FocusMonitor
111+
The `FocusMonitor` is an injectable service that can be used to listen for changes in the focus
112+
state of an element. It's more powerful than just listening for `focus` or `blur` events because it
113+
tells you how the element was focused (via mouse, keyboard, touch, or programmatically). It also
114+
allows listening for focus on descendant elements if desired.
115+
116+
To listen for focus changes on an element, use the `monitor` method which takes an element to
117+
monitor and an optional boolean flag `checkChildren`. Passing true for `checkChildren` will tell the
118+
`FocusMonitor` to consider the element focused if any of its descendants are focused. This option
119+
defaults to `false` if not specified. The `monitor` method will return an Observable that emits the
120+
`FocusOrigin` whenever the focus state changes. The `FocusOrigin` will be one of the following:
121+
122+
* `'mouse'` indicates the element was focused with the mouse
123+
* `'keyboard'` indicates the element was focused with the keyboard
124+
* `'touch'` indicates the element was focused by touching on a touchscreen
125+
* `'program'` indicates the element was focused programmatically
126+
* `null` indicates the element was blurred
127+
128+
In addition to emitting on the observable, the `FocusMonitor` will automatically apply CSS classes
129+
to the element when focused. It will add `.cdk-focused` if the element is focused and will further
130+
add `.cdk-${origin}-focused` (with `${origin}` being `mouse`, `keyboard`, `touch`, or `program`) to
131+
indicate how the element was focused.
132+
133+
Note: currently the `FocusMonitor` emits on the observable _outside_ of the Angular zone. Therefore
134+
if you `markForCheck` in the subscription you must put yourself back in the Angular zone.
135+
136+
```ts
137+
focusMonitor.monitor(el).subscribe(origin => this.ngZone.run(() => /* ... */ ));
138+
```
139+
140+
Any element that is monitored by calling `monitor` should eventually be unmonitored by calling
141+
`stopMonitoring` with the same element.
142+
143+
<!-- example(focus-monitor-overview) -->
144+
145+
It is possible to falsify the `FocusOrigin` when setting the focus programmatically by using the
146+
`focusVia` method of `FocusMonitor`. This method accepts an element to focus and the `FocusOrigin`
147+
to use. If the element being focused is currently being monitored by the `FocusMonitor` it will
148+
report the `FocusOrigin` that was passed in. If the element is not currently being monitored it will
149+
just be focused like normal.
150+
151+
<!-- example(focus-monitor-focus-via) -->
152+
153+
### cdkMonitorElementFocus and cdkMonitorSubtreeFocus
154+
For convenience, the CDK also provides two directives that allow for easily monitoring an element.
155+
`cdkMonitorElementFocus` is the equivalent of calling `monitor` on the host element with
156+
`checkChildren` set to `false`. `cdkMonitorSubtreeFocus` is the equivalent of calling `monitor` on
157+
the host element with `checkChildren` set to `true`. Each of these directives has an `@Output()`
158+
`cdkFocusChange` that will emit the new `FocusOrigin` whenever it changes.
159+
160+
<!-- example(focus-monitor-directives) -->
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.example-focus-monitor {
2+
padding: 20px;
3+
}
4+
5+
.example-focus-monitor .cdk-mouse-focused {
6+
background: rgba(255, 0, 0, 0.5);
7+
}
8+
9+
.example-focus-monitor .cdk-keyboard-focused {
10+
background: rgba(0, 255, 0, 0.5);
11+
}
12+
13+
.example-focus-monitor .cdk-touch-focused {
14+
background: rgba(0, 0, 255, 0.5);
15+
}
16+
17+
.example-focus-monitor .cdk-program-focused {
18+
background: rgba(255, 0, 255, 0.5);
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div class="example-focus-monitor">
2+
<button cdkMonitorSubtreeFocus
3+
(cdkFocusChange)="elementOrigin = formatOrigin($event); markForCheck()">
4+
Focus Monitored Element ({{elementOrigin}})
5+
</button>
6+
</div>
7+
8+
<div class="example-focus-monitor">
9+
<div cdkMonitorSubtreeFocus
10+
(cdkFocusChange)="subtreeOrigin = formatOrigin($event); markForCheck()">
11+
<p>Focus Monitored Subtree ({{subtreeOrigin}})</p>
12+
<button>Child Button 1</button>
13+
<button>Child Button 2</button>
14+
</div>
15+
</div>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {FocusOrigin} from '@angular/cdk/a11y';
2+
import {ChangeDetectorRef, Component, NgZone} from '@angular/core';
3+
4+
/** @title Monitoring focus with FocusMonitor */
5+
@Component({
6+
selector: 'focus-monitor-directives-example',
7+
templateUrl: 'focus-monitor-directives-example.html',
8+
styleUrls: ['focus-monitor-directives-example.css']
9+
})
10+
export class FocusMonitorDirectivesExample {
11+
elementOrigin: string = this.formatOrigin(null);
12+
subtreeOrigin: string = this.formatOrigin(null);
13+
14+
constructor(private ngZone: NgZone, private cdr: ChangeDetectorRef) {}
15+
16+
17+
formatOrigin(origin: FocusOrigin): string {
18+
return origin ? origin + ' focused' : 'blurred';
19+
}
20+
21+
// Workaround for the fact that (cdkFocusChange) emits outside NgZone.
22+
markForCheck() {
23+
this.ngZone.run(() => this.cdr.markForCheck());
24+
}
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.example-focus-monitor {
2+
padding: 20px;
3+
}
4+
5+
.example-focus-monitor .cdk-mouse-focused {
6+
background: rgba(255, 0, 0, 0.5);
7+
}
8+
9+
.example-focus-monitor .cdk-keyboard-focused {
10+
background: rgba(0, 255, 0, 0.5);
11+
}
12+
13+
.example-focus-monitor .cdk-touch-focused {
14+
background: rgba(0, 0, 255, 0.5);
15+
}
16+
17+
.example-focus-monitor .cdk-program-focused {
18+
background: rgba(255, 0, 255, 0.5);
19+
}
20+
21+
.example-focus-monitor button:focus {
22+
box-shadow: 0 0 30px cyan;
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<div class="example-focus-monitor">
2+
<button #monitored>1. Focus Monitored Element ({{origin}})</button>
3+
<button #unmonitored>2. Not Monitored</button>
4+
</div>
5+
6+
<mat-form-field>
7+
<mat-label>Simulated focus origin</mat-label>
8+
<mat-select #simulatedOrigin value="mouse">
9+
<mat-option value="mouse">Mouse</mat-option>
10+
<mat-option value="keyboard">Keyboard</mat-option>
11+
<mat-option value="touch">Touch</mat-option>
12+
<mat-option value="program">Programmatic</mat-option>
13+
</mat-select>
14+
</mat-form-field>
15+
16+
<button (click)="focusMonitor.focusVia(monitored, simulatedOrigin.value)">
17+
Focus button #1
18+
</button>
19+
<button (click)="focusMonitor.focusVia(unmonitored, simulatedOrigin.value)">
20+
Focus button #2
21+
</button>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
2+
import {
3+
ChangeDetectorRef,
4+
Component,
5+
ElementRef,
6+
NgZone,
7+
OnDestroy,
8+
OnInit,
9+
ViewChild
10+
} from '@angular/core';
11+
12+
/** @title Focusing with a specific FocusOrigin */
13+
@Component({
14+
selector: 'focus-monitor-focus-via-example',
15+
templateUrl: 'focus-monitor-focus-via-example.html',
16+
styleUrls: ['focus-monitor-focus-via-example.css']
17+
})
18+
export class FocusMonitorFocusViaExample implements OnDestroy, OnInit {
19+
@ViewChild('monitored') monitoredEl: ElementRef;
20+
21+
origin: string = this.formatOrigin(null);
22+
23+
constructor(public focusMonitor: FocusMonitor,
24+
private cdr: ChangeDetectorRef,
25+
private ngZone: NgZone) {}
26+
27+
ngOnInit() {
28+
this.focusMonitor.monitor(this.monitoredEl.nativeElement)
29+
.subscribe(origin => this.ngZone.run(() => {
30+
this.origin = this.formatOrigin(origin);
31+
this.cdr.markForCheck();
32+
}));
33+
}
34+
35+
ngOnDestroy() {
36+
this.focusMonitor.stopMonitoring(this.monitoredEl.nativeElement);
37+
}
38+
39+
formatOrigin(origin: FocusOrigin): string {
40+
return origin ? origin + ' focused' : 'blurred';
41+
}
42+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.example-focus-monitor {
2+
padding: 20px;
3+
}
4+
5+
.example-focus-monitor .cdk-mouse-focused {
6+
background: rgba(255, 0, 0, 0.5);
7+
}
8+
9+
.example-focus-monitor .cdk-keyboard-focused {
10+
background: rgba(0, 255, 0, 0.5);
11+
}
12+
13+
.example-focus-monitor .cdk-touch-focused {
14+
background: rgba(0, 0, 255, 0.5);
15+
}
16+
17+
.example-focus-monitor .cdk-program-focused {
18+
background: rgba(255, 0, 255, 0.5);
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class="example-focus-monitor">
2+
<button #element>Focus Monitored Element ({{elementOrigin}})</button>
3+
</div>
4+
5+
<div class="example-focus-monitor">
6+
<div #subtree>
7+
<p>Focus Monitored Subtree ({{subtreeOrigin}})</p>
8+
<button>Child Button 1</button>
9+
<button>Child Button 2</button>
10+
</div>
11+
</div>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
2+
import {
3+
ChangeDetectorRef,
4+
Component,
5+
ElementRef,
6+
NgZone,
7+
OnDestroy,
8+
OnInit,
9+
ViewChild
10+
} from '@angular/core';
11+
12+
/** @title Monitoring focus with FocusMonitor */
13+
@Component({
14+
selector: 'focus-monitor-overview-example',
15+
templateUrl: 'focus-monitor-overview-example.html',
16+
styleUrls: ['focus-monitor-overview-example.css']
17+
})
18+
export class FocusMonitorOverviewExample implements OnDestroy, OnInit {
19+
@ViewChild('element') element: ElementRef;
20+
@ViewChild('subtree') subtree: ElementRef;
21+
22+
elementOrigin: string = this.formatOrigin(null);
23+
subtreeOrigin: string = this.formatOrigin(null);
24+
25+
constructor(private focusMonitor: FocusMonitor,
26+
private cdr: ChangeDetectorRef,
27+
private ngZone: NgZone) {}
28+
29+
ngOnInit() {
30+
this.focusMonitor.monitor(this.element.nativeElement)
31+
.subscribe(origin => this.ngZone.run(() => {
32+
this.elementOrigin = this.formatOrigin(origin);
33+
this.cdr.markForCheck();
34+
}));
35+
this.focusMonitor.monitor(this.subtree.nativeElement, true)
36+
.subscribe(origin => this.ngZone.run(() => {
37+
this.subtreeOrigin = this.formatOrigin(origin);
38+
this.cdr.markForCheck();
39+
}));
40+
}
41+
42+
ngOnDestroy() {
43+
this.focusMonitor.stopMonitoring(this.element.nativeElement);
44+
this.focusMonitor.stopMonitoring(this.subtree.nativeElement);
45+
}
46+
47+
formatOrigin(origin: FocusOrigin): string {
48+
return origin ? origin + ' focused' : 'blurred';
49+
}
50+
}

0 commit comments

Comments
 (0)