@samitouri / QOS-React / commits / 70abda5b92

Switching the name property preserves radio selection

Fixes a case where changing the name and checked value of a radio button in the same update would lead to checking the wrong radio input. Also adds a DOM test fixture for related issue. Related issues: https://github.com/facebook/react/issues/7630

landvibe committed Nov 20, 2017 at 00:44 UTC 70abda5b92b8aae4dc0255a8ca00c2025ce05c3d
5 files changed +124 -8
fixtures/dom/src/components/fixtures/input-change-events/RadioNameChangeFixture.js new
+46
@@ -0,0 +1,46 @@
1 +const React = window.React;
2 +const noop = n => n;
3 +
4 +class RadioNameChangeFixture extends React.Component {
5 + state = {
6 + updated: false,
7 + };
8 + onClick = () => {
9 + this.setState(state => {
10 + return {updated: !state.updated};
11 + });
12 + };
13 + render() {
14 + const {updated} = this.state;
15 + const radioName = updated ? 'firstName' : 'secondName';
16 + return (
17 + <div>
18 + <label>
19 + <input
20 + type="radio"
21 + name={radioName}
22 + onChange={noop}
23 + checked={updated === true}
24 + />
25 + First Radio
26 + </label>
27 +
28 + <label>
29 + <input
30 + type="radio"
31 + name={radioName}
32 + onChange={noop}
33 + checked={updated === false}
34 + />
35 + Second Radio
36 + </label>
37 +
38 + <div>
39 + <button type="button" onClick={this.onClick}>Toggle</button>
40 + </div>
41 + </div>
42 + );
43 + }
44 +}
45 +
46 +export default RadioNameChangeFixture;
fixtures/dom/src/components/fixtures/input-change-events/index.js
+19
@@ -3,6 +3,7 @@ import TestCase from '../../TestCase';
3 import RangeKeyboardFixture from './RangeKeyboardFixture';
4 import RadioClickFixture from './RadioClickFixture';
5 import RadioGroupFixture from './RadioGroupFixture';
6 +import RadioNameChangeFixture from './RadioNameChangeFixture';
7 import InputPlaceholderFixture from './InputPlaceholderFixture';
8 const React = window.React;
9
@@ -88,6 +89,24 @@ class InputChangeEvents extends React.Component {
89
90 <InputPlaceholderFixture />
91 </TestCase>
92 + <TestCase
93 + title="Radio button groups with name changes"
94 + description={`
95 + A radio button group should have correct checked value when
96 + the names changes
97 + `}
98 + resolvedBy="#11227"
99 + affectedBrowsers="IE9+">
100 + <TestCase.Steps>
101 + <li>Click the toggle button</li>
102 + </TestCase.Steps>
103 +
104 + <TestCase.ExpectedResult>
105 + The checked radio button should switch between the first and second radio button
106 + </TestCase.ExpectedResult>
107 +
108 + <RadioNameChangeFixture />
109 + </TestCase>
110 </FixtureSet>
111 );
112 }
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+39
@@ -668,6 +668,45 @@ describe('ReactDOMInput', () => {
668 expect(cNode.checked).toBe(true);
669 });
670
671 + it('should check the correct radio when the selected name moves', () => {
672 + class App extends React.Component {
673 + state = {
674 + updated: false,
675 + };
676 + onClick = () => {
677 + this.setState({updated: true});
678 + };
679 + render() {
680 + const {updated} = this.state;
681 + const radioName = updated ? 'secondName' : 'firstName';
682 + return (
683 + <div>
684 + <button type="button" onClick={this.onClick} />
685 + <input
686 + type="radio"
687 + name={radioName}
688 + onChange={emptyFunction}
689 + checked={updated === true}
690 + />
691 + <input
692 + type="radio"
693 + name={radioName}
694 + onChange={emptyFunction}
695 + checked={updated === false}
696 + />
697 + </div>
698 + );
699 + }
700 + }
701 +
702 + var stub = ReactTestUtils.renderIntoDocument(<App />);
703 + var buttonNode = ReactDOM.findDOMNode(stub).childNodes[0];
704 + var firstRadioNode = ReactDOM.findDOMNode(stub).childNodes[1];
705 + expect(firstRadioNode.checked).toBe(false);
706 + ReactTestUtils.Simulate.click(buttonNode);
707 + expect(firstRadioNode.checked).toBe(true);
708 + });
709 +
710 it('should control radio buttons if the tree updates during render', () => {
711 var sharedParent = document.createElement('div');
712 var container1 = document.createElement('div');
packages/react-dom/src/client/ReactDOMFiberComponent.js
+11
@@ -764,6 +764,17 @@ export function updateProperties(
764 lastRawProps: Object,
765 nextRawProps: Object,
766 ): void {
767 + // Update checked *before* name.
768 + // In the middle of an update, it is possible to have multiple checked.
769 + // When a checked radio tries to change name, browser makes another radio's checked false.
770 + if (
771 + tag === 'input' &&
772 + nextRawProps.type === 'radio' &&
773 + nextRawProps.name != null
774 + ) {
775 + ReactDOMFiberInput.updateChecked(domElement, nextRawProps);
776 + }
777 +
778 var wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
779 var isCustomComponentTag = isCustomComponent(tag, nextRawProps);
780 // Apply the diff.
packages/react-dom/src/client/ReactDOMFiberInput.js
+9 -8
@@ -142,6 +142,14 @@ export function initWrapperState(element: Element, props: Object) {
142 };
143 }
144
145 +export function updateChecked(element: Element, props: Object) {
146 + var node = ((element: any): InputWithWrapperState);
147 + var checked = props.checked;
148 + if (checked != null) {
149 + DOMPropertyOperations.setValueForProperty(node, 'checked', checked);
150 + }
151 +}
152 +
153 export function updateWrapper(element: Element, props: Object) {
154 var node = ((element: any): InputWithWrapperState);
155 if (__DEV__) {
@@ -181,14 +189,7 @@ export function updateWrapper(element: Element, props: Object) {
189 }
190 }
191
184 - var checked = props.checked;
185 - if (checked != null) {
186 - DOMPropertyOperations.setValueForProperty(
187 - node,
188 - 'checked',
189 - checked || false,
190 - );
191 - }
192 + updateChecked(element, props);
193
194 var value = props.value;
195 if (value != null) {