@samitouri / QOS-React-2 / commits / 7b28ad119e

[Flare] EventPriority enum (#15823)

* Same as previous commit, but for Flare I don't know what the public API for setting the event priority should be. Right now it accepts a numeric enum, but is this what we want? Maybe it should be a string enum? I've punted on this for now. * Add test for hover events

Andrew Clark committed Jun 4, 2019 at 17:20 UTC 7b28ad119e96c85bdc4c23cc8ff6c9e9f5a8a73c
12 files changed +191 -64
packages/events/ReactSyntheticEventType.js
+1 -6
@@ -9,14 +9,9 @@
9 */
10
11 import type {Fiber} from 'react-reconciler/src/ReactFiber';
12 +import type {EventPriority} from 'shared/ReactTypes';
13 import type {TopLevelType} from './TopLevelEventTypes';
14
14 -export opaque type EventPriority = 0 | 1 | 2;
15 -
16 -export const DiscreteEvent: EventPriority = 0;
17 -export const UserBlockingEvent: EventPriority = 1;
18 -export const ContinuousEvent: EventPriority = 2;
19 -
15 export type DispatchConfig = {
16 dependencies: Array<TopLevelType>,
17 phasedRegistrationNames?: {
packages/react-dom/src/events/DOMEventResponderSystem.js
+43 -13
@@ -23,6 +23,7 @@ import type {
23 ReactEventComponentInstance,
24 ReactResponderContext,
25 ReactResponderEvent,
26 + EventPriority,
27 } from 'shared/ReactTypes';
28 import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
29 import {
@@ -41,6 +42,20 @@ import {
42 } from 'react-reconciler/src/ReactFiberEvents';
43
44 import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
45 +import {
46 + ContinuousEvent,
47 + UserBlockingEvent,
48 + DiscreteEvent,
49 +} from 'shared/ReactTypes';
50 +import {enableUserBlockingEvents} from 'shared/ReactFeatureFlags';
51 +
52 +// Intentionally not named imports because Rollup would use dynamic dispatch for
53 +// CommonJS interop named imports.
54 +import * as Scheduler from 'scheduler';
55 +const {
56 + unstable_UserBlockingPriority: UserBlockingPriority,
57 + unstable_runWithPriority: runWithPriority,
58 +} = Scheduler;
59
60 export let listenToResponderEventTypesImpl;
61
@@ -54,7 +69,7 @@ type EventObjectType = $Shape<PartialEventObject>;
69
70 type EventQueue = {
71 events: Array<EventObjectType>,
57 - discrete: boolean,
72 + eventPriority: EventPriority,
73 };
74
75 type PartialEventObject = {
@@ -107,7 +122,7 @@ const eventResponderContext: ReactResponderContext = {
122 dispatchEvent(
123 possibleEventObject: Object,
124 listener: ($Shape<PartialEventObject>) => void,
110 - discrete: boolean,
125 + eventPriority: EventPriority,
126 ): void {
127 validateResponderContext();
128 const {target, type, timeStamp} = possibleEventObject;
@@ -169,9 +184,7 @@ const eventResponderContext: ReactResponderContext = {
184 PartialEventObject,
185 >);
186 const eventQueue = ((currentEventQueue: any): EventQueue);
172 - if (discrete) {
173 - eventQueue.discrete = true;
174 - }
187 + eventQueue.eventPriority = eventPriority;
188 eventListeners.set(eventObject, listener);
189 eventQueue.events.push(eventObject);
190 },
@@ -585,7 +598,7 @@ function createResponderEvent(
598 function createEventQueue(): EventQueue {
599 return {
600 events: [],
588 - discrete: false,
601 + eventPriority: ContinuousEvent,
602 };
603 }
604
@@ -604,18 +617,35 @@ function processEvents(events: Array<EventObjectType>): void {
617 }
618
619 export function processEventQueue(): void {
607 - const {events, discrete} = ((currentEventQueue: any): EventQueue);
620 + const {events, eventPriority} = ((currentEventQueue: any): EventQueue);
621
622 if (events.length === 0) {
623 return;
624 }
612 - if (discrete) {
613 - flushDiscreteUpdatesIfNeeded(currentTimeStamp);
614 - discreteUpdates(() => {
625 +
626 + switch (eventPriority) {
627 + case DiscreteEvent: {
628 + flushDiscreteUpdatesIfNeeded(currentTimeStamp);
629 + discreteUpdates(() => {
630 + batchedEventUpdates(processEvents, events);
631 + });
632 + break;
633 + }
634 + case UserBlockingEvent: {
635 + if (enableUserBlockingEvents) {
636 + runWithPriority(
637 + UserBlockingPriority,
638 + batchedEventUpdates.bind(null, processEvents, events),
639 + );
640 + } else {
641 + batchedEventUpdates(processEvents, events);
642 + }
643 + break;
644 + }
645 + case ContinuousEvent: {
646 batchedEventUpdates(processEvents, events);
616 - });
617 - } else {
618 - batchedEventUpdates(processEvents, events);
647 + break;
648 + }
649 }
650 }
651
packages/react-dom/src/events/ReactDOMEventListener.js
+1 -1
@@ -52,7 +52,7 @@ import {
52 UserBlockingEvent,
53 ContinuousEvent,
54 DiscreteEvent,
55 -} from 'events/ReactSyntheticEventType';
55 +} from 'shared/ReactTypes';
56
57 const {
58 unstable_UserBlockingPriority: UserBlockingPriority,
packages/react-dom/src/events/SimpleEventPlugin.js
+4 -4
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {EventPriority} from 'shared/ReactTypes';
11 import type {
12 TopLevelType,
13 DOMTopLevelEventType,
@@ -14,18 +15,17 @@ import type {
15 import type {
16 DispatchConfig,
17 ReactSyntheticEvent,
17 - EventPriority,
18 } from 'events/ReactSyntheticEventType';
19 import type {Fiber} from 'react-reconciler/src/ReactFiber';
20 import type {EventTypes, PluginModule} from 'events/PluginModuleType';
21
22 -import {accumulateTwoPhaseDispatches} from 'events/EventPropagators';
23 -import SyntheticEvent from 'events/SyntheticEvent';
22 import {
23 DiscreteEvent,
24 UserBlockingEvent,
25 ContinuousEvent,
28 -} from 'events/ReactSyntheticEventType';
26 +} from 'shared/ReactTypes';
27 +import {accumulateTwoPhaseDispatches} from 'events/EventPropagators';
28 +import SyntheticEvent from 'events/SyntheticEvent';
29
30 import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes';
31 import warningWithoutStack from 'shared/warningWithoutStack';
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+22 -6
@@ -13,6 +13,10 @@ let React;
13 let ReactFeatureFlags;
14 let ReactDOM;
15
16 +// FIXME: What should the public API be for setting an event's priority? Right
17 +// now it's an enum but is that what we want? Hard coding this for now.
18 +const DiscreteEvent = 0;
19 +
20 function createReactEventComponent({
21 targetEventTypes,
22 rootEventTypes,
@@ -403,7 +407,11 @@ describe('DOMEventResponderSystem', () => {
407 phase: 'bubble',
408 timeStamp: context.getTimeStamp(),
409 };
406 - context.dispatchEvent(syntheticEvent, props.onMagicClick, true);
410 + context.dispatchEvent(
411 + syntheticEvent,
412 + props.onMagicClick,
413 + DiscreteEvent,
414 + );
415 }
416 },
417 onEventCapture: (event, context, props) => {
@@ -414,7 +422,11 @@ describe('DOMEventResponderSystem', () => {
422 phase: 'capture',
423 timeStamp: context.getTimeStamp(),
424 };
417 - context.dispatchEvent(syntheticEvent, props.onMagicClick, true);
425 + context.dispatchEvent(
426 + syntheticEvent,
427 + props.onMagicClick,
428 + DiscreteEvent,
429 + );
430 }
431 },
432 });
@@ -456,7 +468,7 @@ describe('DOMEventResponderSystem', () => {
468 phase,
469 timeStamp: context.getTimeStamp(),
470 };
459 - context.dispatchEvent(pressEvent, props.onPress, true);
471 + context.dispatchEvent(pressEvent, props.onPress, DiscreteEvent);
472
473 context.setTimeout(() => {
474 if (props.onLongPress) {
@@ -466,7 +478,11 @@ describe('DOMEventResponderSystem', () => {
478 phase,
479 timeStamp: context.getTimeStamp(),
480 };
469 - context.dispatchEvent(longPressEvent, props.onLongPress, true);
481 + context.dispatchEvent(
482 + longPressEvent,
483 + props.onLongPress,
484 + DiscreteEvent,
485 + );
486 }
487
488 if (props.onLongPressChange) {
@@ -479,7 +495,7 @@ describe('DOMEventResponderSystem', () => {
495 context.dispatchEvent(
496 longPressChangeEvent,
497 props.onLongPressChange,
482 - true,
498 + DiscreteEvent,
499 );
500 }
501 }, 500);
@@ -838,7 +854,7 @@ describe('DOMEventResponderSystem', () => {
854 type: 'click',
855 timeStamp: context.getTimeStamp(),
856 };
841 - context.dispatchEvent(syntheticEvent, props.onClick, true);
857 + context.dispatchEvent(syntheticEvent, props.onClick, DiscreteEvent);
858 },
859 });
860
packages/react-events/src/Drag.js
+15 -7
@@ -10,9 +10,11 @@
10 import type {
11 ReactResponderEvent,
12 ReactResponderContext,
13 + EventPriority,
14 } from 'shared/ReactTypes';
15
16 import React from 'react';
17 +import {DiscreteEvent, UserBlockingEvent} from 'shared/ReactTypes';
18
19 const targetEventTypes = ['pointerdown'];
20 const rootEventTypes = [
@@ -74,12 +76,12 @@ function dispatchDragEvent(
76 name: DragEventType,
77 listener: DragEvent => void,
78 state: DragState,
77 - discrete: boolean,
79 + eventPriority: EventPriority,
80 eventData?: EventData,
81 ): void {
82 const target = ((state.dragTarget: any): Element | Document);
83 const syntheticEvent = createDragEvent(context, name, target, eventData);
82 - context.dispatchEvent(syntheticEvent, listener, discrete);
84 + context.dispatchEvent(syntheticEvent, listener, eventPriority);
85 }
86
87 const DragResponder = {
@@ -130,7 +132,7 @@ const DragResponder = {
132 'dragstart',
133 props.onDragStart,
134 state,
133 - true,
135 + DiscreteEvent,
136 );
137 }
138
@@ -184,7 +186,7 @@ const DragResponder = {
186 'dragchange',
187 dragChangeEventListener,
188 state,
187 - true,
189 + UserBlockingEvent,
190 );
191 }
192 } else {
@@ -203,7 +205,7 @@ const DragResponder = {
205 'dragmove',
206 props.onDragMove,
207 state,
206 - false,
208 + UserBlockingEvent,
209 eventData,
210 );
211 }
@@ -222,7 +224,13 @@ const DragResponder = {
224 context.releaseOwnership();
225 }
226 if (props.onDragEnd) {
225 - dispatchDragEvent(context, 'dragend', props.onDragEnd, state, true);
227 + dispatchDragEvent(
228 + context,
229 + 'dragend',
230 + props.onDragEnd,
231 + state,
232 + DiscreteEvent,
233 + );
234 }
235 if (props.onDragChange) {
236 const dragChangeEventListener = () => {
@@ -233,7 +241,7 @@ const DragResponder = {
241 'dragchange',
242 dragChangeEventListener,
243 state,
236 - true,
244 + UserBlockingEvent,
245 );
246 }
247 state.isDragging = false;
packages/react-events/src/Focus.js
+7 -6
@@ -13,6 +13,7 @@ import type {
13 } from 'shared/ReactTypes';
14
15 import React from 'react';
16 +import {DiscreteEvent} from 'shared/ReactTypes';
17
18 type FocusProps = {
19 disabled: boolean,
@@ -97,7 +98,7 @@ function dispatchFocusInEvents(
98 target,
99 pointerType,
100 );
100 - context.dispatchEvent(syntheticEvent, props.onFocus, true);
101 + context.dispatchEvent(syntheticEvent, props.onFocus, DiscreteEvent);
102 }
103 if (props.onFocusChange) {
104 const listener = () => {
@@ -109,7 +110,7 @@ function dispatchFocusInEvents(
110 target,
111 pointerType,
112 );
112 - context.dispatchEvent(syntheticEvent, listener, true);
113 + context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
114 }
115 if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
116 const listener = () => {
@@ -121,7 +122,7 @@ function dispatchFocusInEvents(
122 target,
123 pointerType,
124 );
124 - context.dispatchEvent(syntheticEvent, listener, true);
125 + context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
126 }
127 }
128
@@ -139,7 +140,7 @@ function dispatchFocusOutEvents(
140 target,
141 pointerType,
142 );
142 - context.dispatchEvent(syntheticEvent, props.onBlur, true);
143 + context.dispatchEvent(syntheticEvent, props.onBlur, DiscreteEvent);
144 }
145 if (props.onFocusChange) {
146 const listener = () => {
@@ -151,7 +152,7 @@ function dispatchFocusOutEvents(
152 target,
153 pointerType,
154 );
154 - context.dispatchEvent(syntheticEvent, listener, true);
155 + context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
156 }
157 dispatchFocusVisibleOutEvent(context, props, state);
158 }
@@ -173,7 +174,7 @@ function dispatchFocusVisibleOutEvent(
174 target,
175 pointerType,
176 );
176 - context.dispatchEvent(syntheticEvent, listener, true);
177 + context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
178 state.isLocalFocusVisible = false;
179 }
180 }
packages/react-events/src/Hover.js
+13 -4
@@ -14,6 +14,7 @@ import type {
14
15 import React from 'react';
16 import {isEventPositionWithinTouchHitTarget} from './utils';
17 +import {UserBlockingEvent} from 'shared/ReactTypes';
18
19 type HoverProps = {
20 disabled: boolean,
@@ -117,7 +118,7 @@ function dispatchHoverChangeEvent(
118 'hoverchange',
119 ((state.hoverTarget: any): Element | Document),
120 );
120 - context.dispatchEvent(syntheticEvent, listener, true);
121 + context.dispatchEvent(syntheticEvent, listener, UserBlockingEvent);
122 }
123
124 function dispatchHoverStartEvents(
@@ -155,7 +156,11 @@ function dispatchHoverStartEvents(
156 'hoverstart',
157 ((target: any): Element | Document),
158 );
158 - context.dispatchEvent(syntheticEvent, props.onHoverStart, true);
159 + context.dispatchEvent(
160 + syntheticEvent,
161 + props.onHoverStart,
162 + UserBlockingEvent,
163 + );
164 }
165 if (props.onHoverChange) {
166 dispatchHoverChangeEvent(event, context, props, state);
@@ -214,7 +219,11 @@ function dispatchHoverEndEvents(
219 'hoverend',
220 ((target: any): Element | Document),
221 );
217 - context.dispatchEvent(syntheticEvent, props.onHoverEnd, true);
222 + context.dispatchEvent(
223 + syntheticEvent,
224 + props.onHoverEnd,
225 + UserBlockingEvent,
226 + );
227 }
228 if (props.onHoverChange) {
229 dispatchHoverChangeEvent(event, context, props, state);
@@ -357,7 +366,7 @@ const HoverResponder = {
366 context.dispatchEvent(
367 syntheticEvent,
368 props.onHoverMove,
360 - true,
369 + UserBlockingEvent,
370 );
371 }
372 }
packages/react-events/src/Press.js
+26 -10
@@ -11,9 +11,11 @@ import type {
11 ReactResponderEvent,
12 ReactResponderContext,
13 } from 'shared/ReactTypes';
14 +import type {EventPriority} from 'shared/ReactTypes';
15
16 import React from 'react';
17 import {isEventPositionWithinTouchHitTarget} from './utils';
18 +import {DiscreteEvent, UserBlockingEvent} from 'shared/ReactTypes';
19
20 type PressProps = {
21 disabled: boolean,
@@ -205,7 +207,7 @@ function dispatchEvent(
207 state: PressState,
208 name: PressEventType,
209 listener: (e: Object) => void,
208 - discrete: boolean,
210 + eventPriority: EventPriority,
211 ): void {
212 const target = ((state.pressTarget: any): Element | Document);
213 const pointerType = state.pointerType;
@@ -216,7 +218,7 @@ function dispatchEvent(
218 pointerType,
219 event,
220 );
219 - context.dispatchEvent(syntheticEvent, listener, discrete);
221 + context.dispatchEvent(syntheticEvent, listener, eventPriority);
222 }
223
224 function dispatchPressChangeEvent(
@@ -229,7 +231,7 @@ function dispatchPressChangeEvent(
231 const listener = () => {
232 props.onPressChange(bool);
233 };
232 - dispatchEvent(event, context, state, 'presschange', listener, true);
234 + dispatchEvent(event, context, state, 'presschange', listener, DiscreteEvent);
235 }
236
237 function dispatchLongPressChangeEvent(
@@ -242,7 +244,14 @@ function dispatchLongPressChangeEvent(
244 const listener = () => {
245 props.onLongPressChange(bool);
246 };
245 - dispatchEvent(event, context, state, 'longpresschange', listener, true);
247 + dispatchEvent(
248 + event,
249 + context,
250 + state,
251 + 'longpresschange',
252 + listener,
253 + DiscreteEvent,
254 + );
255 }
256
257 function activate(event: ReactResponderEvent, context, props, state) {
@@ -261,7 +270,7 @@ function activate(event: ReactResponderEvent, context, props, state) {
270 state,
271 'pressstart',
272 props.onPressStart,
264 - true,
273 + DiscreteEvent,
274 );
275 }
276 if (!wasActivePressed && props.onPressChange) {
@@ -275,7 +284,14 @@ function deactivate(event: ?ReactResponderEvent, context, props, state) {
284 state.isLongPressed = false;
285
286 if (props.onPressEnd) {
278 - dispatchEvent(event, context, state, 'pressend', props.onPressEnd, true);
287 + dispatchEvent(
288 + event,
289 + context,
290 + state,
291 + 'pressend',
292 + props.onPressEnd,
293 + DiscreteEvent,
294 + );
295 }
296 if (props.onPressChange) {
297 dispatchPressChangeEvent(event, context, props, state);
@@ -321,7 +337,7 @@ function dispatchPressStartEvents(
337 state,
338 'longpress',
339 props.onLongPress,
324 - true,
340 + DiscreteEvent,
341 );
342 }
343 if (props.onLongPressChange) {
@@ -684,7 +700,7 @@ const PressResponder = {
700 state,
701 'contextmenu',
702 props.onContextMenu,
687 - true,
703 + DiscreteEvent,
704 );
705 }
706 break;
@@ -763,7 +779,7 @@ const PressResponder = {
779 state,
780 'pressmove',
781 props.onPressMove,
766 - false,
782 + UserBlockingEvent,
783 );
784 }
785 if (
@@ -845,7 +861,7 @@ const PressResponder = {
861 state,
862 'press',
863 props.onPress,
848 - true,
864 + DiscreteEvent,
865 );
866 }
867 }
packages/react-events/src/Swipe.js
+8 -6
@@ -11,8 +11,10 @@ import type {
11 ReactResponderEvent,
12 ReactResponderContext,
13 } from 'shared/ReactTypes';
14 +import type {EventPriority} from 'shared/ReactTypes';
15
16 import React from 'react';
17 +import {UserBlockingEvent, DiscreteEvent} from 'shared/ReactTypes';
18
19 const targetEventTypes = ['pointerdown'];
20 const rootEventTypes = [
@@ -64,12 +66,12 @@ function dispatchSwipeEvent(
66 name: SwipeEventType,
67 listener: SwipeEvent => void,
68 state: SwipeState,
67 - discrete: boolean,
69 + eventPriority: EventPriority,
70 eventData?: EventData,
71 ) {
72 const target = ((state.swipeTarget: any): Element | Document);
73 const syntheticEvent = createSwipeEvent(context, name, target, eventData);
72 - context.dispatchEvent(syntheticEvent, listener, discrete);
74 + context.dispatchEvent(syntheticEvent, listener, eventPriority);
75 }
76
77 type SwipeState = {
@@ -197,7 +199,7 @@ const SwipeResponder = {
199 'swipemove',
200 props.onSwipeMove,
201 state,
200 - false,
202 + UserBlockingEvent,
203 eventData,
204 );
205 (nativeEvent: any).preventDefault();
@@ -226,7 +228,7 @@ const SwipeResponder = {
228 'swipeleft',
229 props.onSwipeLeft,
230 state,
229 - true,
231 + DiscreteEvent,
232 );
233 } else if (props.onSwipeRight && direction === 1) {
234 dispatchSwipeEvent(
@@ -234,7 +236,7 @@ const SwipeResponder = {
236 'swiperight',
237 props.onSwipeRight,
238 state,
237 - true,
239 + DiscreteEvent,
240 );
241 }
242 }
@@ -244,7 +246,7 @@ const SwipeResponder = {
246 'swipeend',
247 props.onSwipeEnd,
248 state,
247 - true,
249 + DiscreteEvent,
250 );
251 }
252 state.lastDirection = direction;
packages/react-events/src/__tests__/Hover-test.internal.js
+44
@@ -12,6 +12,8 @@
12 let React;
13 let ReactFeatureFlags;
14 let ReactDOM;
15 +let TestUtils;
16 +let Scheduler;
17 let Hover;
18
19 const createPointerEvent = (type, data) => {
@@ -229,6 +231,48 @@ describe('Hover event responder', () => {
231 expect(onHoverChange).toHaveBeenCalledTimes(2);
232 expect(onHoverChange).toHaveBeenCalledWith(false);
233 });
234 +
235 + it('should be user-blocking but not discrete', async () => {
236 + // This is currently behind a feature flag
237 + jest.resetModules();
238 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
239 + ReactFeatureFlags.enableEventAPI = true;
240 + ReactFeatureFlags.enableUserBlockingEvents = true;
241 + React = require('react');
242 + ReactDOM = require('react-dom');
243 + TestUtils = require('react-dom/test-utils');
244 + Scheduler = require('scheduler');
245 +
246 + const {act} = TestUtils;
247 + const {useState} = React;
248 +
249 + const newContainer = document.createElement('div');
250 + document.body.appendChild(newContainer);
251 + const root = ReactDOM.unstable_createRoot(newContainer);
252 +
253 + const target = React.createRef(null);
254 + function Foo() {
255 + const [isHover, setHover] = useState(false);
256 + return (
257 + <Hover onHoverChange={setHover}>
258 + <div ref={target}>{isHover ? 'hovered' : 'not hovered'}</div>
259 + </Hover>
260 + );
261 + }
262 +
263 + await act(async () => {
264 + root.render(<Foo />);
265 + });
266 + expect(newContainer.textContent).toEqual('not hovered');
267 +
268 + await act(async () => {
269 + target.current.dispatchEvent(createPointerEvent('mouseover'));
270 +
271 + // 3s should be enough to expire the updates
272 + Scheduler.advanceTime(3000);
273 + expect(newContainer.textContent).toEqual('hovered');
274 + });
275 + });
276 });
277
278 describe('onHoverEnd', () => {
packages/shared/ReactTypes.js
+7 -1
@@ -158,11 +158,17 @@ export type ReactResponderEvent = {
158 passiveSupported: boolean,
159 };
160
161 +export opaque type EventPriority = 0 | 1 | 2;
162 +
163 +export const DiscreteEvent: EventPriority = 0;
164 +export const UserBlockingEvent: EventPriority = 1;
165 +export const ContinuousEvent: EventPriority = 2;
166 +
167 export type ReactResponderContext = {
168 dispatchEvent: (
169 eventObject: Object,
170 listener: (Object) => void,
165 - discrete: boolean,
171 + eventPriority: EventPriority,
172 ) => void,
173 isTargetWithinElement: (
174 childTarget: Element | Document,