Skip to content

Commit

Permalink
Remove loose check when assigning non-number inputs
Browse files Browse the repository at this point in the history
This commit removes a check I added when working on number input
issues where we perform a loose check on an input's value before we
assign it. This prevented controlled text inputs from disallowing
numeric text entry.

I also added a DOM fixture text case.

Related issues:

#9561 (comment)
  • Loading branch information
nhunzaker committed May 2, 2017
1 parent 4d81744 commit 0df1709
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 56 deletions.
62 changes: 62 additions & 0 deletions fixtures/dom/src/components/fixtures/text-inputs/InputTestCase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const React = window.React;

import Fixture from '../../Fixture';

class InputTestCase extends React.Component {
static defaultProps = {
type: 'text',
defaultValue: '',
parseAs: 'text'
}

constructor () {
super(...arguments);

this.state = {
value: this.props.defaultValue
};
}

onChange = (event) => {
const raw = event.target.value;

switch (this.props.type) {
case 'number':
const parsed = parseFloat(event.target.value, 10);

this.setState({ value: isNaN(parsed) ? '' : parsed });

break;
default:
this.setState({ value: raw });
}
}

render() {
const { children, type, defaultValue } = this.props;
const { value } = this.state;

return (
<Fixture>
<div>{children}</div>

<div className="control-box">
<fieldset>
<legend>Controlled {type}</legend>
<input type={type} value={value} onChange={this.onChange} />
<p className="hint">
Value: {JSON.stringify(this.state.value)}
</p>
</fieldset>

<fieldset>
<legend>Uncontrolled {type}</legend>
<input type={type} defaultValue={defaultValue} />
</fieldset>
</div>
</Fixture>
);
}
}

export default InputTestCase;
104 changes: 53 additions & 51 deletions fixtures/dom/src/components/fixtures/text-inputs/index.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,64 @@
const React = window.React;

class TextInputFixtures extends React.Component {
state = {
color: '#ffaaee',
};
import Fixture from '../../Fixture';
import FixtureSet from '../../FixtureSet';
import TestCase from '../../TestCase';
import InputTestCase from './InputTestCase';

renderControlled = (type) => {
let id = `controlled_${type}`;
class TextInputFixtures extends React.Component {
render() {
return (
<FixtureSet
title="Inputs"
description="Common behavior across controled and uncontrolled inputs"
>
<TestCase title="Numbers in a controlled text field with no handler">
<TestCase.Steps>
<li>Move the cursor to after "2" in the text field</li>
<li>Type ".2" into the text input</li>
</TestCase.Steps>

let onChange = e => {
let value = e.target.value;
if (type === 'number') {
value = value === '' ? '' : parseFloat(value, 10) || 0;
}
this.setState({
[type] : value,
});
};
<TestCase.ExpectedResult>
The text field's value should not update.
</TestCase.ExpectedResult>

let state = this.state[type] || '';
<Fixture>
<div className="control-box">
<fieldset>
<legend>Value as number</legend>
<input value={2} onChange={() => {}} />
</fieldset>

return (
<div key={type} className="field">
<label className="control-label" htmlFor={id}>{type}</label>
<input id={id} type={type} value={state} onChange={onChange} />
&nbsp; &rarr; {JSON.stringify(state)}
</div>
);
}
<fieldset>
<legend>Value as string</legend>
<input value={"2"} onChange={() => {}} />
</fieldset>
</div>
</Fixture>

renderUncontrolled = (type) => {
let id = `uncontrolled_${type}`;
return (
<div key={type} className="field">
<label className="control-label" htmlFor={id}>{type}</label>
<input id={id} type={type} />
</div>
);
}
<p className="footnote">
This issue was first introduced when we added extra logic
to number inputs to prevent unexpected behavior in Chrome
and Safari (see the number input test case).
</p>
</TestCase>

render() {
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
let types = [
'text', 'email', 'number', 'url', 'tel',
'color', 'date', 'datetime-local',
'time', 'month', 'week', 'range', 'password',
];
return (
<form onSubmit={event => event.preventDefault()}>
<fieldset>
<legend>Controlled</legend>
{types.map(this.renderControlled)}
</fieldset>
<fieldset>
<legend>Uncontrolled</legend>
{types.map(this.renderUncontrolled)}
</fieldset>
</form>
<TestCase title="All inputs" description="General test of all inputs">
<InputTestCase type="text" defaultValue="Text" />
<InputTestCase type="email" defaultValue="user@example.com"/>
<InputTestCase type="number" defaultValue={0} />
<InputTestCase type="url" defaultValue="http://example.com"/>
<InputTestCase type="tel" defaultValue="555-555-5555"/>
<InputTestCase type="color" defaultValue="#ff0000" />
<InputTestCase type="date" defaultValue="2017-01-01" />
<InputTestCase type="datetime-local" defaultValue="2017-01-01T01:00" />
<InputTestCase type="time" defaultValue="01:00" />
<InputTestCase type="month" defaultValue="2017-01" />
<InputTestCase type="week" defaultValue="2017-W01" />
<InputTestCase type="range" defaultValue={0.5} />
<InputTestCase type="password" defaultValue="" />
</TestCase>
</FixtureSet>
);
}
}
Expand Down
4 changes: 4 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,9 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should properly control a value even if no event listener exists
* should control a value in reentrant events
* should control values in reentrant events with different targets
* does change the number 2 to "2.0" with no change handler
* does change the string "2" to "2.0" with no change handler
* changes the number 2 to "2.0" using a change handler
* should display `defaultValue` of number 0
* only assigns defaultValue if it changes
* should display "true" for `defaultValue` of `true`
Expand Down Expand Up @@ -1671,6 +1674,7 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
* can deprioritize a tree from without dropping work
* can resume work in a subtree even when a parent bails out
* can resume work in a bailed subtree within one pass
* can resume mounting a class component
* can reuse work done after being preempted
* can reuse work that began but did not complete, after being preempted
* can reuse work if shouldComponentUpdate is false, after being preempted
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ var ReactDOMInput = {
node.value = '' + value;
}
// eslint-disable-next-line
} else if (value != node.value) {
} else {
// 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;
Expand Down
60 changes: 57 additions & 3 deletions src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,60 @@ describe('ReactDOMInput', () => {
document.body.removeChild(container);
});

describe('switching text inputs between numeric and string numbers', () => {
it('does change the number 2 to "2.0" with no change handler', () => {
var stub = <input type="text" value={2} onChange={jest.fn()} />;
stub = ReactTestUtils.renderIntoDocument(stub);
var node = ReactDOM.findDOMNode(stub);

node.value = '2.0';

ReactTestUtils.Simulate.change(stub);

expect(node.getAttribute('value')).toBe('2');
expect(node.value).toBe('2');
});

it('does change the string "2" to "2.0" with no change handler', () => {
var stub = <input type="text" value={'2'} onChange={jest.fn()} />;
stub = ReactTestUtils.renderIntoDocument(stub);
var node = ReactDOM.findDOMNode(stub);

node.value = '2.0';

ReactTestUtils.Simulate.change(stub);

expect(node.getAttribute('value')).toBe('2');
expect(node.value).toBe('2');
});

it('changes the number 2 to "2.0" using a change handler', () => {
class Stub extends React.Component {
state = {
value: 2,
};
onChange = event => {
this.setState({value: event.target.value});
};
render() {
const {value} = this.state;

return <input type="text" value={value} onChange={this.onChange} />;
}
}

var stub = ReactTestUtils.renderIntoDocument(<Stub />);
var node = ReactDOM.findDOMNode(stub);

node.value = '2.0';

ReactTestUtils.Simulate.change(node);

expect(node.getAttribute('value')).toBe('2.0');
expect(node.value).toBe('2.0');
});
});

it('should display `defaultValue` of number 0', () => {
var stub = <input type="text" defaultValue={0} />;
stub = ReactTestUtils.renderIntoDocument(stub);
Expand Down Expand Up @@ -411,10 +465,10 @@ describe('ReactDOMInput', () => {
});

ReactDOM.render(<input value="a" />, container);
expect(nodeValueSetter.mock.calls.length).toBe(0);
expect(nodeValueSetter.mock.calls.length).toBe(1);

ReactDOM.render(<input value="b" />, container);
expect(nodeValueSetter.mock.calls.length).toBe(1);
expect(nodeValueSetter.mock.calls.length).toBe(2);
});

it('should properly control a value of number `0`', () => {
Expand All @@ -434,7 +488,7 @@ describe('ReactDOMInput', () => {

node.value = '0.0';
ReactTestUtils.Simulate.change(node, {target: {value: '0.0'}});
expect(node.value).toBe('0.0');
expect(node.value).toBe('0');
});

it('should properly control 0.0 for a number input', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom/stack/client/wrappers/ReactDOMInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ var ReactDOMInput = {
node.value = '' + value;
}
// eslint-disable-next-line
} else if (value != node.value) {
} else {
// 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;
Expand Down

0 comments on commit 0df1709

Please sign in to comment.