Merge branch 'master' into rollup

This commit is contained in:
Dan Abramov
2017-03-29 13:59:44 +01:00
16 changed files with 532 additions and 36 deletions
+58 -8
View File
@@ -126,16 +126,29 @@
class ExampleApplication extends React.Component {
constructor() {
super();
this.state = { seconds: 0 };
this.state = {
seconds: 0,
useTimeSlicing: true,
};
this.tick = this.tick.bind(this);
this.onTimeSlicingChange = this.onTimeSlicingChange.bind(this);
}
componentDidMount() {
this.intervalID = setInterval(this.tick, 1000);
}
tick() {
ReactDOMFiber.unstable_deferredUpdates(() =>
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }))
);
if (this.state.useTimeSlicing) {
// Update is time-sliced.
ReactDOMFiber.unstable_deferredUpdates(() => {
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }));
});
} else {
// Update is not time-sliced. Causes demo to stutter.
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }));
}
}
onTimeSlicingChange(value) {
this.setState(() => ({ useTimeSlicing: value }));
}
componentWillUnmount() {
clearInterval(this.intervalID);
@@ -147,17 +160,54 @@
const scale = 1 + (t > 5 ? 10 - t : t) / 10;
const transform = 'scaleX(' + (scale / 2.1) + ') scaleY(0.7) translateZ(0.1px)';
return (
<div style={{ ...containerStyle, transform }}>
<div>
<div>
<SierpinskiTriangle x={0} y={0} s={1000}>
{this.state.seconds}
</SierpinskiTriangle>
<h3>Time-slicing</h3>
<p>Toggle this and observe the effect</p>
<Toggle
onLabel="On"
offLabel="Off"
onChange={this.onTimeSlicingChange}
value={this.state.useTimeSlicing}
/>
</div>
<div style={{ ...containerStyle, transform }}>
<div>
<SierpinskiTriangle x={0} y={0} s={1000}>
{this.state.seconds}
</SierpinskiTriangle>
</div>
</div>
</div>
);
}
}
class Toggle extends React.Component {
constructor(props) {
super();
this.onChange = this.onChange.bind(this);
}
onChange(event) {
this.props.onChange(event.target.value === 'on');
}
render() {
const value = this.props.value;
return (
<label onChange={this.onChange}>
<label>
{this.props.onLabel}
<input type="radio" name="value" value="on" checked={value} />
</label>
<label>
{this.props.offLabel}
<input type="radio" name="value" value="off" checked={!value} />
</label>
</label>
);
}
}
var start = new Date().getTime();
function update() {
ReactDOMFiber.render(
+1
View File
@@ -44,6 +44,7 @@ const Header = React.createClass({
<option value="/">Select a Fixture</option>
<option value="/range-inputs">Range Inputs</option>
<option value="/text-inputs">Text Inputs</option>
<option value="/number-inputs">Number Input</option>
<option value="/selects">Selects</option>
<option value="/textareas">Textareas</option>
<option value="/input-change-events">Input change events</option>
@@ -4,6 +4,7 @@ import TextInputFixtures from './text-inputs';
import SelectFixtures from './selects';
import TextAreaFixtures from './textareas';
import InputChangeEvents from './input-change-events';
import NumberInputFixtures from './number-inputs/';
/**
* A simple routing component that renders the appropriate
@@ -22,6 +23,8 @@ const FixturesPage = React.createClass({
return <TextAreaFixtures />;
case '/input-change-events':
return <InputChangeEvents />;
case '/number-inputs':
return <NumberInputFixtures />;
default:
return <p>Please select a test fixture.</p>;
}
@@ -0,0 +1,37 @@
const React = window.React;
import Fixture from '../../Fixture';
const NumberTestCase = React.createClass({
getInitialState() {
return { value: '' };
},
onChange(event) {
const parsed = parseFloat(event.target.value, 10)
const value = isNaN(parsed) ? '' : parsed
this.setState({ value })
},
render() {
return (
<Fixture>
<div>{this.props.children}</div>
<div className="control-box">
<fieldset>
<legend>Controlled</legend>
<input type="number" value={this.state.value} onChange={this.onChange} />
<span className="hint"> Value: {JSON.stringify(this.state.value)}</span>
</fieldset>
<fieldset>
<legend>Uncontrolled</legend>
<input type="number" defaultValue={0.5} />
</fieldset>
</div>
</Fixture>
);
},
});
export default NumberTestCase;
@@ -0,0 +1,167 @@
const React = window.React;
import FixtureSet from '../../FixtureSet';
import TestCase from '../../TestCase';
import NumberTestCase from './NumberTestCase';
const NumberInputs = React.createClass({
render() {
return (
<FixtureSet
title="Number inputs"
description="Number inputs inconsistently assign and report the value
property depending on the browser."
>
<TestCase
title="Backspacing"
description="The decimal place should not be lost"
>
<TestCase.Steps>
<li>Type "3.1"</li>
<li>Press backspace, eliminating the "1"</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "3.", preserving the decimal place
</TestCase.ExpectedResult>
<NumberTestCase />
<p className="footnote">
<b>Notes:</b> Chrome and Safari clear trailing
decimals on blur. React makes this concession so that the
value attribute remains in sync with the value property.
</p>
</TestCase>
<TestCase
title="Decimal precision"
description="Supports decimal precision greater than 2 places"
>
<TestCase.Steps>
<li>Type "0.01"</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "0.01"
</TestCase.ExpectedResult>
<NumberTestCase />
</TestCase>
<TestCase
title="Exponent form"
description="Supports exponent form ('2e4')"
>
<TestCase.Steps>
<li>Type "2e"</li>
<li>Type 4, to read "2e4"</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "2e4". The parsed value should read "20000"
</TestCase.ExpectedResult>
<NumberTestCase />
</TestCase>
<TestCase
title="Exponent Form"
description="Pressing 'e' at the end"
>
<TestCase.Steps>
<li>Type "3.14"</li>
<li>Press "e", so that the input reads "3.14e"</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "3.14e", the parsed value should be empty
</TestCase.ExpectedResult>
<NumberTestCase />
</TestCase>
<TestCase
title="Exponent Form"
description="Supports pressing 'ee' in the middle of a number"
>
<TestCase.Steps>
<li>Type "3.14"</li>
<li>Move the text cursor to after the decimal place</li>
<li>Press "e" twice, so that the value reads "3.ee14"</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "3.ee14"
</TestCase.ExpectedResult>
<NumberTestCase />
</TestCase>
<TestCase
title="Trailing Zeroes"
description="Typing '3.0' preserves the trailing zero"
>
<TestCase.Steps>
<li>Type "3.0"</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "3.0"
</TestCase.ExpectedResult>
<NumberTestCase />
</TestCase>
<TestCase
title="Inserting decimals precision"
description="Inserting '.' in to '300' maintains the trailing zeroes"
>
<TestCase.Steps>
<li>Type "300"</li>
<li>Move the cursor to after the "3"</li>
<li>Type "."</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "3.00", not "3"
</TestCase.ExpectedResult>
<NumberTestCase />
</TestCase>
<TestCase
title="Replacing numbers with -"
description="Replacing a number with the '-' sign should not clear the value"
>
<TestCase.Steps>
<li>Type "3"</li>
<li>Select the entire value"</li>
<li>Type '-' to replace '3' with '-'</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "-", not be blank.
</TestCase.ExpectedResult>
<NumberTestCase />
</TestCase>
<TestCase
title="Negative numbers"
description="Typing minus when inserting a negative number should work"
>
<TestCase.Steps>
<li>Type "-"</li>
<li>Type '3'</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The field should read "-3".
</TestCase.ExpectedResult>
<NumberTestCase />
</TestCase>
</FixtureSet>
);
},
});
export default NumberInputs;
+3 -3
View File
@@ -8,9 +8,9 @@ const fs = require('fs');
const os = require('os');
const path = require('path');
const SearchSource = require('jest-cli').SearchSource;
const TestRunner = require('jest-cli').TestRunner;
const TestWatcher = require('jest-cli/build/TestWatcher');
const SearchSource = require('jest').SearchSource;
const TestRunner = require('jest').TestRunner;
const TestWatcher = require('jest').TestWatcher;
const createHasteContext = require('jest-runtime').createHasteContext;
const readConfig = require('jest-config').readConfig;
+11
View File
@@ -837,6 +837,8 @@ src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
* should set className to empty string instead of null
* should remove property properly for boolean properties
* should remove property properly even with different name
* should update an empty attribute to zero
* should always assign the value attribute for non-inputs
* should remove attributes for normal properties
* should not remove attributes for special properties
* should not leave all options selected when deleting multiple
@@ -1449,6 +1451,10 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should allow setting `value` to `objToString`
* should not incur unnecessary DOM mutations
* should properly control a value of number `0`
* should properly control 0.0 for a text input
* should properly control 0.0 for a number input
* should properly transition from an empty value to 0
* should properly transition from 0 to an empty value
* should have the correct target value
* should not set a value for submit buttons unnecessarily
* should control radio buttons
@@ -1482,6 +1488,11 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* sets value properly with type coming later in props
* does not raise a validation warning when it switches types
* resets value of date/time input to fix bugs in iOS Safari
* always sets the attribute when values change on text inputs
* does not set the value attribute on number inputs if focused
* sets the value attribute on number inputs on blur
* an uncontrolled number input will not update the value attribute on blur
* an uncontrolled text input will not update the value attribute on blur
src/renderers/dom/shared/wrappers/__tests__/ReactDOMOption-test.js
* should flatten children to a string
@@ -68,10 +68,12 @@ var {
var DOC_FRAGMENT_TYPE = 11;
function getDeclarationErrorAddendum() {
var ownerName = getCurrentFiberOwnerName();
if (ownerName) {
// TODO: also report the stack.
return '\n\nThis DOM node was rendered by `' + ownerName + '`.';
if (__DEV__) {
var ownerName = getCurrentFiberOwnerName();
if (ownerName) {
// TODO: also report the stack.
return '\n\nThis DOM node was rendered by `' + ownerName + '`.';
}
}
return '';
}
@@ -138,11 +138,8 @@ var ReactDOMInput = {
? props.checked
: props.defaultChecked,
initialValue: props.value != null ? props.value : defaultValue,
controlled: isControlled(props),
};
if (__DEV__) {
node._wrapperState.controlled = isControlled(props);
}
},
updateWrapper: function(element: Element, props: Object) {
@@ -195,13 +192,24 @@ var ReactDOMInput = {
var value = props.value;
if (value != null) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
var newValue = '' + value;
if (value === 0 && node.value === '') {
node.value = '0';
// Note: IE9 reports a number inputs as 'text', so check props instead.
} else if (props.type === 'number') {
// Simulate `input.valueAsNumber`. IE9 does not support it
var valueAsNumber = parseFloat(node.value, 10) || 0;
// To avoid side effects (such as losing text selection), only set value if changed
if (newValue !== node.value) {
node.value = newValue;
// eslint-disable-next-line
if (value != valueAsNumber) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
node.value = '' + value;
}
// eslint-disable-next-line
} else if (value != node.value) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
node.value = '' + value;
}
} else {
if (props.value == null && props.defaultValue != null) {
@@ -210,6 +210,34 @@ var HTMLDOMPropertyConfig = {
httpEquiv: 'http-equiv',
},
DOMPropertyNames: {},
DOMMutationMethods: {
value: function(node, value) {
if (value == null) {
return node.removeAttribute('value');
}
// Number inputs get special treatment due to some edge cases in
// Chrome. Let everything else assign the value attribute as normal.
// https://github.com/facebook/react/issues/7253#issuecomment-236074326
if (node.type !== 'number' || node.hasAttribute('value') === false) {
node.setAttribute('value', '' + value);
} else if (
node.validity &&
!node.validity.badInput &&
node.ownerDocument.activeElement !== node
) {
// Don't assign an attribute if validation reports bad
// input. Chrome will clear the value. Additionally, don't
// operate on inputs that have focus, otherwise Chrome might
// strip off trailing decimal places and cause the user's
// cursor position to jump to the beginning of the input.
//
// In ReactDOMInput, we have an onBlur event that will trigger
// this function again when focus is lost.
node.setAttribute('value', '' + value);
}
},
},
};
module.exports = HTMLDOMPropertyConfig;
@@ -296,6 +296,35 @@ describe('DOMPropertyOperations', () => {
});
});
describe('value mutation method', function() {
it('should update an empty attribute to zero', function() {
var stubNode = document.createElement('input');
var stubInstance = {_debugID: 1};
ReactDOMComponentTree.precacheNode(stubInstance, stubNode);
stubNode.setAttribute('type', 'radio');
DOMPropertyOperations.setValueForProperty(stubNode, 'value', '');
spyOn(stubNode, 'setAttribute');
DOMPropertyOperations.setValueForProperty(stubNode, 'value', 0);
expect(stubNode.setAttribute.calls.count()).toBe(1);
});
it('should always assign the value attribute for non-inputs', function() {
var stubNode = document.createElement('progress');
var stubInstance = {_debugID: 1};
ReactDOMComponentTree.precacheNode(stubInstance, stubNode);
spyOn(stubNode, 'setAttribute');
DOMPropertyOperations.setValueForProperty(stubNode, 'value', 30);
DOMPropertyOperations.setValueForProperty(stubNode, 'value', '30');
expect(stubNode.setAttribute.calls.count()).toBe(2);
});
});
describe('deleteValueForProperty', () => {
var stubNode;
var stubInstance;
@@ -258,6 +258,26 @@ function getTargetInstForInputOrChangeEvent(topLevelType, targetInst) {
}
}
function handleControlledInputBlur(inst, node) {
// TODO: In IE, inst is occasionally null. Why?
if (inst == null) {
return;
}
// Fiber and ReactDOM keep wrapper state in separate places
let state = inst._wrapperState || node._wrapperState;
if (!state || !state.controlled || node.type !== 'number') {
return;
}
// If controlled, assign the value attribute to the current value on blur
let value = '' + node.value;
if (node.getAttribute('value') !== value) {
node.setAttribute('value', value);
}
}
/**
* This plugin creates an `onChange` event that normalizes change events
* across form elements. This event fires at a time when it's possible to
@@ -316,6 +336,11 @@ var ChangeEventPlugin = {
if (handleEventFunc) {
handleEventFunc(topLevelType, targetNode, targetInst);
}
// When blurring, set the value attribute for number inputs
if (topLevelType === 'topBlur') {
handleControlledInputBlur(targetInst, targetNode);
}
},
};
@@ -423,6 +423,48 @@ describe('ReactDOMInput', () => {
expect(node.value).toBe('0');
});
it('should properly control 0.0 for a text input', () => {
var stub = <input type="text" value={0} onChange={emptyFunction} />;
stub = ReactTestUtils.renderIntoDocument(stub);
var node = ReactDOM.findDOMNode(stub);
node.value = '0.0';
ReactTestUtils.Simulate.change(node, {target: {value: '0.0'}});
expect(node.value).toBe('0.0');
});
it('should properly control 0.0 for a number input', () => {
var stub = <input type="number" value={0} onChange={emptyFunction} />;
stub = ReactTestUtils.renderIntoDocument(stub);
var node = ReactDOM.findDOMNode(stub);
node.value = '0.0';
ReactTestUtils.Simulate.change(node, {target: {value: '0.0'}});
expect(node.value).toBe('0.0');
});
it('should properly transition from an empty value to 0', function() {
var container = document.createElement('div');
ReactDOM.render(<input type="text" value="" />, container);
ReactDOM.render(<input type="text" value={0} />, container);
var node = container.firstChild;
expect(node.value).toBe('0');
});
it('should properly transition from 0 to an empty value', function() {
var container = document.createElement('div');
ReactDOM.render(<input type="text" value={0} />, container);
ReactDOM.render(<input type="text" value="" />, container);
var node = container.firstChild;
expect(node.value).toBe('');
});
it('should have the correct target value', () => {
var handled = false;
var handler = function(event) {
@@ -1074,4 +1116,88 @@ describe('ReactDOMInput', () => {
'node.setAttribute("checked", "")',
]);
});
describe('assigning the value attribute on controlled inputs', function() {
function getTestInput() {
return React.createClass({
getInitialState: function() {
return {
value: this.props.value == null ? '' : this.props.value,
};
},
onChange: function(event) {
this.setState({value: event.target.value});
},
render: function() {
var type = this.props.type;
var value = this.state.value;
return <input type={type} value={value} onChange={this.onChange} />;
},
});
}
it('always sets the attribute when values change on text inputs', function() {
var Input = getTestInput();
var stub = ReactTestUtils.renderIntoDocument(<Input type="text" />);
var node = ReactDOM.findDOMNode(stub);
ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
expect(node.getAttribute('value')).toBe('2');
});
it('does not set the value attribute on number inputs if focused', () => {
var Input = getTestInput();
var stub = ReactTestUtils.renderIntoDocument(
<Input type="number" value="1" />,
);
var node = ReactDOM.findDOMNode(stub);
node.focus();
ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
expect(node.getAttribute('value')).toBe('1');
});
it('sets the value attribute on number inputs on blur', () => {
var Input = getTestInput();
var stub = ReactTestUtils.renderIntoDocument(
<Input type="number" value="1" />,
);
var node = ReactDOM.findDOMNode(stub);
ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
ReactTestUtils.SimulateNative.blur(node);
expect(node.getAttribute('value')).toBe('2');
});
it('an uncontrolled number input will not update the value attribute on blur', () => {
var stub = ReactTestUtils.renderIntoDocument(
<input type="number" defaultValue="1" />,
);
var node = ReactDOM.findDOMNode(stub);
node.value = 4;
ReactTestUtils.SimulateNative.blur(node);
expect(node.getAttribute('value')).toBe('1');
});
it('an uncontrolled text input will not update the value attribute on blur', () => {
var stub = ReactTestUtils.renderIntoDocument(
<input type="text" defaultValue="1" />,
);
var node = ReactDOM.findDOMNode(stub);
node.value = 4;
ReactTestUtils.SimulateNative.blur(node);
expect(node.getAttribute('value')).toBe('1');
});
});
});
@@ -128,11 +128,8 @@ var ReactDOMInput = {
: props.defaultChecked,
initialValue: props.value != null ? props.value : defaultValue,
listeners: null,
controlled: isControlled(props),
};
if (__DEV__) {
inst._wrapperState.controlled = isControlled(props);
}
},
updateWrapper: function(inst) {
@@ -188,13 +185,24 @@ var ReactDOMInput = {
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
var value = props.value;
if (value != null) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
var newValue = '' + value;
if (value === 0 && node.value === '') {
node.value = '0';
// Note: IE9 reports a number inputs as 'text', so check props instead.
} else if (props.type === 'number') {
// Simulate `input.valueAsNumber`. IE9 does not support it
var valueAsNumber = parseFloat(node.value, 10) || 0;
// To avoid side effects (such as losing text selection), only set value if changed
if (newValue !== node.value) {
node.value = newValue;
// eslint-disable-next-line
if (value != valueAsNumber) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
node.value = '' + value;
}
// eslint-disable-next-line
} else if (value != node.value) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
node.value = '' + value;
}
} else {
if (props.value == null && props.defaultValue != null) {
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeFeatureFlags
* @flow
*/
'use strict';
@@ -93,7 +93,7 @@ function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
* requiring that the `dispatchMarker` be the same as the dispatched ID.
*/
function accumulateDispatches(inst, ignoredDirection, event) {
if (event && event.dispatchConfig.registrationName) {
if (inst && event && event.dispatchConfig.registrationName) {
var registrationName = event.dispatchConfig.registrationName;
var listener = getListener(inst, registrationName);
if (listener) {