@@ -3,7 +3,12 @@ import {Observable} from 'rxjs/Observable';
3
3
import { Subject } from 'rxjs/Subject' ;
4
4
5
5
6
- export type FocusOrigin = 'mouse' | 'keyboard' | 'program' ;
6
+ // This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
7
+ // that a value of around 650ms seems appropriate.
8
+ export const TOUCH_BUFFER_MS = 650 ;
9
+
10
+
11
+ export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' ;
7
12
8
13
9
14
/** Monitors mouse and keyboard events to determine the cause of focus events. */
@@ -18,14 +23,40 @@ export class FocusOriginMonitor {
18
23
/** Whether the window has just been focused. */
19
24
private _windowFocused = false ;
20
25
26
+ /** The target of the last touch event. */
27
+ private _lastTouchTarget : EventTarget ;
28
+
29
+ /** The timeout id of the touch timeout, used to cancel timeout later. */
30
+ private _touchTimeout : number ;
31
+
21
32
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 ) ;
33
+ // Note: we listen to events in the capture phase so we can detect them even if the user stops
34
+ // propagation.
35
+
36
+ // On keydown record the origin and clear any touch event that may be in progress.
37
+ document . addEventListener ( 'keydown' , ( ) => {
38
+ this . _lastTouchTarget = null ;
39
+ this . _setOriginForCurrentEventQueue ( 'keyboard' ) ;
40
+ } , true ) ;
41
+
42
+ // On mousedown record the origin only if there is not touch target, since a mousedown can
43
+ // happen as a result of a touch event.
44
+ document . addEventListener ( 'mousedown' , ( ) => {
45
+ if ( ! this . _lastTouchTarget ) {
46
+ this . _setOriginForCurrentEventQueue ( 'mouse' ) ;
47
+ }
48
+ } , true ) ;
49
+
50
+ // When the touchstart event fires the focus event is not yet in the event queue. This means we
51
+ // can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to see if
52
+ // a focus happens.
53
+ document . addEventListener ( 'touchstart' , ( event : Event ) => {
54
+ if ( this . _touchTimeout != null ) {
55
+ clearTimeout ( this . _touchTimeout ) ;
56
+ }
57
+ this . _lastTouchTarget = event . target ;
58
+ this . _touchTimeout = setTimeout ( ( ) => this . _lastTouchTarget = null , TOUCH_BUFFER_MS ) ;
59
+ } , true ) ;
29
60
30
61
// Make a note of when the window regains focus, so we can restore the origin info for the
31
62
// focused element.
@@ -38,7 +69,8 @@ export class FocusOriginMonitor {
38
69
/** Register an element to receive focus classes. */
39
70
registerElementForFocusClasses ( element : Element , renderer : Renderer ) : Observable < FocusOrigin > {
40
71
let subject = new Subject < FocusOrigin > ( ) ;
41
- renderer . listen ( element , 'focus' , ( ) => this . _onFocus ( element , renderer , subject ) ) ;
72
+ renderer . listen ( element , 'focus' ,
73
+ ( event : Event ) => this . _onFocus ( event , element , renderer , subject ) ) ;
42
74
renderer . listen ( element , 'blur' , ( ) => this . _onBlur ( element , renderer , subject ) ) ;
43
75
return subject . asObservable ( ) ;
44
76
}
@@ -55,34 +87,64 @@ export class FocusOriginMonitor {
55
87
setTimeout ( ( ) => this . _origin = null , 0 ) ;
56
88
}
57
89
90
+ /** Checks whether the given focus event was caused by a touchstart event. */
91
+ private _wasCausedByTouch ( event : Event ) : boolean {
92
+ // Note(mmalerba): This implementation is not quite perfect, there is a small edge case.
93
+ // Consider the following dom structure:
94
+ //
95
+ // <div #parent tabindex="0" cdkFocusClasses>
96
+ // <div #child (click)="#parent.focus()"></div>
97
+ // </div>
98
+ //
99
+ // If the user touches the #child element and the #parent is programmatically focused as a
100
+ // result, this code will still consider it to have been caused by the touch event and will
101
+ // apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
102
+ // relatively small edge-case that can be worked around by using
103
+ // focusVia(parentEl, renderer, 'program') to focus the parent element.
104
+ //
105
+ // If we decide that we absolutely must handle this case correctly, we can do so by listening
106
+ // for the first focus event after the touchstart, and then the first blur event after that
107
+ // focus event. When that blur event fires we know that whatever follows is not a result of the
108
+ // touchstart.
109
+ let focusTarget = event . target ;
110
+ return this . _lastTouchTarget instanceof Node && focusTarget instanceof Node &&
111
+ ( focusTarget == this . _lastTouchTarget || focusTarget . contains ( this . _lastTouchTarget ) ) ;
112
+ }
113
+
58
114
/** Handles focus events on a registered element. */
59
- private _onFocus ( element : Element , renderer : Renderer , subject : Subject < FocusOrigin > ) {
115
+ private _onFocus ( event : Event , element : Element , renderer : Renderer ,
116
+ subject : Subject < FocusOrigin > ) {
60
117
// If we couldn't detect a cause for the focus event, it's due to one of two reasons:
61
118
// 1) The window has just regained focus, in which case we want to restore the focused state of
62
119
// the element from before the window blurred.
63
- // 2) The element was programmatically focused, in which case we should mark the origin as
120
+ // 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
121
+ // 3) The element was programmatically focused, in which case we should mark the origin as
64
122
// 'program'.
65
123
if ( ! this . _origin ) {
66
124
if ( this . _windowFocused && this . _lastFocusOrigin ) {
67
125
this . _origin = this . _lastFocusOrigin ;
126
+ } else if ( this . _wasCausedByTouch ( event ) ) {
127
+ this . _origin = 'touch' ;
68
128
} else {
69
129
this . _origin = 'program' ;
70
130
}
71
131
}
72
132
73
133
renderer . setElementClass ( element , 'cdk-focused' , true ) ;
134
+ renderer . setElementClass ( element , 'cdk-touch-focused' , this . _origin == 'touch' ) ;
74
135
renderer . setElementClass ( element , 'cdk-keyboard-focused' , this . _origin == 'keyboard' ) ;
75
136
renderer . setElementClass ( element , 'cdk-mouse-focused' , this . _origin == 'mouse' ) ;
76
137
renderer . setElementClass ( element , 'cdk-program-focused' , this . _origin == 'program' ) ;
77
-
78
138
subject . next ( this . _origin ) ;
139
+
79
140
this . _lastFocusOrigin = this . _origin ;
80
141
this . _origin = null ;
81
142
}
82
143
83
144
/** Handles blur events on a registered element. */
84
145
private _onBlur ( element : Element , renderer : Renderer , subject : Subject < FocusOrigin > ) {
85
146
renderer . setElementClass ( element , 'cdk-focused' , false ) ;
147
+ renderer . setElementClass ( element , 'cdk-touch-focused' , false ) ;
86
148
renderer . setElementClass ( element , 'cdk-keyboard-focused' , false ) ;
87
149
renderer . setElementClass ( element , 'cdk-mouse-focused' , false ) ;
88
150
renderer . setElementClass ( element , 'cdk-program-focused' , false ) ;
0 commit comments