BeforeInput is fired with a wrong text at a wrong time on IE (#7107)
tokikuch committed
Sep 13, 2016 at 11:15 UTC
a64ca9b697d0808a2966465fea2e658e4f310080
2 files changed
+214
-4
src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js
+5
-4
@@ -345,11 +345,12 @@ function getNativeBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) {
345
function getFallbackBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) {
346
// If we are currently composing (IME) and using a fallback to do so,
347
// try to extract the composed characters from the fallback object.
348
+ // If composition event is available, we extract a string only at
349
+ // compositionevent, otherwise extract it at fallback events.
350
if (currentComposition) {
349
- if (
350
- topLevelType === 'topCompositionEnd' ||
351
- isFallbackCompositionEnd(topLevelType, nativeEvent)
352
- ) {
351
+ if (topLevelType === 'topCompositionEnd'
352
+ || (!canUseCompositionEvent
353
+ && isFallbackCompositionEnd(topLevelType, nativeEvent))) {
354
var chars = currentComposition.getData();
355
FallbackCompositionState.release(currentComposition);
356
currentComposition = null;
src/renderers/dom/client/eventPlugins/__tests__/BeforeInputEventPlugin-test.js
new
+209
@@ -0,0 +1,209 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+
12
+'use strict';
13
+
14
+var React = require('React');
15
+var ReactTestUtils = require('ReactTestUtils');
16
+
17
+var EventMapping = {
18
+ compositionstart : 'topCompositionStart',
19
+ compositionend : 'topCompositionEnd',
20
+ keyup : 'topKeyUp',
21
+ keydown : 'topKeyDown',
22
+ textInput : 'topTextInput',
23
+ textinput : null, // Not defined now
24
+};
25
+
26
+describe('BeforeInputEventPlugin', function() {
27
+ var ModuleCache;
28
+
29
+ function simulateIE11() {
30
+ document.documentMode = 11;
31
+ window.CompositionEvent = {};
32
+ delete window.TextEvent;
33
+ }
34
+
35
+ function simulateWebkit() {
36
+ delete document.documentMode;
37
+ window.CompositionEvent = {};
38
+ window.TextEvent = {};
39
+ }
40
+
41
+ function initialize(simulator) {
42
+ // Need to delete cached modules before executing simulator
43
+ jest.resetModuleRegistry();
44
+
45
+ // Initialize variables in the scope of BeforeInputEventPlugin
46
+ simulator();
47
+
48
+ // Modules which have dependency on BeforeInputEventPlugin are stored
49
+ // in ModuleCache so that we can use these modules ouside test functions.
50
+ this.ReactDOM = require('ReactDOM');
51
+ this.ReactDOMComponentTree = require('ReactDOMComponentTree');
52
+ this.SyntheticCompositionEvent = require('SyntheticCompositionEvent');
53
+ this.SyntheticInputEvent = require('SyntheticInputEvent');
54
+ this.BeforeInputEventPlugin = require('BeforeInputEventPlugin');
55
+ }
56
+
57
+ function extract(node, eventType, optionalData) {
58
+ var evt = document.createEvent('HTMLEvents');
59
+ evt.initEvent(eventType, true, true);
60
+ evt = Object.assign(evt, optionalData);
61
+ return ModuleCache.BeforeInputEventPlugin.extractEvents(
62
+ EventMapping[eventType],
63
+ ModuleCache.ReactDOMComponentTree.getInstanceFromNode(node),
64
+ evt,
65
+ node
66
+ );
67
+ }
68
+
69
+ function setElementText(node) {
70
+ return (args) => node.innerHTML = args;
71
+ }
72
+
73
+ function accumulateEvents(node, events) {
74
+ // We don't use accumulateInto module to apply partial application.
75
+ return function() {
76
+ var newArgs = [node].concat(Array.prototype.slice.call(arguments));
77
+ var newEvents = extract.apply(this, newArgs);
78
+ Array.prototype.push.apply(events, newEvents);
79
+ };
80
+ }
81
+
82
+ function EventMismatchError(idx, message) {
83
+ this.name = 'EventMismatchError';
84
+ this.message = '[' + idx + '] ' + message;
85
+ }
86
+ EventMismatchError.prototype = Object.create(Error.prototype);
87
+
88
+ function verifyEvents(actualEvents, expectedEvents) {
89
+ expect(actualEvents.length).toBe(expectedEvents.length);
90
+ expectedEvents.forEach(function(expected, idx) {
91
+ var actual = actualEvents[idx];
92
+ expect(function() {
93
+ if (actual === null && expected.type === null) {
94
+ // Both are null. Expected.
95
+ } else if (actual === null) {
96
+ throw new EventMismatchError(idx, 'Expected not to be null');
97
+ } else if (expected.type === null
98
+ || !(actual instanceof expected.type)) {
99
+ throw new EventMismatchError(idx, 'Unexpected type: ' + actual);
100
+ } else {
101
+ // Type match.
102
+ Object.keys(expected.data).forEach(function(expectedKey) {
103
+ if (!(expectedKey in actual)) {
104
+ throw new EventMismatchError(idx, 'KeyNotFound: ' + expectedKey);
105
+ } else if (actual[expectedKey] !== expected.data[expectedKey]) {
106
+ throw new EventMismatchError(idx,
107
+ 'ValueMismatch: ' + actual[expectedKey]);
108
+ }
109
+ });
110
+ }
111
+ }).not.toThrow();
112
+ });
113
+ }
114
+
115
+ // IE fires an event named `textinput` with all lowercase characters,
116
+ // instead of a standard name `textInput`. As of now, React does not have
117
+ // a corresponding topEvent to IE's textinput, but both events are added to
118
+ // this scenario data for future use.
119
+ var Scenario_Composition = [
120
+ {run: accumulateEvents, arg: ['compositionstart', {data: ''}]},
121
+ {run: accumulateEvents, arg: ['textInput', {data: 'A'}]},
122
+ {run: accumulateEvents, arg: ['textinput', {data: 'A'}]},
123
+ {run: accumulateEvents, arg: ['keyup', {keyCode: 65}]},
124
+ {run: setElementText, arg: ['ABC']},
125
+ {run: accumulateEvents, arg: ['textInput', {data: 'abc'}]},
126
+ {run: accumulateEvents, arg: ['textinput', {data: 'abc'}]},
127
+ {run: accumulateEvents, arg: ['keyup', {keyCode: 32}]},
128
+ {run: setElementText, arg: ['XYZ']},
129
+ {run: accumulateEvents, arg: ['textInput', {data: 'xyz'}]},
130
+ {run: accumulateEvents, arg: ['textinput', {data: 'xyz'}]},
131
+ {run: accumulateEvents, arg: ['keyup', {keyCode: 32}]},
132
+ {run: accumulateEvents, arg: ['compositionend', {data: 'Hello'}]},
133
+ ];
134
+
135
+ /* Defined expected results as a factory of result data because we need
136
+ lazy evaluation for event modules.
137
+ Event modules are reloaded to simulate a different platform per testcase.
138
+ If we define expected results as a simple dictionary here, the comparison
139
+ of 'instanceof' fails after module cache is reset. */
140
+
141
+ // Webkit behavior is simple. We expect SyntheticInputEvent at each
142
+ // textInput, SyntheticCompositionEvent at composition, and nothing from
143
+ // keyUp.
144
+ var Expected_Webkit = () => [
145
+ {type: ModuleCache.SyntheticCompositionEvent, data: {}}, {type: null},
146
+ {type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'A'}},
147
+ {type: null}, {type: null}, // textinput of A
148
+ {type: null}, {type: null}, // keyUp of 65
149
+ {type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'abc'}},
150
+ {type: null}, {type: null}, // textinput of abc
151
+ {type: null}, {type: null}, // keyUp of 32
152
+ {type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'xyz'}},
153
+ {type: null}, {type: null}, // textinput of xyz
154
+ {type: null}, {type: null}, // keyUp of 32
155
+ {type: ModuleCache.SyntheticCompositionEvent, data: {data: 'Hello'}},
156
+ {type: null},
157
+ ];
158
+
159
+ // For IE11, we use fallback data instead of IE's textinput events.
160
+ // We expect no SyntheticInputEvent from textinput. Fallback beforeInput is
161
+ // expected to be triggered at compositionend with a text of the target
162
+ // element, not event data.
163
+ var Expected_IE11 = () => [
164
+ {type: ModuleCache.SyntheticCompositionEvent, data: {}}, {type: null},
165
+ {type: null}, {type: null}, // textInput of A
166
+ {type: null}, {type: null}, // textinput of A
167
+ {type: null}, {type: null}, // keyUp of 65
168
+ {type: null}, {type: null}, // textInput of abc
169
+ {type: null}, {type: null}, // textinput of abc
170
+
171
+ // fallbackData should NOT be set at keyUp with any of END_KEYCODES
172
+ {type: null}, {type: null}, // keyUp of 32
173
+
174
+ {type: null}, {type: null}, // textInput of xyz
175
+ {type: null}, {type: null}, // textinput of xyz
176
+ {type: null}, {type: null}, // keyUp of 32
177
+
178
+ // fallbackData is retrieved from the element, which is XYZ,
179
+ // at a time of compositionend
180
+ {type: ModuleCache.SyntheticCompositionEvent, data: {}},
181
+ {type: ModuleCache.SyntheticInputEvent, data: {data: 'XYZ'}},
182
+ ];
183
+
184
+ function TestEditableReactComponent(Emulator, Scenario, ExpectedResult) {
185
+ ModuleCache = new initialize(Emulator);
186
+
187
+ var EditableDiv = React.createClass({
188
+ render: () => (<div contentEditable="true" />),
189
+ });
190
+ var rendered = ReactTestUtils.renderIntoDocument(<EditableDiv />);
191
+
192
+ var node = ModuleCache.ReactDOM.findDOMNode(rendered);
193
+ var events = [];
194
+
195
+ Scenario.forEach((el) =>
196
+ el.run.call(this, node, events).apply(this, el.arg));
197
+ verifyEvents(events, ExpectedResult());
198
+ }
199
+
200
+ it('extract onBeforeInput from native textinput events', function() {
201
+ TestEditableReactComponent(
202
+ simulateWebkit, Scenario_Composition, Expected_Webkit);
203
+ });
204
+
205
+ it('extract onBeforeInput from fallback objects', function() {
206
+ TestEditableReactComponent(
207
+ simulateIE11, Scenario_Composition, Expected_IE11);
208
+ });
209
+});