Skip to content

Commit 073882f

Browse files
[RadioGroup] Remove cloneElement, use the context (#15069)
* [RadioGroup] Use the context * review
1 parent f036b5c commit 073882f

File tree

8 files changed

+68
-97
lines changed

8 files changed

+68
-97
lines changed

packages/material-ui/src/Radio/Radio.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import clsx from 'clsx';
44
import SwitchBase from '../internal/SwitchBase';
55
import RadioButtonUncheckedIcon from '../internal/svg-icons/RadioButtonUnchecked';
66
import RadioButtonCheckedIcon from '../internal/svg-icons/RadioButtonChecked';
7-
import { capitalize } from '../utils/helpers';
7+
import { capitalize, createChainedFunction } from '../utils/helpers';
88
import withStyles from '../styles/withStyles';
9+
import RadioGroupContext from '../RadioGroup/RadioGroupContext';
910

1011
export const styles = theme => ({
1112
/* Styles applied to the root element. */
@@ -37,7 +38,28 @@ export const styles = theme => ({
3738
});
3839

3940
const Radio = React.forwardRef(function Radio(props, ref) {
40-
const { classes, color, ...other } = props;
41+
const {
42+
checked: checkedProp,
43+
classes,
44+
color,
45+
name: nameProp,
46+
onChange: onChangeProp,
47+
...other
48+
} = props;
49+
const radioGroup = React.useContext(RadioGroupContext);
50+
51+
let checked = checkedProp;
52+
const onChange = createChainedFunction(onChangeProp, radioGroup && radioGroup.onChange);
53+
let name = nameProp;
54+
55+
if (radioGroup) {
56+
if (typeof checked === 'undefined') {
57+
checked = radioGroup.value === props.value;
58+
}
59+
if (typeof name === 'undefined') {
60+
name = radioGroup.name;
61+
}
62+
}
4163

4264
return (
4365
<SwitchBase
@@ -49,6 +71,9 @@ const Radio = React.forwardRef(function Radio(props, ref) {
4971
checked: classes.checked,
5072
disabled: classes.disabled,
5173
}}
74+
name={name}
75+
checked={checked}
76+
onChange={onChange}
5277
ref={ref}
5378
{...other}
5479
/>
@@ -97,6 +122,10 @@ Radio.propTypes = {
97122
* Use that property to pass a ref callback to the native input component.
98123
*/
99124
inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
125+
/**
126+
* Name attribute of the `input` element.
127+
*/
128+
name: PropTypes.string,
100129
/**
101130
* Callback fired when the state is changed.
102131
*

packages/material-ui/src/Radio/Radio.test.js

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,14 @@ import React from 'react';
22
import { assert } from 'chai';
33
import RadioButtonCheckedIcon from '../internal/svg-icons/RadioButtonChecked';
44
import RadioButtonUncheckedIcon from '../internal/svg-icons/RadioButtonUnchecked';
5-
import {
6-
getClasses,
7-
createShallow,
8-
createMount,
9-
describeConformance,
10-
} from '@material-ui/core/test-utils';
11-
import SwitchBase from '../internal/SwitchBase';
5+
import { getClasses, createMount, describeConformance } from '@material-ui/core/test-utils';
126
import Radio from './Radio';
137

148
describe('<Radio />', () => {
15-
let shallow;
169
let classes;
1710
let mount;
1811

1912
before(() => {
20-
shallow = createShallow({ dive: true });
2113
classes = getClasses(<Radio />);
2214
mount = createMount();
2315
});
@@ -42,11 +34,6 @@ describe('<Radio />', () => {
4234
});
4335
});
4436

45-
it('should be using SwitchBase', () => {
46-
const wrapper = shallow(<Radio />);
47-
assert.strictEqual(wrapper.type(), SwitchBase);
48-
});
49-
5037
describe('prop: unchecked', () => {
5138
it('should render an unchecked icon', () => {
5239
const wrapper = mount(<Radio />);

packages/material-ui/src/RadioGroup/RadioGroup.js

Lines changed: 20 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,31 @@ import React from 'react';
44
import PropTypes from 'prop-types';
55
import warning from 'warning';
66
import FormGroup from '../FormGroup';
7-
import { createChainedFunction, find } from '../utils/helpers';
7+
import { setRef } from '../utils/reactHelpers';
8+
import RadioGroupContext from './RadioGroupContext';
89

910
const RadioGroup = React.forwardRef(function RadioGroup(props, ref) {
10-
const { actions, children, defaultValue, name, value: valueProp, onChange, ...other } = props;
11-
const radiosRef = React.useRef([]);
11+
const { actions, children, name, value: valueProp, onChange, ...other } = props;
12+
const rootRef = React.useRef();
1213
const { current: isControlled } = React.useRef(props.value != null);
1314
const [valueState, setValue] = React.useState(() => {
1415
if (!isControlled) {
15-
return defaultValue;
16+
return props.defaultValue;
1617
}
1718
return null;
1819
});
1920

2021
React.useImperativeHandle(actions, () => ({
2122
focus: () => {
22-
const radios = radiosRef.current;
23-
if (!radios.length) {
24-
return;
25-
}
26-
27-
const focusRadios = radios.filter(n => !n.disabled);
23+
let input = rootRef.current.querySelector('input:not(:disabled):checked');
2824

29-
if (!focusRadios.length) {
30-
return;
25+
if (!input) {
26+
input = rootRef.current.querySelector('input:not(:disabled)');
3127
}
3228

33-
const selectedRadio = find(focusRadios, n => n.checked);
34-
35-
if (selectedRadio) {
36-
selectedRadio.focus();
37-
return;
29+
if (input) {
30+
input.focus();
3831
}
39-
40-
focusRadios[0].focus();
4132
},
4233
}));
4334

@@ -67,34 +58,18 @@ const RadioGroup = React.forwardRef(function RadioGroup(props, ref) {
6758
onChange(event, event.target.value);
6859
}
6960
};
61+
const context = { name, onChange: handleChange, value };
7062

71-
radiosRef.current = [];
7263
return (
73-
<FormGroup role="radiogroup" ref={ref} defaultValue={defaultValue} {...other}>
74-
{React.Children.map(children, child => {
75-
if (!React.isValidElement(child)) {
76-
return null;
77-
}
78-
79-
warning(
80-
child.type !== React.Fragment,
81-
[
82-
"Material-UI: the RadioGroup component doesn't accept a Fragment as a child.",
83-
'Consider providing an array instead.',
84-
].join('\n'),
85-
);
86-
87-
return React.cloneElement(child, {
88-
name,
89-
inputRef: node => {
90-
if (node) {
91-
radiosRef.current.push(node);
92-
}
93-
},
94-
checked: value === child.props.value,
95-
onChange: createChainedFunction(child.props.onChange, handleChange),
96-
});
97-
})}
64+
<FormGroup
65+
role="radiogroup"
66+
ref={nodeRef => {
67+
setRef(ref, nodeRef);
68+
setRef(rootRef, nodeRef);
69+
}}
70+
{...other}
71+
>
72+
<RadioGroupContext.Provider value={context}>{children}</RadioGroupContext.Provider>
9873
</FormGroup>
9974
);
10075
});

packages/material-ui/src/RadioGroup/RadioGroup.test.js

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -117,49 +117,35 @@ describe('<RadioGroup />', () => {
117117

118118
it('should focus the selected radio', () => {
119119
const actionsRef = React.createRef();
120-
const zeroRadioOnFocus = spy();
121-
const oneRadioOnFocus = spy();
122120
const twoRadioOnFocus = spy();
123-
const threeRadioOnFocus = spy();
124121

125122
mount(
126123
<RadioGroup actions={actionsRef} value="two">
127-
<Radio value="zero" disabled onFocus={zeroRadioOnFocus} />
128-
<Radio value="one" onFocus={oneRadioOnFocus} />
124+
<Radio value="zero" disabled />
125+
<Radio value="one" />
129126
<Radio value="two" onFocus={twoRadioOnFocus} />
130-
<Radio value="three" onFocus={threeRadioOnFocus} />
127+
<Radio value="three" />
131128
</RadioGroup>,
132129
);
133130

134131
actionsRef.current.focus();
135-
136-
assert.strictEqual(zeroRadioOnFocus.callCount, 0);
137-
assert.strictEqual(oneRadioOnFocus.callCount, 0);
138132
assert.strictEqual(twoRadioOnFocus.callCount, 1);
139-
assert.strictEqual(threeRadioOnFocus.callCount, 0);
140133
});
141134

142135
it('should focus the non-disabled radio rather than the disabled selected radio', () => {
143136
const actionsRef = React.createRef();
144-
const zeroRadioOnFocus = spy();
145-
const oneRadioOnFocus = spy();
146-
const twoRadioOnFocus = spy();
147137
const threeRadioOnFocus = spy();
148138

149139
mount(
150140
<RadioGroup actions={actionsRef} value="two">
151-
<Radio value="zero" disabled onFocus={zeroRadioOnFocus} />
152-
<Radio value="one" disabled onFocus={oneRadioOnFocus} />
153-
<Radio value="two" disabled onFocus={twoRadioOnFocus} />
141+
<Radio value="zero" disabled />
142+
<Radio value="one" disabled />
143+
<Radio value="two" disabled />
154144
<Radio value="three" onFocus={threeRadioOnFocus} />
155145
</RadioGroup>,
156146
);
157147

158148
actionsRef.current.focus();
159-
160-
assert.strictEqual(zeroRadioOnFocus.callCount, 0);
161-
assert.strictEqual(oneRadioOnFocus.callCount, 0);
162-
assert.strictEqual(twoRadioOnFocus.callCount, 0);
163149
assert.strictEqual(threeRadioOnFocus.callCount, 1);
164150
});
165151

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react';
2+
3+
/**
4+
* @ignore - internal component.
5+
*/
6+
const RadioGroupContext = React.createContext();
7+
8+
export default RadioGroupContext;

packages/material-ui/src/utils/helpers.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ export function findIndex(arr, pred) {
3232
return -1;
3333
}
3434

35-
export function find(arr, pred) {
36-
const index = findIndex(arr, pred);
37-
return index > -1 ? arr[index] : undefined;
38-
}
39-
4035
/**
4136
* Safe chained function
4237
*

packages/material-ui/src/utils/helpers.test.js

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert } from 'chai';
2-
import { capitalize, contains, find } from './helpers';
2+
import { capitalize, contains } from './helpers';
33

44
describe('utils/helpers.js', () => {
55
describe('capitalize', () => {
@@ -14,16 +14,6 @@ describe('utils/helpers.js', () => {
1414
});
1515
});
1616

17-
describe('find(arr, pred)', () => {
18-
it('should search for an item in an array containing the predicate', () => {
19-
const array = ['woofHelpers', 'meow', { foo: 'bar' }, { woofHelpers: 'meow' }];
20-
assert.strictEqual(find(array, 'lol'), undefined);
21-
assert.strictEqual(find(array, 'woofHelpers'), array[0]);
22-
assert.strictEqual(find(array, { foo: 'bar' }), array[2]);
23-
assert.strictEqual(find(array, n => n && n.woofHelpers === 'meow'), array[3]);
24-
});
25-
});
26-
2717
describe('contains(obj, pred)', () => {
2818
it('should check if an object contains the partial object', () => {
2919
const obj = { woofHelpers: 'meow', cat: 'dog' };

pages/api/radio.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import Radio from '@material-ui/core/Radio';
2828
| <span class="prop-name">id</span> | <span class="prop-type">string</span> |   | The id of the `input` element. |
2929
| <span class="prop-name">inputProps</span> | <span class="prop-type">object</span> |   | Attributes applied to the `input` element. |
3030
| <span class="prop-name">inputRef</span> | <span class="prop-type">union:&nbsp;func&nbsp;&#124;<br>&nbsp;object<br></span> |   | Use that property to pass a ref callback to the native input component. |
31+
| <span class="prop-name">name</span> | <span class="prop-type">string</span> |   | Name attribute of the `input` element. |
3132
| <span class="prop-name">onChange</span> | <span class="prop-type">func</span> |   | Callback fired when the state is changed.<br><br>**Signature:**<br>`function(event: object, checked: boolean) => void`<br>*event:* The event source of the callback. You can pull out the new value by accessing `event.target.value`.<br>*checked:* The `checked` value of the switch |
3233
| <span class="prop-name">type</span> | <span class="prop-type">string</span> |   | The input component property `type`. |
3334
| <span class="prop-name">value</span> | <span class="prop-type">union:&nbsp;string&nbsp;&#124;<br>&nbsp;number&nbsp;&#124;<br>&nbsp;bool<br></span> |   | The value of the component. |

0 commit comments

Comments
 (0)