@samitouri / QOS-React-2 / commits / af19e2eb2f

Event API: adds pointerType to Focus events (#15645)

Dominic Gannaway committed May 14, 2019 at 15:59 UTC af19e2eb2f8a849a11d2f5fa98d65d46087058e2
2 files changed +161 -31
packages/react-events/src/Focus.js
+88 -31
@@ -26,13 +26,16 @@ type FocusState = {
26 focusTarget: null | Element | Document,
27 isFocused: boolean,
28 isLocalFocusVisible: boolean,
29 + pointerType: PointerType,
30 };
31
32 +type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';
33 type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
34
35 type FocusEvent = {|
36 target: Element | Document,
37 type: FocusEventType,
38 + pointerType: PointerType,
39 timeStamp: number,
40 |};
41
@@ -45,25 +48,33 @@ const rootEventTypes = [
48 'keydown',
49 'keypress',
50 'keyup',
48 - 'mousemove',
49 - 'mousedown',
50 - 'mouseup',
51 'pointermove',
52 'pointerdown',
53 'pointerup',
54 - 'touchmove',
55 - 'touchstart',
56 - 'touchend',
54 ];
55
56 +// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
57 +if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
58 + rootEventTypes.push(
59 + 'mousemove',
60 + 'mousedown',
61 + 'mouseup',
62 + 'touchmove',
63 + 'touchstart',
64 + 'touchend',
65 + );
66 +}
67 +
68 function createFocusEvent(
69 context: ReactResponderContext,
70 type: FocusEventType,
71 target: Element | Document,
72 + pointerType: PointerType,
73 ): FocusEvent {
74 return {
75 target,
76 type,
77 + pointerType,
78 timeStamp: context.getTimeStamp(),
79 };
80 }
@@ -73,16 +84,27 @@ function dispatchFocusInEvents(
84 props: FocusProps,
85 state: FocusState,
86 ) {
87 + const pointerType = state.pointerType;
88 const target = ((state.focusTarget: any): Element | Document);
89 if (props.onFocus) {
78 - const syntheticEvent = createFocusEvent(context, 'focus', target);
90 + const syntheticEvent = createFocusEvent(
91 + context,
92 + 'focus',
93 + target,
94 + pointerType,
95 + );
96 context.dispatchEvent(syntheticEvent, props.onFocus, {discrete: true});
97 }
98 if (props.onFocusChange) {
99 const listener = () => {
100 props.onFocusChange(true);
101 };
85 - const syntheticEvent = createFocusEvent(context, 'focuschange', target);
102 + const syntheticEvent = createFocusEvent(
103 + context,
104 + 'focuschange',
105 + target,
106 + pointerType,
107 + );
108 context.dispatchEvent(syntheticEvent, listener, {discrete: true});
109 }
110 if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
@@ -93,6 +115,7 @@ function dispatchFocusInEvents(
115 context,
116 'focusvisiblechange',
117 target,
118 + pointerType,
119 );
120 context.dispatchEvent(syntheticEvent, listener, {discrete: true});
121 }
@@ -103,16 +126,27 @@ function dispatchFocusOutEvents(
126 props: FocusProps,
127 state: FocusState,
128 ) {
129 + const pointerType = state.pointerType;
130 const target = ((state.focusTarget: any): Element | Document);
131 if (props.onBlur) {
108 - const syntheticEvent = createFocusEvent(context, 'blur', target);
132 + const syntheticEvent = createFocusEvent(
133 + context,
134 + 'blur',
135 + target,
136 + pointerType,
137 + );
138 context.dispatchEvent(syntheticEvent, props.onBlur, {discrete: true});
139 }
140 if (props.onFocusChange) {
141 const listener = () => {
142 props.onFocusChange(false);
143 };
115 - const syntheticEvent = createFocusEvent(context, 'focuschange', target);
144 + const syntheticEvent = createFocusEvent(
145 + context,
146 + 'focuschange',
147 + target,
148 + pointerType,
149 + );
150 context.dispatchEvent(syntheticEvent, listener, {discrete: true});
151 }
152 dispatchFocusVisibleOutEvent(context, props, state);
@@ -123,6 +157,7 @@ function dispatchFocusVisibleOutEvent(
157 props: FocusProps,
158 state: FocusState,
159 ) {
160 + const pointerType = state.pointerType;
161 const target = ((state.focusTarget: any): Element | Document);
162 if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
163 const listener = () => {
@@ -132,6 +167,7 @@ function dispatchFocusVisibleOutEvent(
167 context,
168 'focusvisiblechange',
169 target,
170 + pointerType,
171 );
172 context.dispatchEvent(syntheticEvent, listener, {discrete: true});
173 state.isLocalFocusVisible = false;
@@ -148,6 +184,31 @@ function unmountResponder(
184 }
185 }
186
187 +function handleRootPointerEvent(
188 + event: ReactResponderEvent,
189 + context: ReactResponderContext,
190 + props: FocusProps,
191 + state: FocusState,
192 +): void {
193 + const {type, target} = event;
194 + // Ignore a Safari quirks where 'mousemove' is dispatched on the 'html'
195 + // element when the window blurs.
196 + if (type === 'mousemove' && target.nodeName === 'HTML') {
197 + return;
198 + }
199 +
200 + isGlobalFocusVisible = false;
201 +
202 + // Focus should stop being visible if a pointer is used on the element
203 + // after it was focused using a keyboard.
204 + if (
205 + state.focusTarget === context.getEventCurrentTarget(event) &&
206 + (type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
207 + ) {
208 + dispatchFocusVisibleOutEvent(context, props, state);
209 + }
210 +}
211 +
212 let isGlobalFocusVisible = true;
213
214 const FocusResponder = {
@@ -158,6 +219,7 @@ const FocusResponder = {
219 focusTarget: null,
220 isFocused: false,
221 isLocalFocusVisible: false,
222 + pointerType: '',
223 };
224 },
225 stopLocalPropagation: true,
@@ -208,36 +270,30 @@ const FocusResponder = {
270 props: FocusProps,
271 state: FocusState,
272 ): void {
211 - const {type, target} = event;
273 + const {type} = event;
274
275 switch (type) {
276 case 'mousemove':
277 case 'mousedown':
216 - case 'mouseup':
278 + case 'mouseup': {
279 + state.pointerType = 'mouse';
280 + handleRootPointerEvent(event, context, props, state);
281 + break;
282 + }
283 case 'pointermove':
284 case 'pointerdown':
219 - case 'pointerup':
285 + case 'pointerup': {
286 + // $FlowFixMe: Flow doesn't know about PointerEvents
287 + const nativeEvent = ((event.nativeEvent: any): PointerEvent);
288 + state.pointerType = nativeEvent.pointerType;
289 + handleRootPointerEvent(event, context, props, state);
290 + break;
291 + }
292 case 'touchmove':
293 case 'touchstart':
294 case 'touchend': {
223 - // Ignore a Safari quirks where 'mousemove' is dispatched on the 'html'
224 - // element when the window blurs.
225 - if (type === 'mousemove' && target.nodeName === 'HTML') {
226 - return;
227 - }
228 -
229 - isGlobalFocusVisible = false;
230 -
231 - // Focus should stop being visible if a pointer is used on the element
232 - // after it was focused using a keyboard.
233 - if (
234 - state.focusTarget === context.getEventCurrentTarget(event) &&
235 - (type === 'mousedown' ||
236 - type === 'touchstart' ||
237 - type === 'pointerdown')
238 - ) {
239 - dispatchFocusVisibleOutEvent(context, props, state);
240 - }
295 + state.pointerType = 'touch';
296 + handleRootPointerEvent(event, context, props, state);
297 break;
298 }
299
@@ -249,6 +305,7 @@ const FocusResponder = {
305 nativeEvent.key === 'Tab' &&
306 !(nativeEvent.metaKey || nativeEvent.altKey || nativeEvent.ctrlKey)
307 ) {
308 + state.pointerType = 'keyboard';
309 isGlobalFocusVisible = true;
310 }
311 break;
packages/react-events/src/__tests__/Focus-test.internal.js
+73
@@ -131,6 +131,79 @@ describe('Focus event responder', () => {
131 target.dispatchEvent(createFocusEvent('focus'));
132 expect(onFocus).not.toBeCalled();
133 });
134 +
135 + it('is called with the correct pointerType using pointer events', () => {
136 + // Pointer mouse
137 + ref.current.dispatchEvent(
138 + createPointerEvent('pointerdown', {
139 + pointerType: 'mouse',
140 + }),
141 + );
142 + ref.current.dispatchEvent(createFocusEvent('focus'));
143 + expect(onFocus).toHaveBeenCalledTimes(1);
144 + expect(onFocus).toHaveBeenCalledWith(
145 + expect.objectContaining({pointerType: 'mouse'}),
146 + );
147 + ref.current.dispatchEvent(createFocusEvent('blur'));
148 +
149 + // Pointer touch
150 + ref.current.dispatchEvent(
151 + createPointerEvent('pointerdown', {
152 + pointerType: 'touch',
153 + }),
154 + );
155 + ref.current.dispatchEvent(createFocusEvent('focus'));
156 + expect(onFocus).toHaveBeenCalledTimes(2);
157 + expect(onFocus).toHaveBeenCalledWith(
158 + expect.objectContaining({pointerType: 'touch'}),
159 + );
160 + ref.current.dispatchEvent(createFocusEvent('blur'));
161 +
162 + // Pointer pen
163 + ref.current.dispatchEvent(
164 + createPointerEvent('pointerdown', {
165 + pointerType: 'pen',
166 + }),
167 + );
168 + ref.current.dispatchEvent(createFocusEvent('focus'));
169 + expect(onFocus).toHaveBeenCalledTimes(3);
170 + expect(onFocus).toHaveBeenCalledWith(
171 + expect.objectContaining({pointerType: 'pen'}),
172 + );
173 + });
174 +
175 + it('is called with the correct pointerType without pointer events', () => {
176 + // Mouse
177 + ref.current.dispatchEvent(createPointerEvent('mousedown'));
178 + ref.current.dispatchEvent(createFocusEvent('focus'));
179 + expect(onFocus).toHaveBeenCalledTimes(1);
180 + expect(onFocus).toHaveBeenCalledWith(
181 + expect.objectContaining({pointerType: 'mouse'}),
182 + );
183 + ref.current.dispatchEvent(createFocusEvent('blur'));
184 +
185 + // Touch
186 + ref.current.dispatchEvent(createPointerEvent('touchstart'));
187 + ref.current.dispatchEvent(createFocusEvent('focus'));
188 + expect(onFocus).toHaveBeenCalledTimes(2);
189 + expect(onFocus).toHaveBeenCalledWith(
190 + expect.objectContaining({pointerType: 'touch'}),
191 + );
192 + });
193 +
194 + it('is called with the correct pointerType using a keyboard', () => {
195 + // Keyboard tab
196 + ref.current.dispatchEvent(
197 + createPointerEvent('keypress', {
198 + key: 'Tab',
199 + }),
200 + );
201 + ref.current.dispatchEvent(createFocusEvent('focus'));
202 + expect(onFocus).toHaveBeenCalledTimes(1);
203 + expect(onFocus).toHaveBeenCalledWith(
204 + expect.objectContaining({pointerType: 'keyboard'}),
205 + );
206 + });
207 });
208
209 describe('onFocusChange', () => {