@samitouri / QOS-React-2 / commits / 107521a952

[react-events] Focus/FocusWithin responders with fallbacks (#16343)

Separate the PointerEvent and fallback implementations. Fix the unit tests to cover both PointerEvent and non-PointerEvent environments. Fix the focus-visible related callbacks to get called when keys other than "Tab" are used.

Nicolas Gallagher committed Aug 12, 2019 at 10:10 UTC 107521a9526a5066d7ad7bc8e2e73224b2f5de24
6 files changed +405 -392
packages/react-events/src/dom/Focus.js
+57 -50
@@ -33,6 +33,7 @@ type FocusState = {
33 isFocused: boolean,
34 isFocusVisible: boolean,
35 pointerType: PointerType,
36 + isEmulatingMouseEvents: boolean,
37 };
38
39 type FocusProps = {
@@ -66,25 +67,12 @@ const isMac =
67
68 const targetEventTypes = ['focus', 'blur'];
69
69 -const rootEventTypes = [
70 - 'keydown',
71 - 'keyup',
72 - 'pointermove',
73 - 'pointerdown',
74 - 'pointerup',
75 -];
76 -
77 -// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
78 -if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
79 - rootEventTypes.push(
80 - 'mousemove',
81 - 'mousedown',
82 - 'mouseup',
83 - 'touchmove',
84 - 'touchstart',
85 - 'touchend',
86 - );
87 -}
70 +const hasPointerEvents =
71 + typeof window !== 'undefined' && window.PointerEvent != null;
72 +
73 +const rootEventTypes = hasPointerEvents
74 + ? ['keydown', 'keyup', 'pointermove', 'pointerdown', 'pointerup']
75 + : ['keydown', 'keyup', 'mousedown', 'touchmove', 'touchstart', 'touchend'];
76
77 function isFunction(obj): boolean {
78 return typeof obj === 'function';
@@ -110,13 +98,7 @@ function handleRootPointerEvent(
98 state: FocusState,
99 callback: boolean => void,
100 ): void {
113 - const {type, target} = event;
114 - // Ignore a Safari quirks where 'mousemove' is dispatched on the 'html'
115 - // element when the window blurs.
116 - if (type === 'mousemove' && target.nodeName === 'HTML') {
117 - return;
118 - }
119 -
101 + const {type} = event;
102 isGlobalFocusVisible = false;
103
104 // Focus should stop being visible if a pointer is used on the element
@@ -124,7 +106,7 @@ function handleRootPointerEvent(
106 const focusTarget = state.focusTarget;
107 if (
108 focusTarget !== null &&
127 - context.isTargetWithinNode(event.target, focusTarget) &&
109 + context.isTargetWithinResponderScope(focusTarget) &&
110 (type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
111 ) {
112 callback(false);
@@ -140,13 +122,6 @@ function handleRootEvent(
122 const {type} = event;
123
124 switch (type) {
143 - case 'mousemove':
144 - case 'mousedown':
145 - case 'mouseup': {
146 - state.pointerType = 'mouse';
147 - handleRootPointerEvent(event, context, state, callback);
148 - break;
149 - }
125 case 'pointermove':
126 case 'pointerdown':
127 case 'pointerup': {
@@ -156,27 +131,45 @@ function handleRootEvent(
131 handleRootPointerEvent(event, context, state, callback);
132 break;
133 }
134 +
135 + case 'keydown':
136 + case 'keyup': {
137 + const nativeEvent = event.nativeEvent;
138 + const focusTarget = state.focusTarget;
139 + const {key, metaKey, altKey, ctrlKey} = (nativeEvent: any);
140 + const validKey =
141 + key === 'Enter' ||
142 + key === ' ' ||
143 + (key === 'Tab' && !(metaKey || (!isMac && altKey) || ctrlKey));
144 +
145 + if (validKey) {
146 + state.pointerType = 'keyboard';
147 + isGlobalFocusVisible = true;
148 + if (
149 + focusTarget !== null &&
150 + context.isTargetWithinResponderScope(focusTarget)
151 + ) {
152 + callback(true);
153 + }
154 + }
155 + break;
156 + }
157 +
158 + // fallbacks for no PointerEvent support
159 case 'touchmove':
160 case 'touchstart':
161 case 'touchend': {
162 state.pointerType = 'touch';
163 + state.isEmulatingMouseEvents = true;
164 handleRootPointerEvent(event, context, state, callback);
165 break;
166 }
166 -
167 - case 'keydown':
168 - case 'keyup': {
169 - const nativeEvent = event.nativeEvent;
170 - if (
171 - nativeEvent.key === 'Tab' &&
172 - !(
173 - nativeEvent.metaKey ||
174 - (!isMac && nativeEvent.altKey) ||
175 - nativeEvent.ctrlKey
176 - )
177 - ) {
178 - state.pointerType = 'keyboard';
179 - isGlobalFocusVisible = true;
167 + case 'mousedown': {
168 + if (!state.isEmulatingMouseEvents) {
169 + state.pointerType = 'mouse';
170 + handleRootPointerEvent(event, context, state, callback);
171 + } else {
172 + state.isEmulatingMouseEvents = false;
173 }
174 break;
175 }
@@ -271,6 +264,7 @@ const focusResponderImpl = {
264 getInitialState(): FocusState {
265 return {
266 focusTarget: null,
267 + isEmulatingMouseEvents: false,
268 isFocused: false,
269 isFocusVisible: false,
270 pointerType: '',
@@ -303,6 +297,7 @@ const focusResponderImpl = {
297 state.isFocusVisible = isGlobalFocusVisible;
298 dispatchFocusEvents(context, props, state);
299 }
300 + state.isEmulatingMouseEvents = false;
301 break;
302 }
303 case 'blur': {
@@ -311,6 +306,17 @@ const focusResponderImpl = {
306 state.isFocusVisible = isGlobalFocusVisible;
307 state.isFocused = false;
308 }
309 + // This covers situations where focus is lost to another document in
310 + // the same window (e.g., iframes). Any action that restores focus to
311 + // the document (e.g., touch or click) first causes 'focus' to be
312 + // dispatched, which means the 'pointerType' we provide is stale
313 + // (it reflects the *previous* pointer). We cannot determine the
314 + // 'pointerType' in this case, so a blur with no
315 + // relatedTarget is used as a signal to reset the 'pointerType'.
316 + if (event.nativeEvent.relatedTarget == null) {
317 + state.pointerType = '';
318 + }
319 + state.isEmulatingMouseEvents = false;
320 break;
321 }
322 }
@@ -322,7 +328,7 @@ const focusResponderImpl = {
328 state: FocusState,
329 ): void {
330 handleRootEvent(event, context, state, isFocusVisible => {
325 - if (state.isFocusVisible !== isFocusVisible) {
331 + if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
332 state.isFocusVisible = isFocusVisible;
333 dispatchFocusVisibleChangeEvent(context, props, isFocusVisible);
334 }
@@ -402,6 +408,7 @@ const focusWithinResponderImpl = {
408 getInitialState(): FocusState {
409 return {
410 focusTarget: null,
411 + isEmulatingMouseEvents: false,
412 isFocused: false,
413 isFocusVisible: false,
414 pointerType: '',
@@ -460,7 +467,7 @@ const focusWithinResponderImpl = {
467 state: FocusState,
468 ): void {
469 handleRootEvent(event, context, state, isFocusVisible => {
463 - if (state.isFocusVisible !== isFocusVisible) {
470 + if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
471 state.isFocusVisible = isFocusVisible;
472 dispatchFocusWithinVisibleChangeEvent(
473 context,
packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js
+13 -45
@@ -9,7 +9,13 @@
9
10 'use strict';
11
12 -import {createEvent, platform, setPointerEvent} from '../test-utils';
12 +import {
13 + dispatchLongPressContextMenu,
14 + dispatchRightClickContextMenu,
15 + dispatchModifiedClickContextMenu,
16 + platform,
17 + setPointerEvent,
18 +} from '../test-utils';
19
20 let React;
21 let ReactFeatureFlags;
@@ -27,44 +33,6 @@ function initializeModules(hasPointerEvents) {
33 .useContextMenuResponder;
34 }
35
30 -function dispatchContextMenuEvents(ref, options) {
31 - const preventDefault = options.preventDefault || function() {};
32 - const variant = (options.variant: 'mouse' | 'touch' | 'modified');
33 - const dispatchEvent = arg => ref.current.dispatchEvent(arg);
34 -
35 - if (variant === 'mouse') {
36 - // right-click
37 - dispatchEvent(
38 - createEvent('pointerdown', {pointerType: 'mouse', button: 2}),
39 - );
40 - dispatchEvent(createEvent('mousedown', {button: 2}));
41 - dispatchEvent(createEvent('contextmenu', {button: 2, preventDefault}));
42 - } else if (variant === 'modified') {
43 - // left-click + ctrl
44 - dispatchEvent(
45 - createEvent('pointerdown', {pointerType: 'mouse', button: 0}),
46 - );
47 - dispatchEvent(createEvent('mousedown', {button: 0}));
48 - if (platform.get() === 'mac') {
49 - dispatchEvent(
50 - createEvent('contextmenu', {button: 0, ctrlKey: true, preventDefault}),
51 - );
52 - }
53 - } else if (variant === 'touch') {
54 - // long-press
55 - dispatchEvent(
56 - createEvent('pointerdown', {pointerType: 'touch', button: 0}),
57 - );
58 - dispatchEvent(
59 - createEvent('touchstart', {
60 - changedTouches: [],
61 - targetTouches: [],
62 - }),
63 - );
64 - dispatchEvent(createEvent('contextmenu', {button: 0, preventDefault}));
65 - }
66 -}
67 -
36 const forcePointerEvents = true;
37 const table = [[forcePointerEvents], [!forcePointerEvents]];
38
@@ -94,7 +62,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
62 };
63 ReactDOM.render(<Component />, container);
64
97 - dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
65 + dispatchRightClickContextMenu(ref.current, {preventDefault});
66 expect(preventDefault).toHaveBeenCalledTimes(1);
67 expect(onContextMenu).toHaveBeenCalledTimes(1);
68 expect(onContextMenu).toHaveBeenCalledWith(
@@ -112,7 +80,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
80 };
81 ReactDOM.render(<Component />, container);
82
115 - dispatchContextMenuEvents(ref, {variant: 'touch', preventDefault});
83 + dispatchLongPressContextMenu(ref.current, {preventDefault});
84 expect(preventDefault).toHaveBeenCalledTimes(1);
85 expect(onContextMenu).toHaveBeenCalledTimes(1);
86 expect(onContextMenu).toHaveBeenCalledWith(
@@ -132,7 +100,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
100 };
101 ReactDOM.render(<Component />, container);
102
135 - dispatchContextMenuEvents(ref, 'mouse');
103 + dispatchRightClickContextMenu(ref.current);
104 expect(onContextMenu).toHaveBeenCalledTimes(0);
105 });
106
@@ -149,7 +117,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
117 };
118 ReactDOM.render(<Component />, container);
119
152 - dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
120 + dispatchRightClickContextMenu(ref.current, {preventDefault});
121 expect(preventDefault).toHaveBeenCalledTimes(0);
122 expect(onContextMenu).toHaveBeenCalledTimes(1);
123 });
@@ -174,7 +142,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
142 };
143 ReactDOM.render(<Component />, container);
144
177 - dispatchContextMenuEvents(ref, {variant: 'modified'});
145 + dispatchModifiedClickContextMenu(ref.current);
146 expect(onContextMenu).toHaveBeenCalledTimes(1);
147 expect(onContextMenu).toHaveBeenCalledWith(
148 expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
@@ -201,7 +169,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
169 };
170 ReactDOM.render(<Component />, container);
171
204 - dispatchContextMenuEvents(ref, {variant: 'modified'});
172 + dispatchModifiedClickContextMenu(ref.current);
173 expect(onContextMenu).toHaveBeenCalledTimes(0);
174 });
175 });
packages/react-events/src/dom/__tests__/Focus-test.internal.js
+85 -127
@@ -9,47 +9,41 @@
9
10 'use strict';
11
12 +import {
13 + blur,
14 + focus,
15 + keydown,
16 + setPointerEvent,
17 + platform,
18 + dispatchPointerPressDown,
19 + dispatchPointerPressRelease,
20 +} from '../test-utils';
21 +
22 let React;
23 let ReactFeatureFlags;
24 let ReactDOM;
25 let FocusResponder;
26 let useFocusResponder;
27
18 -const createEvent = (type, data) => {
19 - const event = document.createEvent('CustomEvent');
20 - event.initCustomEvent(type, true, true);
21 - if (data != null) {
22 - Object.entries(data).forEach(([key, value]) => {
23 - event[key] = value;
24 - });
25 - }
26 - return event;
27 -};
28 -
29 -const createKeyboardEvent = (type, data) => {
30 - return new KeyboardEvent(type, {
31 - bubbles: true,
32 - cancelable: true,
33 - ...data,
34 - });
35 -};
36 -
37 -const modulesInit = () => {
28 +function initializeModules(hasPointerEvents) {
29 + setPointerEvent(hasPointerEvents);
30 + jest.resetModules();
31 ReactFeatureFlags = require('shared/ReactFeatureFlags');
32 ReactFeatureFlags.enableFlareAPI = true;
33 React = require('react');
34 ReactDOM = require('react-dom');
35 FocusResponder = require('react-events/focus').FocusResponder;
36 useFocusResponder = require('react-events/focus').useFocusResponder;
44 -};
37 +}
38 +
39 +const forcePointerEvents = true;
40 +const table = [[forcePointerEvents], [!forcePointerEvents]];
41
46 -describe('Focus event responder', () => {
42 +describe.each(table)('Focus responder', hasPointerEvents => {
43 let container;
44
45 beforeEach(() => {
50 - jest.resetModules();
51 - modulesInit();
52 -
46 + initializeModules(hasPointerEvents);
47 container = document.createElement('div');
48 document.body.appendChild(container);
49 });
@@ -78,9 +72,10 @@ describe('Focus event responder', () => {
72 ReactDOM.render(<Component />, container);
73 });
74
81 - it('prevents custom events being dispatched', () => {
82 - ref.current.dispatchEvent(createEvent('focus'));
83 - ref.current.dispatchEvent(createEvent('blur'));
75 + it('does not call callbacks', () => {
76 + const dispatch = arg => ref.current.dispatchEvent(arg);
77 + dispatch(focus());
78 + dispatch(blur());
79 expect(onFocus).not.toBeCalled();
80 expect(onBlur).not.toBeCalled();
81 });
@@ -102,8 +97,9 @@ describe('Focus event responder', () => {
97 });
98
99 it('is called after "blur" event', () => {
105 - ref.current.dispatchEvent(createEvent('focus'));
106 - ref.current.dispatchEvent(createEvent('blur'));
100 + const dispatch = arg => ref.current.dispatchEvent(arg);
101 + dispatch(focus());
102 + dispatch(blur());
103 expect(onBlur).toHaveBeenCalledTimes(1);
104 });
105 });
@@ -131,83 +127,52 @@ describe('Focus event responder', () => {
127 beforeEach(componentInit);
128
129 it('is called after "focus" event', () => {
134 - ref.current.dispatchEvent(createEvent('focus'));
130 + ref.current.dispatchEvent(focus());
131 expect(onFocus).toHaveBeenCalledTimes(1);
132 });
133
134 it('is not called if descendants of target receive focus', () => {
139 - const target = innerRef.current;
140 - target.dispatchEvent(createEvent('focus'));
135 + innerRef.current.dispatchEvent(focus());
136 expect(onFocus).not.toBeCalled();
137 });
138
144 - it('is called with the correct pointerType using pointer events', () => {
145 - // Pointer mouse
146 - ref.current.dispatchEvent(
147 - createEvent('pointerdown', {
148 - pointerType: 'mouse',
149 - }),
150 - );
151 - ref.current.dispatchEvent(createEvent('focus'));
139 + it('is called with the correct pointerType: mouse', () => {
140 + const target = ref.current;
141 + dispatchPointerPressDown(target, {pointerType: 'mouse'});
142 + dispatchPointerPressRelease(target, {pointerType: 'mouse'});
143 expect(onFocus).toHaveBeenCalledTimes(1);
144 expect(onFocus).toHaveBeenCalledWith(
145 expect.objectContaining({pointerType: 'mouse'}),
146 );
156 - ref.current.dispatchEvent(createEvent('blur'));
157 -
158 - // Pointer touch
159 - ref.current.dispatchEvent(
160 - createEvent('pointerdown', {
161 - pointerType: 'touch',
162 - }),
163 - );
164 - ref.current.dispatchEvent(createEvent('focus'));
165 - expect(onFocus).toHaveBeenCalledTimes(2);
166 - expect(onFocus).toHaveBeenCalledWith(
167 - expect.objectContaining({pointerType: 'touch'}),
168 - );
169 - ref.current.dispatchEvent(createEvent('blur'));
170 -
171 - // Pointer pen
172 - ref.current.dispatchEvent(
173 - createEvent('pointerdown', {
174 - pointerType: 'pen',
175 - }),
176 - );
177 - ref.current.dispatchEvent(createEvent('focus'));
178 - expect(onFocus).toHaveBeenCalledTimes(3);
179 - expect(onFocus).toHaveBeenCalledWith(
180 - expect.objectContaining({pointerType: 'pen'}),
181 - );
147 });
148
184 - it('is called with the correct pointerType without pointer events', () => {
185 - // Mouse
186 - ref.current.dispatchEvent(createEvent('mousedown'));
187 - ref.current.dispatchEvent(createEvent('focus'));
149 + it('is called with the correct pointerType: touch', () => {
150 + const target = ref.current;
151 + dispatchPointerPressDown(target, {pointerType: 'touch'});
152 + dispatchPointerPressRelease(target, {pointerType: 'touch'});
153 expect(onFocus).toHaveBeenCalledTimes(1);
189 - expect(onFocus).toHaveBeenCalledWith(
190 - expect.objectContaining({pointerType: 'mouse'}),
191 - );
192 - ref.current.dispatchEvent(createEvent('blur'));
193 -
194 - // Touch
195 - ref.current.dispatchEvent(createEvent('touchstart'));
196 - ref.current.dispatchEvent(createEvent('focus'));
197 - expect(onFocus).toHaveBeenCalledTimes(2);
154 expect(onFocus).toHaveBeenCalledWith(
155 expect.objectContaining({pointerType: 'touch'}),
156 );
157 });
158
159 + if (hasPointerEvents) {
160 + it('is called with the correct pointerType: pen', () => {
161 + const target = ref.current;
162 + dispatchPointerPressDown(target, {pointerType: 'pen'});
163 + dispatchPointerPressRelease(target, {pointerType: 'pen'});
164 + expect(onFocus).toHaveBeenCalledTimes(1);
165 + expect(onFocus).toHaveBeenCalledWith(
166 + expect.objectContaining({pointerType: 'pen'}),
167 + );
168 + });
169 + }
170 +
171 it('is called with the correct pointerType using a keyboard', () => {
172 + const target = ref.current;
173 // Keyboard tab
205 - ref.current.dispatchEvent(
206 - createEvent('keydown', {
207 - key: 'Tab',
208 - }),
209 - );
210 - ref.current.dispatchEvent(createEvent('focus'));
174 + target.dispatchEvent(keydown({key: 'Tab'}));
175 + target.dispatchEvent(focus());
176 expect(onFocus).toHaveBeenCalledTimes(1);
177 expect(onFocus).toHaveBeenCalledWith(
178 expect.objectContaining({pointerType: 'keyboard'}),
@@ -215,19 +180,14 @@ describe('Focus event responder', () => {
180 });
181
182 it('is called with the correct pointerType using Tab+altKey on Mac', () => {
183 + platform.set('mac');
184 jest.resetModules();
219 - const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
220 - platformGetter.mockReturnValue('MacIntel');
221 - modulesInit();
185 + initializeModules();
186 componentInit();
187 + const target = ref.current;
188
224 - ref.current.dispatchEvent(
225 - createEvent('keydown', {
226 - key: 'Tab',
227 - altKey: true,
228 - }),
229 - );
230 - ref.current.dispatchEvent(createEvent('focus'));
189 + target.dispatchEvent(keydown({key: 'Tab', altKey: true}));
190 + target.dispatchEvent(focus());
191 expect(onFocus).toHaveBeenCalledTimes(1);
192 expect(onFocus).toHaveBeenCalledWith(
193 expect.objectContaining({
@@ -235,7 +195,7 @@ describe('Focus event responder', () => {
195 }),
196 );
197
238 - platformGetter.mockClear();
198 + platform.clear();
199 });
200 });
201
@@ -260,18 +220,20 @@ describe('Focus event responder', () => {
220 });
221
222 it('is called after "blur" and "focus" events', () => {
263 - ref.current.dispatchEvent(createEvent('focus'));
223 + const target = ref.current;
224 + target.dispatchEvent(focus());
225 expect(onFocusChange).toHaveBeenCalledTimes(1);
226 expect(onFocusChange).toHaveBeenCalledWith(true);
266 - ref.current.dispatchEvent(createEvent('blur'));
227 + target.dispatchEvent(blur());
228 expect(onFocusChange).toHaveBeenCalledTimes(2);
229 expect(onFocusChange).toHaveBeenCalledWith(false);
230 });
231
232 it('is not called after "blur" and "focus" events on descendants', () => {
272 - innerRef.current.dispatchEvent(createEvent('focus'));
233 + const target = innerRef.current;
234 + target.dispatchEvent(focus());
235 expect(onFocusChange).toHaveBeenCalledTimes(0);
274 - innerRef.current.dispatchEvent(createEvent('blur'));
236 + target.dispatchEvent(blur());
237 expect(onFocusChange).toHaveBeenCalledTimes(0);
238 });
239 });
@@ -297,52 +259,48 @@ describe('Focus event responder', () => {
259 });
260
261 it('is called after "focus" and "blur" if keyboard navigation is active', () => {
262 + const target = ref.current;
263 // use keyboard first
301 - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
302 - ref.current.dispatchEvent(createEvent('focus'));
264 + container.dispatchEvent(keydown({key: 'Tab'}));
265 + target.dispatchEvent(focus());
266 expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
267 expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
305 - ref.current.dispatchEvent(
306 - createEvent('blur', {relatedTarget: container}),
307 - );
268 + target.dispatchEvent(blur({relatedTarget: container}));
269 expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
270 expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
271 });
272
273 it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
274 + const target = ref.current;
275 // use keyboard first
314 - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
315 - ref.current.dispatchEvent(createEvent('focus'));
276 + container.dispatchEvent(keydown({key: 'Tab'}));
277 + target.dispatchEvent(focus());
278 expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
279 expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
280 // then use pointer on the target, focus should no longer be visible
319 - ref.current.dispatchEvent(createEvent('pointerdown'));
281 + dispatchPointerPressDown(target);
282 expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
283 expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
284 // onFocusVisibleChange should not be called again
323 - ref.current.dispatchEvent(
324 - createEvent('blur', {relatedTarget: container}),
325 - );
285 + target.dispatchEvent(blur({relatedTarget: container}));
286 expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
287 });
288
289 it('is not called after "focus" and "blur" events without keyboard', () => {
330 - ref.current.dispatchEvent(createEvent('pointerdown'));
331 - ref.current.dispatchEvent(createEvent('focus'));
332 - container.dispatchEvent(createEvent('pointerdown'));
333 - ref.current.dispatchEvent(
334 - createEvent('blur', {relatedTarget: container}),
335 - );
290 + const target = ref.current;
291 + dispatchPointerPressDown(target);
292 + dispatchPointerPressRelease(target);
293 + dispatchPointerPressDown(container);
294 + target.dispatchEvent(blur({relatedTarget: container}));
295 expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
296 });
297
298 it('is not called after "blur" and "focus" events on descendants', () => {
340 - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
341 - innerRef.current.dispatchEvent(createEvent('focus'));
299 + const target = innerRef.current;
300 + container.dispatchEvent(keydown({key: 'Tab'}));
301 + target.dispatchEvent(focus());
302 expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
343 - innerRef.current.dispatchEvent(
344 - createEvent('blur', {relatedTarget: container}),
345 - );
303 + target.dispatchEvent(blur({relatedTarget: container}));
304 expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
305 });
306 });
@@ -380,10 +338,10 @@ describe('Focus event responder', () => {
338
339 ReactDOM.render(<Outer />, container);
340
383 - outerRef.current.dispatchEvent(createEvent('focus'));
384 - outerRef.current.dispatchEvent(createEvent('blur'));
385 - innerRef.current.dispatchEvent(createEvent('focus'));
386 - innerRef.current.dispatchEvent(createEvent('blur'));
341 + outerRef.current.dispatchEvent(focus());
342 + outerRef.current.dispatchEvent(blur());
343 + innerRef.current.dispatchEvent(focus());
344 + innerRef.current.dispatchEvent(blur());
345 expect(events).toEqual([
346 'outer: onFocus',
347 'outer: onFocusChange',
packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js
+74 -102
@@ -9,32 +9,24 @@
9
10 'use strict';
11
12 +import {
13 + blur,
14 + focus,
15 + keydown,
16 + setPointerEvent,
17 + dispatchPointerPressDown,
18 + dispatchPointerPressRelease,
19 +} from '../test-utils';
20 +
21 let React;
22 let ReactFeatureFlags;
23 let ReactDOM;
24 let FocusWithinResponder;
25 let useFocusWithinResponder;
26
18 -const createEvent = (type, data) => {
19 - const event = document.createEvent('CustomEvent');
20 - event.initCustomEvent(type, true, true);
21 - if (data != null) {
22 - Object.entries(data).forEach(([key, value]) => {
23 - event[key] = value;
24 - });
25 - }
26 - return event;
27 -};
28 -
29 -const createKeyboardEvent = (type, data) => {
30 - return new KeyboardEvent(type, {
31 - bubbles: true,
32 - cancelable: true,
33 - ...data,
34 - });
35 -};
36 -
37 -const modulesInit = () => {
27 +const initializeModules = hasPointerEvents => {
28 + setPointerEvent(hasPointerEvents);
29 + jest.resetModules();
30 ReactFeatureFlags = require('shared/ReactFeatureFlags');
31 ReactFeatureFlags.enableFlareAPI = true;
32 React = require('react');
@@ -44,13 +36,14 @@ const modulesInit = () => {
36 .useFocusWithinResponder;
37 };
38
47 -describe('FocusWithin event responder', () => {
39 +const forcePointerEvents = true;
40 +const table = [[forcePointerEvents], [!forcePointerEvents]];
41 +
42 +describe.each(table)('FocusWithin responder', hasPointerEvents => {
43 let container;
44
45 beforeEach(() => {
51 - jest.resetModules();
52 - modulesInit();
53 -
46 + initializeModules();
47 container = document.createElement('div');
48 document.body.appendChild(container);
49 });
@@ -80,8 +73,9 @@ describe('FocusWithin event responder', () => {
73 });
74
75 it('prevents custom events being dispatched', () => {
83 - ref.current.dispatchEvent(createEvent('focus'));
84 - ref.current.dispatchEvent(createEvent('blur'));
76 + const target = ref.current;
77 + target.dispatchEvent(focus());
78 + target.dispatchEvent(blur());
79 expect(onFocusWithinChange).not.toBeCalled();
80 expect(onFocusWithinVisibleChange).not.toBeCalled();
81 });
@@ -110,49 +104,42 @@ describe('FocusWithin event responder', () => {
104 });
105
106 it('is called after "blur" and "focus" events on focus target', () => {
113 - ref.current.dispatchEvent(createEvent('focus'));
107 + const target = ref.current;
108 + target.dispatchEvent(focus());
109 expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
110 expect(onFocusWithinChange).toHaveBeenCalledWith(true);
116 - ref.current.dispatchEvent(
117 - createEvent('blur', {relatedTarget: container}),
118 - );
111 + target.dispatchEvent(blur({relatedTarget: container}));
112 expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
113 expect(onFocusWithinChange).toHaveBeenCalledWith(false);
114 });
115
116 it('is called after "blur" and "focus" events on descendants', () => {
124 - innerRef.current.dispatchEvent(createEvent('focus'));
117 + const target = innerRef.current;
118 + target.dispatchEvent(focus());
119 expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
120 expect(onFocusWithinChange).toHaveBeenCalledWith(true);
127 - innerRef.current.dispatchEvent(
128 - createEvent('blur', {relatedTarget: container}),
129 - );
121 + target.dispatchEvent(blur({relatedTarget: container}));
122 expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
123 expect(onFocusWithinChange).toHaveBeenCalledWith(false);
124 });
125
126 it('is only called once when focus moves within and outside the subtree', () => {
127 + const target = ref.current;
128 + const innerTarget1 = innerRef.current;
129 + const innerTarget2 = innerRef2.current;
130 // focus shifts into subtree
136 - innerRef.current.dispatchEvent(createEvent('focus'));
131 + innerTarget1.dispatchEvent(focus());
132 expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
133 expect(onFocusWithinChange).toHaveBeenCalledWith(true);
134 // focus moves around subtree
140 - innerRef.current.dispatchEvent(
141 - createEvent('blur', {relatedTarget: innerRef2.current}),
142 - );
143 - innerRef2.current.dispatchEvent(createEvent('focus'));
144 - innerRef2.current.dispatchEvent(
145 - createEvent('blur', {relatedTarget: ref.current}),
146 - );
147 - ref.current.dispatchEvent(createEvent('focus'));
148 - ref.current.dispatchEvent(
149 - createEvent('blur', {relatedTarget: innerRef.current}),
150 - );
135 + innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2}));
136 + innerTarget2.dispatchEvent(focus());
137 + innerTarget2.dispatchEvent(blur({relatedTarget: target}));
138 + target.dispatchEvent(focus());
139 + target.dispatchEvent(blur({relatedTarget: innerTarget1}));
140 expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
141 // focus shifts outside subtree
153 - innerRef.current.dispatchEvent(
154 - createEvent('blur', {relatedTarget: container}),
155 - );
142 + innerTarget1.dispatchEvent(blur({relatedTarget: container}));
143 expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
144 expect(onFocusWithinChange).toHaveBeenCalledWith(false);
145 });
@@ -181,102 +168,87 @@ describe('FocusWithin event responder', () => {
168 });
169
170 it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
171 + const target = ref.current;
172 // use keyboard first
185 - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
186 - ref.current.dispatchEvent(createEvent('focus'));
173 + container.dispatchEvent(keydown({key: 'Tab'}));
174 + target.dispatchEvent(focus());
175 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
176 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
189 - ref.current.dispatchEvent(
190 - createEvent('blur', {relatedTarget: container}),
191 - );
177 + target.dispatchEvent(blur({relatedTarget: container}));
178 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
179 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
180 });
181
182 it('is called after "focus" and "blur" on descendants if keyboard was used', () => {
183 + const innerTarget = innerRef.current;
184 // use keyboard first
198 - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
199 - innerRef.current.dispatchEvent(createEvent('focus'));
185 + container.dispatchEvent(keydown({key: 'Tab'}));
186 + innerTarget.dispatchEvent(focus());
187 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
188 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
202 - innerRef.current.dispatchEvent(
203 - createEvent('blur', {relatedTarget: container}),
204 - );
189 + innerTarget.dispatchEvent(blur({relatedTarget: container}));
190 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
191 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
192 });
193
194 it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
195 + const target = ref.current;
196 + const innerTarget1 = innerRef.current;
197 + const innerTarget2 = innerRef2.current;
198 // use keyboard first
211 - ref.current.dispatchEvent(createEvent('focus'));
212 - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
213 - ref.current.dispatchEvent(
214 - createEvent('blur', {relatedTarget: innerRef.current}),
215 - );
216 - innerRef.current.dispatchEvent(createEvent('focus'));
199 + target.dispatchEvent(focus());
200 + target.dispatchEvent(keydown({key: 'Tab'}));
201 + target.dispatchEvent(blur({relatedTarget: innerTarget1}));
202 + innerTarget1.dispatchEvent(focus());
203 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
204 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
205 // then use pointer on the next target, focus should no longer be visible
220 - innerRef2.current.dispatchEvent(createEvent('pointerdown'));
221 - innerRef.current.dispatchEvent(
222 - createEvent('blur', {relatedTarget: innerRef2.current}),
223 - );
224 - innerRef2.current.dispatchEvent(createEvent('focus'));
206 + dispatchPointerPressDown(innerTarget2);
207 + innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2}));
208 + innerTarget2.dispatchEvent(focus());
209 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
210 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
211 // then use keyboard again
228 - innerRef2.current.dispatchEvent(
229 - createKeyboardEvent('keydown', {key: 'Tab', shiftKey: true}),
230 - );
231 - innerRef2.current.dispatchEvent(
232 - createEvent('blur', {relatedTarget: innerRef.current}),
233 - );
234 - innerRef.current.dispatchEvent(createEvent('focus'));
212 + innerTarget2.dispatchEvent(keydown({key: 'Tab', shiftKey: true}));
213 + innerTarget2.dispatchEvent(blur({relatedTarget: innerTarget1}));
214 + innerTarget1.dispatchEvent(focus());
215 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3);
216 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
217 // then use pointer on the target, focus should no longer be visible
238 - innerRef.current.dispatchEvent(createEvent('pointerdown'));
218 + dispatchPointerPressDown(innerTarget1);
219 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
220 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
221 // onFocusVisibleChange should not be called again
242 - innerRef.current.dispatchEvent(
243 - createEvent('blur', {relatedTarget: container}),
244 - );
222 + innerTarget1.dispatchEvent(blur({relatedTarget: container}));
223 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
224 });
225
226 it('is not called after "focus" and "blur" events without keyboard', () => {
249 - innerRef.current.dispatchEvent(createEvent('pointerdown'));
250 - innerRef.current.dispatchEvent(createEvent('focus'));
251 - container.dispatchEvent(createEvent('pointerdown'));
252 - innerRef.current.dispatchEvent(
253 - createEvent('blur', {relatedTarget: container}),
254 - );
227 + const innerTarget = innerRef.current;
228 + dispatchPointerPressDown(innerTarget);
229 + dispatchPointerPressRelease(innerTarget);
230 + innerTarget.dispatchEvent(blur({relatedTarget: container}));
231 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0);
232 });
233
234 it('is only called once when focus moves within and outside the subtree', () => {
235 + const target = ref.current;
236 + const innerTarget1 = innerRef.current;
237 + const innerTarget2 = innerRef2.current;
238 +
239 // focus shifts into subtree
260 - innerRef.current.dispatchEvent(createEvent('focus'));
240 + innerTarget1.dispatchEvent(focus());
241 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
242 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
243 // focus moves around subtree
264 - innerRef.current.dispatchEvent(
265 - createEvent('blur', {relatedTarget: innerRef2.current}),
266 - );
267 - innerRef2.current.dispatchEvent(createEvent('focus'));
268 - innerRef2.current.dispatchEvent(
269 - createEvent('blur', {relatedTarget: ref.current}),
270 - );
271 - ref.current.dispatchEvent(createEvent('focus'));
272 - ref.current.dispatchEvent(
273 - createEvent('blur', {relatedTarget: innerRef.current}),
274 - );
244 + innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2}));
245 + innerTarget2.dispatchEvent(focus());
246 + innerTarget2.dispatchEvent(blur({relatedTarget: target}));
247 + target.dispatchEvent(focus());
248 + target.dispatchEvent(blur({relatedTarget: innerTarget1}));
249 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
250 // focus shifts outside subtree
277 - innerRef.current.dispatchEvent(
278 - createEvent('blur', {relatedTarget: container}),
279 - );
251 + innerTarget1.dispatchEvent(blur({relatedTarget: container}));
252 expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
253 expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
254 });
packages/react-events/src/dom/__tests__/Hover-test.internal.js
+52 -33
@@ -76,9 +76,10 @@ describe.each(table)('Hover responder', hasPointerEvents => {
76 ReactDOM.render(<Component />, container);
77 });
78
79 - it('prevents custom events being dispatched', () => {
80 - dispatchPointerHoverEnter(ref);
81 - dispatchPointerHoverExit(ref);
79 + it('does not call callbacks', () => {
80 + const target = ref.current;
81 + dispatchPointerHoverEnter(target);
82 + dispatchPointerHoverExit(target);
83 expect(onHoverChange).not.toBeCalled();
84 expect(onHoverStart).not.toBeCalled();
85 expect(onHoverMove).not.toBeCalled();
@@ -102,18 +103,21 @@ describe.each(table)('Hover responder', hasPointerEvents => {
103 });
104
105 it('is called for mouse pointers', () => {
105 - dispatchPointerHoverEnter(ref);
106 + const target = ref.current;
107 + dispatchPointerHoverEnter(target);
108 expect(onHoverStart).toHaveBeenCalledTimes(1);
109 });
110
111 it('is not called for touch pointers', () => {
110 - dispatchTouchTap(ref);
112 + const target = ref.current;
113 + dispatchTouchTap(target);
114 expect(onHoverStart).not.toBeCalled();
115 });
116
117 it('is called if a mouse pointer is used after a touch pointer', () => {
115 - dispatchTouchTap(ref);
116 - dispatchPointerHoverEnter(ref);
118 + const target = ref.current;
119 + dispatchTouchTap(target);
120 + dispatchPointerHoverEnter(target);
121 expect(onHoverStart).toHaveBeenCalledTimes(1);
122 });
123 });
@@ -134,16 +138,18 @@ describe.each(table)('Hover responder', hasPointerEvents => {
138 });
139
140 it('is called for mouse pointers', () => {
137 - dispatchPointerHoverEnter(ref);
141 + const target = ref.current;
142 + dispatchPointerHoverEnter(target);
143 expect(onHoverChange).toHaveBeenCalledTimes(1);
144 expect(onHoverChange).toHaveBeenCalledWith(true);
140 - dispatchPointerHoverExit(ref);
145 + dispatchPointerHoverExit(target);
146 expect(onHoverChange).toHaveBeenCalledTimes(2);
147 expect(onHoverChange).toHaveBeenCalledWith(false);
148 });
149
150 it('is not called for touch pointers', () => {
146 - dispatchTouchTap(ref);
151 + const target = ref.current;
152 + dispatchTouchTap(target);
153 expect(onHoverChange).not.toBeCalled();
154 });
155 });
@@ -164,28 +170,31 @@ describe.each(table)('Hover responder', hasPointerEvents => {
170 });
171
172 it('is called for mouse pointers', () => {
167 - dispatchPointerHoverEnter(ref);
168 - dispatchPointerHoverExit(ref);
173 + const target = ref.current;
174 + dispatchPointerHoverEnter(target);
175 + dispatchPointerHoverExit(target);
176 expect(onHoverEnd).toHaveBeenCalledTimes(1);
177 });
178
179 if (hasPointerEvents) {
180 it('is called once for cancelled mouse pointers', () => {
174 - dispatchPointerHoverEnter(ref);
175 - dispatchPointerCancel(ref);
181 + const target = ref.current;
182 + dispatchPointerHoverEnter(target);
183 + dispatchPointerCancel(target);
184 expect(onHoverEnd).toHaveBeenCalledTimes(1);
185
186 // only called once if cancel follows exit
187 onHoverEnd.mockReset();
180 - dispatchPointerHoverEnter(ref);
181 - dispatchPointerHoverExit(ref);
182 - dispatchPointerCancel(ref);
188 + dispatchPointerHoverEnter(target);
189 + dispatchPointerHoverExit(target);
190 + dispatchPointerCancel(target);
191 expect(onHoverEnd).toHaveBeenCalledTimes(1);
192 });
193 }
194
195 it('is not called for touch pointers', () => {
188 - dispatchTouchTap(ref);
196 + const target = ref.current;
197 + dispatchTouchTap(target);
198 expect(onHoverEnd).not.toBeCalled();
199 });
200 });
@@ -201,8 +210,10 @@ describe.each(table)('Hover responder', hasPointerEvents => {
210 return <div ref={ref} listeners={listener} />;
211 };
212 ReactDOM.render(<Component />, container);
204 - dispatchPointerHoverEnter(ref);
205 - dispatchPointerHoverMove(ref, {from: {x: 0, y: 0}, to: {x: 1, y: 1}});
213 +
214 + const target = ref.current;
215 + dispatchPointerHoverEnter(target);
216 + dispatchPointerHoverMove(target, {from: {x: 0, y: 0}, to: {x: 1, y: 1}});
217 expect(onHoverMove).toHaveBeenCalledTimes(2);
218 expect(onHoverMove).toHaveBeenCalledWith(
219 expect.objectContaining({type: 'hovermove'}),
@@ -242,12 +253,15 @@ describe.each(table)('Hover responder', hasPointerEvents => {
253 };
254 ReactDOM.render(<Outer />, container);
255
245 - dispatchPointerHoverEnter(outerRef, {relatedTarget: container});
246 - dispatchPointerHoverExit(outerRef, {relatedTarget: innerRef.current});
247 - dispatchPointerHoverEnter(innerRef, {relatedTarget: outerRef.current});
248 - dispatchPointerHoverExit(innerRef, {relatedTarget: outerRef.current});
249 - dispatchPointerHoverEnter(outerRef, {relatedTarget: innerRef.current});
250 - dispatchPointerHoverExit(outerRef, {relatedTarget: container});
256 + const innerTarget = innerRef.current;
257 + const outerTarget = outerRef.current;
258 +
259 + dispatchPointerHoverEnter(outerTarget, {relatedTarget: container});
260 + dispatchPointerHoverExit(outerTarget, {relatedTarget: innerTarget});
261 + dispatchPointerHoverEnter(innerTarget, {relatedTarget: outerTarget});
262 + dispatchPointerHoverExit(innerTarget, {relatedTarget: outerTarget});
263 + dispatchPointerHoverEnter(outerTarget, {relatedTarget: innerTarget});
264 + dispatchPointerHoverExit(outerTarget, {relatedTarget: container});
265
266 expect(events).toEqual([
267 'outer: onHoverStart',
@@ -300,9 +314,14 @@ describe.each(table)('Hover responder', hasPointerEvents => {
314 };
315 ReactDOM.render(<Component />, container);
316
303 - dispatchPointerHoverEnter(ref, {x: 10, y: 10});
304 - dispatchPointerHoverMove(ref, {from: {x: 10, y: 10}, to: {x: 20, y: 20}});
305 - dispatchPointerHoverExit(ref, {x: 20, y: 20});
317 + const target = ref.current;
318 +
319 + dispatchPointerHoverEnter(target, {x: 10, y: 10});
320 + dispatchPointerHoverMove(target, {
321 + from: {x: 10, y: 10},
322 + to: {x: 20, y: 20},
323 + });
324 + dispatchPointerHoverExit(target, {x: 20, y: 20});
325
326 expect(eventLog).toEqual([
327 {
@@ -312,7 +331,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
331 pageY: 10,
332 clientX: 10,
333 clientY: 10,
315 - target: ref.current,
334 + target,
335 timeStamp: timeStamps[0],
336 type: 'hoverstart',
337 pointerType: 'mouse',
@@ -324,7 +343,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
343 pageY: 10,
344 clientX: 10,
345 clientY: 10,
327 - target: ref.current,
346 + target,
347 timeStamp: timeStamps[1],
348 type: 'hovermove',
349 pointerType: 'mouse',
@@ -336,7 +355,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
355 pageY: 20,
356 clientX: 20,
357 clientY: 20,
339 - target: ref.current,
358 + target,
359 timeStamp: timeStamps[2],
360 type: 'hovermove',
361 pointerType: 'mouse',
@@ -348,7 +367,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
367 pageY: 20,
368 clientX: 20,
369 clientY: 20,
351 - target: ref.current,
370 + target,
371 timeStamp: timeStamps[3],
372 type: 'hoverend',
373 pointerType: 'mouse',
packages/react-events/src/dom/test-utils.js
+124 -35
@@ -95,6 +95,10 @@ function click(data) {
95 return createEvent('click', data);
96 }
97
98 +function contextmenu(data) {
99 + return createEvent('contextmenu', data);
100 +}
101 +
102 function dragstart(data) {
103 return createEvent('dragstart', data);
104 }
@@ -107,6 +111,14 @@ function gotpointercapture(data) {
111 return createEvent('gotpointercapture', data);
112 }
113
114 +function keydown(data) {
115 + return createKeyboardEvent('keydown', data);
116 +}
117 +
118 +function keyup(data) {
119 + return createKeyboardEvent('keyup', data);
120 +}
121 +
122 function lostpointercapture(data) {
123 return createEvent('lostpointercapture', data);
124 }
@@ -191,8 +203,52 @@ function touchstart(data, id) {
203 * Dispatch high-level event sequences
204 */
205
194 -function dispatchPointerHoverEnter(ref, {relatedTarget, x, y} = {}) {
195 - const dispatch = arg => ref.current.dispatchEvent(arg);
206 +function emptyFunction() {}
207 +
208 +function dispatchLongPressContextMenu(
209 + target,
210 + {preventDefault = emptyFunction} = {},
211 +) {
212 + const dispatch = arg => target.dispatchEvent(arg);
213 + const button = 0;
214 + if (hasPointerEvent()) {
215 + dispatch(pointerdown({button, pointerType: 'touch'}));
216 + }
217 + dispatch(touchstart());
218 + dispatch(contextmenu({button, preventDefault}));
219 +}
220 +
221 +function dispatchRightClickContextMenu(
222 + target,
223 + {preventDefault = emptyFunction} = {},
224 +) {
225 + const dispatch = arg => target.dispatchEvent(arg);
226 + const button = 2;
227 + if (hasPointerEvent()) {
228 + dispatch(pointerdown({button, pointerType: 'mouse'}));
229 + }
230 + dispatch(mousedown({button}));
231 + dispatch(contextmenu({button, preventDefault}));
232 +}
233 +
234 +function dispatchModifiedClickContextMenu(
235 + target,
236 + {preventDefault = emptyFunction} = {},
237 +) {
238 + const dispatch = arg => target.dispatchEvent(arg);
239 + const button = 0;
240 + const ctrlKey = true;
241 + if (hasPointerEvent()) {
242 + dispatch(pointerdown({button, ctrlKey, pointerType: 'mouse'}));
243 + }
244 + dispatch(mousedown({button, ctrlKey}));
245 + if (platform.get() === 'mac') {
246 + dispatch(contextmenu({button, ctrlKey, preventDefault}));
247 + }
248 +}
249 +
250 +function dispatchPointerHoverEnter(target, {relatedTarget, x, y} = {}) {
251 + const dispatch = arg => target.dispatchEvent(arg);
252 const button = -1;
253 const pointerType = 'mouse';
254 const event = {
@@ -211,8 +267,8 @@ function dispatchPointerHoverEnter(ref, {relatedTarget, x, y} = {}) {
267 dispatch(mouseover(event));
268 }
269
214 -function dispatchPointerHoverMove(ref, {from, to} = {}) {
215 - const dispatch = arg => ref.current.dispatchEvent(arg);
270 +function dispatchPointerHoverMove(target, {from, to} = {}) {
271 + const dispatch = arg => target.dispatchEvent(arg);
272 const button = -1;
273 const pointerId = 1;
274 const pointerType = 'mouse';
@@ -233,8 +289,8 @@ function dispatchPointerHoverMove(ref, {from, to} = {}) {
289 dispatchMove({x: to.x, y: to.y});
290 }
291
236 -function dispatchPointerHoverExit(ref, {relatedTarget, x, y} = {}) {
237 - const dispatch = arg => ref.current.dispatchEvent(arg);
292 +function dispatchPointerHoverExit(target, {relatedTarget, x, y} = {}) {
293 + const dispatch = arg => target.dispatchEvent(arg);
294 const button = -1;
295 const pointerType = 'mouse';
296 const event = {
@@ -253,51 +309,82 @@ function dispatchPointerHoverExit(ref, {relatedTarget, x, y} = {}) {
309 dispatch(mouseleave(event));
310 }
311
256 -function dispatchPointerCancel(ref, options) {
257 - const dispatchEvent = arg => ref.current.dispatchEvent(arg);
312 +function dispatchPointerCancel(target, options) {
313 + const dispatchEvent = arg => target.dispatchEvent(arg);
314 dispatchEvent(pointercancel({pointerType: 'mouse'}));
315 dispatchEvent(dragstart({pointerType: 'mouse'}));
316 }
317
262 -function dispatchPointerPressDown(ref, {button = 0, pointerType = 'mouse'}) {
263 - const dispatch = arg => ref.current.dispatchEvent(arg);
318 +function dispatchPointerPressDown(
319 + target,
320 + {button = 0, pointerType = 'mouse'} = {},
321 +) {
322 + const dispatch = arg => target.dispatchEvent(arg);
323 const pointerId = 1;
265 - if (hasPointerEvent()) {
266 - dispatch(pointerover({pointerId, pointerType, button}));
267 - dispatch(pointerenter({pointerId, pointerType, button}));
268 - dispatch(pointerdown({pointerId, pointerType, button}));
269 - }
270 - dispatch(touchstart(null, pointerId));
271 - if (hasPointerEvent()) {
272 - dispatch(gotpointercapture({pointerId, pointerType, button}));
324 + if (pointerType !== 'mouse') {
325 + if (hasPointerEvent()) {
326 + dispatch(pointerover({button, pointerId, pointerType}));
327 + dispatch(pointerenter({button, pointerId, pointerType}));
328 + dispatch(pointerdown({button, pointerId, pointerType}));
329 + }
330 + dispatch(touchstart(null, pointerId));
331 + if (hasPointerEvent()) {
332 + dispatch(gotpointercapture({button, pointerId, pointerType}));
333 + }
334 + } else {
335 + if (hasPointerEvent()) {
336 + dispatch(pointerdown({button, pointerId, pointerType}));
337 + }
338 + dispatch(mousedown({button}));
339 + if (document.activeElement !== target) {
340 + dispatch(focus({button}));
341 + }
342 }
343 }
344
276 -function dispatchPointerPressRelease(ref, {button = 0, pointerType = 'mouse'}) {
277 - const dispatch = arg => ref.current.dispatchEvent(arg);
345 +function dispatchPointerPressRelease(
346 + target,
347 + {button = 0, pointerType = 'mouse'} = {},
348 +) {
349 + const dispatch = arg => target.dispatchEvent(arg);
350 const pointerId = 1;
279 - if (hasPointerEvent()) {
280 - dispatch(pointerup({pointerId, pointerType, button}));
281 - dispatch(lostpointercapture({pointerId, pointerType, button}));
282 - dispatch(pointerout({pointerId, pointerType, button}));
283 - dispatch(pointerleave({pointerId, pointerType, button}));
351 + if (pointerType !== 'mouse') {
352 + if (hasPointerEvent()) {
353 + dispatch(pointerup({button, pointerId, pointerType}));
354 + dispatch(lostpointercapture({button, pointerId, pointerType}));
355 + dispatch(pointerout({button, pointerId, pointerType}));
356 + dispatch(pointerleave({button, pointerId, pointerType}));
357 + }
358 + dispatch(touchend(null, pointerId));
359 + dispatch(mouseover({button}));
360 + dispatch(mousemove({button}));
361 + dispatch(mousedown({button}));
362 + if (document.activeElement !== target) {
363 + dispatch(focus({button}));
364 + }
365 + dispatch(mouseup({button}));
366 + dispatch(click({button}));
367 + } else {
368 + if (hasPointerEvent()) {
369 + dispatch(pointerup({button, pointerId, pointerType}));
370 + }
371 + dispatch(mouseup({button}));
372 + dispatch(click({button}));
373 }
285 - dispatch(touchend(null, pointerId));
286 - dispatch(mouseover({button}));
287 - dispatch(mousemove({button}));
288 - dispatch(mousedown({button}));
289 - dispatch(focus({button}));
290 - dispatch(mouseup({button}));
291 - dispatch(click({button}));
374 }
375
294 -function dispatchTouchTap(ref) {
295 - dispatchPointerPressDown(ref, {pointerType: 'touch'});
296 - dispatchPointerPressRelease(ref, {pointerType: 'touch'});
376 +function dispatchTouchTap(target) {
377 + dispatchPointerPressDown(target, {pointerType: 'touch'});
378 + dispatchPointerPressRelease(target, {pointerType: 'touch'});
379 }
380
381 module.exports = {
382 + blur,
383 + focus,
384 createEvent,
385 + dispatchLongPressContextMenu,
386 + dispatchRightClickContextMenu,
387 + dispatchModifiedClickContextMenu,
388 dispatchPointerCancel,
389 dispatchPointerHoverEnter,
390 dispatchPointerHoverExit,
@@ -305,6 +392,8 @@ module.exports = {
392 dispatchPointerPressDown,
393 dispatchPointerPressRelease,
394 dispatchTouchTap,
395 + keydown,
396 + keyup,
397 platform,
398 hasPointerEvent,
399 setPointerEvent,