Skip to content

Commit 6875fa8

Browse files
nhunzakeraweary
authored andcommitted
Remove loose check on non-number controlled inputs. Fix trailing dot issue. (#9584)
* Remove loose check when assigning non-number inputs 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) * Use strict equality as a guard before assigning input.value This commit adds back the guard around assigning the value property to an input, however it does it using a strict equals. This prevents validated inputs, like emails and urls from losing the cursor position. It also adds associated test fixtures. * Add copy command after build for interup with surge.sh
1 parent 50a60db commit 6875fa8

File tree

7 files changed

+203
-56
lines changed

7 files changed

+203
-56
lines changed

fixtures/dom/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"scripts": {
1717
"start": "react-scripts start",
1818
"prestart": "cp ../../build/dist/{react,react-dom}.development.js public/",
19-
"build": "react-scripts build",
19+
"build": "react-scripts build && cp build/index.html build/200.html",
2020
"test": "react-scripts test --env=jsdom",
2121
"eject": "react-scripts eject"
2222
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const React = window.React;
2+
3+
import Fixture from '../../Fixture';
4+
5+
class InputTestCase extends React.Component {
6+
static defaultProps = {
7+
type: 'text',
8+
defaultValue: '',
9+
parseAs: 'text'
10+
}
11+
12+
constructor () {
13+
super(...arguments);
14+
15+
this.state = {
16+
value: this.props.defaultValue
17+
};
18+
}
19+
20+
onChange = (event) => {
21+
const raw = event.target.value;
22+
23+
switch (this.props.type) {
24+
case 'number':
25+
const parsed = parseFloat(event.target.value, 10);
26+
27+
this.setState({ value: isNaN(parsed) ? '' : parsed });
28+
29+
break;
30+
default:
31+
this.setState({ value: raw });
32+
}
33+
}
34+
35+
render() {
36+
const { children, type, defaultValue } = this.props;
37+
const { value } = this.state;
38+
39+
return (
40+
<Fixture>
41+
<div>{children}</div>
42+
43+
<div className="control-box">
44+
<fieldset>
45+
<legend>Controlled {type}</legend>
46+
<input type={type} value={value} onChange={this.onChange} />
47+
<p className="hint">
48+
Value: {JSON.stringify(this.state.value)}
49+
</p>
50+
</fieldset>
51+
52+
<fieldset>
53+
<legend>Uncontrolled {type}</legend>
54+
<input type={type} defaultValue={defaultValue} />
55+
</fieldset>
56+
</div>
57+
</Fixture>
58+
);
59+
}
60+
}
61+
62+
export default InputTestCase;

fixtures/dom/src/components/fixtures/text-inputs/index.js

+80-50
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,92 @@
11
const React = window.React;
22

3+
import Fixture from '../../Fixture';
4+
import FixtureSet from '../../FixtureSet';
5+
import TestCase from '../../TestCase';
6+
import InputTestCase from './InputTestCase';
7+
38
class TextInputFixtures extends React.Component {
4-
state = {
5-
color: '#ffaaee',
6-
};
9+
render() {
10+
return (
11+
<FixtureSet
12+
title="Inputs"
13+
description="Common behavior across controled and uncontrolled inputs"
14+
>
15+
<TestCase title="Numbers in a controlled text field with no handler">
16+
<TestCase.Steps>
17+
<li>Move the cursor to after "2" in the text field</li>
18+
<li>Type ".2" into the text input</li>
19+
</TestCase.Steps>
720

8-
renderControlled = (type) => {
9-
let id = `controlled_${type}`;
21+
<TestCase.ExpectedResult>
22+
The text field's value should not update.
23+
</TestCase.ExpectedResult>
1024

11-
let onChange = e => {
12-
let value = e.target.value;
13-
if (type === 'number') {
14-
value = value === '' ? '' : parseFloat(value, 10) || 0;
15-
}
16-
this.setState({
17-
[type] : value,
18-
});
19-
};
25+
<Fixture>
26+
<div className="control-box">
27+
<fieldset>
28+
<legend>Value as number</legend>
29+
<input value={2} onChange={() => {}} />
30+
</fieldset>
2031

21-
let state = this.state[type] || '';
32+
<fieldset>
33+
<legend>Value as string</legend>
34+
<input value={"2"} onChange={() => {}} />
35+
</fieldset>
36+
</div>
37+
</Fixture>
2238

23-
return (
24-
<div key={type} className="field">
25-
<label className="control-label" htmlFor={id}>{type}</label>
26-
<input id={id} type={type} value={state} onChange={onChange} />
27-
&nbsp; &rarr; {JSON.stringify(state)}
28-
</div>
29-
);
30-
}
39+
<p className="footnote">
40+
This issue was first introduced when we added extra logic
41+
to number inputs to prevent unexpected behavior in Chrome
42+
and Safari (see the number input test case).
43+
</p>
44+
</TestCase>
3145

32-
renderUncontrolled = (type) => {
33-
let id = `uncontrolled_${type}`;
34-
return (
35-
<div key={type} className="field">
36-
<label className="control-label" htmlFor={id}>{type}</label>
37-
<input id={id} type={type} />
38-
</div>
39-
);
40-
}
46+
<TestCase title="Cursor when editing email inputs">
47+
<TestCase.Steps>
48+
<li>Type "[email protected]"</li>
49+
<li>Select "@"</li>
50+
<li>Type ".", to replace "@" with a period</li>
51+
</TestCase.Steps>
4152

42-
render() {
43-
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
44-
let types = [
45-
'text', 'email', 'number', 'url', 'tel',
46-
'color', 'date', 'datetime-local',
47-
'time', 'month', 'week', 'range', 'password',
48-
];
49-
return (
50-
<form onSubmit={event => event.preventDefault()}>
51-
<fieldset>
52-
<legend>Controlled</legend>
53-
{types.map(this.renderControlled)}
54-
</fieldset>
55-
<fieldset>
56-
<legend>Uncontrolled</legend>
57-
{types.map(this.renderUncontrolled)}
58-
</fieldset>
59-
</form>
53+
<TestCase.ExpectedResult>
54+
The text field's cursor should not jump to the end.
55+
</TestCase.ExpectedResult>
56+
57+
<InputTestCase type="email" defaultValue="" />
58+
</TestCase>
59+
60+
<TestCase title="Cursor when editing url inputs">
61+
<TestCase.Steps>
62+
<li>Type "http://www.example.com"</li>
63+
<li>Select "www."</li>
64+
<li>Press backspace/delete</li>
65+
</TestCase.Steps>
66+
67+
<TestCase.ExpectedResult>
68+
The text field's cursor should not jump to the end.
69+
</TestCase.ExpectedResult>
70+
71+
<InputTestCase type="url" defaultValue="" />
72+
</TestCase>
73+
74+
<TestCase title="All inputs" description="General test of all inputs">
75+
<InputTestCase type="text" defaultValue="Text" />
76+
<InputTestCase type="email" defaultValue="[email protected]"/>
77+
<InputTestCase type="number" defaultValue={0} />
78+
<InputTestCase type="url" defaultValue="http://example.com"/>
79+
<InputTestCase type="tel" defaultValue="555-555-5555"/>
80+
<InputTestCase type="color" defaultValue="#ff0000" />
81+
<InputTestCase type="date" defaultValue="2017-01-01" />
82+
<InputTestCase type="datetime-local" defaultValue="2017-01-01T01:00" />
83+
<InputTestCase type="time" defaultValue="01:00" />
84+
<InputTestCase type="month" defaultValue="2017-01" />
85+
<InputTestCase type="week" defaultValue="2017-W01" />
86+
<InputTestCase type="range" defaultValue={0.5} />
87+
<InputTestCase type="password" defaultValue="" />
88+
</TestCase>
89+
</FixtureSet>
6090
);
6191
}
6292
}

scripts/fiber/tests-passing.txt

+3
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,9 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
14521452
* should properly control a value even if no event listener exists
14531453
* should control a value in reentrant events
14541454
* should control values in reentrant events with different targets
1455+
* does change the number 2 to "2.0" with no change handler
1456+
* does change the string "2" to "2.0" with no change handler
1457+
* changes the number 2 to "2.0" using a change handler
14551458
* should display `defaultValue` of number 0
14561459
* only assigns defaultValue if it changes
14571460
* should display "true" for `defaultValue` of `true`

src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,7 @@ var ReactDOMInput = {
209209
// browsers typically do this as necessary, jsdom doesn't.
210210
node.value = '' + value;
211211
}
212-
// eslint-disable-next-line
213-
} else if (value != node.value) {
212+
} else if (node.value !== value) {
214213
// Cast `value` to a string to ensure the value is set correctly. While
215214
// browsers typically do this as necessary, jsdom doesn't.
216215
node.value = '' + value;

src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js

+55-1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,60 @@ describe('ReactDOMInput', () => {
179179
document.body.removeChild(container);
180180
});
181181

182+
describe('switching text inputs between numeric and string numbers', () => {
183+
it('does change the number 2 to "2.0" with no change handler', () => {
184+
var stub = <input type="text" value={2} onChange={jest.fn()} />;
185+
stub = ReactTestUtils.renderIntoDocument(stub);
186+
var node = ReactDOM.findDOMNode(stub);
187+
188+
node.value = '2.0';
189+
190+
ReactTestUtils.Simulate.change(stub);
191+
192+
expect(node.getAttribute('value')).toBe('2');
193+
expect(node.value).toBe('2');
194+
});
195+
196+
it('does change the string "2" to "2.0" with no change handler', () => {
197+
var stub = <input type="text" value={'2'} onChange={jest.fn()} />;
198+
stub = ReactTestUtils.renderIntoDocument(stub);
199+
var node = ReactDOM.findDOMNode(stub);
200+
201+
node.value = '2.0';
202+
203+
ReactTestUtils.Simulate.change(stub);
204+
205+
expect(node.getAttribute('value')).toBe('2');
206+
expect(node.value).toBe('2');
207+
});
208+
209+
it('changes the number 2 to "2.0" using a change handler', () => {
210+
class Stub extends React.Component {
211+
state = {
212+
value: 2,
213+
};
214+
onChange = event => {
215+
this.setState({value: event.target.value});
216+
};
217+
render() {
218+
const {value} = this.state;
219+
220+
return <input type="text" value={value} onChange={this.onChange} />;
221+
}
222+
}
223+
224+
var stub = ReactTestUtils.renderIntoDocument(<Stub />);
225+
var node = ReactDOM.findDOMNode(stub);
226+
227+
node.value = '2.0';
228+
229+
ReactTestUtils.Simulate.change(node);
230+
231+
expect(node.getAttribute('value')).toBe('2.0');
232+
expect(node.value).toBe('2.0');
233+
});
234+
});
235+
182236
it('should display `defaultValue` of number 0', () => {
183237
var stub = <input type="text" defaultValue={0} />;
184238
stub = ReactTestUtils.renderIntoDocument(stub);
@@ -434,7 +488,7 @@ describe('ReactDOMInput', () => {
434488

435489
node.value = '0.0';
436490
ReactTestUtils.Simulate.change(node, {target: {value: '0.0'}});
437-
expect(node.value).toBe('0.0');
491+
expect(node.value).toBe('0');
438492
});
439493

440494
it('should properly control 0.0 for a number input', () => {

src/renderers/dom/stack/client/wrappers/ReactDOMInput.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,7 @@ var ReactDOMInput = {
201201
// browsers typically do this as necessary, jsdom doesn't.
202202
node.value = '' + value;
203203
}
204-
// eslint-disable-next-line
205-
} else if (value != node.value) {
204+
} else if (node.value !== value) {
206205
// Cast `value` to a string to ensure the value is set correctly. While
207206
// browsers typically do this as necessary, jsdom doesn't.
208207
node.value = '' + value;

0 commit comments

Comments
 (0)