[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,