@@ -3,7 +3,12 @@ import {Observable} from 'rxjs/Observable';
33import { Subject } from 'rxjs/Subject' ;
44
55
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' ;
712
813
914/** Monitors mouse and keyboard events to determine the cause of focus events. */
@@ -18,14 +23,40 @@ export class FocusOriginMonitor {
1823 /** Whether the window has just been focused. */
1924 private _windowFocused = false ;
2025
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+
2132 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 ) ;
2960
3061 // Make a note of when the window regains focus, so we can restore the origin info for the
3162 // focused element.
@@ -38,7 +69,8 @@ export class FocusOriginMonitor {
3869 /** Register an element to receive focus classes. */
3970 registerElementForFocusClasses ( element : Element , renderer : Renderer ) : Observable < FocusOrigin > {
4071 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 ) ) ;
4274 renderer . listen ( element , 'blur' , ( ) => this . _onBlur ( element , renderer , subject ) ) ;
4375 return subject . asObservable ( ) ;
4476 }
@@ -55,34 +87,64 @@ export class FocusOriginMonitor {
5587 setTimeout ( ( ) => this . _origin = null , 0 ) ;
5688 }
5789
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+
58114 /** 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 > ) {
60117 // If we couldn't detect a cause for the focus event, it's due to one of two reasons:
61118 // 1) The window has just regained focus, in which case we want to restore the focused state of
62119 // 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
64122 // 'program'.
65123 if ( ! this . _origin ) {
66124 if ( this . _windowFocused && this . _lastFocusOrigin ) {
67125 this . _origin = this . _lastFocusOrigin ;
126+ } else if ( this . _wasCausedByTouch ( event ) ) {
127+ this . _origin = 'touch' ;
68128 } else {
69129 this . _origin = 'program' ;
70130 }
71131 }
72132
73133 renderer . setElementClass ( element , 'cdk-focused' , true ) ;
134+ renderer . setElementClass ( element , 'cdk-touch-focused' , this . _origin == 'touch' ) ;
74135 renderer . setElementClass ( element , 'cdk-keyboard-focused' , this . _origin == 'keyboard' ) ;
75136 renderer . setElementClass ( element , 'cdk-mouse-focused' , this . _origin == 'mouse' ) ;
76137 renderer . setElementClass ( element , 'cdk-program-focused' , this . _origin == 'program' ) ;
77-
78138 subject . next ( this . _origin ) ;
139+
79140 this . _lastFocusOrigin = this . _origin ;
80141 this . _origin = null ;
81142 }
82143
83144 /** Handles blur events on a registered element. */
84145 private _onBlur ( element : Element , renderer : Renderer , subject : Subject < FocusOrigin > ) {
85146 renderer . setElementClass ( element , 'cdk-focused' , false ) ;
147+ renderer . setElementClass ( element , 'cdk-touch-focused' , false ) ;
86148 renderer . setElementClass ( element , 'cdk-keyboard-focused' , false ) ;
87149 renderer . setElementClass ( element , 'cdk-mouse-focused' , false ) ;
88150 renderer . setElementClass ( element , 'cdk-program-focused' , false ) ;
0 commit comments