Skip to content

Commit 3afbff1

Browse files
committed
add touch support to FocusOriginMonitor
1 parent 003a5e7 commit 3afbff1

File tree

3 files changed

+71
-8
lines changed

3 files changed

+71
-8
lines changed

src/demo-app/style/style-demo.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<button (click)="b.focus()">focus programmatically</button>
33

44
<button (click)="fom.focusVia(b, renderer, 'mouse')">focusVia: mouse</button>
5+
<button (click)="fom.focusVia(b, renderer, 'touch')">focusVia: touch</button>
56
<button (click)="fom.focusVia(b, renderer, 'keyboard')">focusVia: keyboard</button>
67
<button (click)="fom.focusVia(b, renderer, 'program')">focusVia: program</button>
78

src/demo-app/style/style-demo.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@
1313
.demo-button.cdk-program-focused {
1414
background: blue;
1515
}
16+
17+
.demo-button.cdk-touch-focused {
18+
background: purple;
19+
}

src/lib/core/style/focus-classes.ts

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import {Observable} from 'rxjs/Observable';
33
import {Subject} from 'rxjs/Subject';
44

55

6-
export type FocusOrigin = 'mouse' | 'keyboard' | 'program';
6+
const TOUCH_BUFFER_MS = 650;
7+
8+
9+
export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program';
710

811

912
/** Monitors mouse and keyboard events to determine the cause of focus events. */
@@ -18,14 +21,59 @@ export class FocusOriginMonitor {
1821
/** Whether the window has just been focused. */
1922
private _windowFocused = false;
2023

24+
/** Whether the current sequence of events was kicked off by a touch event. */
25+
private get _touchActive() { return this._touchActiveBacking; }
26+
private set _touchActive(value: boolean) {
27+
this._touchActiveBacking = value;
28+
29+
if (this._touchActiveBacking) {
30+
// Register a listener to unset the backing variable after a focus followed by a blur.
31+
// This is necessary to avoid accidentally counting a programmatic focus that occurs on a
32+
// touchstart event from being counted as a touch focus.
33+
document.addEventListener('focus', this._touchFocusListener, true);
34+
} else {
35+
// Remove the focus and blur listeners so they don't interfere in the future.
36+
document.removeEventListener('focus', this._touchFocusListener, true);
37+
document.removeEventListener('blur', this._touchBlurListener, true);
38+
}
39+
}
40+
private _touchActiveBacking = false;
41+
42+
/** A focus listener that adds _touchBlurListener listener and then uninstalls itself. */
43+
private _touchFocusListener = () => {
44+
document.addEventListener('blur', this._touchBlurListener, true);
45+
document.removeEventListener('focus', this._touchFocusListener, true);
46+
};
47+
48+
/** Blur listener that unsets the _touchActiveBacking and origin and then uninstalls itself. */
49+
private _touchBlurListener = () => {
50+
this._touchActiveBacking = false;
51+
this._origin = null;
52+
document.removeEventListener('blur', this._touchBlurListener, true);
53+
};
54+
2155
constructor() {
22-
// Listen to keydown and mousedown in the capture phase so we can detect them even if the user
23-
// stops propagation.
24-
// TODO(mmalerba): Figure out how to handle touchstart
25-
document.addEventListener(
26-
'keydown', () => this._setOriginForCurrentEventQueue('keyboard'), true);
27-
document.addEventListener(
28-
'mousedown', () => this._setOriginForCurrentEventQueue('mouse'), true);
56+
// Note: we listen to events in the capture phase so we can detect them even if the user stops
57+
// propagation.
58+
59+
// On keydown record the origin and cancel any touch event that may be in progress.
60+
document.addEventListener('keydown', () => {
61+
this._touchActive = false;
62+
this._setOriginForCurrentEventQueue('keyboard');
63+
}, true);
64+
65+
// On mousedown record the origin, but do not cancel the touch event. A mousedown can happen as
66+
// a result of a touch event.
67+
document.addEventListener('mousedown',
68+
() => this._setOriginForCurrentEventQueue('mouse'), true);
69+
70+
// When the touchstart event fires the focus event is not yet in the event queue. This means we
71+
// can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to see if
72+
// a focus happens.
73+
document.addEventListener('touchstart', () => {
74+
this._touchActive = true;
75+
setTimeout(() => this._touchActive = false, TOUCH_BUFFER_MS);
76+
}, true);
2977

3078
// Make a note of when the window regains focus, so we can restore the origin info for the
3179
// focused element.
@@ -45,6 +93,10 @@ export class FocusOriginMonitor {
4593

4694
/** Focuses the element via the specified focus origin. */
4795
focusVia(element: Node, renderer: Renderer, origin: FocusOrigin) {
96+
// This method may have been called as a result of a touchstart event, so we need to clear
97+
// _touchActive to prevent it from interfering.
98+
this._touchActive = false;
99+
48100
this._setOriginForCurrentEventQueue(origin);
49101
renderer.invokeElementMethod(element, 'focus');
50102
}
@@ -57,6 +109,10 @@ export class FocusOriginMonitor {
57109

58110
/** Handles focus events on a registered element. */
59111
private _onFocus(element: Element, renderer: Renderer, subject: Subject<FocusOrigin>) {
112+
if (this._touchActive) {
113+
this._origin = 'touch';
114+
}
115+
60116
// If we couldn't detect a cause for the focus event, it's due to one of two reasons:
61117
// 1) The window has just regained focus, in which case we want to restore the focused state of
62118
// the element from before the window blurred.
@@ -71,6 +127,7 @@ export class FocusOriginMonitor {
71127
}
72128

73129
renderer.setElementClass(element, 'cdk-focused', true);
130+
renderer.setElementClass(element, 'cdk-touch-focused', this._origin == 'touch');
74131
renderer.setElementClass(element, 'cdk-keyboard-focused', this._origin == 'keyboard');
75132
renderer.setElementClass(element, 'cdk-mouse-focused', this._origin == 'mouse');
76133
renderer.setElementClass(element, 'cdk-program-focused', this._origin == 'program');
@@ -83,6 +140,7 @@ export class FocusOriginMonitor {
83140
/** Handles blur events on a registered element. */
84141
private _onBlur(element: Element, renderer: Renderer, subject: Subject<FocusOrigin>) {
85142
renderer.setElementClass(element, 'cdk-focused', false);
143+
renderer.setElementClass(element, 'cdk-touch-focused', false);
86144
renderer.setElementClass(element, 'cdk-keyboard-focused', false);
87145
renderer.setElementClass(element, 'cdk-mouse-focused', false);
88146
renderer.setElementClass(element, 'cdk-program-focused', false);

0 commit comments

Comments
 (0)