mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-11 11:32:52 +08:00
refactor(radio): rewrite with hook (#24485)
* refactor(radio): rewrite with hook * fix lint
This commit is contained in:
parent
5a1ffb7894
commit
2a3fc818d0
@ -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> {
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user