refactor: add optionType for Radio internally (#34849)

* refactor: add optionType for Radio internally

* fix: lint

* fix: update snapshot

* feat: remove useless condition for RadioGroupContext

* test: add test case for Radio.Button
This commit is contained in:
vagusX 2022-04-08 15:23:52 +08:00 committed by GitHub
parent 452c5835ec
commit afa4442a10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 380 additions and 25 deletions

View File

@ -19168,18 +19168,18 @@ exports[`ConfigProvider components Radio prefixCls 1`] = `
class="prefix-Radio-group prefix-Radio-group-outline"
>
<label
class="prefix-Radio-wrapper prefix-Radio-wrapper-checked"
class="prefix-Radio-button-wrapper prefix-Radio-button-wrapper-checked"
>
<span
class="prefix-Radio prefix-Radio-checked"
class="prefix-Radio-button prefix-Radio-button-checked"
>
<input
checked=""
class="prefix-Radio-input"
class="prefix-Radio-button-input"
type="radio"
/>
<span
class="prefix-Radio-inner"
class="prefix-Radio-button-inner"
/>
</span>
<span>

View File

@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Radio Button rtl render component should be rendered correctly in RTL direction 1`] = `
<label
class="ant-radio-button-wrapper ant-radio-button-wrapper-rtl"
>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value=""
/>
<span
class="ant-radio-button-inner"
/>
</span>
</label>
`;
exports[`Radio Button should render correctly 1`] = `
<label
class="ant-radio-button-wrapper customized"
>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Test
</span>
</label>
`;
exports[`Radio Group passes prefixCls down to radio 1`] = `
<div
class="my-radio-group my-radio-group-outline"
>
<label
class="my-radio-wrapper"
>
<span
class="my-radio"
>
<input
class="my-radio-input"
type="radio"
value="Apple"
/>
<span
class="my-radio-inner"
/>
</span>
<span>
Apple
</span>
</label>
<label
class="my-radio-wrapper"
style="font-size:12px"
>
<span
class="my-radio"
>
<input
class="my-radio-input"
type="radio"
value="Orange"
/>
<span
class="my-radio-inner"
/>
</span>
<span>
Orange
</span>
</label>
</div>
`;

View File

@ -0,0 +1,250 @@
import React from 'react';
import { mount, render } from 'enzyme';
import Radio, { Button } from '..';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
describe('Radio Button', () => {
focusTest(Button, { refFocus: true });
mountTest(Button);
rtlTest(Button);
it('should render correctly', () => {
const wrapper = render(<Button className="customized">Test</Button>);
expect(wrapper).toMatchSnapshot();
});
it('responses hover events', () => {
const onMouseEnter = jest.fn();
const onMouseLeave = jest.fn();
const wrapper = mount(<Button onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />);
wrapper.find('label').simulate('mouseenter');
expect(onMouseEnter).toHaveBeenCalled();
wrapper.find('label').simulate('mouseleave');
expect(onMouseLeave).toHaveBeenCalled();
});
});
describe('Radio Group', () => {
function createRadioGroup(props) {
return (
<Radio.Group {...props}>
<Button value="A">A</Button>
<Button value="B">B</Button>
<Button value="C">C</Button>
</Radio.Group>
);
}
it('responses hover events', () => {
const onMouseEnter = jest.fn();
const onMouseLeave = jest.fn();
const wrapper = mount(
<Radio.Group onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Radio />
</Radio.Group>,
);
wrapper.find('div').at(0).simulate('mouseenter');
expect(onMouseEnter).toHaveBeenCalled();
wrapper.find('div').at(0).simulate('mouseleave');
expect(onMouseLeave).toHaveBeenCalled();
});
it('fire change events when value changes', () => {
const onChange = jest.fn();
const wrapper = mount(
createRadioGroup({
onChange,
}),
);
const radios = wrapper.find('input');
// controlled component
wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
});
it('both of radio and radioGroup will trigger onchange event when they exists', () => {
const onChange = jest.fn();
const onChangeRadioGroup = jest.fn();
const wrapper = mount(
<Radio.Group onChange={onChangeRadioGroup}>
<Radio value="A" onChange={onChange}>
A
</Radio>
<Radio value="B" onChange={onChange}>
B
</Radio>
<Radio value="C" onChange={onChange}>
C
</Radio>
</Radio.Group>,
);
const radios = wrapper.find('input');
// controlled component
wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
});
it('Trigger onChange when both of Button and radioGroup exists', () => {
const onChange = jest.fn();
const wrapper = mount(
<Radio.Group onChange={onChange}>
<Button value="A">A</Button>
<Button value="B">B</Button>
<Button value="C">C</Button>
</Radio.Group>,
);
const radios = wrapper.find('input');
// controlled component
wrapper.setProps({ value: 'A' });
radios.at(1).simulate('change');
expect(onChange.mock.calls.length).toBe(1);
});
it('should only trigger once when in group with options', () => {
const onChange = jest.fn();
const options = [{ label: 'Bamboo', value: 'Bamboo' }];
const wrapper = mount(<Radio.Group options={options} onChange={onChange} />);
wrapper.find('input').simulate('change');
expect(onChange).toHaveBeenCalledTimes(1);
});
it("won't fire change events when value not changes", () => {
const onChange = jest.fn();
const wrapper = mount(
createRadioGroup({
onChange,
}),
);
const radios = wrapper.find('input');
// controlled component
wrapper.setProps({ value: 'A' });
radios.at(0).simulate('change');
expect(onChange.mock.calls.length).toBe(0);
});
it('all children should have a name property', () => {
const GROUP_NAME = 'radiogroup';
const wrapper = mount(createRadioGroup({ name: GROUP_NAME }));
wrapper.find('input[type="radio"]').forEach(el => {
expect(el.props().name).toEqual(GROUP_NAME);
});
});
it('passes prefixCls down to radio', () => {
const options = [
{ label: 'Apple', value: 'Apple' },
{ label: 'Orange', value: 'Orange', style: { fontSize: 12 } },
];
const wrapper = render(<Radio.Group prefixCls="my-radio" options={options} />);
expect(wrapper).toMatchSnapshot();
});
it('should forward ref', () => {
let radioGroupRef;
const wrapper = mount(
createRadioGroup({
ref: ref => {
radioGroupRef = ref;
},
}),
);
expect(radioGroupRef).toBe(wrapper.children().getDOMNode());
});
it('should support data-* or aria-* props', () => {
const wrapper = mount(
createRadioGroup({
'data-radio-group-id': 'radio-group-id',
'aria-label': 'radio-group',
}),
);
expect(wrapper.getDOMNode().getAttribute('data-radio-group-id')).toBe('radio-group-id');
expect(wrapper.getDOMNode().getAttribute('aria-label')).toBe('radio-group');
});
it('Radio type should not be override', () => {
const onChange = jest.fn();
const wrapper = mount(
<Radio.Group onChange={onChange}>
<Radio value={1} type="1">
A
</Radio>
<Radio value={2} type="2">
B
</Radio>
<Radio value={3} type="3">
C
</Radio>
<Radio value={4} type="4">
D
</Radio>
</Radio.Group>,
);
const radios = wrapper.find('input');
radios.at(1).simulate('change');
expect(onChange).toHaveBeenCalled();
expect(radios.at(1).getDOMNode().type).toBe('radio');
});
describe('value is null or undefined', () => {
it('use `defaultValue` when `value` is undefined', () => {
const wrapper = mount(
<Radio.Group defaultValue="bamboo" value={undefined}>
<Button value="bamboo">Bamboo</Button>
</Radio.Group>,
);
expect(
wrapper
.find('.ant-radio-button-wrapper')
.at(0)
.hasClass('ant-radio-button-wrapper-checked'),
).toBe(true);
});
[undefined, null].forEach(newValue => {
it(`should set value back when value change back to ${newValue}`, () => {
const wrapper = mount(
<Radio.Group value="bamboo">
<Button value="bamboo">Bamboo</Button>
</Radio.Group>,
);
expect(
wrapper
.find('.ant-radio-button-wrapper')
.at(0)
.hasClass('ant-radio-button-wrapper-checked'),
).toBe(true);
wrapper.setProps({ value: newValue });
wrapper.update();
expect(
wrapper
.find('.ant-radio-button-wrapper')
.at(0)
.hasClass('ant-radio-button-wrapper-checked'),
).toBe(false);
});
});
});
});

View File

@ -1,8 +1,11 @@
import * as React from 'react';
import { RadioGroupContextProps } from './interface';
import { RadioGroupContextProps, RadioOptionTypeContextProps } from './interface';
const RadioGroupContext = React.createContext<RadioGroupContextProps | null>(null);
export const RadioGroupContextProvider = RadioGroupContext.Provider;
export default RadioGroupContext;
export const RadioOptionTypeContext = React.createContext<RadioOptionTypeContextProps | null>(null);
export const RadioOptionTypeContextProvider = RadioOptionTypeContext.Provider;

View File

@ -33,7 +33,6 @@ const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>((props, ref
prefixCls: customizePrefixCls,
className = '',
options,
optionType,
buttonStyle = 'outline' as RadioGroupButtonStyle,
disabled,
children,
@ -48,14 +47,13 @@ const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>((props, ref
let childrenToRender = children;
// 如果存在 options, 优先使用
if (options && options.length > 0) {
const optionsPrefixCls = optionType === 'button' ? `${prefixCls}-button` : prefixCls;
childrenToRender = options.map(option => {
if (typeof option === 'string' || typeof option === 'number') {
// 此处类型自动推导为 string
return (
<Radio
key={option.toString()}
prefixCls={optionsPrefixCls}
prefixCls={prefixCls}
disabled={disabled}
value={option}
checked={value === option}
@ -68,7 +66,7 @@ const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>((props, ref
return (
<Radio
key={`radio-group-value-options-${option.value}`}
prefixCls={optionsPrefixCls}
prefixCls={prefixCls}
disabled={option.disabled || disabled}
value={option.value}
checked={value === option.value}
@ -112,6 +110,7 @@ const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>((props, ref
value,
disabled: props.disabled,
name: props.name,
optionType: props.optionType,
}}
>
{renderGroup()}

View File

@ -25,6 +25,13 @@ export interface RadioGroupContextProps {
value: any;
disabled?: boolean;
name?: string;
/**
* Control the appearance for Radio to display as button or not
*
* @default 'default'
* @internal
*/
optionType?: RadioGroupOptionType;
}
export type RadioProps = AbstractCheckboxProps<RadioChangeEvent>;
@ -39,3 +46,5 @@ export interface RadioChangeEvent {
preventDefault: () => void;
nativeEvent: MouseEvent;
}
export type RadioOptionTypeContextProps = RadioGroupOptionType;

View File

@ -6,11 +6,13 @@ import { useContext } from 'react';
import { FormItemInputContext } from '../form/context';
import { RadioProps, RadioChangeEvent } from './interface';
import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context';
import RadioGroupContext, { RadioOptionTypeContext } from './context';
import devWarning from '../_util/devWarning';
const InternalRadio: React.ForwardRefRenderFunction<HTMLElement, RadioProps> = (props, ref) => {
const context = React.useContext(RadioGroupContext);
const groupContext = React.useContext(RadioGroupContext);
const radioOptionTypeContext = React.useContext(RadioOptionTypeContext);
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const innerRef = React.useRef<HTMLElement>();
const mergedRef = composeRef(ref, innerRef);
@ -22,17 +24,22 @@ const InternalRadio: React.ForwardRefRenderFunction<HTMLElement, RadioProps> = (
const onChange = (e: RadioChangeEvent) => {
props.onChange?.(e);
context?.onChange?.(e);
groupContext?.onChange?.(e);
};
const { prefixCls: customizePrefixCls, className, children, style, ...restProps } = props;
const prefixCls = getPrefixCls('radio', customizePrefixCls);
const radioPrefixCls = getPrefixCls('radio', customizePrefixCls);
const prefixCls =
(groupContext?.optionType || radioOptionTypeContext) === 'button'
? `${radioPrefixCls}-button`
: radioPrefixCls;
const radioProps: RadioProps = { ...restProps };
if (context) {
radioProps.name = context.name;
if (groupContext) {
radioProps.name = groupContext.name;
radioProps.onChange = onChange;
radioProps.checked = props.value === context.value;
radioProps.disabled = props.disabled || context.disabled;
radioProps.checked = props.value === groupContext.value;
radioProps.disabled = props.disabled || groupContext.disabled;
}
const wrapperClassString = classNames(
`${prefixCls}-wrapper`,

View File

@ -3,21 +3,21 @@ import Radio from './radio';
import { RadioChangeEvent } from './interface';
import { AbstractCheckboxProps } from '../checkbox/Checkbox';
import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context';
import { RadioOptionTypeContextProvider } 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);
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} />;
const prefixCls = getPrefixCls('radio', customizePrefixCls);
return (
<RadioOptionTypeContextProvider value="button">
<Radio prefixCls={prefixCls} {...radioProps} type="radio" ref={ref} />
</RadioOptionTypeContextProvider>
);
};
export default React.forwardRef(RadioButton);