@samitouri / QOS-React / commits / 6875fa867f

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: https://github.com/facebook/react/issues/9561#issuecomment-298394312 * 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

Nathan Hunzaker committed May 3, 2017 at 14:37 UTC 6875fa867f55a730d662c8ac2e6f9e9a3e754901
7 files changed +203 -56
fixtures/dom/package.json
+1 -1
@@ -16,7 +16,7 @@
16 "scripts": {
17 "start": "react-scripts start",
18 "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",
20 "test": "react-scripts test --env=jsdom",
21 "eject": "react-scripts eject"
22 }
fixtures/dom/src/components/fixtures/text-inputs/InputTestCase.js new
+62
@@ -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
@@ -1,62 +1,92 @@
1 const React = window.React;
2
3 +import Fixture from '../../Fixture';
4 +import FixtureSet from '../../FixtureSet';
5 +import TestCase from '../../TestCase';
6 +import InputTestCase from './InputTestCase';
7 +
8 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>
20
8 - renderControlled = (type) => {
9 - let id = `controlled_${type}`;
21 + <TestCase.ExpectedResult>
22 + The text field's value should not update.
23 + </TestCase.ExpectedResult>
24
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>
31
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>
38
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>
45
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 "user@example.com"</li>
49 + <li>Select "@"</li>
50 + <li>Type ".", to replace "@" with a period</li>
51 + </TestCase.Steps>
52
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="user@example.com"/>
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>
90 );
91 }
92 }
scripts/fiber/tests-passing.txt
+3
@@ -1452,6 +1452,9 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
1452 * should properly control a value even if no event listener exists
1453 * should control a value in reentrant events
1454 * 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
1458 * should display `defaultValue` of number 0
1459 * only assigns defaultValue if it changes
1460 * should display "true" for `defaultValue` of `true`
src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js
+1 -2
@@ -209,8 +209,7 @@ var ReactDOMInput = {
209 // browsers typically do this as necessary, jsdom doesn't.
210 node.value = '' + value;
211 }
212 - // eslint-disable-next-line
213 - } else if (value != node.value) {
212 + } else if (node.value !== value) {
213 // Cast `value` to a string to ensure the value is set correctly. While
214 // browsers typically do this as necessary, jsdom doesn't.
215 node.value = '' + value;
src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
+55 -1
@@ -179,6 +179,60 @@ describe('ReactDOMInput', () => {
179 document.body.removeChild(container);
180 });
181
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 +
236 it('should display `defaultValue` of number 0', () => {
237 var stub = <input type="text" defaultValue={0} />;
238 stub = ReactTestUtils.renderIntoDocument(stub);
@@ -434,7 +488,7 @@ describe('ReactDOMInput', () => {
488
489 node.value = '0.0';
490 ReactTestUtils.Simulate.change(node, {target: {value: '0.0'}});
437 - expect(node.value).toBe('0.0');
491 + expect(node.value).toBe('0');
492 });
493
494 it('should properly control 0.0 for a number input', () => {
src/renderers/dom/stack/client/wrappers/ReactDOMInput.js
+1 -2
@@ -201,8 +201,7 @@ var ReactDOMInput = {
201 // browsers typically do this as necessary, jsdom doesn't.
202 node.value = '' + value;
203 }
204 - // eslint-disable-next-line
205 - } else if (value != node.value) {
204 + } else if (node.value !== value) {
205 // Cast `value` to a string to ensure the value is set correctly. While
206 // browsers typically do this as necessary, jsdom doesn't.
207 node.value = '' + value;