mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
BeforeInput is fired with a wrong text at a wrong time on IE (#7107)
This commit is contained in:
@@ -345,11 +345,12 @@ function getNativeBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) {
|
||||
function getFallbackBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) {
|
||||
// If we are currently composing (IME) and using a fallback to do so,
|
||||
// try to extract the composed characters from the fallback object.
|
||||
// If composition event is available, we extract a string only at
|
||||
// compositionevent, otherwise extract it at fallback events.
|
||||
if (currentComposition) {
|
||||
if (
|
||||
topLevelType === 'topCompositionEnd' ||
|
||||
isFallbackCompositionEnd(topLevelType, nativeEvent)
|
||||
) {
|
||||
if (topLevelType === 'topCompositionEnd'
|
||||
|| (!canUseCompositionEvent
|
||||
&& isFallbackCompositionEnd(topLevelType, nativeEvent))) {
|
||||
var chars = currentComposition.getData();
|
||||
FallbackCompositionState.release(currentComposition);
|
||||
currentComposition = null;
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Copyright 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('React');
|
||||
var ReactTestUtils = require('ReactTestUtils');
|
||||
|
||||
var EventMapping = {
|
||||
compositionstart : 'topCompositionStart',
|
||||
compositionend : 'topCompositionEnd',
|
||||
keyup : 'topKeyUp',
|
||||
keydown : 'topKeyDown',
|
||||
textInput : 'topTextInput',
|
||||
textinput : null, // Not defined now
|
||||
};
|
||||
|
||||
describe('BeforeInputEventPlugin', function() {
|
||||
var ModuleCache;
|
||||
|
||||
function simulateIE11() {
|
||||
document.documentMode = 11;
|
||||
window.CompositionEvent = {};
|
||||
delete window.TextEvent;
|
||||
}
|
||||
|
||||
function simulateWebkit() {
|
||||
delete document.documentMode;
|
||||
window.CompositionEvent = {};
|
||||
window.TextEvent = {};
|
||||
}
|
||||
|
||||
function initialize(simulator) {
|
||||
// Need to delete cached modules before executing simulator
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
// Initialize variables in the scope of BeforeInputEventPlugin
|
||||
simulator();
|
||||
|
||||
// Modules which have dependency on BeforeInputEventPlugin are stored
|
||||
// in ModuleCache so that we can use these modules ouside test functions.
|
||||
this.ReactDOM = require('ReactDOM');
|
||||
this.ReactDOMComponentTree = require('ReactDOMComponentTree');
|
||||
this.SyntheticCompositionEvent = require('SyntheticCompositionEvent');
|
||||
this.SyntheticInputEvent = require('SyntheticInputEvent');
|
||||
this.BeforeInputEventPlugin = require('BeforeInputEventPlugin');
|
||||
}
|
||||
|
||||
function extract(node, eventType, optionalData) {
|
||||
var evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent(eventType, true, true);
|
||||
evt = Object.assign(evt, optionalData);
|
||||
return ModuleCache.BeforeInputEventPlugin.extractEvents(
|
||||
EventMapping[eventType],
|
||||
ModuleCache.ReactDOMComponentTree.getInstanceFromNode(node),
|
||||
evt,
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
function setElementText(node) {
|
||||
return (args) => node.innerHTML = args;
|
||||
}
|
||||
|
||||
function accumulateEvents(node, events) {
|
||||
// We don't use accumulateInto module to apply partial application.
|
||||
return function() {
|
||||
var newArgs = [node].concat(Array.prototype.slice.call(arguments));
|
||||
var newEvents = extract.apply(this, newArgs);
|
||||
Array.prototype.push.apply(events, newEvents);
|
||||
};
|
||||
}
|
||||
|
||||
function EventMismatchError(idx, message) {
|
||||
this.name = 'EventMismatchError';
|
||||
this.message = '[' + idx + '] ' + message;
|
||||
}
|
||||
EventMismatchError.prototype = Object.create(Error.prototype);
|
||||
|
||||
function verifyEvents(actualEvents, expectedEvents) {
|
||||
expect(actualEvents.length).toBe(expectedEvents.length);
|
||||
expectedEvents.forEach(function(expected, idx) {
|
||||
var actual = actualEvents[idx];
|
||||
expect(function() {
|
||||
if (actual === null && expected.type === null) {
|
||||
// Both are null. Expected.
|
||||
} else if (actual === null) {
|
||||
throw new EventMismatchError(idx, 'Expected not to be null');
|
||||
} else if (expected.type === null
|
||||
|| !(actual instanceof expected.type)) {
|
||||
throw new EventMismatchError(idx, 'Unexpected type: ' + actual);
|
||||
} else {
|
||||
// Type match.
|
||||
Object.keys(expected.data).forEach(function(expectedKey) {
|
||||
if (!(expectedKey in actual)) {
|
||||
throw new EventMismatchError(idx, 'KeyNotFound: ' + expectedKey);
|
||||
} else if (actual[expectedKey] !== expected.data[expectedKey]) {
|
||||
throw new EventMismatchError(idx,
|
||||
'ValueMismatch: ' + actual[expectedKey]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).not.toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
// IE fires an event named `textinput` with all lowercase characters,
|
||||
// instead of a standard name `textInput`. As of now, React does not have
|
||||
// a corresponding topEvent to IE's textinput, but both events are added to
|
||||
// this scenario data for future use.
|
||||
var Scenario_Composition = [
|
||||
{run: accumulateEvents, arg: ['compositionstart', {data: ''}]},
|
||||
{run: accumulateEvents, arg: ['textInput', {data: 'A'}]},
|
||||
{run: accumulateEvents, arg: ['textinput', {data: 'A'}]},
|
||||
{run: accumulateEvents, arg: ['keyup', {keyCode: 65}]},
|
||||
{run: setElementText, arg: ['ABC']},
|
||||
{run: accumulateEvents, arg: ['textInput', {data: 'abc'}]},
|
||||
{run: accumulateEvents, arg: ['textinput', {data: 'abc'}]},
|
||||
{run: accumulateEvents, arg: ['keyup', {keyCode: 32}]},
|
||||
{run: setElementText, arg: ['XYZ']},
|
||||
{run: accumulateEvents, arg: ['textInput', {data: 'xyz'}]},
|
||||
{run: accumulateEvents, arg: ['textinput', {data: 'xyz'}]},
|
||||
{run: accumulateEvents, arg: ['keyup', {keyCode: 32}]},
|
||||
{run: accumulateEvents, arg: ['compositionend', {data: 'Hello'}]},
|
||||
];
|
||||
|
||||
/* Defined expected results as a factory of result data because we need
|
||||
lazy evaluation for event modules.
|
||||
Event modules are reloaded to simulate a different platform per testcase.
|
||||
If we define expected results as a simple dictionary here, the comparison
|
||||
of 'instanceof' fails after module cache is reset. */
|
||||
|
||||
// Webkit behavior is simple. We expect SyntheticInputEvent at each
|
||||
// textInput, SyntheticCompositionEvent at composition, and nothing from
|
||||
// keyUp.
|
||||
var Expected_Webkit = () => [
|
||||
{type: ModuleCache.SyntheticCompositionEvent, data: {}}, {type: null},
|
||||
{type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'A'}},
|
||||
{type: null}, {type: null}, // textinput of A
|
||||
{type: null}, {type: null}, // keyUp of 65
|
||||
{type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'abc'}},
|
||||
{type: null}, {type: null}, // textinput of abc
|
||||
{type: null}, {type: null}, // keyUp of 32
|
||||
{type: null}, {type: ModuleCache.SyntheticInputEvent, data: {data: 'xyz'}},
|
||||
{type: null}, {type: null}, // textinput of xyz
|
||||
{type: null}, {type: null}, // keyUp of 32
|
||||
{type: ModuleCache.SyntheticCompositionEvent, data: {data: 'Hello'}},
|
||||
{type: null},
|
||||
];
|
||||
|
||||
// For IE11, we use fallback data instead of IE's textinput events.
|
||||
// We expect no SyntheticInputEvent from textinput. Fallback beforeInput is
|
||||
// expected to be triggered at compositionend with a text of the target
|
||||
// element, not event data.
|
||||
var Expected_IE11 = () => [
|
||||
{type: ModuleCache.SyntheticCompositionEvent, data: {}}, {type: null},
|
||||
{type: null}, {type: null}, // textInput of A
|
||||
{type: null}, {type: null}, // textinput of A
|
||||
{type: null}, {type: null}, // keyUp of 65
|
||||
{type: null}, {type: null}, // textInput of abc
|
||||
{type: null}, {type: null}, // textinput of abc
|
||||
|
||||
// fallbackData should NOT be set at keyUp with any of END_KEYCODES
|
||||
{type: null}, {type: null}, // keyUp of 32
|
||||
|
||||
{type: null}, {type: null}, // textInput of xyz
|
||||
{type: null}, {type: null}, // textinput of xyz
|
||||
{type: null}, {type: null}, // keyUp of 32
|
||||
|
||||
// fallbackData is retrieved from the element, which is XYZ,
|
||||
// at a time of compositionend
|
||||
{type: ModuleCache.SyntheticCompositionEvent, data: {}},
|
||||
{type: ModuleCache.SyntheticInputEvent, data: {data: 'XYZ'}},
|
||||
];
|
||||
|
||||
function TestEditableReactComponent(Emulator, Scenario, ExpectedResult) {
|
||||
ModuleCache = new initialize(Emulator);
|
||||
|
||||
var EditableDiv = React.createClass({
|
||||
render: () => (<div contentEditable="true" />),
|
||||
});
|
||||
var rendered = ReactTestUtils.renderIntoDocument(<EditableDiv />);
|
||||
|
||||
var node = ModuleCache.ReactDOM.findDOMNode(rendered);
|
||||
var events = [];
|
||||
|
||||
Scenario.forEach((el) =>
|
||||
el.run.call(this, node, events).apply(this, el.arg));
|
||||
verifyEvents(events, ExpectedResult());
|
||||
}
|
||||
|
||||
it('extract onBeforeInput from native textinput events', function() {
|
||||
TestEditableReactComponent(
|
||||
simulateWebkit, Scenario_Composition, Expected_Webkit);
|
||||
});
|
||||
|
||||
it('extract onBeforeInput from fallback objects', function() {
|
||||
TestEditableReactComponent(
|
||||
simulateIE11, Scenario_Composition, Expected_IE11);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user