Skip to content

Commit

Permalink
Revert "Remove old IE polyfill code" (facebook#10483)
Browse files Browse the repository at this point in the history
**what is the change?:**
This reverts facebook@0b220d0
Which was part of facebook#10238

**why make this change?:**
When trying to sync the latest React with FB's codebase, we had failing
integration tests.

It looks like they are running with an old version of Chrome and there
is something related to file upload that fails when these polyfills are
missing.

For now we'd like to revert this to unblock syncing, but it's worth
revisiting this change to try and add some polyfill for FB and remove it
from React, or to fix whatever the specific issue was with our file
upload error.

**test plan:**
`yarn test` and also built and played with the `dom` and `packaging`
fixtures
  • Loading branch information
flarnie authored Aug 17, 2017
1 parent caaa0b0 commit 18083a8
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 47 deletions.
209 changes: 167 additions & 42 deletions src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@

'use strict';

var EventPluginHub = require('EventPluginHub');
var EventPropagators = require('EventPropagators');
var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
var ReactControlledComponent = require('ReactControlledComponent');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactGenericBatching = require('ReactGenericBatching');
var SyntheticEvent = require('SyntheticEvent');
var getActiveElement = require('fbjs/lib/getActiveElement');

var inputValueTracking = require('inputValueTracking');
var getEventTarget = require('getEventTarget');
var isEventSupported = require('isEventSupported');
var isTextInputElement = require('isTextInputElement');

var eventTypes = {
Expand Down Expand Up @@ -65,53 +68,174 @@ function createAndAccumulateChangeEvent(
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;
}
/**
* For IE shims
*/
var activeElement = null;
var activeElementInst = null;

function getInstIfValueChanged(targetInst, targetNode) {
/**
* SECTION: handle `change` event
*/
function shouldUseChangeEvent(elem) {
var nodeName = elem.nodeName && elem.nodeName.toLowerCase();
return (
nodeName === 'select' || (nodeName === 'input' && elem.type === 'file')
);
}

function manualDispatchChangeEvent(nativeEvent) {
var event = createAndAccumulateChangeEvent(
activeElementInst,
nativeEvent,
getEventTarget(nativeEvent),
);

// If change and propertychange bubbled, we'd just bind to it like all the
// other events and have it go through ReactBrowserEventEmitter. Since it
// doesn't, we manually listen for the events and so we have to enqueue and
// process the abstract event manually.
//
// Batching is necessary here in order to ensure that all event handlers run
// before the next rerender (including event handlers attached to ancestor
// elements instead of directly on the input). Without this, controlled
// components don't work properly in conjunction with event bubbling because
// the component is rerendered and the value reverted before all the event
// handlers can run. See https://github.com/facebook/react/issues/708.
ReactGenericBatching.batchedUpdates(runEventInBatch, event);
}

function runEventInBatch(event) {
EventPluginHub.enqueueEvents(event);
EventPluginHub.processEventQueue(false);
}

function getInstIfValueChanged(targetInst) {
const targetNode = ReactDOMComponentTree.getNodeFromInstance(targetInst);
if (inputValueTracking.updateValueIfChanged(targetNode)) {
return targetInst;
}
}

function getTargetInstForChangeEvent(topLevelType, targetInst) {
if (topLevelType === 'topChange') {
return targetInst;
}
}

/**
* SECTION: handle `input` event
*/

var isTextInputEventSupported = false;
var isInputEventSupported = false;
if (ExecutionEnvironment.canUseDOM) {
isTextInputEventSupported =
!document.documentMode || document.documentMode > 9;
// IE9 claims to support the input event but fails to trigger it when
// deleting text, so we ignore its input events.
isInputEventSupported =
isEventSupported('input') &&
(!document.documentMode || document.documentMode > 9);
}

function getTargetInstForInputEventPolyfill(
topLevelType,
targetInst,
targetNode,
) {
/**
* (For IE <=9) Starts tracking propertychange events on the passed-in element
* and override the value property so that we can distinguish user events from
* value changes in JS.
*/
function startWatchingForValueChange(target, targetInst) {
activeElement = target;
activeElementInst = targetInst;
activeElement.attachEvent('onpropertychange', handlePropertyChange);
}

/**
* (For IE <=9) Removes the event listeners from the currently-tracked element,
* if any exists.
*/
function stopWatchingForValueChange() {
if (!activeElement) {
return;
}
activeElement.detachEvent('onpropertychange', handlePropertyChange);
activeElement = null;
activeElementInst = null;
}

/**
* (For IE <=9) Handles a propertychange event, sending a `change` event if
* the value of the active element has changed.
*/
function handlePropertyChange(nativeEvent) {
if (nativeEvent.propertyName !== 'value') {
return;
}
if (getInstIfValueChanged(activeElementInst)) {
manualDispatchChangeEvent(nativeEvent);
}
}

function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) {
if (topLevelType === 'topFocus') {
// In IE9, propertychange fires for most input events but is buggy and
// doesn't fire when text is deleted, but conveniently, selectionchange
// appears to fire in all of the remaining cases so we catch those and
// forward the event if the value has changed
// In either case, we don't want to call the event handler if the value
// is changed from JS so we redefine a setter for `.value` that updates
// our activeElementValue variable, allowing us to ignore those changes
//
// stopWatching() should be a noop here but we call it just in case we
// missed a blur event somehow.
stopWatchingForValueChange();
startWatchingForValueChange(target, targetInst);
} else if (topLevelType === 'topBlur') {
stopWatchingForValueChange();
}
}

// For IE8 and IE9.
function getTargetInstForInputEventPolyfill(topLevelType, targetInst) {
if (
topLevelType === 'topInput' ||
topLevelType === 'topChange' ||
// These events catch anything the IE9 onInput misses
topLevelType === 'topSelectionChange' ||
topLevelType === 'topKeyUp' ||
topLevelType === 'topKeyDown'
) {
return getInstIfValueChanged(targetInst, targetNode);
// On the selectionchange event, the target is just document which isn't
// helpful for us so just check activeElement instead.
//
// 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
// propertychange on the first input event after setting `value` from a
// script and fires only keydown, keypress, keyup. Catching keyup usually
// gets it and catching keydown lets us fire an event for the first
// keystroke if user does a key repeat (it'll be a little delayed: right
// before the second keystroke). Other input methods (e.g., paste) seem to
// fire selectionchange normally.
return getInstIfValueChanged(activeElementInst);
}
}

function getTargetInstForInputOrChangeEvent(
topLevelType,
targetInst,
targetNode,
) {
if (topLevelType === 'topInput' || topLevelType === 'topChange') {
return getInstIfValueChanged(targetInst, targetNode);
/**
* SECTION: handle `click` event
*/
function shouldUseClickEvent(elem) {
// Use the `click` event to detect changes to checkbox and radio inputs.
// This approach works across all browsers, whereas `change` does not fire
// until `blur` in IE8.
var nodeName = elem.nodeName;
return (
nodeName &&
nodeName.toLowerCase() === 'input' &&
(elem.type === 'checkbox' || elem.type === 'radio')
);
}

function getTargetInstForClickEvent(topLevelType, targetInst) {
if (topLevelType === 'topClick') {
return getInstIfValueChanged(targetInst);
}
}

function getTargetInstForChangeEvent(topLevelType, targetInst, targetNode) {
if (topLevelType === 'topChange') {
return getInstIfValueChanged(targetInst, targetNode);
function getTargetInstForInputOrChangeEvent(topLevelType, targetInst) {
if (topLevelType === 'topInput' || topLevelType === 'topChange') {
return getInstIfValueChanged(targetInst);
}
}

Expand Down Expand Up @@ -148,33 +272,34 @@ function handleControlledInputBlur(inst, node) {
var ChangeEventPlugin = {
eventTypes: eventTypes,

extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
_isInputEventSupported: isInputEventSupported,

extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
var targetNode = targetInst
? ReactDOMComponentTree.getNodeFromInstance(targetInst)
: window;

// On the selectionchange event, the target is the document which isn't
// helpful becasue we need the input, so we use the activeElement instead.
if (!isTextInputEventSupported && topLevelType === 'topSelectionChange') {
nativeEventTarget = targetNode = getActiveElement();

if (targetNode) {
targetInst = ReactDOMComponentTree.getInstanceFromNode(targetNode);
}
}

var getTargetInstFunc, handleEventFunc;

if (shouldUseChangeEvent(targetNode)) {
getTargetInstFunc = getTargetInstForChangeEvent;
} else if (isTextInputElement(targetNode) && !isTextInputEventSupported) {
getTargetInstFunc = getTargetInstForInputEventPolyfill;
} else {
getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else if (isTextInputElement(targetNode)) {
if (isInputEventSupported) {
getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else {
getTargetInstFunc = getTargetInstForInputEventPolyfill;
handleEventFunc = handleEventsForInputEventPolyfill;
}
} else if (shouldUseClickEvent(targetNode)) {
getTargetInstFunc = getTargetInstForClickEvent;
}

if (getTargetInstFunc) {
var inst = getTargetInstFunc(topLevelType, targetInst, targetNode);
var inst = getTargetInstFunc(topLevelType, targetInst);
if (inst) {
var event = createAndAccumulateChangeEvent(
inst,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var React = require('react');
var ReactDOM = require('react-dom');
var ReactTestUtils = require('react-dom/test-utils');
// TODO: can we express this test with only public API?
var ChangeEventPlugin = require('ChangeEventPlugin');
var inputValueTracking = require('inputValueTracking');

function getTrackedValue(elem) {
Expand Down Expand Up @@ -53,7 +54,7 @@ describe('ChangeEventPlugin', () => {
);

setUntrackedValue(input, true);
ReactTestUtils.SimulateNative.change(input);
ReactTestUtils.SimulateNative.click(input);

expect(called).toBe(1);
});
Expand Down Expand Up @@ -102,12 +103,12 @@ describe('ChangeEventPlugin', () => {
);

input.checked = true;
ReactTestUtils.SimulateNative.change(input);
ReactTestUtils.SimulateNative.click(input);
expect(called).toBe(0);

input.checked = false;
setTrackedValue(input, undefined);
ReactTestUtils.SimulateNative.change(input);
ReactTestUtils.SimulateNative.click(input);

expect(called).toBe(1);
});
Expand All @@ -130,8 +131,8 @@ describe('ChangeEventPlugin', () => {
<input type="radio" onChange={cb} />,
);
setUntrackedValue(input, true);
ReactTestUtils.SimulateNative.change(input);
ReactTestUtils.SimulateNative.input(input);
ReactTestUtils.SimulateNative.click(input);
ReactTestUtils.SimulateNative.click(input);
expect(called).toBe(1);
});

Expand Down Expand Up @@ -181,6 +182,10 @@ describe('ChangeEventPlugin', () => {
expect(e.type).toBe('change');
}

if (!ChangeEventPlugin._isInputEventSupported) {
return;
}

var input = ReactTestUtils.renderIntoDocument(
<input type="range" onChange={cb} />,
);
Expand Down

0 comments on commit 18083a8

Please sign in to comment.