@samitouri / QOS-React-2 / commits / 31977d1bb3

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 +});