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;
id?: string;
autoFocus?: boolean;
type?: string;
}
export interface CheckboxProps extends AbstractCheckboxProps<CheckboxChangeEvent> {

View File

@ -52,15 +52,10 @@ describe('Radio Group', () => {
);
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
wrapper.setProps({ value: 'A' });
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', () => {
@ -82,16 +77,10 @@ describe('Radio Group', () => {
);
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
wrapper.setProps({ value: 'A' });
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', () => {
@ -106,15 +95,10 @@ describe('Radio Group', () => {
);
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
wrapper.setProps({ value: 'A' });
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', () => {
@ -136,11 +120,6 @@ describe('Radio Group', () => {
);
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
wrapper.setProps({ value: 'A' });
radios.at(0).simulate('change');
@ -193,6 +172,7 @@ describe('Radio Group', () => {
true,
);
wrapper.setProps({ value: newValue });
wrapper.update();
expect(wrapper.find('.ant-radio-wrapper').at(0).hasClass('ant-radio-wrapper-checked')).toBe(
false,
);

View File

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

View File

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

View File

@ -4,77 +4,68 @@ import classNames from 'classnames';
import RadioGroup from './group';
import RadioButton from './radioButton';
import { RadioProps, RadioChangeEvent } from './interface';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context';
import { composeRef } from '../_util/ref';
export default class Radio extends React.PureComponent<RadioProps, {}> {
static Group: typeof RadioGroup;
static 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>;
}
interface CompoundedComponent
extends React.ForwardRefExoticComponent<RadioProps & React.RefAttributes<HTMLElement>> {
Group: typeof RadioGroup;
Button: typeof RadioButton;
}
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 { RadioChangeEvent } from './interface';
import { AbstractCheckboxProps } from '../checkbox/Checkbox';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context';
export type RadioButtonProps = AbstractCheckboxProps<RadioChangeEvent>;
const RadioButton = (props: RadioButtonProps, ref: React.Ref<any>) => {
const radioGroupContext = React.useContext(RadioGroupContext);
const { getPrefixCls } = React.useContext(ConfigContext);
return (
<ConfigConsumer>
{({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, ...radioProps }: RadioButtonProps = props;
const prefixCls = getPrefixCls('radio-button', customizePrefixCls);
if (radioGroupContext) {
radioProps.checked = props.value === radioGroupContext.value;
radioProps.disabled = props.disabled || radioGroupContext.disabled;
}
return <Radio prefixCls={prefixCls} {...radioProps} type="radio" ref={ref} />;
}}
</ConfigConsumer>
);
const { prefixCls: customizePrefixCls, ...radioProps } = props;
const prefixCls = getPrefixCls('radio-button', customizePrefixCls);
if (radioGroupContext) {
radioProps.checked = props.value === radioGroupContext.value;
radioProps.disabled = props.disabled || radioGroupContext.disabled;
}
return <Radio prefixCls={prefixCls} {...radioProps} type="radio" ref={ref} />;
};
export default React.forwardRef(RadioButton);