Fix uncontrolled radios (#10156)

* Add fixture

* Fix Uncontrolled radio groups

* address feedback

* fix tests; prettier

* Update TestCase.js
This commit is contained in:
Jason Quense 2017-07-13 16:02:31 -04:00 committed by Nathan Hunzaker
parent 2dcdc3c633
commit 999df3e777
9 changed files with 140 additions and 32 deletions

View File

@ -28,9 +28,8 @@ var query = parseQuery(window.location.search);
var version = query.version || 'local';
if (version !== 'local') {
REACT_PATH = 'https://unpkg.com/react@' + version + '/dist/react.min.js';
DOM_PATH =
'https://unpkg.com/react-dom@' + version + '/dist/react-dom.min.js';
REACT_PATH = 'https://unpkg.com/react@' + version + '/dist/react.js';
DOM_PATH = 'https://unpkg.com/react-dom@' + version + '/dist/react-dom.js';
}
document.write('<script src="' + REACT_PATH + '"></script>');

View File

@ -9,6 +9,7 @@ const propTypes = {
children: PropTypes.node.isRequired,
title: PropTypes.node.isRequired,
resolvedIn: semverString,
introducedIn: semverString,
resolvedBy: PropTypes.string,
};
@ -31,6 +32,7 @@ class TestCase extends React.Component {
const {
title,
description,
introducedIn,
resolvedIn,
resolvedBy,
affectedBrowsers,
@ -40,10 +42,10 @@ class TestCase extends React.Component {
let {complete} = this.state;
const {version} = parse(window.location.search);
const isTestRelevant =
const isTestFixed =
!version || !resolvedIn || semver.gte(version, resolvedIn);
complete = !isTestRelevant || complete;
complete = !isTestFixed || complete;
return (
<section className={cn('test-case', complete && 'test-case--complete')}>
@ -60,6 +62,15 @@ class TestCase extends React.Component {
</h2>
<dl className="test-case__details">
{introducedIn && <dt>First broken in: </dt>}
{introducedIn &&
<dd>
<a
href={'https://github.com/facebook/react/tag/v' + introducedIn}>
<code>{introducedIn}</code>
</a>
</dd>}
{resolvedIn && <dt>First supported in: </dt>}
{resolvedIn &&
<dd>
@ -89,7 +100,7 @@ class TestCase extends React.Component {
</p>
<div className="test-case__body">
{!isTestRelevant &&
{!isTestFixed &&
<p className="test-case__invalid-version">
<strong>Note:</strong>
{' '}

View File

@ -0,0 +1,58 @@
import React from 'react';
import Fixture from '../../Fixture';
class RadioGroupFixture extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
changeCount: 0,
};
}
handleChange = () => {
this.setState(({changeCount}) => {
return {
changeCount: changeCount + 1,
};
});
};
handleReset = () => {
this.setState({
changeCount: 0,
});
};
render() {
const {changeCount} = this.state;
const color = changeCount === 2 ? 'green' : 'red';
return (
<Fixture>
<label>
<input
defaultChecked
name="foo"
type="radio"
onChange={this.handleChange}
/>
Radio 1
</label>
<label>
<input name="foo" type="radio" onChange={this.handleChange} />
Radio 2
</label>
{' '}
<p style={{color}}>
<code>onChange</code>{' calls: '}<strong>{changeCount}</strong>
</p>
<button onClick={this.handleReset}>Reset count</button>
</Fixture>
);
}
}
export default RadioGroupFixture;

View File

@ -4,6 +4,7 @@ import FixtureSet from '../../FixtureSet';
import TestCase from '../../TestCase';
import RangeKeyboardFixture from './RangeKeyboardFixture';
import RadioClickFixture from './RadioClickFixture';
import RadioGroupFixture from './RadioGroupFixture';
import InputPlaceholderFixture from './InputPlaceholderFixture';
class InputChangeEvents extends React.Component {
@ -47,6 +48,24 @@ class InputChangeEvents extends React.Component {
<RadioClickFixture />
</TestCase>
<TestCase
title="Uncontrolled radio groups"
description={`
Radio inputs should fire change events when the value moved to
another named input
`}
introducedIn="15.6.0">
<TestCase.Steps>
<li>Click on the "Radio 2"</li>
<li>Click back to "Radio 1"</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The <code>onChange</code> call count should equal 2
</TestCase.ExpectedResult>
<RadioGroupFixture />
</TestCase>
<TestCase
title="Inputs with placeholders"

View File

@ -2199,6 +2199,18 @@ fbjs@^0.8.1, fbjs@^0.8.4:
setimmediate "^1.0.5"
ua-parser-js "^0.7.9"
fbjs@^0.8.9:
version "0.8.12"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04"
dependencies:
core-js "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.9"
figures@^1.3.5:
version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"

View File

@ -754,6 +754,10 @@ var ReactDOMFiberComponent = {
// happen after `updateDOMProperties`. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
ReactDOMFiberInput.updateWrapper(domElement, nextRawProps);
// We also check that we haven't missed a value update, such as a
// Radio group shifting the checked value to another named radio input.
inputValueTracking.updateValueIfChanged((domElement: any));
break;
case 'textarea':
ReactDOMFiberTextarea.updateWrapper(domElement, nextRawProps);

View File

@ -145,15 +145,11 @@ describe('inputValueTracking', () => {
it('should stop tracking', () => {
inputValueTracking.track(mockComponent);
expect(mockComponent._wrapperState.hasOwnProperty('valueTracker')).toBe(
true,
);
expect(mockComponent._wrapperState.valueTracker).not.toEqual(null);
inputValueTracking.stopTracking(mockComponent);
expect(mockComponent._wrapperState.hasOwnProperty('valueTracker')).toBe(
false,
);
expect(mockComponent._wrapperState.valueTracker).toEqual(null);
expect(input.hasOwnProperty('value')).toBe(false);
});

View File

@ -12,6 +12,7 @@
'use strict';
var {ELEMENT_NODE} = require('HTMLNodeType');
import type {Fiber} from 'ReactFiber';
import type {ReactInstance} from 'ReactInstanceType';
@ -23,6 +24,9 @@ type ValueTracker = {
type WrapperState = {_wrapperState: {valueTracker: ?ValueTracker}};
type ElementWithWrapperState = Element & WrapperState;
type InstanceWithWrapperState = ReactInstance & WrapperState;
type SubjectWithWrapperState =
| InstanceWithWrapperState
| ElementWithWrapperState;
var ReactDOMComponentTree = require('ReactDOMComponentTree');
@ -43,15 +47,11 @@ function getTracker(inst: any) {
return inst._wrapperState.valueTracker;
}
function attachTracker(inst: InstanceWithWrapperState, tracker: ?ValueTracker) {
inst._wrapperState.valueTracker = tracker;
function detachTracker(subject: SubjectWithWrapperState) {
subject._wrapperState.valueTracker = null;
}
function detachTracker(inst: InstanceWithWrapperState) {
delete inst._wrapperState.valueTracker;
}
function getValueFromNode(node) {
function getValueFromNode(node: any) {
var value;
if (node) {
value = isCheckable(node) ? '' + node.checked : node.value;
@ -113,40 +113,46 @@ var inputValueTracking = {
return getTracker(ReactDOMComponentTree.getInstanceFromNode(node));
},
trackNode: function(node: ElementWithWrapperState) {
if (node._wrapperState.valueTracker) {
trackNode(node: ElementWithWrapperState) {
if (getTracker(node)) {
return;
}
node._wrapperState.valueTracker = trackValueOnNode(node, node);
},
track: function(inst: InstanceWithWrapperState) {
track(inst: InstanceWithWrapperState) {
if (getTracker(inst)) {
return;
}
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
attachTracker(inst, trackValueOnNode(node, inst));
inst._wrapperState.valueTracker = trackValueOnNode(node, inst);
},
updateValueIfChanged(inst: InstanceWithWrapperState | Fiber) {
if (!inst) {
updateValueIfChanged(subject: SubjectWithWrapperState | Fiber) {
if (!subject) {
return false;
}
var tracker = getTracker(inst);
var tracker = getTracker(subject);
if (!tracker) {
if (typeof (inst: any).tag === 'number') {
inputValueTracking.trackNode((inst: any).stateNode);
if (typeof (subject: any).tag === 'number') {
inputValueTracking.trackNode((subject: any).stateNode);
} else {
inputValueTracking.track((inst: any));
inputValueTracking.track((subject: any));
}
return true;
}
var lastValue = tracker.getValue();
var nextValue = getValueFromNode(
ReactDOMComponentTree.getNodeFromInstance(inst),
);
var node = subject;
// TODO: remove check when the Stack renderer is retired
if ((subject: any).nodeType !== ELEMENT_NODE) {
node = ReactDOMComponentTree.getNodeFromInstance(subject);
}
var nextValue = getValueFromNode(node);
if (nextValue !== lastValue) {
tracker.setValue(nextValue);

View File

@ -860,6 +860,9 @@ ReactDOMComponent.Mixin = {
// happen after `_updateDOMProperties`. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
ReactDOMInput.updateWrapper(this);
// We also check that we haven't missed a value update, such as a
// Radio group shifting the checked value to another named radio input.
inputValueTracking.updateValueIfChanged(this);
break;
case 'textarea':
ReactDOMTextarea.updateWrapper(this);