@samitouri / QOS-React-2 / commits / 6b5deeed50

[Events] Add support for events that are both user-blocking and continuous (#15811)

* [Events] Add EventPriority enum React DOM's DispatchConfig for synthetic events has an `isDiscrete` field that affects how updates triggered by an event are scheduled. Events are either discrete or continuous. This commit adds an additional type of configuration where an event has user-blocking priority, but is not discrete. E.g. updates triggered by hover are more important than the default, but they don't need to be processed serially. Because there are now three types of event priority instead of two, I've replaced the `isDiscrete` boolean with an enum: `eventPriority`. This commit implements the new enum value but does not change any behavior. I'll enable it behind a feature flag in the next commit. I've only implemented this in the legacy event system. I'll leave Flare for a follow-up. * enableUserBlockingEvents feature flag Adds a feature flag to increase the priority of events like `mouseover`, without making them discrete.

Andrew Clark committed Jun 4, 2019 at 13:35 UTC 6b5deeed50769a85e207b436244f7ebe78b800b1
11 files changed +250 -104
packages/events/ReactSyntheticEventType.js
+7 -1
@@ -11,6 +11,12 @@
11 import type {Fiber} from 'react-reconciler/src/ReactFiber';
12 import type {TopLevelType} from './TopLevelEventTypes';
13
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 +
20 export type DispatchConfig = {
21 dependencies: Array<TopLevelType>,
22 phasedRegistrationNames?: {
@@ -18,7 +24,7 @@ export type DispatchConfig = {
24 captured: string,
25 },
26 registrationName?: string,
21 - isDiscrete?: boolean,
27 + eventPriority: EventPriority,
28 };
29
30 export type ReactSyntheticEvent = {
packages/react-dom/src/events/ReactDOMEventListener.js
+56 -7
@@ -11,6 +11,10 @@ import type {AnyNativeEvent} from 'events/PluginModuleType';
11 import type {Fiber} from 'react-reconciler/src/ReactFiber';
12 import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
13
14 +// Intentionally not named imports because Rollup would use dynamic dispatch for
15 +// CommonJS interop named imports.
16 +import * as Scheduler from 'scheduler';
17 +
18 import {
19 batchedEventUpdates,
20 discreteUpdates,
@@ -40,9 +44,22 @@ import SimpleEventPlugin from './SimpleEventPlugin';
44 import {getRawEventName} from './DOMTopLevelEventTypes';
45 import {passiveBrowserEventsSupported} from './checkPassiveEvents';
46
43 -import {enableEventAPI} from 'shared/ReactFeatureFlags';
47 +import {
48 + enableEventAPI,
49 + enableUserBlockingEvents,
50 +} from 'shared/ReactFeatureFlags';
51 +import {
52 + UserBlockingEvent,
53 + ContinuousEvent,
54 + DiscreteEvent,
55 +} from 'events/ReactSyntheticEventType';
56
45 -const {isDiscreteTopLevelEventType} = SimpleEventPlugin;
57 +const {
58 + unstable_UserBlockingPriority: UserBlockingPriority,
59 + unstable_runWithPriority: runWithPriority,
60 +} = Scheduler;
61 +
62 +const {getEventPriority} = SimpleEventPlugin;
63
64 const CALLBACK_BOOKKEEPING_POOL_SIZE = 10;
65 const callbackBookkeepingPool = [];
@@ -212,12 +229,29 @@ function trapEventForPluginEventSystem(
229 topLevelType: DOMTopLevelEventType,
230 capture: boolean,
231 ): void {
215 - const dispatch = isDiscreteTopLevelEventType(topLevelType)
216 - ? dispatchDiscreteEvent
217 - : dispatchEvent;
232 + let listener;
233 + switch (getEventPriority(topLevelType)) {
234 + case DiscreteEvent:
235 + listener = dispatchDiscreteEvent.bind(
236 + null,
237 + topLevelType,
238 + PLUGIN_EVENT_SYSTEM,
239 + );
240 + break;
241 + case UserBlockingEvent:
242 + listener = dispatchUserBlockingUpdate.bind(
243 + null,
244 + topLevelType,
245 + PLUGIN_EVENT_SYSTEM,
246 + );
247 + break;
248 + case ContinuousEvent:
249 + default:
250 + listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
251 + break;
252 + }
253 +
254 const rawEventName = getRawEventName(topLevelType);
219 - // Check if discrete and wrap in discreteUpdates
220 - const listener = dispatch.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
255 if (capture) {
256 addEventCaptureListener(element, rawEventName, listener);
257 } else {
@@ -230,6 +264,21 @@ function dispatchDiscreteEvent(topLevelType, eventSystemFlags, nativeEvent) {
264 discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent);
265 }
266
267 +function dispatchUserBlockingUpdate(
268 + topLevelType,
269 + eventSystemFlags,
270 + nativeEvent,
271 +) {
272 + if (enableUserBlockingEvents) {
273 + runWithPriority(
274 + UserBlockingPriority,
275 + dispatchEvent.bind(null, topLevelType, eventSystemFlags, nativeEvent),
276 + );
277 + } else {
278 + dispatchEvent(topLevelType, eventSystemFlags, nativeEvent);
279 + }
280 +}
281 +
282 function dispatchEventForPluginEventSystem(
283 topLevelType: DOMTopLevelEventType,
284 eventSystemFlags: EventSystemFlags,
packages/react-dom/src/events/SimpleEventPlugin.js
+130 -96
@@ -14,12 +14,18 @@ import type {
14 import type {
15 DispatchConfig,
16 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';
24 +import {
25 + DiscreteEvent,
26 + UserBlockingEvent,
27 + ContinuousEvent,
28 +} from 'events/ReactSyntheticEventType';
29
30 import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes';
31 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -55,84 +61,117 @@ import getEventCharCode from './getEventCharCode';
61 * [TOP_ABORT, { sameConfig }],
62 * ]);
63 */
58 -type EventTuple = [DOMTopLevelEventType, string];
59 -const discreteEventTypeNames: Array<EventTuple> = [
60 - [DOMTopLevelEventTypes.TOP_BLUR, 'blur'],
61 - [DOMTopLevelEventTypes.TOP_CANCEL, 'cancel'],
62 - [DOMTopLevelEventTypes.TOP_CLICK, 'click'],
63 - [DOMTopLevelEventTypes.TOP_CLOSE, 'close'],
64 - [DOMTopLevelEventTypes.TOP_CONTEXT_MENU, 'contextMenu'],
65 - [DOMTopLevelEventTypes.TOP_COPY, 'copy'],
66 - [DOMTopLevelEventTypes.TOP_CUT, 'cut'],
67 - [DOMTopLevelEventTypes.TOP_AUX_CLICK, 'auxClick'],
68 - [DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, 'doubleClick'],
69 - [DOMTopLevelEventTypes.TOP_DRAG_END, 'dragEnd'],
70 - [DOMTopLevelEventTypes.TOP_DRAG_START, 'dragStart'],
71 - [DOMTopLevelEventTypes.TOP_DROP, 'drop'],
72 - [DOMTopLevelEventTypes.TOP_FOCUS, 'focus'],
73 - [DOMTopLevelEventTypes.TOP_INPUT, 'input'],
74 - [DOMTopLevelEventTypes.TOP_INVALID, 'invalid'],
75 - [DOMTopLevelEventTypes.TOP_KEY_DOWN, 'keyDown'],
76 - [DOMTopLevelEventTypes.TOP_KEY_PRESS, 'keyPress'],
77 - [DOMTopLevelEventTypes.TOP_KEY_UP, 'keyUp'],
78 - [DOMTopLevelEventTypes.TOP_MOUSE_DOWN, 'mouseDown'],
79 - [DOMTopLevelEventTypes.TOP_MOUSE_UP, 'mouseUp'],
80 - [DOMTopLevelEventTypes.TOP_PASTE, 'paste'],
81 - [DOMTopLevelEventTypes.TOP_PAUSE, 'pause'],
82 - [DOMTopLevelEventTypes.TOP_PLAY, 'play'],
83 - [DOMTopLevelEventTypes.TOP_POINTER_CANCEL, 'pointerCancel'],
84 - [DOMTopLevelEventTypes.TOP_POINTER_DOWN, 'pointerDown'],
85 - [DOMTopLevelEventTypes.TOP_POINTER_UP, 'pointerUp'],
86 - [DOMTopLevelEventTypes.TOP_RATE_CHANGE, 'rateChange'],
87 - [DOMTopLevelEventTypes.TOP_RESET, 'reset'],
88 - [DOMTopLevelEventTypes.TOP_SEEKED, 'seeked'],
89 - [DOMTopLevelEventTypes.TOP_SUBMIT, 'submit'],
90 - [DOMTopLevelEventTypes.TOP_TOUCH_CANCEL, 'touchCancel'],
91 - [DOMTopLevelEventTypes.TOP_TOUCH_END, 'touchEnd'],
92 - [DOMTopLevelEventTypes.TOP_TOUCH_START, 'touchStart'],
93 - [DOMTopLevelEventTypes.TOP_VOLUME_CHANGE, 'volumeChange'],
94 -];
95 -const continuousEventTypeNames: Array<EventTuple> = [
96 - [DOMTopLevelEventTypes.TOP_ABORT, 'abort'],
97 - [DOMTopLevelEventTypes.TOP_ANIMATION_END, 'animationEnd'],
98 - [DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION, 'animationIteration'],
99 - [DOMTopLevelEventTypes.TOP_ANIMATION_START, 'animationStart'],
100 - [DOMTopLevelEventTypes.TOP_CAN_PLAY, 'canPlay'],
101 - [DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH, 'canPlayThrough'],
102 - [DOMTopLevelEventTypes.TOP_DRAG, 'drag'],
103 - [DOMTopLevelEventTypes.TOP_DRAG_ENTER, 'dragEnter'],
104 - [DOMTopLevelEventTypes.TOP_DRAG_EXIT, 'dragExit'],
105 - [DOMTopLevelEventTypes.TOP_DRAG_LEAVE, 'dragLeave'],
106 - [DOMTopLevelEventTypes.TOP_DRAG_OVER, 'dragOver'],
107 - [DOMTopLevelEventTypes.TOP_DURATION_CHANGE, 'durationChange'],
108 - [DOMTopLevelEventTypes.TOP_EMPTIED, 'emptied'],
109 - [DOMTopLevelEventTypes.TOP_ENCRYPTED, 'encrypted'],
110 - [DOMTopLevelEventTypes.TOP_ENDED, 'ended'],
111 - [DOMTopLevelEventTypes.TOP_ERROR, 'error'],
112 - [DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE, 'gotPointerCapture'],
113 - [DOMTopLevelEventTypes.TOP_LOAD, 'load'],
114 - [DOMTopLevelEventTypes.TOP_LOADED_DATA, 'loadedData'],
115 - [DOMTopLevelEventTypes.TOP_LOADED_METADATA, 'loadedMetadata'],
116 - [DOMTopLevelEventTypes.TOP_LOAD_START, 'loadStart'],
117 - [DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE, 'lostPointerCapture'],
118 - [DOMTopLevelEventTypes.TOP_MOUSE_MOVE, 'mouseMove'],
119 - [DOMTopLevelEventTypes.TOP_MOUSE_OUT, 'mouseOut'],
120 - [DOMTopLevelEventTypes.TOP_MOUSE_OVER, 'mouseOver'],
121 - [DOMTopLevelEventTypes.TOP_PLAYING, 'playing'],
122 - [DOMTopLevelEventTypes.TOP_POINTER_MOVE, 'pointerMove'],
123 - [DOMTopLevelEventTypes.TOP_POINTER_OUT, 'pointerOut'],
124 - [DOMTopLevelEventTypes.TOP_POINTER_OVER, 'pointerOver'],
125 - [DOMTopLevelEventTypes.TOP_PROGRESS, 'progress'],
126 - [DOMTopLevelEventTypes.TOP_SCROLL, 'scroll'],
127 - [DOMTopLevelEventTypes.TOP_SEEKING, 'seeking'],
128 - [DOMTopLevelEventTypes.TOP_STALLED, 'stalled'],
129 - [DOMTopLevelEventTypes.TOP_SUSPEND, 'suspend'],
130 - [DOMTopLevelEventTypes.TOP_TIME_UPDATE, 'timeUpdate'],
131 - [DOMTopLevelEventTypes.TOP_TOGGLE, 'toggle'],
132 - [DOMTopLevelEventTypes.TOP_TOUCH_MOVE, 'touchMove'],
133 - [DOMTopLevelEventTypes.TOP_TRANSITION_END, 'transitionEnd'],
134 - [DOMTopLevelEventTypes.TOP_WAITING, 'waiting'],
135 - [DOMTopLevelEventTypes.TOP_WHEEL, 'wheel'],
64 +
65 +type EventTuple = [DOMTopLevelEventType, string, EventPriority];
66 +
67 +const eventTuples: Array<EventTuple> = [
68 + // Discrete events
69 + [DOMTopLevelEventTypes.TOP_BLUR, 'blur', DiscreteEvent],
70 + [DOMTopLevelEventTypes.TOP_CANCEL, 'cancel', DiscreteEvent],
71 + [DOMTopLevelEventTypes.TOP_CLICK, 'click', DiscreteEvent],
72 + [DOMTopLevelEventTypes.TOP_CLOSE, 'close', DiscreteEvent],
73 + [DOMTopLevelEventTypes.TOP_CONTEXT_MENU, 'contextMenu', DiscreteEvent],
74 + [DOMTopLevelEventTypes.TOP_COPY, 'copy', DiscreteEvent],
75 + [DOMTopLevelEventTypes.TOP_CUT, 'cut', DiscreteEvent],
76 + [DOMTopLevelEventTypes.TOP_AUX_CLICK, 'auxClick', DiscreteEvent],
77 + [DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, 'doubleClick', DiscreteEvent],
78 + [DOMTopLevelEventTypes.TOP_DRAG_END, 'dragEnd', DiscreteEvent],
79 + [DOMTopLevelEventTypes.TOP_DRAG_START, 'dragStart', DiscreteEvent],
80 + [DOMTopLevelEventTypes.TOP_DROP, 'drop', DiscreteEvent],
81 + [DOMTopLevelEventTypes.TOP_FOCUS, 'focus', DiscreteEvent],
82 + [DOMTopLevelEventTypes.TOP_INPUT, 'input', DiscreteEvent],
83 + [DOMTopLevelEventTypes.TOP_INVALID, 'invalid', DiscreteEvent],
84 + [DOMTopLevelEventTypes.TOP_KEY_DOWN, 'keyDown', DiscreteEvent],
85 + [DOMTopLevelEventTypes.TOP_KEY_PRESS, 'keyPress', DiscreteEvent],
86 + [DOMTopLevelEventTypes.TOP_KEY_UP, 'keyUp', DiscreteEvent],
87 + [DOMTopLevelEventTypes.TOP_MOUSE_DOWN, 'mouseDown', DiscreteEvent],
88 + [DOMTopLevelEventTypes.TOP_MOUSE_UP, 'mouseUp', DiscreteEvent],
89 + [DOMTopLevelEventTypes.TOP_PASTE, 'paste', DiscreteEvent],
90 + [DOMTopLevelEventTypes.TOP_PAUSE, 'pause', DiscreteEvent],
91 + [DOMTopLevelEventTypes.TOP_PLAY, 'play', DiscreteEvent],
92 + [DOMTopLevelEventTypes.TOP_POINTER_CANCEL, 'pointerCancel', DiscreteEvent],
93 + [DOMTopLevelEventTypes.TOP_POINTER_DOWN, 'pointerDown', DiscreteEvent],
94 + [DOMTopLevelEventTypes.TOP_POINTER_UP, 'pointerUp', DiscreteEvent],
95 + [DOMTopLevelEventTypes.TOP_RATE_CHANGE, 'rateChange', DiscreteEvent],
96 + [DOMTopLevelEventTypes.TOP_RESET, 'reset', DiscreteEvent],
97 + [DOMTopLevelEventTypes.TOP_SEEKED, 'seeked', DiscreteEvent],
98 + [DOMTopLevelEventTypes.TOP_SUBMIT, 'submit', DiscreteEvent],
99 + [DOMTopLevelEventTypes.TOP_TOUCH_CANCEL, 'touchCancel', DiscreteEvent],
100 + [DOMTopLevelEventTypes.TOP_TOUCH_END, 'touchEnd', DiscreteEvent],
101 + [DOMTopLevelEventTypes.TOP_TOUCH_START, 'touchStart', DiscreteEvent],
102 + [DOMTopLevelEventTypes.TOP_VOLUME_CHANGE, 'volumeChange', DiscreteEvent],
103 +
104 + // User-blocking events
105 + [DOMTopLevelEventTypes.TOP_DRAG, 'drag', UserBlockingEvent],
106 + [DOMTopLevelEventTypes.TOP_DRAG_ENTER, 'dragEnter', UserBlockingEvent],
107 + [DOMTopLevelEventTypes.TOP_DRAG_EXIT, 'dragExit', UserBlockingEvent],
108 + [DOMTopLevelEventTypes.TOP_DRAG_LEAVE, 'dragLeave', UserBlockingEvent],
109 + [DOMTopLevelEventTypes.TOP_DRAG_OVER, 'dragOver', UserBlockingEvent],
110 + [DOMTopLevelEventTypes.TOP_MOUSE_MOVE, 'mouseMove', UserBlockingEvent],
111 + [DOMTopLevelEventTypes.TOP_MOUSE_OUT, 'mouseOut', UserBlockingEvent],
112 + [DOMTopLevelEventTypes.TOP_MOUSE_OVER, 'mouseOver', UserBlockingEvent],
113 + [DOMTopLevelEventTypes.TOP_POINTER_MOVE, 'pointerMove', UserBlockingEvent],
114 + [DOMTopLevelEventTypes.TOP_POINTER_OUT, 'pointerOut', UserBlockingEvent],
115 + [DOMTopLevelEventTypes.TOP_POINTER_OVER, 'pointerOver', UserBlockingEvent],
116 + [DOMTopLevelEventTypes.TOP_SCROLL, 'scroll', UserBlockingEvent],
117 + [DOMTopLevelEventTypes.TOP_TOGGLE, 'toggle', UserBlockingEvent],
118 + [DOMTopLevelEventTypes.TOP_TOUCH_MOVE, 'touchMove', UserBlockingEvent],
119 + [DOMTopLevelEventTypes.TOP_WHEEL, 'wheel', UserBlockingEvent],
120 +
121 + // Continuous events
122 + [DOMTopLevelEventTypes.TOP_ABORT, 'abort', ContinuousEvent],
123 + [DOMTopLevelEventTypes.TOP_ANIMATION_END, 'animationEnd', ContinuousEvent],
124 + [
125 + DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION,
126 + 'animationIteration',
127 + ContinuousEvent,
128 + ],
129 + [
130 + DOMTopLevelEventTypes.TOP_ANIMATION_START,
131 + 'animationStart',
132 + ContinuousEvent,
133 + ],
134 + [DOMTopLevelEventTypes.TOP_CAN_PLAY, 'canPlay', ContinuousEvent],
135 + [
136 + DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH,
137 + 'canPlayThrough',
138 + ContinuousEvent,
139 + ],
140 + [
141 + DOMTopLevelEventTypes.TOP_DURATION_CHANGE,
142 + 'durationChange',
143 + ContinuousEvent,
144 + ],
145 + [DOMTopLevelEventTypes.TOP_EMPTIED, 'emptied', ContinuousEvent],
146 + [DOMTopLevelEventTypes.TOP_ENCRYPTED, 'encrypted', ContinuousEvent],
147 + [DOMTopLevelEventTypes.TOP_ENDED, 'ended', ContinuousEvent],
148 + [DOMTopLevelEventTypes.TOP_ERROR, 'error', ContinuousEvent],
149 + [
150 + DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE,
151 + 'gotPointerCapture',
152 + ContinuousEvent,
153 + ],
154 + [DOMTopLevelEventTypes.TOP_LOAD, 'load', ContinuousEvent],
155 + [DOMTopLevelEventTypes.TOP_LOADED_DATA, 'loadedData', ContinuousEvent],
156 + [
157 + DOMTopLevelEventTypes.TOP_LOADED_METADATA,
158 + 'loadedMetadata',
159 + ContinuousEvent,
160 + ],
161 + [DOMTopLevelEventTypes.TOP_LOAD_START, 'loadStart', ContinuousEvent],
162 + [
163 + DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE,
164 + 'lostPointerCapture',
165 + ContinuousEvent,
166 + ],
167 + [DOMTopLevelEventTypes.TOP_PLAYING, 'playing', ContinuousEvent],
168 + [DOMTopLevelEventTypes.TOP_PROGRESS, 'progress', ContinuousEvent],
169 + [DOMTopLevelEventTypes.TOP_SEEKING, 'seeking', ContinuousEvent],
170 + [DOMTopLevelEventTypes.TOP_STALLED, 'stalled', ContinuousEvent],
171 + [DOMTopLevelEventTypes.TOP_SUSPEND, 'suspend', ContinuousEvent],
172 + [DOMTopLevelEventTypes.TOP_TIME_UPDATE, 'timeUpdate', ContinuousEvent],
173 + [DOMTopLevelEventTypes.TOP_TRANSITION_END, 'transitionEnd', ContinuousEvent],
174 + [DOMTopLevelEventTypes.TOP_WAITING, 'waiting', ContinuousEvent],
175 ];
176
177 const eventTypes: EventTypes = {};
@@ -140,32 +179,27 @@ const topLevelEventsToDispatchConfig: {
179 [key: TopLevelType]: DispatchConfig,
180 } = {};
181
143 -function addEventTypeNameToConfig(
144 - [topEvent, event]: EventTuple,
145 - isDiscrete: boolean,
146 -) {
182 +for (let i = 0; i < eventTuples.length; i++) {
183 + const eventTuple = eventTuples[i];
184 + const topEvent = eventTuple[0];
185 + const event = eventTuple[1];
186 + const eventPriority = eventTuple[2];
187 +
188 const capitalizedEvent = event[0].toUpperCase() + event.slice(1);
189 const onEvent = 'on' + capitalizedEvent;
190
150 - const type = {
191 + const config = {
192 phasedRegistrationNames: {
193 bubbled: onEvent,
194 captured: onEvent + 'Capture',
195 },
196 dependencies: [topEvent],
156 - isDiscrete,
197 + eventPriority,
198 };
158 - eventTypes[event] = type;
159 - topLevelEventsToDispatchConfig[topEvent] = type;
199 + eventTypes[event] = config;
200 + topLevelEventsToDispatchConfig[topEvent] = config;
201 }
202
162 -discreteEventTypeNames.forEach(eventTuple => {
163 - addEventTypeNameToConfig(eventTuple, true);
164 -});
165 -continuousEventTypeNames.forEach(eventTuple => {
166 - addEventTypeNameToConfig(eventTuple, false);
167 -});
168 -
203 // Only used in DEV for exhaustiveness validation.
204 const knownHTMLTopLevelTypes: Array<DOMTopLevelEventType> = [
205 DOMTopLevelEventTypes.TOP_ABORT,
@@ -202,13 +236,13 @@ const knownHTMLTopLevelTypes: Array<DOMTopLevelEventType> = [
236 ];
237
238 const SimpleEventPlugin: PluginModule<MouseEvent> & {
205 - isDiscreteTopLevelEventType: (topLevelType: TopLevelType) => boolean,
239 + getEventPriority: (topLevelType: TopLevelType) => EventPriority,
240 } = {
241 eventTypes: eventTypes,
242
209 - isDiscreteTopLevelEventType(topLevelType: TopLevelType): boolean {
243 + getEventPriority(topLevelType: TopLevelType): EventPriority {
244 const config = topLevelEventsToDispatchConfig[topLevelType];
211 - return config !== undefined && config.isDiscrete === true;
245 + return config !== undefined ? config.eventPriority : ContinuousEvent;
246 },
247
248 extractEvents: function(
packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js
+46
@@ -11,6 +11,7 @@
11
12 let React = require('react');
13 let ReactDOM = require('react-dom');
14 +let TestUtils = require('react-dom/test-utils');
15 let ReactFeatureFlags;
16 let Scheduler;
17
@@ -486,6 +487,7 @@ describe('ChangeEventPlugin', () => {
487 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
488 React = require('react');
489 ReactDOM = require('react-dom');
490 + TestUtils = require('react-dom/test-utils');
491 Scheduler = require('scheduler');
492 });
493
@@ -750,5 +752,49 @@ describe('ChangeEventPlugin', () => {
752 expect(ops).toEqual(['render: ']);
753 expect(input.value).toBe('');
754 });
755 +
756 + it('mouse enter/leave should be user-blocking but not discrete', async () => {
757 + // This is currently behind a feature flag
758 + jest.resetModules();
759 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
760 + ReactFeatureFlags.enableUserBlockingEvents = true;
761 + React = require('react');
762 + ReactDOM = require('react-dom');
763 + TestUtils = require('react-dom/test-utils');
764 + Scheduler = require('scheduler');
765 +
766 + const {act} = TestUtils;
767 + const {useState} = React;
768 +
769 + const root = ReactDOM.unstable_createRoot(container);
770 +
771 + const target = React.createRef(null);
772 + function Foo() {
773 + const [isHover, setHover] = useState(false);
774 + return (
775 + <div
776 + ref={target}
777 + onMouseEnter={() => setHover(true)}
778 + onMouseLeave={() => setHover(false)}>
779 + {isHover ? 'hovered' : 'not hovered'}
780 + </div>
781 + );
782 + }
783 +
784 + await act(async () => {
785 + root.render(<Foo />);
786 + });
787 + expect(container.textContent).toEqual('not hovered');
788 +
789 + await act(async () => {
790 + const mouseOverEvent = document.createEvent('MouseEvents');
791 + mouseOverEvent.initEvent('mouseover', true, true);
792 + target.current.dispatchEvent(mouseOverEvent);
793 +
794 + // 3s should be enough to expire the updates
795 + Scheduler.advanceTime(3000);
796 + expect(container.textContent).toEqual('hovered');
797 + });
798 + });
799 });
800 });
packages/shared/ReactFeatureFlags.js
+5
@@ -73,3 +73,8 @@ export const enableJSXTransformAPI = false;
73 export const warnAboutMissingMockScheduler = false;
74 // Temporary flag to revert the fix in #15650
75 export const revertPassiveEffectsChange = false;
76 +
77 +// Changes priority of some events like mousemove to user-blocking priority,
78 +// but without making them discrete. The flag exists in case it causes
79 +// starvation problems.
80 +export const enableUserBlockingEvents = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -36,6 +36,7 @@ export const enableEventAPI = false;
36 export const enableJSXTransformAPI = false;
37 export const warnAboutMissingMockScheduler = true;
38 export const revertPassiveEffectsChange = false;
39 +export const enableUserBlockingEvents = false;
40
41 // Only used in www builds.
42 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -31,6 +31,7 @@ export const enableEventAPI = false;
31 export const enableJSXTransformAPI = false;
32 export const warnAboutMissingMockScheduler = false;
33 export const revertPassiveEffectsChange = false;
34 +export const enableUserBlockingEvents = false;
35
36 // Only used in www builds.
37 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -31,6 +31,7 @@ export const enableEventAPI = false;
31 export const enableJSXTransformAPI = false;
32 export const warnAboutMissingMockScheduler = true;
33 export const revertPassiveEffectsChange = false;
34 +export const enableUserBlockingEvents = false;
35
36 // Only used in www builds.
37 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -31,6 +31,7 @@ export const enableEventAPI = false;
31 export const enableJSXTransformAPI = false;
32 export const warnAboutMissingMockScheduler = false;
33 export const revertPassiveEffectsChange = false;
34 +export const enableUserBlockingEvents = false;
35
36 // Only used in www builds.
37 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -31,6 +31,7 @@ export const disableYielding = false;
31 export const enableEventAPI = true;
32 export const enableJSXTransformAPI = true;
33 export const warnAboutMissingMockScheduler = true;
34 +export const enableUserBlockingEvents = false;
35
36 // Only used in www builds.
37 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -21,6 +21,7 @@ export const {
21 warnAboutShorthandPropertyCollision,
22 warnAboutDeprecatedSetNativeProps,
23 revertPassiveEffectsChange,
24 + enableUserBlockingEvents,
25 } = require('ReactFeatureFlags');
26
27 // In www, we have experimental support for gathering data