refactor(radio): rewrite with hook (#24485)

* refactor(radio): rewrite with hook

* fix lint
This commit is contained in:
Tom Xu 2020-05-27 10:21:17 +08:00 committed by GitHub
parent 5a1ffb7894
commit 2a3fc818d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 140 additions and 202 deletions

View File

@ -24,6 +24,7 @@ export interface AbstractCheckboxProps<T> {
children?: React.ReactNode; children?: React.ReactNode;
id?: string; id?: string;
autoFocus?: boolean; autoFocus?: boolean;
type?: string;
} }
export interface CheckboxProps extends AbstractCheckboxProps<CheckboxChangeEvent> { export interface CheckboxProps extends AbstractCheckboxProps<CheckboxChangeEvent> {

View File

@ -52,15 +52,10 @@ describe('Radio Group', () => {
); );
const radios = wrapper.find('input'); const radios = wrapper.find('input');
// uncontrolled component
wrapper.setState({ value: 'B' });
radios.at(0).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
// controlled component // controlled component
wrapper.setProps({ value: 'A' }); wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change'); radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(2); expect(onChange.mock.calls.length).toBe(1);
}); });
it('both of radio and radioGroup will trigger onchange event when they exists', () => { it('both of radio and radioGroup will trigger onchange event when they exists', () => {
@ -82,16 +77,10 @@ describe('Radio Group', () => {
); );
const radios = wrapper.find('input'); const radios = wrapper.find('input');
// uncontrolled component
wrapper.setState({ value: 'B' });
radios.at(0).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
expect(onChangeRadioGroup.mock.calls.length).toBe(1);
// controlled component // controlled component
wrapper.setProps({ value: 'A' }); wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change'); radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(2); expect(onChange.mock.calls.length).toBe(1);
}); });
it('Trigger onChange when both of radioButton and radioGroup exists', () => { it('Trigger onChange when both of radioButton and radioGroup exists', () => {
@ -106,15 +95,10 @@ describe('Radio Group', () => {
); );
const radios = wrapper.find('input'); const radios = wrapper.find('input');
// uncontrolled component
wrapper.setState({ value: 'B' });
radios.at(0).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
// controlled component // controlled component
wrapper.setProps({ value: 'A' }); wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change'); radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(2); expect(onChange.mock.calls.length).toBe(1);
}); });
it('should only trigger once when in group with options', () => { it('should only trigger once when in group with options', () => {
@ -136,11 +120,6 @@ describe('Radio Group', () => {
); );
const radios = wrapper.find('input'); const radios = wrapper.find('input');
// uncontrolled component
wrapper.setState({ value: 'B' });
radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(0);
// controlled component // controlled component
wrapper.setProps({ value: 'A' }); wrapper.setProps({ value: 'A' });
radios.at(0).simulate('change'); radios.at(0).simulate('change');
@ -193,6 +172,7 @@ describe('Radio Group', () => {
true, true,
); );
wrapper.setProps({ value: newValue }); wrapper.setProps({ value: newValue });
wrapper.update();
expect(wrapper.find('.ant-radio-wrapper').at(0).hasClass('ant-radio-wrapper-checked')).toBe( expect(wrapper.find('.ant-radio-wrapper').at(0).hasClass('ant-radio-wrapper-checked')).toBe(
false, false,
); );

View File

@ -6,7 +6,7 @@ import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
describe('Radio', () => { describe('Radio', () => {
focusTest(Radio); focusTest(Radio, { refFocus: true });
mountTest(Radio); mountTest(Radio);
mountTest(Group); mountTest(Group);
mountTest(Button); mountTest(Button);

View File

@ -1,63 +1,44 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Radio from './radio'; import Radio from './radio';
import { import { RadioGroupProps, RadioChangeEvent, RadioGroupButtonStyle } from './interface';
RadioGroupProps, import { ConfigContext } from '../config-provider';
RadioGroupState,
RadioChangeEvent,
RadioGroupButtonStyle,
} from './interface';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import SizeContext from '../config-provider/SizeContext'; import SizeContext from '../config-provider/SizeContext';
import { RadioGroupContextProvider } from './context'; import { RadioGroupContextProvider } from './context';
class RadioGroup extends React.PureComponent<RadioGroupProps, RadioGroupState> { const RadioGroup: React.FC<RadioGroupProps> = props => {
static defaultProps = { const { getPrefixCls, direction } = React.useContext(ConfigContext);
buttonStyle: 'outline' as RadioGroupButtonStyle, const size = React.useContext(SizeContext);
};
static getDerivedStateFromProps(nextProps: RadioGroupProps, prevState: RadioGroupState) { let initValue;
const newState: Partial<RadioGroupState> = { if (props.value !== undefined) {
prevPropValue: nextProps.value, initValue = props.value;
}; } else if (props.defaultValue !== undefined) {
initValue = props.defaultValue;
if (nextProps.value !== undefined || prevState.prevPropValue !== nextProps.value) {
newState.value = nextProps.value;
}
return newState;
} }
const [value, setValue] = React.useState(initValue);
const [prevPropValue, setPrevPropValue] = React.useState(props.value);
constructor(props: RadioGroupProps) { React.useEffect(() => {
super(props); setPrevPropValue(props.value);
let value; if (props.value !== undefined || prevPropValue !== props.value) {
if (props.value !== undefined) { setValue(props.value);
value = props.value;
} else if (props.defaultValue !== undefined) {
value = props.defaultValue;
} }
this.state = { }, [props.value]);
value,
prevPropValue: props.value,
};
}
onRadioChange = (ev: RadioChangeEvent) => { const onRadioChange = (ev: RadioChangeEvent) => {
const { value: lastValue } = this.state; const lastValue = value;
const { value } = ev.target; const val = ev.target.value;
if (!('value' in this.props)) { if (!('value' in props)) {
this.setState({ setValue(val);
value,
});
} }
const { onChange } = props;
const { onChange } = this.props; if (onChange && val !== lastValue) {
if (onChange && value !== lastValue) {
onChange(ev); onChange(ev);
} }
}; };
renderGroup = ({ getPrefixCls, direction }: ConfigConsumerProps) => { const renderGroup = () => {
const { const {
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
className = '', className = '',
@ -70,8 +51,7 @@ class RadioGroup extends React.PureComponent<RadioGroupProps, RadioGroupState> {
id, id,
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
} = this.props; } = props;
const { value } = this.state;
const prefixCls = getPrefixCls('radio', customizePrefixCls); const prefixCls = getPrefixCls('radio', customizePrefixCls);
const groupPrefixCls = `${prefixCls}-group`; const groupPrefixCls = `${prefixCls}-group`;
let childrenToRender = children; let childrenToRender = children;
@ -108,49 +88,45 @@ class RadioGroup extends React.PureComponent<RadioGroupProps, RadioGroupState> {
}); });
} }
const mergedSize = customizeSize || size;
const classString = classNames(
groupPrefixCls,
`${groupPrefixCls}-${buttonStyle}`,
{
[`${groupPrefixCls}-${mergedSize}`]: mergedSize,
[`${groupPrefixCls}-rtl`]: direction === 'rtl',
},
className,
);
return ( return (
<SizeContext.Consumer> <div
{size => { className={classString}
const mergedSize = customizeSize || size; style={style}
const classString = classNames( onMouseEnter={onMouseEnter}
groupPrefixCls, onMouseLeave={onMouseLeave}
`${groupPrefixCls}-${buttonStyle}`, id={id}
{ >
[`${groupPrefixCls}-${mergedSize}`]: mergedSize, {childrenToRender}
[`${groupPrefixCls}-rtl`]: direction === 'rtl', </div>
},
className,
);
return (
<div
className={classString}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
id={id}
>
{childrenToRender}
</div>
);
}}
</SizeContext.Consumer>
); );
}; };
render() { return (
return ( <RadioGroupContextProvider
<RadioGroupContextProvider value={{
value={{ onChange: onRadioChange,
onChange: this.onRadioChange, value,
value: this.state.value, disabled: props.disabled,
disabled: this.props.disabled, name: props.name,
name: this.props.name, }}
}} >
> {renderGroup()}
<ConfigConsumer>{this.renderGroup}</ConfigConsumer> </RadioGroupContextProvider>
</RadioGroupContextProvider> );
); };
}
}
export default RadioGroup; RadioGroup.defaultProps = {
buttonStyle: 'outline' as RadioGroupButtonStyle,
};
export default React.memo(RadioGroup);

View File

@ -18,11 +18,6 @@ export interface RadioGroupProps extends AbstractCheckboxGroupProps {
buttonStyle?: RadioGroupButtonStyle; buttonStyle?: RadioGroupButtonStyle;
} }
export interface RadioGroupState {
value: any;
prevPropValue: any;
}
export interface RadioGroupContextProps { export interface RadioGroupContextProps {
onChange: (e: RadioChangeEvent) => void; onChange: (e: RadioChangeEvent) => void;
value: any; value: any;

View File

@ -4,77 +4,68 @@ import classNames from 'classnames';
import RadioGroup from './group'; import RadioGroup from './group';
import RadioButton from './radioButton'; import RadioButton from './radioButton';
import { RadioProps, RadioChangeEvent } from './interface'; import { RadioProps, RadioChangeEvent } from './interface';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context'; import RadioGroupContext from './context';
import { composeRef } from '../_util/ref';
export default class Radio extends React.PureComponent<RadioProps, {}> { interface CompoundedComponent
static Group: typeof RadioGroup; extends React.ForwardRefExoticComponent<RadioProps & React.RefAttributes<HTMLElement>> {
Group: typeof RadioGroup;
static Button: typeof RadioButton; Button: typeof RadioButton;
static defaultProps = {
type: 'radio',
};
static contextType = RadioGroupContext;
private rcCheckbox: any;
saveCheckbox = (node: any) => {
this.rcCheckbox = node;
};
onChange = (e: RadioChangeEvent) => {
if (this.props.onChange) {
this.props.onChange(e);
}
if (this.context?.onChange) {
this.context.onChange(e);
}
};
focus() {
this.rcCheckbox.focus();
}
blur() {
this.rcCheckbox.blur();
}
renderRadio = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const { props, context } = this;
const { prefixCls: customizePrefixCls, className, children, style, ...restProps } = props;
const prefixCls = getPrefixCls('radio', customizePrefixCls);
const radioProps: RadioProps = { ...restProps };
if (context) {
radioProps.name = context.name;
radioProps.onChange = this.onChange;
radioProps.checked = props.value === context.value;
radioProps.disabled = props.disabled || context.disabled;
}
const wrapperClassString = classNames(className, {
[`${prefixCls}-wrapper`]: true,
[`${prefixCls}-wrapper-checked`]: radioProps.checked,
[`${prefixCls}-wrapper-disabled`]: radioProps.disabled,
[`${prefixCls}-wrapper-rtl`]: direction === 'rtl',
});
return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label
className={wrapperClassString}
style={style}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<RcCheckbox {...radioProps} prefixCls={prefixCls} ref={this.saveCheckbox} />
{children !== undefined ? <span>{children}</span> : null}
</label>
);
};
render() {
return <ConfigConsumer>{this.renderRadio}</ConfigConsumer>;
}
} }
const InternalRadio: React.ForwardRefRenderFunction<unknown, RadioProps> = (props, ref) => {
const context = React.useContext(RadioGroupContext);
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const innerRef = React.useRef<HTMLElement>();
const mergedRef = composeRef(ref, innerRef);
const onChange = (e: RadioChangeEvent) => {
if (props.onChange) {
props.onChange(e);
}
if (context?.onChange) {
context.onChange(e);
}
};
const { prefixCls: customizePrefixCls, className, children, style, ...restProps } = props;
const prefixCls = getPrefixCls('radio', customizePrefixCls);
const radioProps: RadioProps = { ...restProps };
if (context) {
radioProps.name = context.name;
radioProps.onChange = onChange;
radioProps.checked = props.value === context.value;
radioProps.disabled = props.disabled || context.disabled;
}
const wrapperClassString = classNames(className, {
[`${prefixCls}-wrapper`]: true,
[`${prefixCls}-wrapper-checked`]: radioProps.checked,
[`${prefixCls}-wrapper-disabled`]: radioProps.disabled,
[`${prefixCls}-wrapper-rtl`]: direction === 'rtl',
});
return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label
className={wrapperClassString}
style={style}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<RcCheckbox {...radioProps} prefixCls={prefixCls} ref={mergedRef as any} />
{children !== undefined ? <span>{children}</span> : null}
</label>
);
};
const Radio = React.forwardRef<unknown, RadioProps>(InternalRadio) as CompoundedComponent;
Radio.displayName = 'Radio';
Radio.Group = RadioGroup;
Radio.Button = RadioButton;
Radio.defaultProps = {
type: 'radio',
};
export default Radio;

View File

@ -2,27 +2,22 @@ import * as React from 'react';
import Radio from './radio'; import Radio from './radio';
import { RadioChangeEvent } from './interface'; import { RadioChangeEvent } from './interface';
import { AbstractCheckboxProps } from '../checkbox/Checkbox'; import { AbstractCheckboxProps } from '../checkbox/Checkbox';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context'; import RadioGroupContext from './context';
export type RadioButtonProps = AbstractCheckboxProps<RadioChangeEvent>; export type RadioButtonProps = AbstractCheckboxProps<RadioChangeEvent>;
const RadioButton = (props: RadioButtonProps, ref: React.Ref<any>) => { const RadioButton = (props: RadioButtonProps, ref: React.Ref<any>) => {
const radioGroupContext = React.useContext(RadioGroupContext); const radioGroupContext = React.useContext(RadioGroupContext);
const { getPrefixCls } = React.useContext(ConfigContext);
return ( const { prefixCls: customizePrefixCls, ...radioProps } = props;
<ConfigConsumer> const prefixCls = getPrefixCls('radio-button', customizePrefixCls);
{({ getPrefixCls }: ConfigConsumerProps) => { if (radioGroupContext) {
const { prefixCls: customizePrefixCls, ...radioProps }: RadioButtonProps = props; radioProps.checked = props.value === radioGroupContext.value;
const prefixCls = getPrefixCls('radio-button', customizePrefixCls); radioProps.disabled = props.disabled || radioGroupContext.disabled;
if (radioGroupContext) { }
radioProps.checked = props.value === radioGroupContext.value; return <Radio prefixCls={prefixCls} {...radioProps} type="radio" ref={ref} />;
radioProps.disabled = props.disabled || radioGroupContext.disabled;
}
return <Radio prefixCls={prefixCls} {...radioProps} type="radio" ref={ref} />;
}}
</ConfigConsumer>
);
}; };
export default React.forwardRef(RadioButton); export default React.forwardRef(RadioButton);