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', () => {