mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Fix uncontrolled radios (#10156)
* Add fixture * Fix Uncontrolled radio groups * address feedback * fix tests; prettier * Update TestCase.js
This commit is contained in:
parent
2dcdc3c633
commit
999df3e777
5
fixtures/dom/public/react-loader.js
vendored
5
fixtures/dom/public/react-loader.js
vendored
|
|
@ -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>');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{' '}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user