Create copy of Legacy event plugins for Modern system (#18637)
Dominic Gannaway committed
Apr 16, 2020 at 16:09 UTC
31977d1bb3eb1b144a45cafa133cfd17341490ab
16 files changed
+3954
-15
packages/react-dom/src/client/ReactDOMClientInjection.js
+23
-15
@@ -12,11 +12,19 @@ import {
12
getInstanceFromNode,
13
getNodeFromInstance,
14
} from './ReactDOMComponentTree';
15
-import BeforeInputEventPlugin from '../events/plugins/LegacyBeforeInputEventPlugin';
16
-import ChangeEventPlugin from '../events/plugins/LegacyChangeEventPlugin';
17
-import EnterLeaveEventPlugin from '../events/plugins/LegacyEnterLeaveEventPlugin';
18
-import SelectEventPlugin from '../events/plugins/LegacySelectEventPlugin';
19
-import SimpleEventPlugin from '../events/plugins/LegacySimpleEventPlugin';
15
+
16
+import LegacyBeforeInputEventPlugin from '../events/plugins/LegacyBeforeInputEventPlugin';
17
+import LegacyChangeEventPlugin from '../events/plugins/LegacyChangeEventPlugin';
18
+import LegacyEnterLeaveEventPlugin from '../events/plugins/LegacyEnterLeaveEventPlugin';
19
+import LegacySelectEventPlugin from '../events/plugins/LegacySelectEventPlugin';
20
+import LegacySimpleEventPlugin from '../events/plugins/LegacySimpleEventPlugin';
21
+
22
+import ModernBeforeInputEventPlugin from '../events/plugins/ModernBeforeInputEventPlugin';
23
+import ModernChangeEventPlugin from '../events/plugins/ModernChangeEventPlugin';
24
+import ModernEnterLeaveEventPlugin from '../events/plugins/ModernEnterLeaveEventPlugin';
25
+import ModernSelectEventPlugin from '../events/plugins/ModernSelectEventPlugin';
26
+import ModernSimpleEventPlugin from '../events/plugins/ModernSimpleEventPlugin';
27
+
28
import {
29
injectEventPluginOrder,
30
injectEventPluginsByName,
@@ -26,11 +34,11 @@ import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
34
35
if (enableModernEventSystem) {
36
injectEventPlugins([
29
- SimpleEventPlugin,
30
- EnterLeaveEventPlugin,
31
- ChangeEventPlugin,
32
- SelectEventPlugin,
33
- BeforeInputEventPlugin,
37
+ ModernSimpleEventPlugin,
38
+ ModernEnterLeaveEventPlugin,
39
+ ModernChangeEventPlugin,
40
+ ModernSelectEventPlugin,
41
+ ModernBeforeInputEventPlugin,
42
]);
43
} else {
44
/**
@@ -65,10 +73,10 @@ if (enableModernEventSystem) {
73
* them).
74
*/
75
injectEventPluginsByName({
68
- SimpleEventPlugin: SimpleEventPlugin,
69
- EnterLeaveEventPlugin: EnterLeaveEventPlugin,
70
- ChangeEventPlugin: ChangeEventPlugin,
71
- SelectEventPlugin: SelectEventPlugin,
72
- BeforeInputEventPlugin: BeforeInputEventPlugin,
76
+ SimpleEventPlugin: LegacySimpleEventPlugin,
77
+ EnterLeaveEventPlugin: LegacyEnterLeaveEventPlugin,
78
+ ChangeEventPlugin: LegacyChangeEventPlugin,
79
+ SelectEventPlugin: LegacySelectEventPlugin,
80
+ BeforeInputEventPlugin: LegacyBeforeInputEventPlugin,
81
});
82
}
packages/react-dom/src/events/plugins/ModernBeforeInputEventPlugin.js
new
+498
@@ -0,0 +1,498 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
9
+
10
+import {canUseDOM} from 'shared/ExecutionEnvironment';
11
+
12
+import {
13
+ TOP_BLUR,
14
+ TOP_COMPOSITION_START,
15
+ TOP_COMPOSITION_END,
16
+ TOP_COMPOSITION_UPDATE,
17
+ TOP_KEY_DOWN,
18
+ TOP_KEY_PRESS,
19
+ TOP_KEY_UP,
20
+ TOP_MOUSE_DOWN,
21
+ TOP_TEXT_INPUT,
22
+ TOP_PASTE,
23
+} from '../DOMTopLevelEventTypes';
24
+import {
25
+ getData as FallbackCompositionStateGetData,
26
+ initialize as FallbackCompositionStateInitialize,
27
+ reset as FallbackCompositionStateReset,
28
+} from '../FallbackCompositionState';
29
+import SyntheticCompositionEvent from '../SyntheticCompositionEvent';
30
+import SyntheticInputEvent from '../SyntheticInputEvent';
31
+import accumulateTwoPhaseListeners from '../accumulateTwoPhaseListeners';
32
+
33
+const END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
34
+const START_KEYCODE = 229;
35
+
36
+const canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window;
37
+
38
+let documentMode = null;
39
+if (canUseDOM && 'documentMode' in document) {
40
+ documentMode = document.documentMode;
41
+}
42
+
43
+// Webkit offers a very useful `textInput` event that can be used to
44
+// directly represent `beforeInput`. The IE `textinput` event is not as
45
+// useful, so we don't use it.
46
+const canUseTextInputEvent =
47
+ canUseDOM && 'TextEvent' in window && !documentMode;
48
+
49
+// In IE9+, we have access to composition events, but the data supplied
50
+// by the native compositionend event may be incorrect. Japanese ideographic
51
+// spaces, for instance (\u3000) are not recorded correctly.
52
+const useFallbackCompositionData =
53
+ canUseDOM &&
54
+ (!canUseCompositionEvent ||
55
+ (documentMode && documentMode > 8 && documentMode <= 11));
56
+
57
+const SPACEBAR_CODE = 32;
58
+const SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
59
+
60
+// Events and their corresponding property names.
61
+const eventTypes = {
62
+ beforeInput: {
63
+ phasedRegistrationNames: {
64
+ bubbled: 'onBeforeInput',
65
+ captured: 'onBeforeInputCapture',
66
+ },
67
+ dependencies: [
68
+ TOP_COMPOSITION_END,
69
+ TOP_KEY_PRESS,
70
+ TOP_TEXT_INPUT,
71
+ TOP_PASTE,
72
+ ],
73
+ },
74
+ compositionEnd: {
75
+ phasedRegistrationNames: {
76
+ bubbled: 'onCompositionEnd',
77
+ captured: 'onCompositionEndCapture',
78
+ },
79
+ dependencies: [
80
+ TOP_BLUR,
81
+ TOP_COMPOSITION_END,
82
+ TOP_KEY_DOWN,
83
+ TOP_KEY_PRESS,
84
+ TOP_KEY_UP,
85
+ TOP_MOUSE_DOWN,
86
+ ],
87
+ },
88
+ compositionStart: {
89
+ phasedRegistrationNames: {
90
+ bubbled: 'onCompositionStart',
91
+ captured: 'onCompositionStartCapture',
92
+ },
93
+ dependencies: [
94
+ TOP_BLUR,
95
+ TOP_COMPOSITION_START,
96
+ TOP_KEY_DOWN,
97
+ TOP_KEY_PRESS,
98
+ TOP_KEY_UP,
99
+ TOP_MOUSE_DOWN,
100
+ ],
101
+ },
102
+ compositionUpdate: {
103
+ phasedRegistrationNames: {
104
+ bubbled: 'onCompositionUpdate',
105
+ captured: 'onCompositionUpdateCapture',
106
+ },
107
+ dependencies: [
108
+ TOP_BLUR,
109
+ TOP_COMPOSITION_UPDATE,
110
+ TOP_KEY_DOWN,
111
+ TOP_KEY_PRESS,
112
+ TOP_KEY_UP,
113
+ TOP_MOUSE_DOWN,
114
+ ],
115
+ },
116
+};
117
+
118
+// Track whether we've ever handled a keypress on the space key.
119
+let hasSpaceKeypress = false;
120
+
121
+/**
122
+ * Return whether a native keypress event is assumed to be a command.
123
+ * This is required because Firefox fires `keypress` events for key commands
124
+ * (cut, copy, select-all, etc.) even though no character is inserted.
125
+ */
126
+function isKeypressCommand(nativeEvent) {
127
+ return (
128
+ (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
129
+ // ctrlKey && altKey is equivalent to AltGr, and is not a command.
130
+ !(nativeEvent.ctrlKey && nativeEvent.altKey)
131
+ );
132
+}
133
+
134
+/**
135
+ * Translate native top level events into event types.
136
+ *
137
+ * @param {string} topLevelType
138
+ * @return {object}
139
+ */
140
+function getCompositionEventType(topLevelType) {
141
+ switch (topLevelType) {
142
+ case TOP_COMPOSITION_START:
143
+ return eventTypes.compositionStart;
144
+ case TOP_COMPOSITION_END:
145
+ return eventTypes.compositionEnd;
146
+ case TOP_COMPOSITION_UPDATE:
147
+ return eventTypes.compositionUpdate;
148
+ }
149
+}
150
+
151
+/**
152
+ * Does our fallback best-guess model think this event signifies that
153
+ * composition has begun?
154
+ *
155
+ * @param {string} topLevelType
156
+ * @param {object} nativeEvent
157
+ * @return {boolean}
158
+ */
159
+function isFallbackCompositionStart(topLevelType, nativeEvent) {
160
+ return topLevelType === TOP_KEY_DOWN && nativeEvent.keyCode === START_KEYCODE;
161
+}
162
+
163
+/**
164
+ * Does our fallback mode think that this event is the end of composition?
165
+ *
166
+ * @param {string} topLevelType
167
+ * @param {object} nativeEvent
168
+ * @return {boolean}
169
+ */
170
+function isFallbackCompositionEnd(topLevelType, nativeEvent) {
171
+ switch (topLevelType) {
172
+ case TOP_KEY_UP:
173
+ // Command keys insert or clear IME input.
174
+ return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
175
+ case TOP_KEY_DOWN:
176
+ // Expect IME keyCode on each keydown. If we get any other
177
+ // code we must have exited earlier.
178
+ return nativeEvent.keyCode !== START_KEYCODE;
179
+ case TOP_KEY_PRESS:
180
+ case TOP_MOUSE_DOWN:
181
+ case TOP_BLUR:
182
+ // Events are not possible without cancelling IME.
183
+ return true;
184
+ default:
185
+ return false;
186
+ }
187
+}
188
+
189
+/**
190
+ * Google Input Tools provides composition data via a CustomEvent,
191
+ * with the `data` property populated in the `detail` object. If this
192
+ * is available on the event object, use it. If not, this is a plain
193
+ * composition event and we have nothing special to extract.
194
+ *
195
+ * @param {object} nativeEvent
196
+ * @return {?string}
197
+ */
198
+function getDataFromCustomEvent(nativeEvent) {
199
+ const detail = nativeEvent.detail;
200
+ if (typeof detail === 'object' && 'data' in detail) {
201
+ return detail.data;
202
+ }
203
+ return null;
204
+}
205
+
206
+/**
207
+ * Check if a composition event was triggered by Korean IME.
208
+ * Our fallback mode does not work well with IE's Korean IME,
209
+ * so just use native composition events when Korean IME is used.
210
+ * Although CompositionEvent.locale property is deprecated,
211
+ * it is available in IE, where our fallback mode is enabled.
212
+ *
213
+ * @param {object} nativeEvent
214
+ * @return {boolean}
215
+ */
216
+function isUsingKoreanIME(nativeEvent) {
217
+ return nativeEvent.locale === 'ko';
218
+}
219
+
220
+// Track the current IME composition status, if any.
221
+let isComposing = false;
222
+
223
+/**
224
+ * @return {?object} A SyntheticCompositionEvent.
225
+ */
226
+function extractCompositionEvent(
227
+ topLevelType,
228
+ targetInst,
229
+ nativeEvent,
230
+ nativeEventTarget,
231
+) {
232
+ let eventType;
233
+ let fallbackData;
234
+
235
+ if (canUseCompositionEvent) {
236
+ eventType = getCompositionEventType(topLevelType);
237
+ } else if (!isComposing) {
238
+ if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
239
+ eventType = eventTypes.compositionStart;
240
+ }
241
+ } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
242
+ eventType = eventTypes.compositionEnd;
243
+ }
244
+
245
+ if (!eventType) {
246
+ return null;
247
+ }
248
+
249
+ if (useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)) {
250
+ // The current composition is stored statically and must not be
251
+ // overwritten while composition continues.
252
+ if (!isComposing && eventType === eventTypes.compositionStart) {
253
+ isComposing = FallbackCompositionStateInitialize(nativeEventTarget);
254
+ } else if (eventType === eventTypes.compositionEnd) {
255
+ if (isComposing) {
256
+ fallbackData = FallbackCompositionStateGetData();
257
+ }
258
+ }
259
+ }
260
+
261
+ const event = SyntheticCompositionEvent.getPooled(
262
+ eventType,
263
+ targetInst,
264
+ nativeEvent,
265
+ nativeEventTarget,
266
+ );
267
+
268
+ if (fallbackData) {
269
+ // Inject data generated from fallback path into the synthetic event.
270
+ // This matches the property of native CompositionEventInterface.
271
+ event.data = fallbackData;
272
+ } else {
273
+ const customData = getDataFromCustomEvent(nativeEvent);
274
+ if (customData !== null) {
275
+ event.data = customData;
276
+ }
277
+ }
278
+
279
+ accumulateTwoPhaseListeners(event);
280
+ return event;
281
+}
282
+
283
+/**
284
+ * @param {TopLevelType} topLevelType Number from `TopLevelType`.
285
+ * @param {object} nativeEvent Native browser event.
286
+ * @return {?string} The string corresponding to this `beforeInput` event.
287
+ */
288
+function getNativeBeforeInputChars(topLevelType: TopLevelType, nativeEvent) {
289
+ switch (topLevelType) {
290
+ case TOP_COMPOSITION_END:
291
+ return getDataFromCustomEvent(nativeEvent);
292
+ case TOP_KEY_PRESS:
293
+ /**
294
+ * If native `textInput` events are available, our goal is to make
295
+ * use of them. However, there is a special case: the spacebar key.
296
+ * In Webkit, preventing default on a spacebar `textInput` event
297
+ * cancels character insertion, but it *also* causes the browser
298
+ * to fall back to its default spacebar behavior of scrolling the
299
+ * page.
300
+ *
301
+ * Tracking at:
302
+ * https://code.google.com/p/chromium/issues/detail?id=355103
303
+ *
304
+ * To avoid this issue, use the keypress event as if no `textInput`
305
+ * event is available.
306
+ */
307
+ const which = nativeEvent.which;
308
+ if (which !== SPACEBAR_CODE) {
309
+ return null;
310
+ }
311
+
312
+ hasSpaceKeypress = true;
313
+ return SPACEBAR_CHAR;
314
+
315
+ case TOP_TEXT_INPUT:
316
+ // Record the characters to be added to the DOM.
317
+ const chars = nativeEvent.data;
318
+
319
+ // If it's a spacebar character, assume that we have already handled
320
+ // it at the keypress level and bail immediately. Android Chrome
321
+ // doesn't give us keycodes, so we need to ignore it.
322
+ if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
323
+ return null;
324
+ }
325
+
326
+ return chars;
327
+
328
+ default:
329
+ // For other native event types, do nothing.
330
+ return null;
331
+ }
332
+}
333
+
334
+/**
335
+ * For browsers that do not provide the `textInput` event, extract the
336
+ * appropriate string to use for SyntheticInputEvent.
337
+ *
338
+ * @param {number} topLevelType Number from `TopLevelEventTypes`.
339
+ * @param {object} nativeEvent Native browser event.
340
+ * @return {?string} The fallback string for this `beforeInput` event.
341
+ */
342
+function getFallbackBeforeInputChars(topLevelType: TopLevelType, nativeEvent) {
343
+ // If we are currently composing (IME) and using a fallback to do so,
344
+ // try to extract the composed characters from the fallback object.
345
+ // If composition event is available, we extract a string only at
346
+ // compositionevent, otherwise extract it at fallback events.
347
+ if (isComposing) {
348
+ if (
349
+ topLevelType === TOP_COMPOSITION_END ||
350
+ (!canUseCompositionEvent &&
351
+ isFallbackCompositionEnd(topLevelType, nativeEvent))
352
+ ) {
353
+ const chars = FallbackCompositionStateGetData();
354
+ FallbackCompositionStateReset();
355
+ isComposing = false;
356
+ return chars;
357
+ }
358
+ return null;
359
+ }
360
+
361
+ switch (topLevelType) {
362
+ case TOP_PASTE:
363
+ // If a paste event occurs after a keypress, throw out the input
364
+ // chars. Paste events should not lead to BeforeInput events.
365
+ return null;
366
+ case TOP_KEY_PRESS:
367
+ /**
368
+ * As of v27, Firefox may fire keypress events even when no character
369
+ * will be inserted. A few possibilities:
370
+ *
371
+ * - `which` is `0`. Arrow keys, Esc key, etc.
372
+ *
373
+ * - `which` is the pressed key code, but no char is available.
374
+ * Ex: 'AltGr + d` in Polish. There is no modified character for
375
+ * this key combination and no character is inserted into the
376
+ * document, but FF fires the keypress for char code `100` anyway.
377
+ * No `input` event will occur.
378
+ *
379
+ * - `which` is the pressed key code, but a command combination is
380
+ * being used. Ex: `Cmd+C`. No character is inserted, and no
381
+ * `input` event will occur.
382
+ */
383
+ if (!isKeypressCommand(nativeEvent)) {
384
+ // IE fires the `keypress` event when a user types an emoji via
385
+ // Touch keyboard of Windows. In such a case, the `char` property
386
+ // holds an emoji character like `\uD83D\uDE0A`. Because its length
387
+ // is 2, the property `which` does not represent an emoji correctly.
388
+ // In such a case, we directly return the `char` property instead of
389
+ // using `which`.
390
+ if (nativeEvent.char && nativeEvent.char.length > 1) {
391
+ return nativeEvent.char;
392
+ } else if (nativeEvent.which) {
393
+ return String.fromCharCode(nativeEvent.which);
394
+ }
395
+ }
396
+ return null;
397
+ case TOP_COMPOSITION_END:
398
+ return useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)
399
+ ? null
400
+ : nativeEvent.data;
401
+ default:
402
+ return null;
403
+ }
404
+}
405
+
406
+/**
407
+ * Extract a SyntheticInputEvent for `beforeInput`, based on either native
408
+ * `textInput` or fallback behavior.
409
+ *
410
+ * @return {?object} A SyntheticInputEvent.
411
+ */
412
+function extractBeforeInputEvent(
413
+ topLevelType,
414
+ targetInst,
415
+ nativeEvent,
416
+ nativeEventTarget,
417
+) {
418
+ let chars;
419
+
420
+ if (canUseTextInputEvent) {
421
+ chars = getNativeBeforeInputChars(topLevelType, nativeEvent);
422
+ } else {
423
+ chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);
424
+ }
425
+
426
+ // If no characters are being inserted, no BeforeInput event should
427
+ // be fired.
428
+ if (!chars) {
429
+ return null;
430
+ }
431
+
432
+ const event = SyntheticInputEvent.getPooled(
433
+ eventTypes.beforeInput,
434
+ targetInst,
435
+ nativeEvent,
436
+ nativeEventTarget,
437
+ );
438
+
439
+ event.data = chars;
440
+ accumulateTwoPhaseListeners(event);
441
+ return event;
442
+}
443
+
444
+/**
445
+ * Create an `onBeforeInput` event to match
446
+ * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
447
+ *
448
+ * This event plugin is based on the native `textInput` event
449
+ * available in Chrome, Safari, Opera, and IE. This event fires after
450
+ * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
451
+ *
452
+ * `beforeInput` is spec'd but not implemented in any browsers, and
453
+ * the `input` event does not provide any useful information about what has
454
+ * actually been added, contrary to the spec. Thus, `textInput` is the best
455
+ * available event to identify the characters that have actually been inserted
456
+ * into the target node.
457
+ *
458
+ * This plugin is also responsible for emitting `composition` events, thus
459
+ * allowing us to share composition fallback code for both `beforeInput` and
460
+ * `composition` event types.
461
+ */
462
+const BeforeInputEventPlugin = {
463
+ eventTypes: eventTypes,
464
+
465
+ extractEvents: function(
466
+ topLevelType,
467
+ targetInst,
468
+ nativeEvent,
469
+ nativeEventTarget,
470
+ eventSystemFlags,
471
+ ) {
472
+ const composition = extractCompositionEvent(
473
+ topLevelType,
474
+ targetInst,
475
+ nativeEvent,
476
+ nativeEventTarget,
477
+ );
478
+
479
+ const beforeInput = extractBeforeInputEvent(
480
+ topLevelType,
481
+ targetInst,
482
+ nativeEvent,
483
+ nativeEventTarget,
484
+ );
485
+
486
+ if (composition === null) {
487
+ return beforeInput;
488
+ }
489
+
490
+ if (beforeInput === null) {
491
+ return composition;
492
+ }
493
+
494
+ return [composition, beforeInput];
495
+ },
496
+};
497
+
498
+export default BeforeInputEventPlugin;
packages/react-dom/src/events/plugins/ModernChangeEventPlugin.js
new
+318
@@ -0,0 +1,318 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {runEventsInBatch} from 'legacy-events/EventBatching';
9
+import SyntheticEvent from 'legacy-events/SyntheticEvent';
10
+import isTextInputElement from '../isTextInputElement';
11
+import {canUseDOM} from 'shared/ExecutionEnvironment';
12
+
13
+import {
14
+ TOP_BLUR,
15
+ TOP_CHANGE,
16
+ TOP_CLICK,
17
+ TOP_FOCUS,
18
+ TOP_INPUT,
19
+ TOP_KEY_DOWN,
20
+ TOP_KEY_UP,
21
+ TOP_SELECTION_CHANGE,
22
+} from '../DOMTopLevelEventTypes';
23
+import getEventTarget from '../getEventTarget';
24
+import isEventSupported from '../isEventSupported';
25
+import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
26
+import {updateValueIfChanged} from '../../client/inputValueTracking';
27
+import {setDefaultValue} from '../../client/ReactDOMInput';
28
+import {enqueueStateRestore} from '../ReactDOMControlledComponent';
29
+
30
+import {
31
+ disableInputAttributeSyncing,
32
+ enableModernEventSystem,
33
+} from 'shared/ReactFeatureFlags';
34
+import accumulateTwoPhaseListeners from '../accumulateTwoPhaseListeners';
35
+import {batchedUpdates} from '../ReactDOMUpdateBatching';
36
+import {dispatchEventsInBatch} from '../DOMModernPluginEventSystem';
37
+
38
+const eventTypes = {
39
+ change: {
40
+ phasedRegistrationNames: {
41
+ bubbled: 'onChange',
42
+ captured: 'onChangeCapture',
43
+ },
44
+ dependencies: [
45
+ TOP_BLUR,
46
+ TOP_CHANGE,
47
+ TOP_CLICK,
48
+ TOP_FOCUS,
49
+ TOP_INPUT,
50
+ TOP_KEY_DOWN,
51
+ TOP_KEY_UP,
52
+ TOP_SELECTION_CHANGE,
53
+ ],
54
+ },
55
+};
56
+
57
+function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
58
+ const event = SyntheticEvent.getPooled(
59
+ eventTypes.change,
60
+ inst,
61
+ nativeEvent,
62
+ target,
63
+ );
64
+ event.type = 'change';
65
+ // Flag this event loop as needing state restore.
66
+ enqueueStateRestore(target);
67
+ accumulateTwoPhaseListeners(event);
68
+ return event;
69
+}
70
+/**
71
+ * For IE shims
72
+ */
73
+let activeElement = null;
74
+let activeElementInst = null;
75
+
76
+/**
77
+ * SECTION: handle `change` event
78
+ */
79
+function shouldUseChangeEvent(elem) {
80
+ const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
81
+ return (
82
+ nodeName === 'select' || (nodeName === 'input' && elem.type === 'file')
83
+ );
84
+}
85
+
86
+function manualDispatchChangeEvent(nativeEvent) {
87
+ const event = createAndAccumulateChangeEvent(
88
+ activeElementInst,
89
+ nativeEvent,
90
+ getEventTarget(nativeEvent),
91
+ );
92
+
93
+ // If change and propertychange bubbled, we'd just bind to it like all the
94
+ // other events and have it go through ReactBrowserEventEmitter. Since it
95
+ // doesn't, we manually listen for the events and so we have to enqueue and
96
+ // process the abstract event manually.
97
+ //
98
+ // Batching is necessary here in order to ensure that all event handlers run
99
+ // before the next rerender (including event handlers attached to ancestor
100
+ // elements instead of directly on the input). Without this, controlled
101
+ // components don't work properly in conjunction with event bubbling because
102
+ // the component is rerendered and the value reverted before all the event
103
+ // handlers can run. See https://github.com/facebook/react/issues/708.
104
+ batchedUpdates(runEventInBatch, event);
105
+}
106
+
107
+function runEventInBatch(event) {
108
+ if (enableModernEventSystem) {
109
+ dispatchEventsInBatch([event]);
110
+ } else {
111
+ runEventsInBatch(event);
112
+ }
113
+}
114
+
115
+function getInstIfValueChanged(targetInst) {
116
+ const targetNode = getNodeFromInstance(targetInst);
117
+ if (updateValueIfChanged(targetNode)) {
118
+ return targetInst;
119
+ }
120
+}
121
+
122
+function getTargetInstForChangeEvent(topLevelType, targetInst) {
123
+ if (topLevelType === TOP_CHANGE) {
124
+ return targetInst;
125
+ }
126
+}
127
+
128
+/**
129
+ * SECTION: handle `input` event
130
+ */
131
+let isInputEventSupported = false;
132
+if (canUseDOM) {
133
+ // IE9 claims to support the input event but fails to trigger it when
134
+ // deleting text, so we ignore its input events.
135
+ isInputEventSupported =
136
+ isEventSupported('input') &&
137
+ (!document.documentMode || document.documentMode > 9);
138
+}
139
+
140
+/**
141
+ * (For IE <=9) Starts tracking propertychange events on the passed-in element
142
+ * and override the value property so that we can distinguish user events from
143
+ * value changes in JS.
144
+ */
145
+function startWatchingForValueChange(target, targetInst) {
146
+ activeElement = target;
147
+ activeElementInst = targetInst;
148
+ activeElement.attachEvent('onpropertychange', handlePropertyChange);
149
+}
150
+
151
+/**
152
+ * (For IE <=9) Removes the event listeners from the currently-tracked element,
153
+ * if any exists.
154
+ */
155
+function stopWatchingForValueChange() {
156
+ if (!activeElement) {
157
+ return;
158
+ }
159
+ activeElement.detachEvent('onpropertychange', handlePropertyChange);
160
+ activeElement = null;
161
+ activeElementInst = null;
162
+}
163
+
164
+/**
165
+ * (For IE <=9) Handles a propertychange event, sending a `change` event if
166
+ * the value of the active element has changed.
167
+ */
168
+function handlePropertyChange(nativeEvent) {
169
+ if (nativeEvent.propertyName !== 'value') {
170
+ return;
171
+ }
172
+ if (getInstIfValueChanged(activeElementInst)) {
173
+ manualDispatchChangeEvent(nativeEvent);
174
+ }
175
+}
176
+
177
+function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) {
178
+ if (topLevelType === TOP_FOCUS) {
179
+ // In IE9, propertychange fires for most input events but is buggy and
180
+ // doesn't fire when text is deleted, but conveniently, selectionchange
181
+ // appears to fire in all of the remaining cases so we catch those and
182
+ // forward the event if the value has changed
183
+ // In either case, we don't want to call the event handler if the value
184
+ // is changed from JS so we redefine a setter for `.value` that updates
185
+ // our activeElementValue variable, allowing us to ignore those changes
186
+ //
187
+ // stopWatching() should be a noop here but we call it just in case we
188
+ // missed a blur event somehow.
189
+ stopWatchingForValueChange();
190
+ startWatchingForValueChange(target, targetInst);
191
+ } else if (topLevelType === TOP_BLUR) {
192
+ stopWatchingForValueChange();
193
+ }
194
+}
195
+
196
+// For IE8 and IE9.
197
+function getTargetInstForInputEventPolyfill(topLevelType, targetInst) {
198
+ if (
199
+ topLevelType === TOP_SELECTION_CHANGE ||
200
+ topLevelType === TOP_KEY_UP ||
201
+ topLevelType === TOP_KEY_DOWN
202
+ ) {
203
+ // On the selectionchange event, the target is just document which isn't
204
+ // helpful for us so just check activeElement instead.
205
+ //
206
+ // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
207
+ // propertychange on the first input event after setting `value` from a
208
+ // script and fires only keydown, keypress, keyup. Catching keyup usually
209
+ // gets it and catching keydown lets us fire an event for the first
210
+ // keystroke if user does a key repeat (it'll be a little delayed: right
211
+ // before the second keystroke). Other input methods (e.g., paste) seem to
212
+ // fire selectionchange normally.
213
+ return getInstIfValueChanged(activeElementInst);
214
+ }
215
+}
216
+
217
+/**
218
+ * SECTION: handle `click` event
219
+ */
220
+function shouldUseClickEvent(elem) {
221
+ // Use the `click` event to detect changes to checkbox and radio inputs.
222
+ // This approach works across all browsers, whereas `change` does not fire
223
+ // until `blur` in IE8.
224
+ const nodeName = elem.nodeName;
225
+ return (
226
+ nodeName &&
227
+ nodeName.toLowerCase() === 'input' &&
228
+ (elem.type === 'checkbox' || elem.type === 'radio')
229
+ );
230
+}
231
+
232
+function getTargetInstForClickEvent(topLevelType, targetInst) {
233
+ if (topLevelType === TOP_CLICK) {
234
+ return getInstIfValueChanged(targetInst);
235
+ }
236
+}
237
+
238
+function getTargetInstForInputOrChangeEvent(topLevelType, targetInst) {
239
+ if (topLevelType === TOP_INPUT || topLevelType === TOP_CHANGE) {
240
+ return getInstIfValueChanged(targetInst);
241
+ }
242
+}
243
+
244
+function handleControlledInputBlur(node) {
245
+ const state = node._wrapperState;
246
+
247
+ if (!state || !state.controlled || node.type !== 'number') {
248
+ return;
249
+ }
250
+
251
+ if (!disableInputAttributeSyncing) {
252
+ // If controlled, assign the value attribute to the current value on blur
253
+ setDefaultValue(node, 'number', node.value);
254
+ }
255
+}
256
+
257
+/**
258
+ * This plugin creates an `onChange` event that normalizes change events
259
+ * across form elements. This event fires at a time when it's possible to
260
+ * change the element's value without seeing a flicker.
261
+ *
262
+ * Supported elements are:
263
+ * - input (see `isTextInputElement`)
264
+ * - textarea
265
+ * - select
266
+ */
267
+const ChangeEventPlugin = {
268
+ eventTypes: eventTypes,
269
+
270
+ _isInputEventSupported: isInputEventSupported,
271
+
272
+ extractEvents: function(
273
+ topLevelType,
274
+ targetInst,
275
+ nativeEvent,
276
+ nativeEventTarget,
277
+ eventSystemFlags,
278
+ ) {
279
+ const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
280
+
281
+ let getTargetInstFunc, handleEventFunc;
282
+ if (shouldUseChangeEvent(targetNode)) {
283
+ getTargetInstFunc = getTargetInstForChangeEvent;
284
+ } else if (isTextInputElement(targetNode)) {
285
+ if (isInputEventSupported) {
286
+ getTargetInstFunc = getTargetInstForInputOrChangeEvent;
287
+ } else {
288
+ getTargetInstFunc = getTargetInstForInputEventPolyfill;
289
+ handleEventFunc = handleEventsForInputEventPolyfill;
290
+ }
291
+ } else if (shouldUseClickEvent(targetNode)) {
292
+ getTargetInstFunc = getTargetInstForClickEvent;
293
+ }
294
+
295
+ if (getTargetInstFunc) {
296
+ const inst = getTargetInstFunc(topLevelType, targetInst);
297
+ if (inst) {
298
+ const event = createAndAccumulateChangeEvent(
299
+ inst,
300
+ nativeEvent,
301
+ nativeEventTarget,
302
+ );
303
+ return event;
304
+ }
305
+ }
306
+
307
+ if (handleEventFunc) {
308
+ handleEventFunc(topLevelType, targetNode, targetInst);
309
+ }
310
+
311
+ // When blurring, set the value attribute for number inputs
312
+ if (topLevelType === TOP_BLUR) {
313
+ handleControlledInputBlur(targetNode);
314
+ }
315
+ },
316
+};
317
+
318
+export default ChangeEventPlugin;
packages/react-dom/src/events/plugins/ModernEnterLeaveEventPlugin.js
new
+192
@@ -0,0 +1,192 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {
9
+ TOP_MOUSE_OUT,
10
+ TOP_MOUSE_OVER,
11
+ TOP_POINTER_OUT,
12
+ TOP_POINTER_OVER,
13
+} from '../DOMTopLevelEventTypes';
14
+import {
15
+ IS_REPLAYED,
16
+ IS_FIRST_ANCESTOR,
17
+} from 'react-dom/src/events/EventSystemFlags';
18
+import SyntheticMouseEvent from '../SyntheticMouseEvent';
19
+import SyntheticPointerEvent from '../SyntheticPointerEvent';
20
+import {
21
+ getClosestInstanceFromNode,
22
+ getNodeFromInstance,
23
+} from '../../client/ReactDOMComponentTree';
24
+import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
25
+import {getNearestMountedFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
26
+import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
27
+import accumulateEnterLeaveListeners from '../accumulateEnterLeaveListeners';
28
+
29
+const eventTypes = {
30
+ mouseEnter: {
31
+ registrationName: 'onMouseEnter',
32
+ dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER],
33
+ },
34
+ mouseLeave: {
35
+ registrationName: 'onMouseLeave',
36
+ dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER],
37
+ },
38
+ pointerEnter: {
39
+ registrationName: 'onPointerEnter',
40
+ dependencies: [TOP_POINTER_OUT, TOP_POINTER_OVER],
41
+ },
42
+ pointerLeave: {
43
+ registrationName: 'onPointerLeave',
44
+ dependencies: [TOP_POINTER_OUT, TOP_POINTER_OVER],
45
+ },
46
+};
47
+
48
+const EnterLeaveEventPlugin = {
49
+ eventTypes: eventTypes,
50
+
51
+ /**
52
+ * For almost every interaction we care about, there will be both a top-level
53
+ * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that
54
+ * we do not extract duplicate events. However, moving the mouse into the
55
+ * browser from outside will not fire a `mouseout` event. In this case, we use
56
+ * the `mouseover` top-level event.
57
+ */
58
+ extractEvents: function(
59
+ topLevelType,
60
+ targetInst,
61
+ nativeEvent,
62
+ nativeEventTarget,
63
+ eventSystemFlags,
64
+ ) {
65
+ const isOverEvent =
66
+ topLevelType === TOP_MOUSE_OVER || topLevelType === TOP_POINTER_OVER;
67
+ const isOutEvent =
68
+ topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT;
69
+
70
+ if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0) {
71
+ const related = nativeEvent.relatedTarget || nativeEvent.fromElement;
72
+ if (related) {
73
+ if (enableModernEventSystem) {
74
+ // Due to the fact we don't add listeners to the document with the
75
+ // modern event system and instead attach listeners to roots, we
76
+ // need to handle the over event case. To ensure this, we just need to
77
+ // make sure the node that we're coming from is managed by React.
78
+ const inst = getClosestInstanceFromNode(related);
79
+ if (inst !== null) {
80
+ return null;
81
+ }
82
+ } else {
83
+ // If this is an over event with a target, then we've already dispatched
84
+ // the event in the out event of the other target. If this is replayed,
85
+ // then it's because we couldn't dispatch against this target previously
86
+ // so we have to do it now instead.
87
+ return null;
88
+ }
89
+ }
90
+ }
91
+
92
+ if (!isOutEvent && !isOverEvent) {
93
+ // Must not be a mouse or pointer in or out - ignoring.
94
+ return null;
95
+ }
96
+
97
+ let win;
98
+ if (nativeEventTarget.window === nativeEventTarget) {
99
+ // `nativeEventTarget` is probably a window object.
100
+ win = nativeEventTarget;
101
+ } else {
102
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
103
+ const doc = nativeEventTarget.ownerDocument;
104
+ if (doc) {
105
+ win = doc.defaultView || doc.parentWindow;
106
+ } else {
107
+ win = window;
108
+ }
109
+ }
110
+
111
+ let from;
112
+ let to;
113
+ if (isOutEvent) {
114
+ from = targetInst;
115
+ const related = nativeEvent.relatedTarget || nativeEvent.toElement;
116
+ to = related ? getClosestInstanceFromNode(related) : null;
117
+ if (to !== null) {
118
+ const nearestMounted = getNearestMountedFiber(to);
119
+ if (
120
+ to !== nearestMounted ||
121
+ (to.tag !== HostComponent && to.tag !== HostText)
122
+ ) {
123
+ to = null;
124
+ }
125
+ }
126
+ } else {
127
+ // Moving to a node from outside the window.
128
+ from = null;
129
+ to = targetInst;
130
+ }
131
+
132
+ if (from === to) {
133
+ // Nothing pertains to our managed components.
134
+ return null;
135
+ }
136
+
137
+ let eventInterface, leaveEventType, enterEventType, eventTypePrefix;
138
+
139
+ if (topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_MOUSE_OVER) {
140
+ eventInterface = SyntheticMouseEvent;
141
+ leaveEventType = eventTypes.mouseLeave;
142
+ enterEventType = eventTypes.mouseEnter;
143
+ eventTypePrefix = 'mouse';
144
+ } else if (
145
+ topLevelType === TOP_POINTER_OUT ||
146
+ topLevelType === TOP_POINTER_OVER
147
+ ) {
148
+ eventInterface = SyntheticPointerEvent;
149
+ leaveEventType = eventTypes.pointerLeave;
150
+ enterEventType = eventTypes.pointerEnter;
151
+ eventTypePrefix = 'pointer';
152
+ }
153
+
154
+ const fromNode = from == null ? win : getNodeFromInstance(from);
155
+ const toNode = to == null ? win : getNodeFromInstance(to);
156
+
157
+ const leave = eventInterface.getPooled(
158
+ leaveEventType,
159
+ from,
160
+ nativeEvent,
161
+ nativeEventTarget,
162
+ );
163
+ leave.type = eventTypePrefix + 'leave';
164
+ leave.target = fromNode;
165
+ leave.relatedTarget = toNode;
166
+
167
+ const enter = eventInterface.getPooled(
168
+ enterEventType,
169
+ to,
170
+ nativeEvent,
171
+ nativeEventTarget,
172
+ );
173
+ enter.type = eventTypePrefix + 'enter';
174
+ enter.target = toNode;
175
+ enter.relatedTarget = fromNode;
176
+
177
+ accumulateEnterLeaveListeners(leave, enter, from, to);
178
+
179
+ if (!enableModernEventSystem) {
180
+ // If we are not processing the first ancestor, then we
181
+ // should not process the same nativeEvent again, as we
182
+ // will have already processed it in the first ancestor.
183
+ if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) {
184
+ return [leave];
185
+ }
186
+ }
187
+
188
+ return [leave, enter];
189
+ },
190
+};
191
+
192
+export default EnterLeaveEventPlugin;
packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js
new
+234
@@ -0,0 +1,234 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {canUseDOM} from 'shared/ExecutionEnvironment';
9
+import SyntheticEvent from 'legacy-events/SyntheticEvent';
10
+import isTextInputElement from '../isTextInputElement';
11
+import shallowEqual from 'shared/shallowEqual';
12
+
13
+import {
14
+ TOP_BLUR,
15
+ TOP_CONTEXT_MENU,
16
+ TOP_DRAG_END,
17
+ TOP_FOCUS,
18
+ TOP_KEY_DOWN,
19
+ TOP_KEY_UP,
20
+ TOP_MOUSE_DOWN,
21
+ TOP_MOUSE_UP,
22
+ TOP_SELECTION_CHANGE,
23
+} from '../DOMTopLevelEventTypes';
24
+import getActiveElement from '../../client/getActiveElement';
25
+import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
26
+import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
27
+import {DOCUMENT_NODE} from '../../shared/HTMLNodeType';
28
+import {isListeningToAllDependencies} from '../DOMEventListenerMap';
29
+import accumulateTwoPhaseListeners from '../accumulateTwoPhaseListeners';
30
+
31
+const skipSelectionChangeEvent =
32
+ canUseDOM && 'documentMode' in document && document.documentMode <= 11;
33
+
34
+const eventTypes = {
35
+ select: {
36
+ phasedRegistrationNames: {
37
+ bubbled: 'onSelect',
38
+ captured: 'onSelectCapture',
39
+ },
40
+ dependencies: [
41
+ TOP_BLUR,
42
+ TOP_CONTEXT_MENU,
43
+ TOP_DRAG_END,
44
+ TOP_FOCUS,
45
+ TOP_KEY_DOWN,
46
+ TOP_KEY_UP,
47
+ TOP_MOUSE_DOWN,
48
+ TOP_MOUSE_UP,
49
+ TOP_SELECTION_CHANGE,
50
+ ],
51
+ },
52
+};
53
+
54
+let activeElement = null;
55
+let activeElementInst = null;
56
+let lastSelection = null;
57
+let mouseDown = false;
58
+
59
+/**
60
+ * Get an object which is a unique representation of the current selection.
61
+ *
62
+ * The return value will not be consistent across nodes or browsers, but
63
+ * two identical selections on the same node will return identical objects.
64
+ *
65
+ * @param {DOMElement} node
66
+ * @return {object}
67
+ */
68
+function getSelection(node) {
69
+ if ('selectionStart' in node && hasSelectionCapabilities(node)) {
70
+ return {
71
+ start: node.selectionStart,
72
+ end: node.selectionEnd,
73
+ };
74
+ } else {
75
+ const win =
76
+ (node.ownerDocument && node.ownerDocument.defaultView) || window;
77
+ const selection = win.getSelection();
78
+ return {
79
+ anchorNode: selection.anchorNode,
80
+ anchorOffset: selection.anchorOffset,
81
+ focusNode: selection.focusNode,
82
+ focusOffset: selection.focusOffset,
83
+ };
84
+ }
85
+}
86
+
87
+/**
88
+ * Get document associated with the event target.
89
+ *
90
+ * @param {object} nativeEventTarget
91
+ * @return {Document}
92
+ */
93
+function getEventTargetDocument(eventTarget) {
94
+ return eventTarget.window === eventTarget
95
+ ? eventTarget.document
96
+ : eventTarget.nodeType === DOCUMENT_NODE
97
+ ? eventTarget
98
+ : eventTarget.ownerDocument;
99
+}
100
+
101
+/**
102
+ * Poll selection to see whether it's changed.
103
+ *
104
+ * @param {object} nativeEvent
105
+ * @param {object} nativeEventTarget
106
+ * @return {?SyntheticEvent}
107
+ */
108
+function constructSelectEvent(nativeEvent, nativeEventTarget) {
109
+ // Ensure we have the right element, and that the user is not dragging a
110
+ // selection (this matches native `select` event behavior). In HTML5, select
111
+ // fires only on input and textarea thus if there's no focused element we
112
+ // won't dispatch.
113
+ const doc = getEventTargetDocument(nativeEventTarget);
114
+
115
+ if (
116
+ mouseDown ||
117
+ activeElement == null ||
118
+ activeElement !== getActiveElement(doc)
119
+ ) {
120
+ return null;
121
+ }
122
+
123
+ // Only fire when selection has actually changed.
124
+ const currentSelection = getSelection(activeElement);
125
+ if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
126
+ lastSelection = currentSelection;
127
+
128
+ const syntheticEvent = SyntheticEvent.getPooled(
129
+ eventTypes.select,
130
+ activeElementInst,
131
+ nativeEvent,
132
+ nativeEventTarget,
133
+ );
134
+
135
+ syntheticEvent.type = 'select';
136
+ syntheticEvent.target = activeElement;
137
+
138
+ accumulateTwoPhaseListeners(syntheticEvent);
139
+
140
+ return syntheticEvent;
141
+ }
142
+
143
+ return null;
144
+}
145
+
146
+/**
147
+ * This plugin creates an `onSelect` event that normalizes select events
148
+ * across form elements.
149
+ *
150
+ * Supported elements are:
151
+ * - input (see `isTextInputElement`)
152
+ * - textarea
153
+ * - contentEditable
154
+ *
155
+ * This differs from native browser implementations in the following ways:
156
+ * - Fires on contentEditable fields as well as inputs.
157
+ * - Fires for collapsed selection.
158
+ * - Fires after user input.
159
+ */
160
+const SelectEventPlugin = {
161
+ eventTypes: eventTypes,
162
+
163
+ extractEvents: function(
164
+ topLevelType,
165
+ targetInst,
166
+ nativeEvent,
167
+ nativeEventTarget,
168
+ eventSystemFlags,
169
+ container,
170
+ ) {
171
+ const containerOrDoc =
172
+ container || getEventTargetDocument(nativeEventTarget);
173
+ // Track whether all listeners exists for this plugin. If none exist, we do
174
+ // not extract events. See #3639.
175
+ if (
176
+ !containerOrDoc ||
177
+ !isListeningToAllDependencies('onSelect', containerOrDoc)
178
+ ) {
179
+ return null;
180
+ }
181
+
182
+ const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
183
+
184
+ switch (topLevelType) {
185
+ // Track the input node that has focus.
186
+ case TOP_FOCUS:
187
+ if (
188
+ isTextInputElement(targetNode) ||
189
+ targetNode.contentEditable === 'true'
190
+ ) {
191
+ activeElement = targetNode;
192
+ activeElementInst = targetInst;
193
+ lastSelection = null;
194
+ }
195
+ break;
196
+ case TOP_BLUR:
197
+ activeElement = null;
198
+ activeElementInst = null;
199
+ lastSelection = null;
200
+ break;
201
+ // Don't fire the event while the user is dragging. This matches the
202
+ // semantics of the native select event.
203
+ case TOP_MOUSE_DOWN:
204
+ mouseDown = true;
205
+ break;
206
+ case TOP_CONTEXT_MENU:
207
+ case TOP_MOUSE_UP:
208
+ case TOP_DRAG_END:
209
+ mouseDown = false;
210
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
211
+ // Chrome and IE fire non-standard event when selection is changed (and
212
+ // sometimes when it hasn't). IE's event fires out of order with respect
213
+ // to key and input events on deletion, so we discard it.
214
+ //
215
+ // Firefox doesn't support selectionchange, so check selection status
216
+ // after each key entry. The selection changes after keydown and before
217
+ // keyup, but we check on keydown as well in the case of holding down a
218
+ // key, when multiple keydown events are fired but only one keyup is.
219
+ // This is also our approach for IE handling, for the reason above.
220
+ case TOP_SELECTION_CHANGE:
221
+ if (skipSelectionChangeEvent) {
222
+ break;
223
+ }
224
+ // falls through
225
+ case TOP_KEY_DOWN:
226
+ case TOP_KEY_UP:
227
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
228
+ }
229
+
230
+ return null;
231
+ },
232
+};
233
+
234
+export default SelectEventPlugin;
packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js
new
+225
@@ -0,0 +1,225 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {
11
+ TopLevelType,
12
+ DOMTopLevelEventType,
13
+} from 'legacy-events/TopLevelEventTypes';
14
+import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
15
+import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
16
+import type {PluginModule} from 'legacy-events/PluginModuleType';
17
+import type {EventSystemFlags} from '../EventSystemFlags';
18
+
19
+import SyntheticEvent from 'legacy-events/SyntheticEvent';
20
+import {IS_TARGET_PHASE_ONLY} from '../EventSystemFlags';
21
+
22
+import * as DOMTopLevelEventTypes from '../DOMTopLevelEventTypes';
23
+import {
24
+ topLevelEventsToDispatchConfig,
25
+ simpleEventPluginEventTypes,
26
+} from '../DOMEventProperties';
27
+
28
+import SyntheticAnimationEvent from '../SyntheticAnimationEvent';
29
+import SyntheticClipboardEvent from '../SyntheticClipboardEvent';
30
+import SyntheticFocusEvent from '../SyntheticFocusEvent';
31
+import SyntheticKeyboardEvent from '../SyntheticKeyboardEvent';
32
+import SyntheticMouseEvent from '../SyntheticMouseEvent';
33
+import SyntheticPointerEvent from '../SyntheticPointerEvent';
34
+import SyntheticDragEvent from '../SyntheticDragEvent';
35
+import SyntheticTouchEvent from '../SyntheticTouchEvent';
36
+import SyntheticTransitionEvent from '../SyntheticTransitionEvent';
37
+import SyntheticUIEvent from '../SyntheticUIEvent';
38
+import SyntheticWheelEvent from '../SyntheticWheelEvent';
39
+import getEventCharCode from '../getEventCharCode';
40
+import accumulateTwoPhaseListeners from '../accumulateTwoPhaseListeners';
41
+import accumulateEventTargetListeners from '../accumulateEventTargetListeners';
42
+
43
+import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
44
+
45
+// Only used in DEV for exhaustiveness validation.
46
+const knownHTMLTopLevelTypes: Array<DOMTopLevelEventType> = [
47
+ DOMTopLevelEventTypes.TOP_ABORT,
48
+ DOMTopLevelEventTypes.TOP_CANCEL,
49
+ DOMTopLevelEventTypes.TOP_CAN_PLAY,
50
+ DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH,
51
+ DOMTopLevelEventTypes.TOP_CLOSE,
52
+ DOMTopLevelEventTypes.TOP_DURATION_CHANGE,
53
+ DOMTopLevelEventTypes.TOP_EMPTIED,
54
+ DOMTopLevelEventTypes.TOP_ENCRYPTED,
55
+ DOMTopLevelEventTypes.TOP_ENDED,
56
+ DOMTopLevelEventTypes.TOP_ERROR,
57
+ DOMTopLevelEventTypes.TOP_INPUT,
58
+ DOMTopLevelEventTypes.TOP_INVALID,
59
+ DOMTopLevelEventTypes.TOP_LOAD,
60
+ DOMTopLevelEventTypes.TOP_LOADED_DATA,
61
+ DOMTopLevelEventTypes.TOP_LOADED_METADATA,
62
+ DOMTopLevelEventTypes.TOP_LOAD_START,
63
+ DOMTopLevelEventTypes.TOP_PAUSE,
64
+ DOMTopLevelEventTypes.TOP_PLAY,
65
+ DOMTopLevelEventTypes.TOP_PLAYING,
66
+ DOMTopLevelEventTypes.TOP_PROGRESS,
67
+ DOMTopLevelEventTypes.TOP_RATE_CHANGE,
68
+ DOMTopLevelEventTypes.TOP_RESET,
69
+ DOMTopLevelEventTypes.TOP_SEEKED,
70
+ DOMTopLevelEventTypes.TOP_SEEKING,
71
+ DOMTopLevelEventTypes.TOP_STALLED,
72
+ DOMTopLevelEventTypes.TOP_SUBMIT,
73
+ DOMTopLevelEventTypes.TOP_SUSPEND,
74
+ DOMTopLevelEventTypes.TOP_TIME_UPDATE,
75
+ DOMTopLevelEventTypes.TOP_TOGGLE,
76
+ DOMTopLevelEventTypes.TOP_VOLUME_CHANGE,
77
+ DOMTopLevelEventTypes.TOP_WAITING,
78
+];
79
+
80
+const SimpleEventPlugin: PluginModule<MouseEvent> = {
81
+ // simpleEventPluginEventTypes gets populated from
82
+ // the DOMEventProperties module.
83
+ eventTypes: simpleEventPluginEventTypes,
84
+ extractEvents: function(
85
+ topLevelType: TopLevelType,
86
+ targetInst: null | Fiber,
87
+ nativeEvent: MouseEvent,
88
+ nativeEventTarget: null | EventTarget,
89
+ eventSystemFlags?: EventSystemFlags,
90
+ targetContainer?: null | EventTarget,
91
+ ): null | ReactSyntheticEvent {
92
+ const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);
93
+ if (!dispatchConfig) {
94
+ return null;
95
+ }
96
+ let EventConstructor;
97
+ switch (topLevelType) {
98
+ case DOMTopLevelEventTypes.TOP_KEY_PRESS:
99
+ // Firefox creates a keypress event for function keys too. This removes
100
+ // the unwanted keypress events. Enter is however both printable and
101
+ // non-printable. One would expect Tab to be as well (but it isn't).
102
+ if (getEventCharCode(nativeEvent) === 0) {
103
+ return null;
104
+ }
105
+ /* falls through */
106
+ case DOMTopLevelEventTypes.TOP_KEY_DOWN:
107
+ case DOMTopLevelEventTypes.TOP_KEY_UP:
108
+ EventConstructor = SyntheticKeyboardEvent;
109
+ break;
110
+ case DOMTopLevelEventTypes.TOP_BLUR:
111
+ case DOMTopLevelEventTypes.TOP_FOCUS:
112
+ case DOMTopLevelEventTypes.TOP_BEFORE_BLUR:
113
+ case DOMTopLevelEventTypes.TOP_AFTER_BLUR:
114
+ EventConstructor = SyntheticFocusEvent;
115
+ break;
116
+ case DOMTopLevelEventTypes.TOP_CLICK:
117
+ // Firefox creates a click event on right mouse clicks. This removes the
118
+ // unwanted click events.
119
+ if (nativeEvent.button === 2) {
120
+ return null;
121
+ }
122
+ /* falls through */
123
+ case DOMTopLevelEventTypes.TOP_AUX_CLICK:
124
+ case DOMTopLevelEventTypes.TOP_DOUBLE_CLICK:
125
+ case DOMTopLevelEventTypes.TOP_MOUSE_DOWN:
126
+ case DOMTopLevelEventTypes.TOP_MOUSE_MOVE:
127
+ case DOMTopLevelEventTypes.TOP_MOUSE_UP:
128
+ // TODO: Disabled elements should not respond to mouse events
129
+ /* falls through */
130
+ case DOMTopLevelEventTypes.TOP_MOUSE_OUT:
131
+ case DOMTopLevelEventTypes.TOP_MOUSE_OVER:
132
+ case DOMTopLevelEventTypes.TOP_CONTEXT_MENU:
133
+ EventConstructor = SyntheticMouseEvent;
134
+ break;
135
+ case DOMTopLevelEventTypes.TOP_DRAG:
136
+ case DOMTopLevelEventTypes.TOP_DRAG_END:
137
+ case DOMTopLevelEventTypes.TOP_DRAG_ENTER:
138
+ case DOMTopLevelEventTypes.TOP_DRAG_EXIT:
139
+ case DOMTopLevelEventTypes.TOP_DRAG_LEAVE:
140
+ case DOMTopLevelEventTypes.TOP_DRAG_OVER:
141
+ case DOMTopLevelEventTypes.TOP_DRAG_START:
142
+ case DOMTopLevelEventTypes.TOP_DROP:
143
+ EventConstructor = SyntheticDragEvent;
144
+ break;
145
+ case DOMTopLevelEventTypes.TOP_TOUCH_CANCEL:
146
+ case DOMTopLevelEventTypes.TOP_TOUCH_END:
147
+ case DOMTopLevelEventTypes.TOP_TOUCH_MOVE:
148
+ case DOMTopLevelEventTypes.TOP_TOUCH_START:
149
+ EventConstructor = SyntheticTouchEvent;
150
+ break;
151
+ case DOMTopLevelEventTypes.TOP_ANIMATION_END:
152
+ case DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION:
153
+ case DOMTopLevelEventTypes.TOP_ANIMATION_START:
154
+ EventConstructor = SyntheticAnimationEvent;
155
+ break;
156
+ case DOMTopLevelEventTypes.TOP_TRANSITION_END:
157
+ EventConstructor = SyntheticTransitionEvent;
158
+ break;
159
+ case DOMTopLevelEventTypes.TOP_SCROLL:
160
+ EventConstructor = SyntheticUIEvent;
161
+ break;
162
+ case DOMTopLevelEventTypes.TOP_WHEEL:
163
+ EventConstructor = SyntheticWheelEvent;
164
+ break;
165
+ case DOMTopLevelEventTypes.TOP_COPY:
166
+ case DOMTopLevelEventTypes.TOP_CUT:
167
+ case DOMTopLevelEventTypes.TOP_PASTE:
168
+ EventConstructor = SyntheticClipboardEvent;
169
+ break;
170
+ case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE:
171
+ case DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE:
172
+ case DOMTopLevelEventTypes.TOP_POINTER_CANCEL:
173
+ case DOMTopLevelEventTypes.TOP_POINTER_DOWN:
174
+ case DOMTopLevelEventTypes.TOP_POINTER_MOVE:
175
+ case DOMTopLevelEventTypes.TOP_POINTER_OUT:
176
+ case DOMTopLevelEventTypes.TOP_POINTER_OVER:
177
+ case DOMTopLevelEventTypes.TOP_POINTER_UP:
178
+ EventConstructor = SyntheticPointerEvent;
179
+ break;
180
+ default:
181
+ if (__DEV__) {
182
+ if (
183
+ knownHTMLTopLevelTypes.indexOf(topLevelType) === -1 &&
184
+ dispatchConfig.customEvent !== true
185
+ ) {
186
+ console.error(
187
+ 'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' +
188
+ 'is likely caused by a bug in React. Please file an issue.',
189
+ topLevelType,
190
+ );
191
+ }
192
+ }
193
+ // HTML Events
194
+ // @see http://www.w3.org/TR/html5/index.html#events-0
195
+ EventConstructor = SyntheticEvent;
196
+ break;
197
+ }
198
+ const event = EventConstructor.getPooled(
199
+ dispatchConfig,
200
+ targetInst,
201
+ nativeEvent,
202
+ nativeEventTarget,
203
+ );
204
+
205
+ // For TargetEvent only accumulation, we do not traverse through
206
+ // the React tree looking for managed React DOM elements that have
207
+ // events. Instead we only check the EventTarget Store Map to see
208
+ // if the container has listeners for the particular phase we're
209
+ // interested in. This is because we attach the native event listener
210
+ // only in the given phase.
211
+ if (
212
+ enableUseEventAPI &&
213
+ eventSystemFlags !== undefined &&
214
+ eventSystemFlags & IS_TARGET_PHASE_ONLY &&
215
+ targetContainer != null
216
+ ) {
217
+ accumulateEventTargetListeners(event, targetContainer);
218
+ } else {
219
+ accumulateTwoPhaseListeners(event, true);
220
+ }
221
+ return event;
222
+ },
223
+};
224
+
225
+export default SimpleEventPlugin;
packages/react-dom/src/events/plugins/__tests__/LegacyBeforeInputEventPlugin-test.js
renamed
packages/react-dom/src/events/plugins/__tests__/LegacyChangeEventPlugin-test.js
renamed
packages/react-dom/src/events/plugins/__tests__/LegacyEnterLeaveEventPlugin-test.js
renamed
packages/react-dom/src/events/plugins/__tests__/LegacySelectEventPlugin-test.js
renamed
packages/react-dom/src/events/plugins/__tests__/LegacySimpleEventPlugin-test.js
renamed
packages/react-dom/src/events/plugins/__tests__/ModernBeforeInputEventPlugin-test.internal.js
new
+828
@@ -0,0 +1,828 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let React;
13
+let ReactDOM;
14
+
15
+describe('BeforeInputEventPlugin', () => {
16
+ let container;
17
+
18
+ function loadReactDOM(envSimulator) {
19
+ jest.resetModules();
20
+ if (envSimulator) {
21
+ envSimulator();
22
+ }
23
+ return require('react-dom');
24
+ }
25
+
26
+ function simulateIE11() {
27
+ document.documentMode = 11;
28
+ window.CompositionEvent = {};
29
+ }
30
+
31
+ function simulateWebkit() {
32
+ window.CompositionEvent = {};
33
+ window.TextEvent = {};
34
+ }
35
+
36
+ function simulateComposition() {
37
+ window.CompositionEvent = {};
38
+ }
39
+
40
+ function simulateNoComposition() {
41
+ // no composition event in Window - will use fallback
42
+ }
43
+
44
+ function simulateEvent(elem, type, data) {
45
+ const event = new Event(type, {bubbles: true});
46
+ Object.assign(event, data);
47
+ elem.dispatchEvent(event);
48
+ }
49
+
50
+ function simulateKeyboardEvent(elem, type, data) {
51
+ const {char, value, ...rest} = data;
52
+ const event = new KeyboardEvent(type, {
53
+ bubbles: true,
54
+ ...rest,
55
+ });
56
+ if (char) {
57
+ event.char = char;
58
+ }
59
+ if (value) {
60
+ elem.value = value;
61
+ }
62
+ elem.dispatchEvent(event);
63
+ }
64
+
65
+ function simulatePaste(elem) {
66
+ const pasteEvent = new Event('paste', {
67
+ bubbles: true,
68
+ });
69
+ pasteEvent.clipboardData = {
70
+ dropEffect: null,
71
+ effectAllowed: null,
72
+ files: null,
73
+ items: null,
74
+ types: null,
75
+ };
76
+ elem.dispatchEvent(pasteEvent);
77
+ }
78
+
79
+ beforeEach(() => {
80
+ React = require('react');
81
+ container = document.createElement('div');
82
+ document.body.appendChild(container);
83
+ });
84
+
85
+ afterEach(() => {
86
+ delete document.documentMode;
87
+ delete window.CompositionEvent;
88
+ delete window.TextEvent;
89
+ delete window.opera;
90
+ document.body.removeChild(container);
91
+ container = null;
92
+ });
93
+
94
+ function keyCode(char) {
95
+ return char.charCodeAt(0);
96
+ }
97
+
98
+ const scenarios = [
99
+ {
100
+ eventSimulator: simulateEvent,
101
+ eventSimulatorArgs: [
102
+ 'compositionstart',
103
+ {detail: {data: 'test'}, data: 'test'},
104
+ ],
105
+ },
106
+ {
107
+ eventSimulator: simulateEvent,
108
+ eventSimulatorArgs: [
109
+ 'compositionupdate',
110
+ {detail: {data: 'test string'}, data: 'test string'},
111
+ ],
112
+ },
113
+ {
114
+ eventSimulator: simulateEvent,
115
+ eventSimulatorArgs: [
116
+ 'compositionend',
117
+ {detail: {data: 'test string 3'}, data: 'test string 3'},
118
+ ],
119
+ },
120
+ {
121
+ eventSimulator: simulateEvent,
122
+ eventSimulatorArgs: ['textInput', {data: 'abcß'}],
123
+ },
124
+ {
125
+ eventSimulator: simulateKeyboardEvent,
126
+ eventSimulatorArgs: ['keypress', {which: keyCode('a')}],
127
+ },
128
+ {
129
+ eventSimulator: simulateKeyboardEvent,
130
+ eventSimulatorArgs: ['keypress', {which: keyCode(' ')}, ' '],
131
+ },
132
+ {
133
+ eventSimulator: simulateEvent,
134
+ eventSimulatorArgs: ['textInput', {data: ' '}],
135
+ },
136
+ {
137
+ eventSimulator: simulateKeyboardEvent,
138
+ eventSimulatorArgs: ['keypress', {which: keyCode('a'), ctrlKey: true}],
139
+ },
140
+ {
141
+ eventSimulator: simulateKeyboardEvent,
142
+ eventSimulatorArgs: ['keypress', {which: keyCode('b'), altKey: true}],
143
+ },
144
+ {
145
+ eventSimulator: simulateKeyboardEvent,
146
+ eventSimulatorArgs: [
147
+ 'keypress',
148
+ {which: keyCode('c'), altKey: true, ctrlKey: true},
149
+ ],
150
+ },
151
+ {
152
+ eventSimulator: simulateKeyboardEvent,
153
+ eventSimulatorArgs: [
154
+ 'keypress',
155
+ {which: keyCode('X'), char: '\uD83D\uDE0A'},
156
+ ],
157
+ },
158
+ {
159
+ eventSimulator: simulateEvent,
160
+ eventSimulatorArgs: ['textInput', {data: '\uD83D\uDE0A'}],
161
+ },
162
+ {
163
+ eventSimulator: simulateKeyboardEvent,
164
+ eventSimulatorArgs: ['keydown', {keyCode: 229, value: 'foo'}],
165
+ },
166
+ {
167
+ eventSimulator: simulateKeyboardEvent,
168
+ eventSimulatorArgs: ['keydown', {keyCode: 9, value: 'foobar'}],
169
+ },
170
+ {
171
+ eventSimulator: simulateKeyboardEvent,
172
+ eventSimulatorArgs: ['keydown', {keyCode: 229, value: 'foofoo'}],
173
+ },
174
+ {
175
+ eventSimulator: simulateKeyboardEvent,
176
+ eventSimulatorArgs: ['keyup', {keyCode: 9, value: 'fooBARfoo'}],
177
+ },
178
+ {
179
+ eventSimulator: simulateKeyboardEvent,
180
+ eventSimulatorArgs: ['keydown', {keyCode: 229, value: 'foofoo'}],
181
+ },
182
+ {
183
+ eventSimulator: simulateKeyboardEvent,
184
+ eventSimulatorArgs: ['keypress', {keyCode: 60, value: 'Barfoofoo'}],
185
+ },
186
+ {
187
+ eventSimulator: simulatePaste,
188
+ eventSimulatorArgs: [],
189
+ },
190
+ ];
191
+
192
+ const environments = [
193
+ {
194
+ emulator: simulateWebkit,
195
+ assertions: [
196
+ {
197
+ run: ({
198
+ beforeInputEvent,
199
+ compositionStartEvent,
200
+ spyOnBeforeInput,
201
+ spyOnCompositionStart,
202
+ }) => {
203
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
204
+ expect(beforeInputEvent).toBeNull();
205
+ expect(spyOnCompositionStart).toHaveBeenCalledTimes(1);
206
+ expect(compositionStartEvent.type).toBe('compositionstart');
207
+ expect(compositionStartEvent.data).toBe('test');
208
+ },
209
+ },
210
+ {
211
+ run: ({
212
+ beforeInputEvent,
213
+ compositionUpdateEvent,
214
+ spyOnBeforeInput,
215
+ spyOnCompositionUpdate,
216
+ }) => {
217
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
218
+ expect(beforeInputEvent).toBeNull();
219
+ expect(spyOnCompositionUpdate).toHaveBeenCalledTimes(1);
220
+ expect(compositionUpdateEvent.type).toBe('compositionupdate');
221
+ expect(compositionUpdateEvent.data).toBe('test string');
222
+ },
223
+ },
224
+ {
225
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
226
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
227
+ expect(beforeInputEvent.type).toBe('compositionend');
228
+ expect(beforeInputEvent.data).toBe('test string 3');
229
+ },
230
+ },
231
+ {
232
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
233
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
234
+ expect(beforeInputEvent.type).toBe('textInput');
235
+ expect(beforeInputEvent.data).toBe('abcß');
236
+ },
237
+ },
238
+ {
239
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
240
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
241
+ expect(beforeInputEvent).toBeNull();
242
+ },
243
+ },
244
+ {
245
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
246
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
247
+ expect(beforeInputEvent.type).toBe('keypress');
248
+ expect(beforeInputEvent.data).toBe(' ');
249
+ },
250
+ },
251
+ {
252
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
253
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
254
+ expect(beforeInputEvent).toBeNull();
255
+ },
256
+ },
257
+ {
258
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
259
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
260
+ expect(beforeInputEvent).toBeNull();
261
+ },
262
+ },
263
+ {
264
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
265
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
266
+ expect(beforeInputEvent).toBeNull();
267
+ },
268
+ },
269
+ {
270
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
271
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
272
+ expect(beforeInputEvent).toBeNull();
273
+ },
274
+ },
275
+ {
276
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
277
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
278
+ expect(beforeInputEvent).toBeNull();
279
+ },
280
+ },
281
+ {
282
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
283
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
284
+ expect(beforeInputEvent.type).toBe('textInput');
285
+ expect(beforeInputEvent.data).toBe('\uD83D\uDE0A');
286
+ },
287
+ },
288
+ {
289
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
290
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
291
+ expect(beforeInputEvent).toBeNull();
292
+ },
293
+ },
294
+ {
295
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
296
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
297
+ expect(beforeInputEvent).toBeNull();
298
+ },
299
+ },
300
+ {
301
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
302
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
303
+ expect(beforeInputEvent).toBeNull();
304
+ },
305
+ },
306
+ {
307
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
308
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
309
+ expect(beforeInputEvent).toBeNull();
310
+ },
311
+ },
312
+ {
313
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
314
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
315
+ expect(beforeInputEvent).toBeNull();
316
+ },
317
+ },
318
+ {
319
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
320
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
321
+ expect(beforeInputEvent).toBeNull();
322
+ },
323
+ },
324
+ {
325
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
326
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
327
+ expect(beforeInputEvent).toBeNull();
328
+ },
329
+ },
330
+ ],
331
+ },
332
+ {
333
+ emulator: simulateIE11,
334
+ assertions: [
335
+ {
336
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
337
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
338
+ expect(beforeInputEvent).toBeNull();
339
+ },
340
+ },
341
+ {
342
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
343
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
344
+ expect(beforeInputEvent).toBeNull();
345
+ },
346
+ },
347
+ {
348
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
349
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
350
+ expect(beforeInputEvent).toBeNull();
351
+ },
352
+ },
353
+ {
354
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
355
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
356
+ expect(beforeInputEvent).toBeNull();
357
+ },
358
+ },
359
+ {
360
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
361
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
362
+ expect(beforeInputEvent.type).toBe('keypress');
363
+ expect(beforeInputEvent.data).toBe('a');
364
+ },
365
+ },
366
+ {
367
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
368
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
369
+ expect(beforeInputEvent.type).toBe('keypress');
370
+ expect(beforeInputEvent.data).toBe(' ');
371
+ },
372
+ },
373
+ {
374
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
375
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
376
+ expect(beforeInputEvent).toBeNull();
377
+ },
378
+ },
379
+ {
380
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
381
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
382
+ expect(beforeInputEvent).toBeNull();
383
+ },
384
+ },
385
+ {
386
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
387
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
388
+ expect(beforeInputEvent).toBeNull();
389
+ },
390
+ },
391
+ {
392
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
393
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
394
+ expect(beforeInputEvent.type).toBe('keypress');
395
+ expect(beforeInputEvent.data).toBe('c');
396
+ },
397
+ },
398
+ {
399
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
400
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
401
+ expect(beforeInputEvent.type).toBe('keypress');
402
+ expect(beforeInputEvent.data).toBe('\uD83D\uDE0A');
403
+ },
404
+ },
405
+ {
406
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
407
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
408
+ expect(beforeInputEvent).toBeNull();
409
+ },
410
+ },
411
+ {
412
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
413
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
414
+ expect(beforeInputEvent).toBeNull();
415
+ },
416
+ },
417
+ {
418
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
419
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
420
+ expect(beforeInputEvent).toBeNull();
421
+ },
422
+ },
423
+ {
424
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
425
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
426
+ expect(beforeInputEvent).toBeNull();
427
+ },
428
+ },
429
+ {
430
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
431
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
432
+ expect(beforeInputEvent).toBeNull();
433
+ },
434
+ },
435
+ {
436
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
437
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
438
+ expect(beforeInputEvent).toBeNull();
439
+ },
440
+ },
441
+ {
442
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
443
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
444
+ expect(beforeInputEvent).toBeNull();
445
+ },
446
+ },
447
+ {
448
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
449
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
450
+ expect(beforeInputEvent).toBeNull();
451
+ },
452
+ },
453
+ ],
454
+ },
455
+ {
456
+ emulator: simulateNoComposition,
457
+ assertions: [
458
+ {
459
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
460
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
461
+ expect(beforeInputEvent).toBeNull();
462
+ },
463
+ },
464
+ {
465
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
466
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
467
+ expect(beforeInputEvent).toBeNull();
468
+ },
469
+ },
470
+ {
471
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
472
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
473
+ expect(beforeInputEvent).toBeNull();
474
+ },
475
+ },
476
+ {
477
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
478
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
479
+ expect(beforeInputEvent).toBeNull();
480
+ },
481
+ },
482
+ {
483
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
484
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
485
+ expect(beforeInputEvent.type).toBe('keypress');
486
+ expect(beforeInputEvent.data).toBe('a');
487
+ },
488
+ },
489
+ {
490
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
491
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
492
+ expect(beforeInputEvent.type).toBe('keypress');
493
+ expect(beforeInputEvent.data).toBe(' ');
494
+ },
495
+ },
496
+ {
497
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
498
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
499
+ expect(beforeInputEvent).toBeNull();
500
+ },
501
+ },
502
+ {
503
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
504
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
505
+ expect(beforeInputEvent).toBeNull();
506
+ },
507
+ },
508
+ {
509
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
510
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
511
+ expect(beforeInputEvent).toBeNull();
512
+ },
513
+ },
514
+ {
515
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
516
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
517
+ expect(beforeInputEvent.type).toBe('keypress');
518
+ expect(beforeInputEvent.data).toBe('c');
519
+ },
520
+ },
521
+ {
522
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
523
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
524
+ expect(beforeInputEvent.type).toBe('keypress');
525
+ expect(beforeInputEvent.data).toBe('\uD83D\uDE0A');
526
+ },
527
+ },
528
+ {
529
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
530
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
531
+ expect(beforeInputEvent).toBeNull();
532
+ },
533
+ },
534
+ {
535
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
536
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
537
+ expect(beforeInputEvent).toBeNull();
538
+ },
539
+ },
540
+ {
541
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
542
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
543
+ expect(beforeInputEvent.type).toBe('keydown');
544
+ expect(beforeInputEvent.data).toBe('bar');
545
+ },
546
+ },
547
+ {
548
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
549
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
550
+ expect(beforeInputEvent).toBeNull();
551
+ },
552
+ },
553
+ {
554
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
555
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
556
+ expect(beforeInputEvent.type).toBe('keyup');
557
+ expect(beforeInputEvent.data).toBe('BAR');
558
+ },
559
+ },
560
+ {
561
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
562
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
563
+ expect(beforeInputEvent).toBeNull();
564
+ },
565
+ },
566
+ {
567
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
568
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
569
+ expect(beforeInputEvent.type).toBe('keypress');
570
+ expect(beforeInputEvent.data).toBe('Bar');
571
+ },
572
+ },
573
+ {
574
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
575
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
576
+ expect(beforeInputEvent).toBeNull();
577
+ },
578
+ },
579
+ ],
580
+ },
581
+ {
582
+ emulator: simulateComposition,
583
+ assertions: [
584
+ {
585
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
586
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
587
+ expect(beforeInputEvent).toBeNull();
588
+ },
589
+ },
590
+ {
591
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
592
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
593
+ expect(beforeInputEvent).toBeNull();
594
+ },
595
+ },
596
+ {
597
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
598
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
599
+ expect(beforeInputEvent.type).toBe('compositionend');
600
+ expect(beforeInputEvent.data).toBe('test string 3');
601
+ },
602
+ },
603
+ {
604
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
605
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
606
+ expect(beforeInputEvent).toBeNull();
607
+ },
608
+ },
609
+ {
610
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
611
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
612
+ expect(beforeInputEvent.type).toBe('keypress');
613
+ expect(beforeInputEvent.data).toBe('a');
614
+ },
615
+ },
616
+ {
617
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
618
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
619
+ expect(beforeInputEvent.type).toBe('keypress');
620
+ expect(beforeInputEvent.data).toBe(' ');
621
+ },
622
+ },
623
+ {
624
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
625
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
626
+ expect(beforeInputEvent).toBeNull();
627
+ },
628
+ },
629
+ {
630
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
631
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
632
+ expect(beforeInputEvent).toBeNull();
633
+ },
634
+ },
635
+ {
636
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
637
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
638
+ expect(beforeInputEvent).toBeNull();
639
+ },
640
+ },
641
+ {
642
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
643
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
644
+ expect(beforeInputEvent.type).toBe('keypress');
645
+ expect(beforeInputEvent.data).toBe('c');
646
+ },
647
+ },
648
+ {
649
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
650
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
651
+ expect(beforeInputEvent.type).toBe('keypress');
652
+ expect(beforeInputEvent.data).toBe('\uD83D\uDE0A');
653
+ },
654
+ },
655
+ {
656
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
657
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
658
+ expect(beforeInputEvent).toBeNull();
659
+ },
660
+ },
661
+ {
662
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
663
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
664
+ expect(beforeInputEvent).toBeNull();
665
+ },
666
+ },
667
+ {
668
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
669
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
670
+ expect(beforeInputEvent).toBeNull();
671
+ },
672
+ },
673
+ {
674
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
675
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
676
+ expect(beforeInputEvent).toBeNull();
677
+ },
678
+ },
679
+ {
680
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
681
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
682
+ expect(beforeInputEvent).toBeNull();
683
+ },
684
+ },
685
+ {
686
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
687
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
688
+ expect(beforeInputEvent).toBeNull();
689
+ },
690
+ },
691
+ {
692
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
693
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
694
+ expect(beforeInputEvent).toBeNull();
695
+ },
696
+ },
697
+ {
698
+ run: ({beforeInputEvent, spyOnBeforeInput}) => {
699
+ expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
700
+ expect(beforeInputEvent).toBeNull();
701
+ },
702
+ },
703
+ ],
704
+ },
705
+ ];
706
+
707
+ const testInputComponent = (env, scenes) => {
708
+ let beforeInputEvent;
709
+ let compositionStartEvent;
710
+ let compositionUpdateEvent;
711
+ let spyOnBeforeInput;
712
+ let spyOnCompositionStart;
713
+ let spyOnCompositionUpdate;
714
+ ReactDOM = loadReactDOM(env.emulator);
715
+ const node = ReactDOM.render(
716
+ <input
717
+ type="text"
718
+ onBeforeInput={({type, data}) => {
719
+ spyOnBeforeInput();
720
+ beforeInputEvent = {type, data};
721
+ }}
722
+ onCompositionStart={({type, data}) => {
723
+ spyOnCompositionStart();
724
+ compositionStartEvent = {type, data};
725
+ }}
726
+ onCompositionUpdate={({type, data}) => {
727
+ spyOnCompositionUpdate();
728
+ compositionUpdateEvent = {type, data};
729
+ }}
730
+ />,
731
+ container,
732
+ );
733
+
734
+ scenes.forEach((s, id) => {
735
+ beforeInputEvent = null;
736
+ compositionStartEvent = null;
737
+ compositionUpdateEvent = null;
738
+ spyOnBeforeInput = jest.fn();
739
+ spyOnCompositionStart = jest.fn();
740
+ spyOnCompositionUpdate = jest.fn();
741
+ s.eventSimulator.apply(null, [node, ...s.eventSimulatorArgs]);
742
+ env.assertions[id].run({
743
+ beforeInputEvent,
744
+ compositionStartEvent,
745
+ compositionUpdateEvent,
746
+ spyOnBeforeInput,
747
+ spyOnCompositionStart,
748
+ spyOnCompositionUpdate,
749
+ });
750
+ });
751
+ };
752
+
753
+ const testContentEditableComponent = (env, scenes) => {
754
+ let beforeInputEvent;
755
+ let compositionStartEvent;
756
+ let compositionUpdateEvent;
757
+ let spyOnBeforeInput;
758
+ let spyOnCompositionStart;
759
+ let spyOnCompositionUpdate;
760
+ ReactDOM = loadReactDOM(env.emulator);
761
+ const node = ReactDOM.render(
762
+ <div
763
+ contentEditable={true}
764
+ onBeforeInput={({type, data}) => {
765
+ spyOnBeforeInput();
766
+ beforeInputEvent = {type, data};
767
+ }}
768
+ onCompositionStart={({type, data}) => {
769
+ spyOnCompositionStart();
770
+ compositionStartEvent = {type, data};
771
+ }}
772
+ onCompositionUpdate={({type, data}) => {
773
+ spyOnCompositionUpdate();
774
+ compositionUpdateEvent = {type, data};
775
+ }}
776
+ />,
777
+ container,
778
+ );
779
+
780
+ scenes.forEach((s, id) => {
781
+ beforeInputEvent = null;
782
+ compositionStartEvent = null;
783
+ compositionUpdateEvent = null;
784
+ spyOnBeforeInput = jest.fn();
785
+ spyOnCompositionStart = jest.fn();
786
+ spyOnCompositionUpdate = jest.fn();
787
+ s.eventSimulator.apply(null, [node, ...s.eventSimulatorArgs]);
788
+ env.assertions[id].run({
789
+ beforeInputEvent,
790
+ compositionStartEvent,
791
+ compositionUpdateEvent,
792
+ spyOnBeforeInput,
793
+ spyOnCompositionStart,
794
+ spyOnCompositionUpdate,
795
+ });
796
+ });
797
+ };
798
+
799
+ it('should extract onBeforeInput when simulating in Webkit on input[type=text]', () => {
800
+ testInputComponent(environments[0], scenarios);
801
+ });
802
+ it('should extract onBeforeInput when simulating in Webkit on contenteditable', () => {
803
+ testContentEditableComponent(environments[0], scenarios);
804
+ });
805
+
806
+ it('should extract onBeforeInput when simulating in IE11 on input[type=text]', () => {
807
+ testInputComponent(environments[1], scenarios);
808
+ });
809
+ it('should extract onBeforeInput when simulating in IE11 on contenteditable', () => {
810
+ testContentEditableComponent(environments[1], scenarios);
811
+ });
812
+
813
+ it('should extract onBeforeInput when simulating in env with no CompositionEvent on input[type=text]', () => {
814
+ testInputComponent(environments[2], scenarios);
815
+ });
816
+
817
+ // in an environment using composition fallback onBeforeInput will not work
818
+ // as expected on a contenteditable as keydown and keyup events are translated
819
+ // to keypress events
820
+
821
+ it('should extract onBeforeInput when simulating in env with only CompositionEvent on input[type=text]', () => {
822
+ testInputComponent(environments[3], scenarios);
823
+ });
824
+
825
+ it('should extract onBeforeInput when simulating in env with only CompositionEvent on contenteditable', () => {
826
+ testContentEditableComponent(environments[3], scenarios);
827
+ });
828
+});
packages/react-dom/src/events/plugins/__tests__/ModernChangeEventPlugin-test.internal.js
new
+763
@@ -0,0 +1,763 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
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
+
18
+const setUntrackedChecked = Object.getOwnPropertyDescriptor(
19
+ HTMLInputElement.prototype,
20
+ 'checked',
21
+).set;
22
+
23
+const setUntrackedValue = Object.getOwnPropertyDescriptor(
24
+ HTMLInputElement.prototype,
25
+ 'value',
26
+).set;
27
+
28
+const setUntrackedTextareaValue = Object.getOwnPropertyDescriptor(
29
+ HTMLTextAreaElement.prototype,
30
+ 'value',
31
+).set;
32
+
33
+describe('ChangeEventPlugin', () => {
34
+ let container;
35
+
36
+ beforeEach(() => {
37
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
38
+ // TODO pull this into helper method, reduce repetition.
39
+ // mock the browser APIs which are used in schedule:
40
+ // - calling 'window.postMessage' should actually fire postmessage handlers
41
+ const originalAddEventListener = global.addEventListener;
42
+ let postMessageCallback;
43
+ global.addEventListener = function(eventName, callback, useCapture) {
44
+ if (eventName === 'message') {
45
+ postMessageCallback = callback;
46
+ } else {
47
+ originalAddEventListener(eventName, callback, useCapture);
48
+ }
49
+ };
50
+ global.postMessage = function(messageKey, targetOrigin) {
51
+ const postMessageEvent = {source: window, data: messageKey};
52
+ if (postMessageCallback) {
53
+ postMessageCallback(postMessageEvent);
54
+ }
55
+ };
56
+ jest.resetModules();
57
+ container = document.createElement('div');
58
+ document.body.appendChild(container);
59
+ });
60
+
61
+ afterEach(() => {
62
+ document.body.removeChild(container);
63
+ container = null;
64
+ });
65
+
66
+ // We try to avoid firing "duplicate" React change events.
67
+ // However, to tell which events are "duplicates" and should be ignored,
68
+ // we are tracking the "current" input value, and only respect events
69
+ // that occur after it changes. In most of these tests, we verify that we
70
+ // keep track of the "current" value and only fire events when it changes.
71
+ // See https://github.com/facebook/react/pull/5746.
72
+
73
+ it('should consider initial text value to be current', () => {
74
+ let called = 0;
75
+
76
+ function cb(e) {
77
+ called++;
78
+ expect(e.type).toBe('change');
79
+ }
80
+
81
+ const node = ReactDOM.render(
82
+ <input type="text" onChange={cb} defaultValue="foo" />,
83
+ container,
84
+ );
85
+ node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
86
+ node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
87
+
88
+ if (ReactFeatureFlags.disableInputAttributeSyncing) {
89
+ // TODO: figure out why. This might be a bug.
90
+ expect(called).toBe(1);
91
+ } else {
92
+ // There should be no React change events because the value stayed the same.
93
+ expect(called).toBe(0);
94
+ }
95
+ });
96
+
97
+ it('should consider initial checkbox checked=true to be current', () => {
98
+ let called = 0;
99
+
100
+ function cb(e) {
101
+ called++;
102
+ expect(e.type).toBe('change');
103
+ }
104
+
105
+ const node = ReactDOM.render(
106
+ <input type="checkbox" onChange={cb} defaultChecked={true} />,
107
+ container,
108
+ );
109
+
110
+ // Secretly, set `checked` to false, so that dispatching the `click` will
111
+ // make it `true` again. Thus, at the time of the event, React should not
112
+ // consider it a change from the initial `true` value.
113
+ setUntrackedChecked.call(node, false);
114
+ node.dispatchEvent(
115
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
116
+ );
117
+ // There should be no React change events because the value stayed the same.
118
+ expect(called).toBe(0);
119
+ });
120
+
121
+ it('should consider initial checkbox checked=false to be current', () => {
122
+ let called = 0;
123
+
124
+ function cb(e) {
125
+ called++;
126
+ expect(e.type).toBe('change');
127
+ }
128
+
129
+ const node = ReactDOM.render(
130
+ <input type="checkbox" onChange={cb} defaultChecked={false} />,
131
+ container,
132
+ );
133
+
134
+ // Secretly, set `checked` to true, so that dispatching the `click` will
135
+ // make it `false` again. Thus, at the time of the event, React should not
136
+ // consider it a change from the initial `false` value.
137
+ setUntrackedChecked.call(node, true);
138
+ node.dispatchEvent(
139
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
140
+ );
141
+ // There should be no React change events because the value stayed the same.
142
+ expect(called).toBe(0);
143
+ });
144
+
145
+ it('should fire change for checkbox input', () => {
146
+ let called = 0;
147
+
148
+ function cb(e) {
149
+ called++;
150
+ expect(e.type).toBe('change');
151
+ }
152
+
153
+ const node = ReactDOM.render(
154
+ <input type="checkbox" onChange={cb} />,
155
+ container,
156
+ );
157
+
158
+ expect(node.checked).toBe(false);
159
+ node.dispatchEvent(
160
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
161
+ );
162
+ // Note: unlike with text input events, dispatching `click` actually
163
+ // toggles the checkbox and updates its `checked` value.
164
+ expect(node.checked).toBe(true);
165
+ expect(called).toBe(1);
166
+
167
+ expect(node.checked).toBe(true);
168
+ node.dispatchEvent(
169
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
170
+ );
171
+ expect(node.checked).toBe(false);
172
+ expect(called).toBe(2);
173
+ });
174
+
175
+ it('should not fire change setting the value programmatically', () => {
176
+ let called = 0;
177
+
178
+ function cb(e) {
179
+ called++;
180
+ expect(e.type).toBe('change');
181
+ }
182
+
183
+ const input = ReactDOM.render(
184
+ <input type="text" defaultValue="foo" onChange={cb} />,
185
+ container,
186
+ );
187
+
188
+ // Set it programmatically.
189
+ input.value = 'bar';
190
+ // Even if a DOM input event fires, React sees that the real input value now
191
+ // ('bar') is the same as the "current" one we already recorded.
192
+ input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
193
+ expect(input.value).toBe('bar');
194
+ // In this case we don't expect to get a React event.
195
+ expect(called).toBe(0);
196
+
197
+ // However, we can simulate user typing by calling the underlying setter.
198
+ setUntrackedValue.call(input, 'foo');
199
+ // Now, when the event fires, the real input value ('foo') differs from the
200
+ // "current" one we previously recorded ('bar').
201
+ input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
202
+ expect(input.value).toBe('foo');
203
+ // In this case React should fire an event for it.
204
+ expect(called).toBe(1);
205
+
206
+ // Verify again that extra events without real changes are ignored.
207
+ input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
208
+ expect(called).toBe(1);
209
+ });
210
+
211
+ it('should not distinguish equal string and number values', () => {
212
+ let called = 0;
213
+
214
+ function cb(e) {
215
+ called++;
216
+ expect(e.type).toBe('change');
217
+ }
218
+
219
+ const input = ReactDOM.render(
220
+ <input type="text" defaultValue="42" onChange={cb} />,
221
+ container,
222
+ );
223
+
224
+ // When we set `value` as a property, React updates the "current" value
225
+ // that it tracks internally. The "current" value is later used to determine
226
+ // whether a change event is a duplicate or not.
227
+ // Even though we set value to a number, we still shouldn't get a change
228
+ // event because as a string, it's equal to the initial value ('42').
229
+ input.value = 42;
230
+ input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
231
+ expect(input.value).toBe('42');
232
+ expect(called).toBe(0);
233
+ });
234
+
235
+ // See a similar input test above for a detailed description of why.
236
+ it('should not fire change when setting checked programmatically', () => {
237
+ let called = 0;
238
+
239
+ function cb(e) {
240
+ called++;
241
+ expect(e.type).toBe('change');
242
+ }
243
+
244
+ const input = ReactDOM.render(
245
+ <input type="checkbox" onChange={cb} defaultChecked={false} />,
246
+ container,
247
+ );
248
+
249
+ // Set the value, updating the "current" value that React tracks to true.
250
+ input.checked = true;
251
+ // Under the hood, uncheck the box so that the click will "check" it again.
252
+ setUntrackedChecked.call(input, false);
253
+ input.click();
254
+ expect(input.checked).toBe(true);
255
+ // We don't expect a React event because at the time of the click, the real
256
+ // checked value (true) was the same as the last recorded "current" value
257
+ // (also true).
258
+ expect(called).toBe(0);
259
+
260
+ // However, simulating a normal click should fire a React event because the
261
+ // real value (false) would have changed from the last tracked value (true).
262
+ input.click();
263
+ expect(called).toBe(1);
264
+ });
265
+
266
+ it('should unmount', () => {
267
+ const input = ReactDOM.render(<input />, container);
268
+
269
+ ReactDOM.unmountComponentAtNode(container);
270
+ });
271
+
272
+ it('should only fire change for checked radio button once', () => {
273
+ let called = 0;
274
+
275
+ function cb(e) {
276
+ called++;
277
+ expect(e.type).toBe('change');
278
+ }
279
+
280
+ const input = ReactDOM.render(
281
+ <input type="radio" onChange={cb} />,
282
+ container,
283
+ );
284
+
285
+ setUntrackedChecked.call(input, true);
286
+ input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
287
+ input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
288
+ expect(called).toBe(1);
289
+ });
290
+
291
+ it('should track radio button cousins in a group', () => {
292
+ let called1 = 0;
293
+ let called2 = 0;
294
+
295
+ function cb1(e) {
296
+ called1++;
297
+ expect(e.type).toBe('change');
298
+ }
299
+
300
+ function cb2(e) {
301
+ called2++;
302
+ expect(e.type).toBe('change');
303
+ }
304
+
305
+ const div = ReactDOM.render(
306
+ <div>
307
+ <input type="radio" name="group" onChange={cb1} />
308
+ <input type="radio" name="group" onChange={cb2} />
309
+ </div>,
310
+ container,
311
+ );
312
+ const option1 = div.childNodes[0];
313
+ const option2 = div.childNodes[1];
314
+
315
+ // Select first option.
316
+ option1.click();
317
+ expect(called1).toBe(1);
318
+ expect(called2).toBe(0);
319
+
320
+ // Select second option.
321
+ option2.click();
322
+ expect(called1).toBe(1);
323
+ expect(called2).toBe(1);
324
+
325
+ // Select the first option.
326
+ // It should receive the React change event again.
327
+ option1.click();
328
+ expect(called1).toBe(2);
329
+ expect(called2).toBe(1);
330
+ });
331
+
332
+ it('should deduplicate input value change events', () => {
333
+ let called = 0;
334
+
335
+ function cb(e) {
336
+ called++;
337
+ expect(e.type).toBe('change');
338
+ }
339
+
340
+ let input;
341
+ ['text', 'number', 'range'].forEach(type => {
342
+ called = 0;
343
+ input = ReactDOM.render(<input type={type} onChange={cb} />, container);
344
+ // Should be ignored (no change):
345
+ input.dispatchEvent(
346
+ new Event('change', {bubbles: true, cancelable: true}),
347
+ );
348
+ setUntrackedValue.call(input, '42');
349
+ input.dispatchEvent(
350
+ new Event('change', {bubbles: true, cancelable: true}),
351
+ );
352
+ // Should be ignored (no change):
353
+ input.dispatchEvent(
354
+ new Event('change', {bubbles: true, cancelable: true}),
355
+ );
356
+ expect(called).toBe(1);
357
+ ReactDOM.unmountComponentAtNode(container);
358
+
359
+ called = 0;
360
+ input = ReactDOM.render(<input type={type} onChange={cb} />, container);
361
+ // Should be ignored (no change):
362
+ input.dispatchEvent(
363
+ new Event('input', {bubbles: true, cancelable: true}),
364
+ );
365
+ setUntrackedValue.call(input, '42');
366
+ input.dispatchEvent(
367
+ new Event('input', {bubbles: true, cancelable: true}),
368
+ );
369
+ // Should be ignored (no change):
370
+ input.dispatchEvent(
371
+ new Event('input', {bubbles: true, cancelable: true}),
372
+ );
373
+ expect(called).toBe(1);
374
+ ReactDOM.unmountComponentAtNode(container);
375
+
376
+ called = 0;
377
+ input = ReactDOM.render(<input type={type} onChange={cb} />, container);
378
+ // Should be ignored (no change):
379
+ input.dispatchEvent(
380
+ new Event('change', {bubbles: true, cancelable: true}),
381
+ );
382
+ setUntrackedValue.call(input, '42');
383
+ input.dispatchEvent(
384
+ new Event('input', {bubbles: true, cancelable: true}),
385
+ );
386
+ // Should be ignored (no change):
387
+ input.dispatchEvent(
388
+ new Event('change', {bubbles: true, cancelable: true}),
389
+ );
390
+ expect(called).toBe(1);
391
+ ReactDOM.unmountComponentAtNode(container);
392
+ });
393
+ });
394
+
395
+ it('should listen for both change and input events when supported', () => {
396
+ let called = 0;
397
+
398
+ function cb(e) {
399
+ called++;
400
+ expect(e.type).toBe('change');
401
+ }
402
+
403
+ const input = ReactDOM.render(
404
+ <input type="range" onChange={cb} />,
405
+ container,
406
+ );
407
+
408
+ setUntrackedValue.call(input, 10);
409
+ input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
410
+
411
+ setUntrackedValue.call(input, 20);
412
+ input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
413
+
414
+ expect(called).toBe(2);
415
+ });
416
+
417
+ it('should only fire events when the value changes for range inputs', () => {
418
+ let called = 0;
419
+
420
+ function cb(e) {
421
+ called++;
422
+ expect(e.type).toBe('change');
423
+ }
424
+
425
+ const input = ReactDOM.render(
426
+ <input type="range" onChange={cb} />,
427
+ container,
428
+ );
429
+ setUntrackedValue.call(input, '40');
430
+ input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
431
+ input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
432
+
433
+ setUntrackedValue.call(input, 'foo');
434
+ input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
435
+ input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
436
+
437
+ expect(called).toBe(2);
438
+ });
439
+
440
+ it('does not crash for nodes with custom value property', () => {
441
+ let originalCreateElement;
442
+ // https://github.com/facebook/react/issues/10196
443
+ try {
444
+ originalCreateElement = document.createElement;
445
+ document.createElement = function() {
446
+ const node = originalCreateElement.apply(this, arguments);
447
+ Object.defineProperty(node, 'value', {
448
+ get() {},
449
+ set() {},
450
+ });
451
+ return node;
452
+ };
453
+ const div = document.createElement('div');
454
+ // Mount
455
+ const node = ReactDOM.render(<input type="text" />, div);
456
+ // Update
457
+ ReactDOM.render(<input type="text" />, div);
458
+ // Change
459
+ node.dispatchEvent(
460
+ new Event('change', {bubbles: true, cancelable: true}),
461
+ );
462
+ // Unmount
463
+ ReactDOM.unmountComponentAtNode(div);
464
+ } finally {
465
+ document.createElement = originalCreateElement;
466
+ }
467
+ });
468
+
469
+ describe('concurrent mode', () => {
470
+ beforeEach(() => {
471
+ jest.resetModules();
472
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
473
+
474
+ React = require('react');
475
+ ReactDOM = require('react-dom');
476
+ TestUtils = require('react-dom/test-utils');
477
+ Scheduler = require('scheduler');
478
+ });
479
+
480
+ // @gate experimental
481
+ it('text input', () => {
482
+ const root = ReactDOM.createRoot(container);
483
+ let input;
484
+
485
+ class ControlledInput extends React.Component {
486
+ state = {value: 'initial'};
487
+ onChange = event => this.setState({value: event.target.value});
488
+ render() {
489
+ Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
490
+ const controlledValue =
491
+ this.state.value === 'changed' ? 'changed [!]' : this.state.value;
492
+ return (
493
+ <input
494
+ ref={el => (input = el)}
495
+ type="text"
496
+ value={controlledValue}
497
+ onChange={this.onChange}
498
+ />
499
+ );
500
+ }
501
+ }
502
+
503
+ // Initial mount. Test that this is async.
504
+ root.render(<ControlledInput />);
505
+ // Should not have flushed yet.
506
+ expect(Scheduler).toHaveYielded([]);
507
+ expect(input).toBe(undefined);
508
+ // Flush callbacks.
509
+ expect(Scheduler).toFlushAndYield(['render: initial']);
510
+ expect(input.value).toBe('initial');
511
+
512
+ // Trigger a change event.
513
+ setUntrackedValue.call(input, 'changed');
514
+ input.dispatchEvent(
515
+ new Event('input', {bubbles: true, cancelable: true}),
516
+ );
517
+ // Change should synchronously flush
518
+ expect(Scheduler).toHaveYielded(['render: changed']);
519
+ // Value should be the controlled value, not the original one
520
+ expect(input.value).toBe('changed [!]');
521
+ });
522
+
523
+ // @gate experimental
524
+ it('checkbox input', () => {
525
+ const root = ReactDOM.createRoot(container);
526
+ let input;
527
+
528
+ class ControlledInput extends React.Component {
529
+ state = {checked: false};
530
+ onChange = event => {
531
+ this.setState({checked: event.target.checked});
532
+ };
533
+ render() {
534
+ Scheduler.unstable_yieldValue(`render: ${this.state.checked}`);
535
+ const controlledValue = this.props.reverse
536
+ ? !this.state.checked
537
+ : this.state.checked;
538
+ return (
539
+ <input
540
+ ref={el => (input = el)}
541
+ type="checkbox"
542
+ checked={controlledValue}
543
+ onChange={this.onChange}
544
+ />
545
+ );
546
+ }
547
+ }
548
+
549
+ // Initial mount. Test that this is async.
550
+ root.render(<ControlledInput reverse={false} />);
551
+ // Should not have flushed yet.
552
+ expect(Scheduler).toHaveYielded([]);
553
+ expect(input).toBe(undefined);
554
+ // Flush callbacks.
555
+ expect(Scheduler).toFlushAndYield(['render: false']);
556
+ expect(input.checked).toBe(false);
557
+
558
+ // Trigger a change event.
559
+ input.dispatchEvent(
560
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
561
+ );
562
+ // Change should synchronously flush
563
+ expect(Scheduler).toHaveYielded(['render: true']);
564
+ expect(input.checked).toBe(true);
565
+
566
+ // Now let's make sure we're using the controlled value.
567
+ root.render(<ControlledInput reverse={true} />);
568
+ expect(Scheduler).toFlushAndYield(['render: true']);
569
+
570
+ // Trigger another change event.
571
+ input.dispatchEvent(
572
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
573
+ );
574
+ // Change should synchronously flush
575
+ expect(Scheduler).toHaveYielded(['render: true']);
576
+ expect(input.checked).toBe(false);
577
+ });
578
+
579
+ // @gate experimental
580
+ it('textarea', () => {
581
+ const root = ReactDOM.createRoot(container);
582
+ let textarea;
583
+
584
+ class ControlledTextarea extends React.Component {
585
+ state = {value: 'initial'};
586
+ onChange = event => this.setState({value: event.target.value});
587
+ render() {
588
+ Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
589
+ const controlledValue =
590
+ this.state.value === 'changed' ? 'changed [!]' : this.state.value;
591
+ return (
592
+ <textarea
593
+ ref={el => (textarea = el)}
594
+ type="text"
595
+ value={controlledValue}
596
+ onChange={this.onChange}
597
+ />
598
+ );
599
+ }
600
+ }
601
+
602
+ // Initial mount. Test that this is async.
603
+ root.render(<ControlledTextarea />);
604
+ // Should not have flushed yet.
605
+ expect(Scheduler).toHaveYielded([]);
606
+ expect(textarea).toBe(undefined);
607
+ // Flush callbacks.
608
+ expect(Scheduler).toFlushAndYield(['render: initial']);
609
+ expect(textarea.value).toBe('initial');
610
+
611
+ // Trigger a change event.
612
+ setUntrackedTextareaValue.call(textarea, 'changed');
613
+ textarea.dispatchEvent(
614
+ new Event('input', {bubbles: true, cancelable: true}),
615
+ );
616
+ // Change should synchronously flush
617
+ expect(Scheduler).toHaveYielded(['render: changed']);
618
+ // Value should be the controlled value, not the original one
619
+ expect(textarea.value).toBe('changed [!]');
620
+ });
621
+
622
+ // @gate experimental
623
+ it('parent of input', () => {
624
+ const root = ReactDOM.createRoot(container);
625
+ let input;
626
+
627
+ class ControlledInput extends React.Component {
628
+ state = {value: 'initial'};
629
+ onChange = event => this.setState({value: event.target.value});
630
+ render() {
631
+ Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
632
+ const controlledValue =
633
+ this.state.value === 'changed' ? 'changed [!]' : this.state.value;
634
+ return (
635
+ <div onChange={this.onChange}>
636
+ <input
637
+ ref={el => (input = el)}
638
+ type="text"
639
+ value={controlledValue}
640
+ onChange={() => {
641
+ // Does nothing. Parent handler is responsible for updating.
642
+ }}
643
+ />
644
+ </div>
645
+ );
646
+ }
647
+ }
648
+
649
+ // Initial mount. Test that this is async.
650
+ root.render(<ControlledInput />);
651
+ // Should not have flushed yet.
652
+ expect(Scheduler).toHaveYielded([]);
653
+ expect(input).toBe(undefined);
654
+ // Flush callbacks.
655
+ expect(Scheduler).toFlushAndYield(['render: initial']);
656
+ expect(input.value).toBe('initial');
657
+
658
+ // Trigger a change event.
659
+ setUntrackedValue.call(input, 'changed');
660
+ input.dispatchEvent(
661
+ new Event('input', {bubbles: true, cancelable: true}),
662
+ );
663
+ // Change should synchronously flush
664
+ expect(Scheduler).toHaveYielded(['render: changed']);
665
+ // Value should be the controlled value, not the original one
666
+ expect(input.value).toBe('changed [!]');
667
+ });
668
+
669
+ // @gate experimental
670
+ it('is async for non-input events', () => {
671
+ const root = ReactDOM.createRoot(container);
672
+ let input;
673
+
674
+ class ControlledInput extends React.Component {
675
+ state = {value: 'initial'};
676
+ onChange = event => this.setState({value: event.target.value});
677
+ reset = () => {
678
+ this.setState({value: ''});
679
+ };
680
+ render() {
681
+ Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
682
+ const controlledValue =
683
+ this.state.value === 'changed' ? 'changed [!]' : this.state.value;
684
+ return (
685
+ <input
686
+ ref={el => (input = el)}
687
+ type="text"
688
+ value={controlledValue}
689
+ onChange={this.onChange}
690
+ onClick={this.reset}
691
+ />
692
+ );
693
+ }
694
+ }
695
+
696
+ // Initial mount. Test that this is async.
697
+ root.render(<ControlledInput />);
698
+ // Should not have flushed yet.
699
+ expect(Scheduler).toHaveYielded([]);
700
+ expect(input).toBe(undefined);
701
+ // Flush callbacks.
702
+ expect(Scheduler).toFlushAndYield(['render: initial']);
703
+ expect(input.value).toBe('initial');
704
+
705
+ // Trigger a click event
706
+ input.dispatchEvent(
707
+ new Event('click', {bubbles: true, cancelable: true}),
708
+ );
709
+ // Nothing should have changed
710
+ expect(Scheduler).toHaveYielded([]);
711
+ expect(input.value).toBe('initial');
712
+
713
+ // Flush callbacks.
714
+ // Now the click update has flushed.
715
+ expect(Scheduler).toFlushAndYield(['render: ']);
716
+ expect(input.value).toBe('');
717
+ });
718
+
719
+ // @gate experimental
720
+ it('mouse enter/leave should be user-blocking but not discrete', async () => {
721
+ // This is currently behind a feature flag
722
+ jest.resetModules();
723
+ React = require('react');
724
+ ReactDOM = require('react-dom');
725
+ TestUtils = require('react-dom/test-utils');
726
+ Scheduler = require('scheduler');
727
+
728
+ const {act} = TestUtils;
729
+ const {useState} = React;
730
+
731
+ const root = ReactDOM.createRoot(container);
732
+
733
+ const target = React.createRef(null);
734
+ function Foo() {
735
+ const [isHover, setHover] = useState(false);
736
+ return (
737
+ <div
738
+ ref={target}
739
+ onMouseEnter={() => setHover(true)}
740
+ onMouseLeave={() => setHover(false)}>
741
+ {isHover ? 'hovered' : 'not hovered'}
742
+ </div>
743
+ );
744
+ }
745
+
746
+ await act(async () => {
747
+ root.render(<Foo />);
748
+ });
749
+ expect(container.textContent).toEqual('not hovered');
750
+
751
+ await act(async () => {
752
+ const mouseOverEvent = document.createEvent('MouseEvents');
753
+ mouseOverEvent.initEvent('mouseover', true, true);
754
+ target.current.dispatchEvent(mouseOverEvent);
755
+
756
+ // 3s should be enough to expire the updates
757
+ Scheduler.unstable_advanceTime(3000);
758
+ expect(Scheduler).toFlushExpired([]);
759
+ expect(container.textContent).toEqual('hovered');
760
+ });
761
+ });
762
+ });
763
+});
packages/react-dom/src/events/plugins/__tests__/ModernEnterLeaveEventPlugin-test.internal.js
new
+239
@@ -0,0 +1,239 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let React;
13
+let ReactDOM;
14
+
15
+describe('EnterLeaveEventPlugin', () => {
16
+ let container;
17
+
18
+ beforeEach(() => {
19
+ jest.resetModules();
20
+
21
+ React = require('react');
22
+ ReactDOM = require('react-dom');
23
+
24
+ // The container has to be attached for events to fire.
25
+ container = document.createElement('div');
26
+ document.body.appendChild(container);
27
+ });
28
+
29
+ afterEach(() => {
30
+ document.body.removeChild(container);
31
+ container = null;
32
+ });
33
+
34
+ it('should set onMouseLeave relatedTarget properly in iframe', () => {
35
+ const iframe = document.createElement('iframe');
36
+ container.appendChild(iframe);
37
+ const iframeDocument = iframe.contentDocument;
38
+ iframeDocument.write(
39
+ '<!DOCTYPE html><html><head></head><body><div></div></body></html>',
40
+ );
41
+ iframeDocument.close();
42
+
43
+ const leaveEvents = [];
44
+ const node = ReactDOM.render(
45
+ <div
46
+ onMouseLeave={e => {
47
+ e.persist();
48
+ leaveEvents.push(e);
49
+ }}
50
+ />,
51
+ iframeDocument.body.getElementsByTagName('div')[0],
52
+ );
53
+
54
+ node.dispatchEvent(
55
+ new MouseEvent('mouseout', {
56
+ bubbles: true,
57
+ cancelable: true,
58
+ relatedTarget: iframe.contentWindow,
59
+ }),
60
+ );
61
+
62
+ expect(leaveEvents.length).toBe(1);
63
+ expect(leaveEvents[0].target).toBe(node);
64
+ expect(leaveEvents[0].relatedTarget).toBe(iframe.contentWindow);
65
+ });
66
+
67
+ it('should set onMouseEnter relatedTarget properly in iframe', () => {
68
+ const iframe = document.createElement('iframe');
69
+ container.appendChild(iframe);
70
+ const iframeDocument = iframe.contentDocument;
71
+ iframeDocument.write(
72
+ '<!DOCTYPE html><html><head></head><body><div></div></body></html>',
73
+ );
74
+ iframeDocument.close();
75
+
76
+ const enterEvents = [];
77
+ const node = ReactDOM.render(
78
+ <div
79
+ onMouseEnter={e => {
80
+ e.persist();
81
+ enterEvents.push(e);
82
+ }}
83
+ />,
84
+ iframeDocument.body.getElementsByTagName('div')[0],
85
+ );
86
+
87
+ node.dispatchEvent(
88
+ new MouseEvent('mouseover', {
89
+ bubbles: true,
90
+ cancelable: true,
91
+ relatedTarget: null,
92
+ }),
93
+ );
94
+
95
+ expect(enterEvents.length).toBe(1);
96
+ expect(enterEvents[0].target).toBe(node);
97
+ expect(enterEvents[0].relatedTarget).toBe(iframe.contentWindow);
98
+ });
99
+
100
+ // Regression test for https://github.com/facebook/react/issues/10906.
101
+ it('should find the common parent after updates', () => {
102
+ let parentEnterCalls = 0;
103
+ let childEnterCalls = 0;
104
+ let parent = null;
105
+
106
+ class Parent extends React.Component {
107
+ render() {
108
+ return (
109
+ <div
110
+ onMouseEnter={() => parentEnterCalls++}
111
+ ref={node => (parent = node)}>
112
+ {this.props.showChild && (
113
+ <div onMouseEnter={() => childEnterCalls++} />
114
+ )}
115
+ </div>
116
+ );
117
+ }
118
+ }
119
+
120
+ ReactDOM.render(<Parent />, container);
121
+ // The issue only reproduced on insertion during the first update.
122
+ ReactDOM.render(<Parent showChild={true} />, container);
123
+
124
+ // Enter from parent into the child.
125
+ parent.dispatchEvent(
126
+ new MouseEvent('mouseout', {
127
+ bubbles: true,
128
+ cancelable: true,
129
+ relatedTarget: parent.firstChild,
130
+ }),
131
+ );
132
+
133
+ // Entering a child should fire on the child, not on the parent.
134
+ expect(childEnterCalls).toBe(1);
135
+ expect(parentEnterCalls).toBe(0);
136
+ });
137
+
138
+ // Test for https://github.com/facebook/react/issues/16763.
139
+ it('should call mouseEnter once from sibling rendered inside a rendered component', done => {
140
+ const mockFn = jest.fn();
141
+
142
+ class Parent extends React.Component {
143
+ constructor(props) {
144
+ super(props);
145
+ this.parentEl = React.createRef();
146
+ }
147
+
148
+ componentDidMount() {
149
+ ReactDOM.render(<MouseEnterDetect />, this.parentEl.current);
150
+ }
151
+
152
+ render() {
153
+ return <div ref={this.parentEl} />;
154
+ }
155
+ }
156
+
157
+ class MouseEnterDetect extends React.Component {
158
+ constructor(props) {
159
+ super(props);
160
+ this.firstEl = React.createRef();
161
+ this.siblingEl = React.createRef();
162
+ }
163
+
164
+ componentDidMount() {
165
+ this.siblingEl.current.dispatchEvent(
166
+ new MouseEvent('mouseout', {
167
+ bubbles: true,
168
+ cancelable: true,
169
+ relatedTarget: this.firstEl.current,
170
+ }),
171
+ );
172
+ expect(mockFn.mock.calls.length).toBe(1);
173
+ done();
174
+ }
175
+
176
+ render() {
177
+ return (
178
+ <React.Fragment>
179
+ <div ref={this.firstEl} onMouseEnter={mockFn} />
180
+ <div ref={this.siblingEl} />
181
+ </React.Fragment>
182
+ );
183
+ }
184
+ }
185
+
186
+ ReactDOM.render(<Parent />, container);
187
+ });
188
+
189
+ it('should call mouseEnter when pressing a non tracked React node', done => {
190
+ const mockFn = jest.fn();
191
+
192
+ class Parent extends React.Component {
193
+ constructor(props) {
194
+ super(props);
195
+ this.parentEl = React.createRef();
196
+ }
197
+
198
+ componentDidMount() {
199
+ ReactDOM.render(<MouseEnterDetect />, this.parentEl.current);
200
+ }
201
+
202
+ render() {
203
+ return <div ref={this.parentEl} />;
204
+ }
205
+ }
206
+
207
+ class MouseEnterDetect extends React.Component {
208
+ constructor(props) {
209
+ super(props);
210
+ this.divRef = React.createRef();
211
+ this.siblingEl = React.createRef();
212
+ }
213
+
214
+ componentDidMount() {
215
+ const attachedNode = document.createElement('div');
216
+ this.divRef.current.appendChild(attachedNode);
217
+ attachedNode.dispatchEvent(
218
+ new MouseEvent('mouseout', {
219
+ bubbles: true,
220
+ cancelable: true,
221
+ relatedTarget: this.siblingEl.current,
222
+ }),
223
+ );
224
+ expect(mockFn.mock.calls.length).toBe(1);
225
+ done();
226
+ }
227
+
228
+ render() {
229
+ return (
230
+ <div ref={this.divRef}>
231
+ <div ref={this.siblingEl} onMouseEnter={mockFn} />
232
+ </div>
233
+ );
234
+ }
235
+ }
236
+
237
+ ReactDOM.render(<Parent />, container);
238
+ });
239
+});
packages/react-dom/src/events/plugins/__tests__/ModernSelectEventPlugin-test.internal.js
new
+145
@@ -0,0 +1,145 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let React;
13
+let ReactDOM;
14
+
15
+describe('SelectEventPlugin', () => {
16
+ let container;
17
+
18
+ beforeEach(() => {
19
+ React = require('react');
20
+ ReactDOM = require('react-dom');
21
+
22
+ container = document.createElement('div');
23
+ document.body.appendChild(container);
24
+ });
25
+
26
+ afterEach(() => {
27
+ document.body.removeChild(container);
28
+ container = null;
29
+ });
30
+
31
+ // See https://github.com/facebook/react/pull/3639 for details.
32
+ it('does not get confused when dependent events are registered independently', () => {
33
+ const select = jest.fn();
34
+ const onSelect = event => {
35
+ expect(typeof event).toBe('object');
36
+ expect(event.type).toBe('select');
37
+ expect(event.target).toBe(node);
38
+ select(event.currentTarget);
39
+ };
40
+
41
+ // Pass `onMouseDown` so React registers a top-level listener.
42
+ const node = ReactDOM.render(
43
+ <input type="text" onMouseDown={function() {}} />,
44
+ container,
45
+ );
46
+
47
+ // Trigger `mousedown` and `mouseup`. Note that
48
+ // React is not currently listening to `mouseup`.
49
+ node.dispatchEvent(
50
+ new MouseEvent('mousedown', {
51
+ bubbles: true,
52
+ cancelable: true,
53
+ }),
54
+ );
55
+ node.dispatchEvent(
56
+ new MouseEvent('mouseup', {
57
+ bubbles: true,
58
+ cancelable: true,
59
+ }),
60
+ );
61
+
62
+ // Now subscribe to `onSelect`.
63
+ ReactDOM.render(<input type="text" onSelect={onSelect} />, container);
64
+ node.focus();
65
+
66
+ // This triggers a `select` event in our polyfill.
67
+ node.dispatchEvent(
68
+ new KeyboardEvent('keydown', {bubbles: true, cancelable: true}),
69
+ );
70
+
71
+ // Verify that it doesn't get "stuck" waiting for
72
+ // a `mouseup` event that it has "missed" because
73
+ // a top-level listener didn't exist yet.
74
+ expect(select).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ it('should fire `onSelect` when a listener is present', () => {
78
+ const select = jest.fn();
79
+ const onSelect = event => {
80
+ expect(typeof event).toBe('object');
81
+ expect(event.type).toBe('select');
82
+ expect(event.target).toBe(node);
83
+ select(event.currentTarget);
84
+ };
85
+
86
+ const node = ReactDOM.render(
87
+ <input type="text" onSelect={onSelect} />,
88
+ container,
89
+ );
90
+ node.focus();
91
+
92
+ let nativeEvent = new MouseEvent('focus', {
93
+ bubbles: true,
94
+ cancelable: true,
95
+ });
96
+ node.dispatchEvent(nativeEvent);
97
+ expect(select).toHaveBeenCalledTimes(0);
98
+
99
+ nativeEvent = new MouseEvent('mousedown', {
100
+ bubbles: true,
101
+ cancelable: true,
102
+ });
103
+ node.dispatchEvent(nativeEvent);
104
+ expect(select).toHaveBeenCalledTimes(0);
105
+
106
+ nativeEvent = new MouseEvent('mouseup', {bubbles: true, cancelable: true});
107
+ node.dispatchEvent(nativeEvent);
108
+ expect(select).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ // Regression test for https://github.com/facebook/react/issues/11379
112
+ it('should not wait for `mouseup` after receiving `dragend`', () => {
113
+ const select = jest.fn();
114
+ const onSelect = event => {
115
+ expect(typeof event).toBe('object');
116
+ expect(event.type).toBe('select');
117
+ expect(event.target).toBe(node);
118
+ select(event.currentTarget);
119
+ };
120
+
121
+ const node = ReactDOM.render(
122
+ <input type="text" onSelect={onSelect} />,
123
+ container,
124
+ );
125
+ node.focus();
126
+
127
+ let nativeEvent = new MouseEvent('focus', {
128
+ bubbles: true,
129
+ cancelable: true,
130
+ });
131
+ node.dispatchEvent(nativeEvent);
132
+ expect(select).toHaveBeenCalledTimes(0);
133
+
134
+ nativeEvent = new MouseEvent('mousedown', {
135
+ bubbles: true,
136
+ cancelable: true,
137
+ });
138
+ node.dispatchEvent(nativeEvent);
139
+ expect(select).toHaveBeenCalledTimes(0);
140
+
141
+ nativeEvent = new MouseEvent('dragend', {bubbles: true, cancelable: true});
142
+ node.dispatchEvent(nativeEvent);
143
+ expect(select).toHaveBeenCalledTimes(1);
144
+ });
145
+});
packages/react-dom/src/events/plugins/__tests__/ModernSimpleEventPlugin-test.internal.js
new
+489
@@ -0,0 +1,489 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+describe('SimpleEventPlugin', function() {
13
+ let React;
14
+ let ReactDOM;
15
+ let Scheduler;
16
+
17
+ let onClick;
18
+ let container;
19
+
20
+ function expectClickThru(element) {
21
+ element.click();
22
+ expect(onClick).toHaveBeenCalledTimes(1);
23
+ }
24
+
25
+ function expectNoClickThru(element) {
26
+ element.click();
27
+ expect(onClick).toHaveBeenCalledTimes(0);
28
+ }
29
+
30
+ function mounted(element) {
31
+ container = document.createElement('div');
32
+ document.body.appendChild(container);
33
+ element = ReactDOM.render(element, container);
34
+ return element;
35
+ }
36
+
37
+ beforeEach(function() {
38
+ jest.resetModules();
39
+ React = require('react');
40
+ ReactDOM = require('react-dom');
41
+ Scheduler = require('scheduler');
42
+
43
+ onClick = jest.fn();
44
+ });
45
+
46
+ afterEach(() => {
47
+ if (container && document.body.contains(container)) {
48
+ document.body.removeChild(container);
49
+ container = null;
50
+ }
51
+ });
52
+
53
+ it('A non-interactive tags click when disabled', function() {
54
+ const element = <div onClick={onClick} />;
55
+ expectClickThru(mounted(element));
56
+ });
57
+
58
+ it('A non-interactive tags clicks bubble when disabled', function() {
59
+ const element = mounted(
60
+ <div onClick={onClick}>
61
+ <div />
62
+ </div>,
63
+ );
64
+ const child = element.firstChild;
65
+ child.click();
66
+ expect(onClick).toHaveBeenCalledTimes(1);
67
+ });
68
+
69
+ it('does not register a click when clicking a child of a disabled element', function() {
70
+ const element = mounted(
71
+ <button onClick={onClick} disabled={true}>
72
+ <span />
73
+ </button>,
74
+ );
75
+ const child = element.querySelector('span');
76
+
77
+ child.click();
78
+ expect(onClick).toHaveBeenCalledTimes(0);
79
+ });
80
+
81
+ it('triggers click events for children of disabled elements', function() {
82
+ const element = mounted(
83
+ <button disabled={true}>
84
+ <span onClick={onClick} />
85
+ </button>,
86
+ );
87
+ const child = element.querySelector('span');
88
+
89
+ child.click();
90
+ expect(onClick).toHaveBeenCalledTimes(1);
91
+ });
92
+
93
+ it('triggers parent captured click events when target is a child of a disabled elements', function() {
94
+ const element = mounted(
95
+ <div onClickCapture={onClick}>
96
+ <button disabled={true}>
97
+ <span />
98
+ </button>
99
+ </div>,
100
+ );
101
+ const child = element.querySelector('span');
102
+
103
+ child.click();
104
+ expect(onClick).toHaveBeenCalledTimes(1);
105
+ });
106
+
107
+ it('triggers captured click events for children of disabled elements', function() {
108
+ const element = mounted(
109
+ <button disabled={true}>
110
+ <span onClickCapture={onClick} />
111
+ </button>,
112
+ );
113
+ const child = element.querySelector('span');
114
+
115
+ child.click();
116
+ expect(onClick).toHaveBeenCalledTimes(1);
117
+ });
118
+
119
+ ['button', 'input', 'select', 'textarea'].forEach(function(tagName) {
120
+ describe(tagName, function() {
121
+ it('should forward clicks when it starts out not disabled', () => {
122
+ const element = React.createElement(tagName, {
123
+ onClick: onClick,
124
+ });
125
+
126
+ expectClickThru(mounted(element));
127
+ });
128
+
129
+ it('should not forward clicks when it starts out disabled', () => {
130
+ const element = React.createElement(tagName, {
131
+ onClick: onClick,
132
+ disabled: true,
133
+ });
134
+
135
+ expectNoClickThru(mounted(element));
136
+ });
137
+
138
+ it('should forward clicks when it becomes not disabled', () => {
139
+ container = document.createElement('div');
140
+ document.body.appendChild(container);
141
+ let element = ReactDOM.render(
142
+ React.createElement(tagName, {onClick: onClick, disabled: true}),
143
+ container,
144
+ );
145
+ element = ReactDOM.render(
146
+ React.createElement(tagName, {onClick: onClick}),
147
+ container,
148
+ );
149
+ expectClickThru(element);
150
+ });
151
+
152
+ it('should not forward clicks when it becomes disabled', () => {
153
+ container = document.createElement('div');
154
+ document.body.appendChild(container);
155
+ let element = ReactDOM.render(
156
+ React.createElement(tagName, {onClick: onClick}),
157
+ container,
158
+ );
159
+ element = ReactDOM.render(
160
+ React.createElement(tagName, {onClick: onClick, disabled: true}),
161
+ container,
162
+ );
163
+ expectNoClickThru(element);
164
+ });
165
+
166
+ it('should work correctly if the listener is changed', () => {
167
+ container = document.createElement('div');
168
+ document.body.appendChild(container);
169
+ let element = ReactDOM.render(
170
+ React.createElement(tagName, {onClick: onClick, disabled: true}),
171
+ container,
172
+ );
173
+ element = ReactDOM.render(
174
+ React.createElement(tagName, {onClick: onClick, disabled: false}),
175
+ container,
176
+ );
177
+ expectClickThru(element);
178
+ });
179
+ });
180
+ });
181
+
182
+ it('batches updates that occur as a result of a nested event dispatch', () => {
183
+ container = document.createElement('div');
184
+ document.body.appendChild(container);
185
+
186
+ let button;
187
+ class Button extends React.Component {
188
+ state = {count: 0};
189
+ increment = () =>
190
+ this.setState(state => ({
191
+ count: state.count + 1,
192
+ }));
193
+ componentDidUpdate() {
194
+ Scheduler.unstable_yieldValue(`didUpdate - Count: ${this.state.count}`);
195
+ }
196
+ render() {
197
+ return (
198
+ <button
199
+ ref={el => (button = el)}
200
+ onFocus={this.increment}
201
+ onClick={() => {
202
+ // The focus call synchronously dispatches a nested event. All of
203
+ // the updates in this handler should be batched together.
204
+ this.increment();
205
+ button.focus();
206
+ this.increment();
207
+ }}>
208
+ Count: {this.state.count}
209
+ </button>
210
+ );
211
+ }
212
+ }
213
+
214
+ function click() {
215
+ button.dispatchEvent(
216
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
217
+ );
218
+ }
219
+
220
+ ReactDOM.render(<Button />, container);
221
+ expect(button.textContent).toEqual('Count: 0');
222
+ expect(Scheduler).toHaveYielded([]);
223
+
224
+ click();
225
+
226
+ // There should be exactly one update.
227
+ expect(Scheduler).toHaveYielded(['didUpdate - Count: 3']);
228
+ expect(button.textContent).toEqual('Count: 3');
229
+ });
230
+
231
+ describe('interactive events, in concurrent mode', () => {
232
+ beforeEach(() => {
233
+ jest.resetModules();
234
+
235
+ ReactDOM = require('react-dom');
236
+ Scheduler = require('scheduler');
237
+ });
238
+
239
+ // @gate experimental
240
+ it('flushes pending interactive work before extracting event handler', () => {
241
+ container = document.createElement('div');
242
+ const root = ReactDOM.createRoot(container);
243
+ document.body.appendChild(container);
244
+
245
+ let button;
246
+ class Button extends React.Component {
247
+ state = {disabled: false};
248
+ onClick = () => {
249
+ // Perform some side-effect
250
+ Scheduler.unstable_yieldValue('Side-effect');
251
+ // Disable the button
252
+ this.setState({disabled: true});
253
+ };
254
+ render() {
255
+ Scheduler.unstable_yieldValue(
256
+ `render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
257
+ );
258
+ return (
259
+ <button
260
+ ref={el => (button = el)}
261
+ // Handler is removed after the first click
262
+ onClick={this.state.disabled ? null : this.onClick}
263
+ />
264
+ );
265
+ }
266
+ }
267
+
268
+ // Initial mount
269
+ root.render(<Button />);
270
+ // Should not have flushed yet because it's async
271
+ expect(Scheduler).toHaveYielded([]);
272
+ expect(button).toBe(undefined);
273
+ // Flush async work
274
+ expect(Scheduler).toFlushAndYield(['render button: enabled']);
275
+
276
+ function click() {
277
+ button.dispatchEvent(
278
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
279
+ );
280
+ }
281
+
282
+ // Click the button to trigger the side-effect
283
+ click();
284
+ expect(Scheduler).toHaveYielded([
285
+ // The handler fired
286
+ 'Side-effect',
287
+ // but the component did not re-render yet, because it's async
288
+ ]);
289
+
290
+ // Click the button again
291
+ click();
292
+ expect(Scheduler).toHaveYielded([
293
+ // Before handling this second click event, the previous interactive
294
+ // update is flushed
295
+ 'render button: disabled',
296
+ // The event handler was removed from the button, so there's no second
297
+ // side-effect
298
+ ]);
299
+
300
+ // The handler should not fire again no matter how many times we
301
+ // click the handler.
302
+ click();
303
+ click();
304
+ click();
305
+ click();
306
+ click();
307
+ expect(Scheduler).toFlushAndYield([]);
308
+ });
309
+
310
+ // @gate experimental
311
+ it('end result of many interactive updates is deterministic', () => {
312
+ container = document.createElement('div');
313
+ const root = ReactDOM.createRoot(container);
314
+ document.body.appendChild(container);
315
+
316
+ let button;
317
+ class Button extends React.Component {
318
+ state = {count: 0};
319
+ render() {
320
+ return (
321
+ <button
322
+ ref={el => (button = el)}
323
+ onClick={() =>
324
+ // Intentionally not using the updater form here
325
+ this.setState({count: this.state.count + 1})
326
+ }>
327
+ Count: {this.state.count}
328
+ </button>
329
+ );
330
+ }
331
+ }
332
+
333
+ // Initial mount
334
+ root.render(<Button />);
335
+ // Should not have flushed yet because it's async
336
+ expect(button).toBe(undefined);
337
+ // Flush async work
338
+ Scheduler.unstable_flushAll();
339
+ expect(button.textContent).toEqual('Count: 0');
340
+
341
+ function click() {
342
+ button.dispatchEvent(
343
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
344
+ );
345
+ }
346
+
347
+ // Click the button a single time
348
+ click();
349
+ // The counter should not have updated yet because it's async
350
+ expect(button.textContent).toEqual('Count: 0');
351
+
352
+ // Click the button many more times
353
+ click();
354
+ click();
355
+ click();
356
+ click();
357
+ click();
358
+ click();
359
+
360
+ // Flush the remaining work
361
+ Scheduler.unstable_flushAll();
362
+ // The counter should equal the total number of clicks
363
+ expect(button.textContent).toEqual('Count: 7');
364
+ });
365
+
366
+ // @gate experimental
367
+ it('flushes discrete updates in order', () => {
368
+ container = document.createElement('div');
369
+ document.body.appendChild(container);
370
+
371
+ let button;
372
+ class Button extends React.Component {
373
+ state = {lowPriCount: 0};
374
+ render() {
375
+ const text = `High-pri count: ${this.props.highPriCount}, Low-pri count: ${this.state.lowPriCount}`;
376
+ Scheduler.unstable_yieldValue(text);
377
+ return (
378
+ <button
379
+ ref={el => (button = el)}
380
+ onClick={() => {
381
+ Scheduler.unstable_next(() => {
382
+ this.setState(state => ({
383
+ lowPriCount: state.lowPriCount + 1,
384
+ }));
385
+ });
386
+ }}>
387
+ {text}
388
+ </button>
389
+ );
390
+ }
391
+ }
392
+
393
+ class Wrapper extends React.Component {
394
+ state = {highPriCount: 0};
395
+ render() {
396
+ return (
397
+ <div
398
+ onClick={
399
+ // Intentionally not using the updater form here, to test
400
+ // that updates are serially processed.
401
+ () => {
402
+ this.setState({highPriCount: this.state.highPriCount + 1});
403
+ }
404
+ }>
405
+ <Button highPriCount={this.state.highPriCount} />
406
+ </div>
407
+ );
408
+ }
409
+ }
410
+
411
+ // Initial mount
412
+ const root = ReactDOM.createRoot(container);
413
+ root.render(<Wrapper />);
414
+ expect(Scheduler).toFlushAndYield([
415
+ 'High-pri count: 0, Low-pri count: 0',
416
+ ]);
417
+ expect(button.textContent).toEqual('High-pri count: 0, Low-pri count: 0');
418
+
419
+ function click() {
420
+ button.dispatchEvent(
421
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
422
+ );
423
+ }
424
+
425
+ // Click the button a single time
426
+ click();
427
+ // Nothing should flush on the first click.
428
+ expect(Scheduler).toHaveYielded([]);
429
+ // Click again. This will force the previous discrete update to flush. But
430
+ // only the high-pri count will increase.
431
+ click();
432
+ expect(Scheduler).toHaveYielded(['High-pri count: 1, Low-pri count: 0']);
433
+ expect(button.textContent).toEqual('High-pri count: 1, Low-pri count: 0');
434
+
435
+ // Click the button many more times
436
+ click();
437
+ click();
438
+ click();
439
+ click();
440
+ click();
441
+ click();
442
+
443
+ // Flush the remaining work.
444
+ expect(Scheduler).toHaveYielded([
445
+ 'High-pri count: 2, Low-pri count: 0',
446
+ 'High-pri count: 3, Low-pri count: 0',
447
+ 'High-pri count: 4, Low-pri count: 0',
448
+ 'High-pri count: 5, Low-pri count: 0',
449
+ 'High-pri count: 6, Low-pri count: 0',
450
+ 'High-pri count: 7, Low-pri count: 0',
451
+ ]);
452
+
453
+ // At the end, both counters should equal the total number of clicks
454
+ expect(Scheduler).toFlushAndYield([
455
+ 'High-pri count: 8, Low-pri count: 0',
456
+ 'High-pri count: 8, Low-pri count: 8',
457
+ ]);
458
+ expect(button.textContent).toEqual('High-pri count: 8, Low-pri count: 8');
459
+ });
460
+ });
461
+
462
+ describe('iOS bubbling click fix', function() {
463
+ // See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
464
+
465
+ it('does not add a local click to interactive elements', function() {
466
+ container = document.createElement('div');
467
+
468
+ ReactDOM.render(<button onClick={onClick} />, container);
469
+
470
+ const node = container.firstChild;
471
+
472
+ node.dispatchEvent(new MouseEvent('click'));
473
+
474
+ expect(onClick).toHaveBeenCalledTimes(0);
475
+ });
476
+
477
+ it('adds a local click listener to non-interactive elements', function() {
478
+ container = document.createElement('div');
479
+
480
+ ReactDOM.render(<div onClick={onClick} />, container);
481
+
482
+ const node = container.firstChild;
483
+
484
+ node.dispatchEvent(new MouseEvent('click'));
485
+
486
+ expect(onClick).toHaveBeenCalledTimes(0);
487
+ });
488
+ });
489
+});