mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 11:10:01 +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;
|
||||
id?: string;
|
||||
autoFocus?: boolean;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface CheckboxProps extends AbstractCheckboxProps<CheckboxChangeEvent> {
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user