merge feature into master-to-merge-feature

This commit is contained in:
afc163 2020-06-19 13:22:11 +08:00
commit bbc95a12a4
128 changed files with 2883 additions and 1043 deletions

View File

@ -80,6 +80,46 @@ exports[`renders ./components/cascader/demo/change-on-select.md correctly 1`] =
</span>
`;
exports[`renders ./components/cascader/demo/custom-dropdown.md correctly 1`] = `
<span
class="ant-cascader-picker"
tabindex="0"
>
<span
class="ant-cascader-picker-label"
/>
<input
autocomplete="off"
class="ant-input ant-cascader-input "
placeholder="Please select"
readonly=""
tabindex="-1"
type="text"
value=""
/>
<span
aria-label="down"
class="anticon anticon-down ant-cascader-picker-arrow"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
`;
exports[`renders ./components/cascader/demo/custom-render.md correctly 1`] = `
<span
class="ant-cascader-picker"
@ -551,7 +591,7 @@ exports[`renders ./components/cascader/demo/size.md correctly 1`] = `
`;
exports[`renders ./components/cascader/demo/suffix.md correctly 1`] = `
<div>
Array [
<span
class="ant-cascader-picker"
tabindex="0"
@ -588,10 +628,11 @@ exports[`renders ./components/cascader/demo/suffix.md correctly 1`] = `
/>
</svg>
</span>
</span>
</span>,
<br />,
<br />,
<span
class="ant-cascader-picker"
style="margin-top:1rem"
tabindex="0"
>
<span
@ -611,6 +652,84 @@ exports[`renders ./components/cascader/demo/suffix.md correctly 1`] = `
>
ab
</span>
</span>
</div>
</span>,
<br />,
<br />,
<span
class="ant-cascader-picker"
tabindex="0"
>
<span
class="ant-cascader-picker-label"
/>
<input
autocomplete="off"
class="ant-input ant-cascader-input "
placeholder="Please select"
readonly=""
tabindex="-1"
type="text"
value=""
/>
<span
aria-label="down"
class="anticon anticon-down ant-cascader-picker-arrow"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>,
<br />,
<br />,
<span
class="ant-cascader-picker"
tabindex="0"
>
<span
class="ant-cascader-picker-label"
/>
<input
autocomplete="off"
class="ant-input ant-cascader-input "
placeholder="Please select"
readonly=""
tabindex="-1"
type="text"
value=""
/>
<span
aria-label="down"
class="anticon anticon-down ant-cascader-picker-arrow"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>,
]
`;

View File

@ -0,0 +1,72 @@
---
order: 12
title:
zh-CN: 扩展菜单
en-US: Custom dropdown
---
## zh-CN
使用 `dropdownRender` 对下拉菜单进行自由扩展。
## en-US
Customize the dropdown menu via `dropdownRender`.
```jsx
import { Cascader, Divider } from 'antd';
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
];
function dropdownRender(menus) {
return (
<div>
{menus}
<Divider style={{ margin: 0 }} />
<div style={{ padding: 8 }}>The footer is not very short.</div>
</div>
);
}
ReactDOM.render(
<Cascader
options={options}
dropdownRender={dropdownRender}
placeholder="Please select"
/>,
mountNode,
);
```

View File

@ -2,17 +2,17 @@
order: 11
debug: true
title:
zh-CN: 后缀图标
en-US: Suffix
zh-CN: 自定义图标
en-US: Custom Icons
---
## zh-CN
省市区级联
通过 `suffixIcon` 自定义选择框后缀图标,通过 `expandIcon` 自定义次级菜单展开图标
## en-US
Cascade selection box for selecting province/city/district.
Use `suffixIcon` to customize the selection box suffix icon, and use `expandIcon` to customize the current item expand icon.
```jsx
import { Cascader } from 'antd';
@ -58,21 +58,28 @@ function onChange(value) {
}
ReactDOM.render(
<div>
<>
<Cascader
suffixIcon={<SmileOutlined />}
options={options}
onChange={onChange}
placeholder="Please select"
/>
<br />
<br />
<Cascader suffixIcon="ab" options={options} onChange={onChange} placeholder="Please select" />
<br />
<br />
<Cascader
suffixIcon="ab"
style={{ marginTop: '1rem' }}
expandIcon={<SmileOutlined />}
options={options}
onChange={onChange}
placeholder="Please select"
/>
</div>,
<br />
<br />
<Cascader expandIcon="ab" options={options} onChange={onChange} placeholder="Please select" />
</>,
mountNode,
);
```

View File

@ -30,6 +30,7 @@ Cascade selection box.
| disabled | whether disabled select | boolean | false | |
| displayRender | render function of displaying selected options | `(label, selectedOptions) => ReactNode` | `label => label.join(' / ')` | |
| expandTrigger | expand current item when click or hover, one of 'click' 'hover' | string | `click` | |
| expandIcon | customize the current item expand icon | ReactNode | - | 4.4.0 |
| fieldNames | custom field name for label and value and children | object | `{ label: 'label', value: 'value', children: 'children' }` | |
| getPopupContainer | Parent Node which the selector should be rendered to. Default to `body`. When position issues happen, try to modify it into scrollable content and position it relative.[example](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | () => document.body | |
| loadData | To load option lazily, and it cannot work with `showSearch` | `(selectedOptions) => void` | - | |
@ -44,6 +45,7 @@ Cascade selection box.
| style | additional style | CSSProperties | - | |
| suffixIcon | The custom suffix icon | ReactNode | - | |
| value | selected value | string\[] \| number\[] | - | |
| dropdownRender | Customize dropdown content | `(menus: ReactNode) => ReactNode` | - | 4.4.0 |
| onChange | callback when finishing cascader select | `(value, selectedOptions) => void` | - | |
| onPopupVisibleChange | callback when popup shown or hidden | `(value) => void` | - | |

View File

@ -97,6 +97,7 @@ export interface CascaderProps {
loadData?: (selectedOptions?: CascaderOptionType[]) => void;
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
expandTrigger?: CascaderExpandTrigger;
expandIcon?: React.ReactNode;
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
changeOnSelect?: boolean;
/** 浮层可见变化时回调 */
@ -108,6 +109,7 @@ export interface CascaderProps {
/** use this after antd@3.7.0 */
fieldNames?: FieldNamesType;
suffixIcon?: React.ReactNode;
dropdownRender?: (menus: React.ReactNode) => React.ReactNode
}
export interface CascaderState {
@ -458,11 +460,14 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
allowClear,
showSearch = false,
suffixIcon,
expandIcon,
notFoundContent,
popupClassName,
bordered,
dropdownRender,
...otherProps
} = props;
const mergedSize = customizeSize || size;
const { value, inputFocused } = state;
@ -592,9 +597,11 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
</span>
);
let expandIcon = <RightOutlined />;
if (isRtlLayout) {
expandIcon = <LeftOutlined />;
let expandIconNode;
if (expandIcon) {
expandIconNode = expandIcon;
} else {
expandIconNode = isRtlLayout ? <LeftOutlined /> : <RightOutlined />;
}
const loadingIcon = (
@ -621,10 +628,11 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
onPopupVisibleChange={this.handlePopupVisibleChange}
onChange={this.handleChange}
dropdownMenuColumnStyle={dropdownMenuColumnStyle}
expandIcon={expandIcon}
expandIcon={expandIconNode}
loadingIcon={loadingIcon}
popupClassName={rcCascaderPopupClassName}
popupPlacement={this.getPopupPlacement(direction)}
dropdownRender={dropdownRender}
>
{input}
</RcCascader>

View File

@ -31,6 +31,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
| disabled | 禁用 | boolean | false | |
| displayRender | 选择后展示的渲染函数 | `(label, selectedOptions) => ReactNode` | `label => label.join(' / ')` | |
| expandTrigger | 次级菜单的展开方式,可选 'click' 和 'hover' | string | `click` | |
| expandIcon | 自定义次级菜单展开图标 | ReactNode | - | 4.4.0 |
| fieldNames | 自定义 options 中 label name children 的字段 | object | `{ label: 'label', value: 'value', children: 'children' }` | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | () => document.body | |
| loadData | 用于动态加载选项,无法与 `showSearch` 一起使用 | `(selectedOptions) => void` | - | |
@ -45,6 +46,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
| style | 自定义样式 | CSSProperties | - | |
| suffixIcon | 自定义的选择框后缀图标 | ReactNode | - | |
| value | 指定选中项 | string\[] \| number\[] | - | |
| dropdownRender | 自定义下拉框内容 | `(menus: ReactNode) => ReactNode` | - | 4.4.0 |
| onChange | 选择完成后的回调 | `(value, selectedOptions) => void` | - | |
| onPopupVisibleChange | 显示/隐藏浮层的回调 | `(value) => void` | - | |

View File

@ -23,6 +23,7 @@ export interface CollapseProps {
prefixCls?: string;
expandIcon?: (panelProps: PanelProps) => React.ReactNode;
expandIconPosition?: ExpandIconPosition;
ghost?: boolean;
}
interface PanelProps {
@ -42,7 +43,7 @@ interface CollapseInterface extends React.FC<CollapseProps> {
const Collapse: CollapseInterface = props => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const { prefixCls: customizePrefixCls, className = '', bordered } = props;
const { prefixCls: customizePrefixCls, className = '', bordered, ghost } = props;
const prefixCls = getPrefixCls('collapse', customizePrefixCls);
const getIconPosition = () => {
@ -72,6 +73,7 @@ const Collapse: CollapseInterface = props => {
[`${prefixCls}-borderless`]: !bordered,
[`${prefixCls}-icon-position-${iconPosition}`]: true,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-ghost`]: !!ghost,
},
className,
);

View File

@ -718,6 +718,125 @@ exports[`renders ./components/collapse/demo/extra.md correctly 1`] = `
</div>
`;
exports[`renders ./components/collapse/demo/ghost.md correctly 1`] = `
<div
class="ant-collapse ant-collapse-icon-position-left ant-collapse-ghost"
>
<div
class="ant-collapse-item ant-collapse-item-active"
>
<div
aria-expanded="true"
class="ant-collapse-header"
role="button"
tabindex="0"
>
<span
aria-label="right"
class="anticon anticon-right ant-collapse-arrow"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
style="-ms-transform:rotate(90deg);transform:rotate(90deg)"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
This is panel header 1
</div>
<div
class="ant-collapse-content ant-collapse-content-active"
>
<div
class="ant-collapse-content-box"
>
<p>
A dog is a type of domesticated animal.
Known for its loyalty and faithfulness,
it can be found as a welcome guest in many households across the world.
</p>
</div>
</div>
</div>
<div
class="ant-collapse-item"
>
<div
aria-expanded="false"
class="ant-collapse-header"
role="button"
tabindex="0"
>
<span
aria-label="right"
class="anticon anticon-right ant-collapse-arrow"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
This is panel header 2
</div>
</div>
<div
class="ant-collapse-item"
>
<div
aria-expanded="false"
class="ant-collapse-header"
role="button"
tabindex="0"
>
<span
aria-label="right"
class="anticon anticon-right ant-collapse-arrow"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
This is panel header 3
</div>
</div>
</div>
`;
exports[`renders ./components/collapse/demo/mix.md correctly 1`] = `
<div
class="ant-collapse ant-collapse-icon-position-left"

View File

@ -0,0 +1,41 @@
---
order: 6
title:
zh-CN: 幽灵折叠面板
en-US: Ghost Collapse
---
## zh-CN
将折叠面板的背景变成透明。
## en-US
Making collapse's background to transparent.
```jsx
import { Collapse } from 'antd';
const { Panel } = Collapse;
const text = `
A dog is a type of domesticated animal.
Known for its loyalty and faithfulness,
it can be found as a welcome guest in many households across the world.
`;
ReactDOM.render(
<Collapse defaultActiveKey={['1']} ghost>
<Panel header="This is panel header 1" key="1">
<p>{text}</p>
</Panel>
<Panel header="This is panel header 2" key="2">
<p>{text}</p>
</Panel>
<Panel header="This is panel header 3" key="3">
<p>{text}</p>
</Panel>
</Collapse>,
mountNode,
);
```

View File

@ -27,6 +27,7 @@ A content area which can be collapsed and expanded.
| expandIcon | allow to customize collapse icon | (panelProps) => ReactNode | - | |
| expandIconPosition | Set expand icon position | `left` \| `right` | - | |
| destroyInactivePanel | Destroy Inactive Panel | boolean | false | |
| ghost | make the collapse borderless and its background transparent | boolean | false | 4.4.0 |
### Collapse.Panel

View File

@ -28,6 +28,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/IxH16B9RD/Collapse.svg
| expandIcon | 自定义切换图标 | (panelProps) => ReactNode | - | |
| expandIconPosition | 设置图标位置 | `left` \| `right` | - | |
| destroyInactivePanel | 销毁折叠隐藏的面板 | boolean | false | |
| ghost | 使折叠面板透明且无边框 | boolean | false | 4.4.0 |
### Collapse.Panel

View File

@ -125,6 +125,22 @@
padding-top: 4px;
}
&-ghost {
background-color: transparent;
border: 0;
> .@{collapse-prefix-cls}-item {
border-bottom: 0;
> .@{collapse-prefix-cls}-content {
background-color: transparent;
border-top: 0;
> .@{collapse-prefix-cls}-content-box {
padding-top: 12px;
padding-bottom: 12px;
}
}
}
}
& &-item-disabled > &-header {
&,
& > .arrow {

View File

@ -24,7 +24,9 @@ exports[`MonthPicker and WeekPicker render MonthPicker 1`] = `
tabindex="-1"
type="button"
>
«
<span
class="ant-picker-super-prev-icon"
/>
</button>
<div
class="ant-picker-header-view"
@ -41,7 +43,9 @@ exports[`MonthPicker and WeekPicker render MonthPicker 1`] = `
tabindex="-1"
type="button"
>
»
<span
class="ant-picker-super-next-icon"
/>
</button>
</div>
<div

View File

@ -92,6 +92,7 @@ The following APIs are shared by DatePicker, YearPicker, MonthPicker, RangePicke
| onChange | a callback function, can be executed when the selected time is changing | function(date: moment, dateString: string) | - | |
| onOk | callback when click ok button | function() | - | |
| onPanelChange | Callback function for panel changing | function(value, mode) | - | |
| showNow | Whether to show 'Now' button on panel when `showTime` is set | boolean | - | 4.4.0 |
### YearPicker

View File

@ -94,6 +94,7 @@ import 'moment/locale/zh-cn';
| onChange | 时间发生变化的回调 | function(date: moment, dateString: string) | - | |
| onOk | 点击确定按钮的回调 | function() | - | |
| onPanelChange | 日期面板变化时的回调 | function(value, mode) | - | |
| showNow | 当设定了 `showTime` 的时候,面板是否显示“此刻”按钮 | boolean | - | 4.4.0 |
### YearPicker

View File

@ -125,4 +125,13 @@ describe('Drawer', () => {
);
expect(wrapper2.find('button.forceRender').length).toBe(1);
});
it('support closeIcon', () => {
const wrapper = render(
<Drawer visible closable closeIcon={<span>close</span>} width={400} getContainer={false}>
Here is content of Drawer
</Drawer>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -82,7 +82,7 @@ describe('Drawer', () => {
expect(wrapper.instance().state.visible).toBe(true);
});
it('destroyOnClose is true onClose', () => {
it('dom should be removed after close when destroyOnClose is true', () => {
const wrapper = mount(<DrawerEventTester destroyOnClose />);
wrapper.find('button.ant-btn').simulate('click');
expect(wrapper.find('.ant-drawer-wrapper-body').exists()).toBe(true);
@ -94,6 +94,18 @@ describe('Drawer', () => {
expect(wrapper.find('.ant-drawer-wrapper-body').exists()).toBe(false);
});
it('dom should be existed after close when destroyOnClose is false', () => {
const wrapper = mount(<DrawerEventTester />);
wrapper.find('button.ant-btn').simulate('click');
expect(wrapper.find('.ant-drawer-wrapper-body').exists()).toBe(true);
wrapper.setState({
visible: false,
});
wrapper.find('.ant-drawer-wrapper-body').simulate('transitionend');
expect(wrapper.find('.ant-drawer-wrapper-body').exists()).toBe(true);
});
it('no mask and no closable', () => {
const wrapper = mount(<DrawerEventTester destroyOnClose />);

View File

@ -558,3 +558,49 @@ exports[`Drawer style/drawerStyle/headerStyle/bodyStyle should work 1`] = `
</div>
</div>
`;
exports[`Drawer support closeIcon 1`] = `
<div
class=""
>
<div
class="ant-drawer ant-drawer-right"
tabindex="-1"
>
<div
class="ant-drawer-mask"
/>
<div
class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:400px"
>
<div
class="ant-drawer-content"
>
<div
class="ant-drawer-wrapper-body"
>
<div
class="ant-drawer-header-no-title"
>
<button
aria-label="Close"
class="ant-drawer-close"
style="--scroll-bar:0px"
>
<span>
close
</span>
</button>
</div>
<div
class="ant-drawer-body"
>
Here is content of Drawer
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -21,6 +21,7 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr
| Props | Description | Type | Default |
| --- | --- | --- | --- |
| closable | Whether a close (x) button is visible on top right of the Drawer dialog or not. | boolean | true |
| closeIcon | custom close icon | ReactNode | `<CloseOutlined />` |
| destroyOnClose | Whether to unmount child components on closing drawer or not. | boolean | false |
| forceRender | Prerender Drawer component forcely | boolean | false |
| getContainer | Return the mounted node for Drawer. | HTMLElement \| `() => HTMLElement` \| Selectors \| false | 'body' |

View File

@ -21,6 +21,7 @@ const PlacementTypes = tuple('top', 'right', 'bottom', 'left');
type placementType = typeof PlacementTypes[number];
export interface DrawerProps {
closable?: boolean;
closeIcon?: React.ReactNode;
destroyOnClose?: boolean;
forceRender?: boolean;
getContainer?: string | HTMLElement | getContainerFunc | false;
@ -195,7 +196,7 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
}
renderCloseIcon() {
const { closable, prefixCls, onClose } = this.props;
const { closable, closeIcon = <CloseOutlined />, prefixCls, onClose } = this.props;
return (
closable && (
// eslint-disable-next-line react/button-has-type
@ -209,7 +210,7 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
} as any
}
>
<CloseOutlined />
{closeIcon}
</button>
)
);
@ -283,6 +284,7 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
'zIndex',
'style',
'closable',
'closeIcon',
'destroyOnClose',
'drawerStyle',
'headerStyle',

View File

@ -20,6 +20,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| closable | 是否显示右上角的关闭按钮 | boolean | true |
| closeIcon | 自定义关闭图标 | ReactNode | `<CloseOutlined />` |
| destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false |
| forceRender | 预渲染 Drawer 内元素 | boolean | false |
| getContainer | 指定 Drawer 挂载的 HTML 节点, false 为挂载在当前 dom | HTMLElement \| `() => HTMLElement` \| Selectors \| false | 'body' |

View File

@ -1,5 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/dropdown/demo/arrow.md correctly 1`] = `
Array [
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
bottomLeft
</span>
</button>,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
bottomCenter
</span>
</button>,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
bottomRight
</span>
</button>,
<br />,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
topLeft
</span>
</button>,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
topCenter
</span>
</button>,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
topRight
</span>
</button>,
]
`;
exports[`renders ./components/dropdown/demo/basic.md correctly 1`] = `
<a
class="ant-dropdown-link ant-dropdown-trigger"

View File

@ -0,0 +1,75 @@
---
order: 2
title:
zh-CN: 箭头
en-US: Arrow
---
## zh-CN
可以展示一个箭头。
## en-US
You could display an arrow.
```jsx
import { Menu, Dropdown, Button } from 'antd';
const menu = (
<Menu>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="http://www.alipay.com/">
1st menu item
</a>
</Menu.Item>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="http://www.taobao.com/">
2nd menu item
</a>
</Menu.Item>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="http://www.tmall.com/">
3rd menu item
</a>
</Menu.Item>
</Menu>
);
ReactDOM.render(
<>
<Dropdown overlay={menu} placement="bottomLeft" arrow>
<Button>bottomLeft</Button>
</Dropdown>
<Dropdown overlay={menu} placement="bottomCenter" arrow>
<Button>bottomCenter</Button>
</Dropdown>
<Dropdown overlay={menu} placement="bottomRight" arrow>
<Button>bottomRight</Button>
</Dropdown>
<br />
<Dropdown overlay={menu} placement="topLeft" arrow>
<Button>topLeft</Button>
</Dropdown>
<Dropdown overlay={menu} placement="topCenter" arrow>
<Button>topCenter</Button>
</Dropdown>
<Dropdown overlay={menu} placement="topRight" arrow>
<Button>topRight</Button>
</Dropdown>
</>,
mountNode,
);
```
```css
#components-dropdown-demo-arrow .ant-btn {
margin-right: 8px;
margin-bottom: 8px;
}
.ant-row-rtl #components-dropdown-demo-arrow .ant-btn {
margin-right: 0;
margin-bottom: 8px;
margin-left: 8px;
}
```

View File

@ -35,6 +35,7 @@ type Align = {
};
export interface DropDownProps {
arrow?: boolean;
trigger?: ('click' | 'hover' | 'contextMenu')[];
overlay: React.ReactElement | OverlayFunc;
onVisibleChange?: (visible: boolean) => void;
@ -130,6 +131,7 @@ const Dropdown: DropdownInterface = props => {
};
const {
arrow,
prefixCls: customizePrefixCls,
children,
trigger,
@ -160,6 +162,7 @@ const Dropdown: DropdownInterface = props => {
return (
<RcDropdown
arrow={arrow}
alignPoint={alignPoint}
{...props}
overlayClassName={overlayClassNameCustomized}

View File

@ -17,7 +17,8 @@ When there are more than a few options to choose from, you can wrap them in a `D
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| disabled | Whether the dropdown menu is disabled | boolean | false | |
| arrow | Whether the dropdown arrow should be visible | boolean | false | |
| disabled | Whether the dropdown menu is disabled | boolean | - | |
| getPopupContainer | To set the container of the dropdown menu. The default is to create a `div` element in `body`, but you can reset it to the scrolling area and make a relative reposition. [Example on CodePen](https://codepen.io/afc163/pen/zEjNOy?editors=0010). | Function(triggerNode) | `() => document.body` | |
| overlay | The dropdown menu | [Menu](/components/menu) \| () => Menu | - | |
| overlayClassName | Class name of the dropdown root element | string | - | |

View File

@ -21,7 +21,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| disabled | 菜单是否禁用 | boolean | false | |
| arrow | 下拉框箭头是否显示 | boolean | false | |
| disabled | 菜单是否禁用 | boolean | - | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | `() => document.body` | |
| overlay | 菜单 | [Menu](/components/menu) \| () => Menu | - | |
| overlayClassName | 下拉根元素的类名称 | string | - | |

View File

@ -47,6 +47,76 @@
display: none;
}
// Offset the popover to account for the dropdown arrow
&-show-arrow&-placement-topCenter,
&-show-arrow&-placement-topLeft,
&-show-arrow&-placement-topRight {
padding-bottom: @popover-distance;
}
&-show-arrow&-placement-bottomCenter,
&-show-arrow&-placement-bottomLeft,
&-show-arrow&-placement-bottomRight {
padding-top: @popover-distance;
}
// Arrows
// .popover-arrow is outer, .popover-arrow:after is inner
&-arrow {
position: absolute;
z-index: 1; // lift it up so the menu wouldn't cask shadow on it
display: block;
width: sqrt(@popover-arrow-width * @popover-arrow-width * 2);
height: sqrt(@popover-arrow-width * @popover-arrow-width * 2);
background: transparent;
border-style: solid;
border-width: sqrt(@popover-arrow-width * @popover-arrow-width * 2) / 2;
transform: rotate(45deg);
}
&-placement-topCenter > &-arrow,
&-placement-topLeft > &-arrow,
&-placement-topRight > &-arrow {
bottom: @popover-distance - @popover-arrow-width + 2.2px;
border-top-color: transparent;
border-right-color: @popover-bg;
border-bottom-color: @popover-bg;
border-left-color: transparent;
box-shadow: 3px 3px 7px fade(@black, 7%);
}
&-placement-topCenter > &-arrow {
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
&-placement-topLeft > &-arrow {
left: 16px;
}
&-placement-topRight > &-arrow {
right: 16px;
}
&-placement-bottomCenter > &-arrow,
&-placement-bottomLeft > &-arrow,
&-placement-bottomRight > &-arrow {
top: @popover-distance - @popover-arrow-width + 2px;
border-top-color: @popover-bg;
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: @popover-bg;
box-shadow: -2px -2px 5px fade(@black, 6%);
}
&-placement-bottomCenter > &-arrow {
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
&-placement-bottomLeft > &-arrow {
left: 16px;
}
&-placement-bottomRight > &-arrow {
right: 16px;
}
&-menu {
position: relative;
margin: 0;

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import omit from 'omit.js';
import classNames from 'classnames';
import FieldForm, { List } from 'rc-field-form';
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
@ -8,7 +7,7 @@ import { ColProps } from '../grid/col';
import { ConfigContext, ConfigConsumerProps } from '../config-provider';
import { FormContext } from './context';
import { FormLabelAlign } from './interface';
import { useForm, FormInstance } from './util';
import useForm, { FormInstance } from './hooks/useForm';
import SizeContext, { SizeType, SizeContextProvider } from '../config-provider/SizeContext';
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
@ -31,21 +30,24 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
const contextSize = React.useContext(SizeContext);
const { getPrefixCls, direction }: ConfigConsumerProps = React.useContext(ConfigContext);
const { name } = props;
const {
prefixCls: customizePrefixCls,
className = '',
size = contextSize,
form,
colon,
name,
labelAlign,
labelCol,
wrapperCol,
prefixCls: customizePrefixCls,
hideRequiredMark,
className = '',
layout = 'horizontal',
size = contextSize,
scrollToFirstError,
onFinishFailed,
...restFormProps
} = props;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const formClassName = classNames(
@ -59,20 +61,9 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
className,
);
const formProps = omit(props, [
'prefixCls',
'className',
'layout',
'hideRequiredMark',
'wrapperCol',
'labelAlign',
'labelCol',
'colon',
'scrollToFirstError',
]);
const [wrapForm] = useForm(form);
wrapForm.__INTERNAL__.name = name;
const { __INTERNAL__ } = wrapForm;
__INTERNAL__.name = name;
const formContextValue = React.useMemo(
() => ({
@ -82,6 +73,7 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
wrapperCol,
vertical: layout === 'vertical',
colon,
itemRef: __INTERNAL__.itemRef,
}),
[name, labelAlign, labelCol, wrapperCol, layout, colon],
);
@ -100,12 +92,10 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
return (
<SizeContextProvider size={size}>
<FormContext.Provider
value={formContextValue}
>
<FormContext.Provider value={formContextValue}>
<FieldForm
id={name}
{...formProps}
{...restFormProps}
onFinishFailed={onInternalFinishFailed}
form={wrapForm}
className={formClassName}

View File

@ -5,6 +5,7 @@ import { Field, FormInstance } from 'rc-field-form';
import { FieldProps } from 'rc-field-form/lib/Field';
import FieldContext from 'rc-field-form/lib/FieldContext';
import { Meta, NamePath } from 'rc-field-form/lib/interface';
import { supportRef } from 'rc-util/lib/ref';
import omit from 'omit.js';
import Row from '../grid/row';
import { ConfigContext } from '../config-provider';
@ -13,8 +14,10 @@ import devWarning from '../_util/devWarning';
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput';
import { FormContext, FormItemContext } from './context';
import { toArray, getFieldId, useFrameState } from './util';
import { toArray, getFieldId } from './util';
import { cloneElement, isValidElement } from '../_util/reactNode';
import useFrameState from './hooks/useFrameState';
import useItemRef from './hooks/useItemRef';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type ValidateStatus = typeof ValidateStatuses[number];
@ -81,7 +84,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
} = props;
const destroyRef = React.useRef(false);
const { getPrefixCls } = React.useContext(ConfigContext);
const formContext = React.useContext(FormContext);
const { name: formName } = React.useContext(FormContext);
const { updateItemErrors } = React.useContext(FormItemContext);
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
const prevValidateStatusRef = React.useRef<ValidateStatus | undefined>(validateStatus);
@ -97,7 +100,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
}
}
const { name: formName } = formContext;
const hasName = hasValidName(name);
// Cache Field NamePath
@ -126,6 +128,9 @@ function FormItem(props: FormItemProps): React.ReactElement {
}
};
// ===================== Children Ref =====================
const getItemRef = useItemRef();
function renderLayout(
baseChildren: React.ReactNode,
fieldId?: string,
@ -323,6 +328,10 @@ function FormItem(props: FormItemProps): React.ReactElement {
childProps.id = fieldId;
}
if (supportRef(children)) {
childProps.ref = getItemRef(mergedName, children);
}
// We should keep user origin event handler
const triggers = new Set<string>([
...toArray(trigger),

View File

@ -10,7 +10,7 @@ import CSSMotion from 'rc-animate/lib/CSSMotion';
import Col, { ColProps } from '../grid/col';
import { ValidateStatus } from './FormItem';
import { FormContext } from './context';
import { useCacheErrors } from './util';
import useCacheErrors from './hooks/useCacheErrors';
interface FormItemInputMiscProps {
prefixCls: string;

View File

@ -2196,6 +2196,84 @@ exports[`renders ./components/form/demo/normal-login.md correctly 1`] = `
</form>
`;
exports[`renders ./components/form/demo/ref-item.md correctly 1`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
for="test"
title="test"
>
test
</label>
</div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="test"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="list_0"
type="text"
value="light"
/>
</div>
</div>
</div>
</div>
<button
class="ant-btn ant-btn-button"
type="button"
>
<span>
Focus Form.Item
</span>
</button>
<button
class="ant-btn"
type="button"
>
<span>
Focus Form.List
</span>
</button>
</form>
`;
exports[`renders ./components/form/demo/register.md correctly 1`] = `
<form
class="ant-form ant-form-horizontal"

View File

@ -0,0 +1,91 @@
/* eslint-disable react/jsx-key */
import React from 'react';
import { mount } from 'enzyme';
import Form from '..';
import Input from '../../input';
import Button from '../../button';
describe('Form.Ref', () => {
const Test = ({
onRef,
show,
}: {
onRef: (node: React.ReactElement, originRef: React.RefObject<any>) => void;
show?: boolean;
}) => {
const [form] = Form.useForm();
const removeRef = React.useRef<any>();
const testRef = React.useRef<any>();
const listRef = React.useRef<any>();
return (
<Form form={form} initialValues={{ list: ['light'] }}>
{show && (
<Form.Item name="remove" label="remove">
<Input ref={removeRef} />
</Form.Item>
)}
<Form.Item name="test" label="test">
<Input ref={testRef} />
</Form.Item>
<Form.List name="list">
{fields =>
fields.map(field => (
<Form.Item {...field}>
<Input ref={listRef} />
</Form.Item>
))
}
</Form.List>
<Button
className="ref-item"
onClick={() => {
onRef(form.getFieldInstance('test'), testRef.current);
}}
>
Form.Item
</Button>
<Button
className="ref-list"
onClick={() => {
onRef(form.getFieldInstance(['list', 0]), listRef.current);
}}
>
Form.List
</Button>
<Button
className="ref-remove"
onClick={() => {
onRef(form.getFieldInstance('remove'), removeRef.current);
}}
>
Removed
</Button>
</Form>
);
};
it('should ref work', () => {
const onRef = jest.fn();
const wrapper = mount(<Test onRef={onRef} show />);
wrapper.find('.ref-item').last().simulate('click');
expect(onRef).toHaveBeenCalled();
expect(onRef.mock.calls[0][0]).toBe(onRef.mock.calls[0][1]);
onRef.mockReset();
wrapper.find('.ref-list').last().simulate('click');
expect(onRef).toHaveBeenCalled();
expect(onRef.mock.calls[0][0]).toBe(onRef.mock.calls[0][1]);
onRef.mockReset();
wrapper.setProps({ show: false });
wrapper.update();
wrapper.find('.ref-remove').last().simulate('click');
expect(onRef).toHaveBeenCalledWith(undefined, null);
});
});

View File

@ -16,11 +16,13 @@ export interface FormContextProps {
labelAlign?: FormLabelAlign;
labelCol?: ColProps;
wrapperCol?: ColProps;
itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
}
export const FormContext = React.createContext<FormContextProps>({
labelAlign: 'right',
vertical: false,
itemRef: (() => {}) as any,
});
/**

View File

@ -0,0 +1,61 @@
---
order: 999999
title:
zh-CN: 引用字段
en-US: Ref item
debug: true
---
## zh-CN
请优先使用 `ref`
## en-US
Use `ref` first!
```jsx
import React from 'react';
import { Button, Form, Input } from 'antd';
const Demo = () => {
const [form] = Form.useForm();
const ref = React.useRef();
return (
<Form form={form} initialValues={{ list: ['light'] }}>
<Form.Item name="test" label="test">
<Input ref={ref} />
</Form.Item>
<Form.List name="list">
{fields =>
fields.map(field => (
<Form.Item key={field.key} {...field}>
<Input ref={ref} />
</Form.Item>
))
}
</Form.List>
<Button
type="button"
onClick={() => {
form.getFieldInstance('test').focus();
}}
>
Focus Form.Item
</Button>
<Button
onClick={() => {
form.getFieldInstance(['list', 0]).focus();
}}
>
Focus Form.List
</Button>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -0,0 +1,48 @@
import * as React from 'react';
/**
* Always debounce error to avoid [error -> null -> error] blink
*/
export default function useCacheErrors(
errors: React.ReactNode[],
changeTrigger: (visible: boolean) => void,
directly: boolean,
): [boolean, React.ReactNode[]] {
const cacheRef = React.useRef({
errors,
visible: !!errors.length,
});
const [, forceUpdate] = React.useState({});
const update = () => {
const prevVisible = cacheRef.current.visible;
const newVisible = !!errors.length;
const prevErrors = cacheRef.current.errors;
cacheRef.current.errors = errors;
cacheRef.current.visible = newVisible;
if (prevVisible !== newVisible) {
changeTrigger(newVisible);
} else if (
prevErrors.length !== errors.length ||
prevErrors.some((prevErr, index) => prevErr !== errors[index])
) {
forceUpdate({});
}
};
React.useEffect(() => {
if (!directly) {
const timeout = setTimeout(update, 10);
return () => clearTimeout(timeout);
}
}, [errors]);
if (directly) {
update();
}
return [cacheRef.current.visible, cacheRef.current.errors];
}

View File

@ -0,0 +1,64 @@
import { useRef, useMemo } from 'react';
import { useForm as useRcForm, FormInstance as RcFormInstance } from 'rc-field-form';
import scrollIntoView from 'scroll-into-view-if-needed';
import { ScrollOptions, NamePath, InternalNamePath } from '../interface';
import { toArray, getFieldId } from '../util';
export interface FormInstance extends RcFormInstance {
scrollToField: (name: NamePath, options?: ScrollOptions) => void;
/** This is an internal usage. Do not use in your prod */
__INTERNAL__: {
/** No! Do not use this in your code! */
name?: string;
/** No! Do not use this in your code! */
itemRef: (name: InternalNamePath) => (node: React.ReactElement) => void;
};
getFieldInstance: (name: NamePath) => any;
}
function toNamePathStr(name: NamePath) {
const namePath = toArray(name);
return namePath.join('_');
}
export default function useForm(form?: FormInstance): [FormInstance] {
const [rcForm] = useRcForm();
const itemsRef = useRef<Record<string, React.ReactElement>>({});
const wrapForm: FormInstance = useMemo(
() =>
form || {
...rcForm,
__INTERNAL__: {
itemRef: (name: InternalNamePath) => (node: React.ReactElement) => {
const namePathStr = toNamePathStr(name);
if (node) {
itemsRef.current[namePathStr] = node;
} else {
delete itemsRef.current[namePathStr];
}
},
},
scrollToField: (name: string, options: ScrollOptions = {}) => {
const namePath = toArray(name);
const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
const node: HTMLElement | null = fieldId ? document.getElementById(fieldId) : null;
if (node) {
scrollIntoView(node, {
scrollMode: 'if-needed',
block: 'nearest',
...options,
});
}
},
getFieldInstance: (name: string) => {
const namePathStr = toNamePathStr(name);
return itemsRef.current[namePathStr];
},
},
[form, rcForm],
);
return [wrapForm];
}

View File

@ -0,0 +1,48 @@
import * as React from 'react';
import { useRef } from 'react';
import raf from 'raf';
type Updater<ValueType> = (prev?: ValueType) => ValueType;
export default function useFrameState<ValueType>(
defaultValue: ValueType,
): [ValueType, (updater: Updater<ValueType>) => void] {
const [value, setValue] = React.useState(defaultValue);
const frameRef = useRef<number | null>(null);
const batchRef = useRef<Updater<ValueType>[]>([]);
const destroyRef = useRef(false);
React.useEffect(
() => () => {
destroyRef.current = true;
raf.cancel(frameRef.current!);
},
[],
);
function setFrameValue(updater: Updater<ValueType>) {
if (destroyRef.current) {
return;
}
if (frameRef.current === null) {
batchRef.current = [];
frameRef.current = raf(() => {
frameRef.current = null;
setValue(prevValue => {
let current = prevValue;
batchRef.current.forEach(func => {
current = func(current);
});
return current;
});
});
}
batchRef.current.push(updater);
}
return [value, setFrameValue];
}

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import { composeRef } from 'rc-util/lib/ref';
import { FormContext } from '../context';
import { InternalNamePath } from '../interface';
export default function useItemRef() {
const { itemRef } = React.useContext(FormContext);
const cacheRef = React.useRef<{
name?: string;
originRef?: React.Ref<any>;
ref?: React.Ref<any>;
}>({});
function getRef(name: InternalNamePath, children: any) {
const childrenRef: React.Ref<React.ReactElement> =
children && typeof children === 'object' && children.ref;
const nameStr = name.join('_');
if (cacheRef.current.name !== nameStr || cacheRef.current.originRef !== childrenRef) {
cacheRef.current.name = nameStr;
cacheRef.current.originRef = childrenRef;
cacheRef.current.ref = composeRef(itemRef(name), childrenRef);
}
return cacheRef.current.ref;
}
return getRef;
}

View File

@ -185,21 +185,22 @@ Provide linkage between forms. If a sub form with `name` prop update, it will au
### FormInstance
| Name | Description | Type |
| --- | --- | --- |
| getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any |
| getFieldsValue | Get values by a set of field names. Return according to the corresponding structure | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any |
| getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] |
| getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] |
| isFieldTouched | Check if a field has been operated | (name: [NamePath](#NamePath)) => boolean |
| isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
| isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean |
| resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
| scrollToField | Scroll to field position | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void |
| setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void |
| setFieldsValue | Set fields value | (values) => void |
| submit | Submit the form. It's same as click `submit` button | () => void |
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise |
| Name | Description | Type | Version |
| --- | --- | --- | --- |
| getFieldInstance | Get field instance | (name: [NamePath](#NamePath)) => any | 4.4.0 |
| getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any | |
| getFieldsValue | Get values by a set of field names. Return according to the corresponding structure | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | |
| getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] | |
| getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | |
| isFieldTouched | Check if a field has been operated | (name: [NamePath](#NamePath)) => boolean | |
| isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | |
| isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean | |
| resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | |
| scrollToField | Scroll to field position | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | |
| setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void | |
| setFieldsValue | Set fields value | (values) => void | |
| submit | Submit the form. It's same as click `submit` button | () => void | |
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise | |
#### validateFields return sample

View File

@ -186,21 +186,22 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
### FormInstance
| 名称 | 说明 | 类型 |
| --- | --- | --- |
| getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any |
| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any |
| getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] |
| getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] |
| isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean |
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean |
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void |
| setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void |
| setFieldsValue | 设置表单的值 | (values) => void |
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void |
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise |
| 名称 | 说明 | 类型 | 版本 |
| --- | --- | --- | --- |
| getFieldInstance | 获取对应字段示例 | (name: [NamePath](#NamePath)) => any | 4.4.0 |
| getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any | |
| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | |
| getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] | |
| getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | |
| isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean | |
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | |
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean | |
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | |
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | |
| setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void | |
| setFieldsValue | 设置表单的值 | (values) => void | |
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void | |
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise | |
#### validateFields 返回示例

View File

@ -1,3 +1,3 @@
export { Options as ScrollOptions } from 'scroll-into-view-if-needed';
export type FormLabelAlign = 'left' | 'right';
export { Store, StoreValue } from 'rc-field-form/lib/interface';
export { Store, StoreValue, NamePath, InternalNamePath } from 'rc-field-form/lib/interface';

View File

@ -1,61 +1,4 @@
import * as React from 'react';
import raf from 'raf';
import { useForm as useRcForm, FormInstance as RcFormInstance } from 'rc-field-form';
import scrollIntoView from 'scroll-into-view-if-needed';
import { ScrollOptions } from './interface';
type InternalNamePath = (string | number)[];
/**
* Always debounce error to avoid [error -> null -> error] blink
*/
export function useCacheErrors(
errors: React.ReactNode[],
changeTrigger: (visible: boolean) => void,
directly: boolean,
): [boolean, React.ReactNode[]] {
const cacheRef = React.useRef({
errors,
visible: !!errors.length,
});
const [, forceUpdate] = React.useState({});
const update = (newErrors: React.ReactNode[]) => {
const prevVisible = cacheRef.current.visible;
const newVisible = !!newErrors.length;
const prevErrors = cacheRef.current.errors;
cacheRef.current.errors = newErrors;
cacheRef.current.visible = newVisible;
if (prevVisible !== newVisible) {
changeTrigger(newVisible);
} else if (
prevErrors.length !== newErrors.length ||
prevErrors.some((prevErr, index) => prevErr !== newErrors[index])
) {
forceUpdate({});
}
};
React.useEffect(() => {
if (!directly) {
const timeout = setTimeout(() => {
update(errors);
}, 10);
return () => {
clearTimeout(timeout);
};
}
}, [errors]);
if (directly) {
update(errors);
}
return [cacheRef.current.visible, cacheRef.current.errors];
}
import { InternalNamePath } from './interface';
export function toArray<T>(candidate?: T | T[] | false): T[] {
if (candidate === undefined || candidate === false) return [];
@ -69,83 +12,3 @@ export function getFieldId(namePath: InternalNamePath, formName?: string): strin
const mergedId = namePath.join('_');
return formName ? `${formName}_${mergedId}` : mergedId;
}
export interface FormInstance extends RcFormInstance {
scrollToField: (name: string | number | InternalNamePath, options?: ScrollOptions) => void;
__INTERNAL__: {
name?: string;
};
}
export function useForm(form?: FormInstance): [FormInstance] {
const [rcForm] = useRcForm();
const wrapForm: FormInstance = React.useMemo(
() =>
form || {
...rcForm,
__INTERNAL__: {},
scrollToField: (name: string, options: ScrollOptions = {}) => {
const namePath = toArray(name);
const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
const node: HTMLElement | null = fieldId ? document.getElementById(fieldId) : null;
if (node) {
scrollIntoView(node, {
scrollMode: 'if-needed',
block: 'nearest',
...options,
});
}
},
},
[form, rcForm],
);
return [wrapForm];
}
type Updater<ValueType> = (prev?: ValueType) => ValueType;
export function useFrameState<ValueType>(
defaultValue: ValueType,
): [ValueType, (updater: Updater<ValueType>) => void] {
const [value, setValue] = React.useState(defaultValue);
const frameRef = React.useRef<number | null>(null);
const batchRef = React.useRef<Updater<ValueType>[]>([]);
const destroyRef = React.useRef(false);
React.useEffect(
() => () => {
destroyRef.current = true;
raf.cancel(frameRef.current!);
},
[],
);
function setFrameValue(updater: Updater<ValueType>) {
if (destroyRef.current) {
return;
}
if (frameRef.current === null) {
batchRef.current = [];
frameRef.current = raf(() => {
frameRef.current = null;
setValue(prevValue => {
let current = prevValue;
batchRef.current.forEach(func => {
current = func(current);
});
return current;
});
});
}
batchRef.current.push(updater);
}
return [value, setFrameValue];
}

View File

@ -1,158 +0,0 @@
import * as React from 'react';
import ResizeObserver from 'rc-resize-observer';
import omit from 'omit.js';
import classNames from 'classnames';
import calculateNodeHeight from './calculateNodeHeight';
import raf from '../_util/raf';
import { TextAreaProps } from './TextArea';
const RESIZE_STATUS_NONE = 0;
const RESIZE_STATUS_RESIZING = 1;
const RESIZE_STATUS_RESIZED = 2;
export interface AutoSizeType {
minRows?: number;
maxRows?: number;
}
export interface TextAreaState {
textareaStyles?: React.CSSProperties;
/** We need add process style to disable scroll first and then add back to avoid unexpected scrollbar */
resizeStatus?:
| typeof RESIZE_STATUS_NONE
| typeof RESIZE_STATUS_RESIZING
| typeof RESIZE_STATUS_RESIZED;
}
class ResizableTextArea extends React.Component<TextAreaProps, TextAreaState> {
nextFrameActionId: number;
resizeFrameId: number;
constructor(props: TextAreaProps) {
super(props);
this.state = {
textareaStyles: {},
resizeStatus: RESIZE_STATUS_NONE,
};
}
textArea: HTMLTextAreaElement;
saveTextArea = (textArea: HTMLTextAreaElement) => {
this.textArea = textArea;
};
componentDidMount() {
this.resizeTextarea();
}
componentDidUpdate(prevProps: TextAreaProps) {
// Re-render with the new content then recalculate the height as required.
if (prevProps.value !== this.props.value) {
this.resizeTextarea();
}
}
handleResize = (size: { width: number; height: number }) => {
const { resizeStatus } = this.state;
const { autoSize, onResize } = this.props;
if (resizeStatus !== RESIZE_STATUS_NONE) {
return;
}
if (typeof onResize === 'function') {
onResize(size);
}
if (autoSize) {
this.resizeOnNextFrame();
}
};
resizeOnNextFrame = () => {
raf.cancel(this.nextFrameActionId);
this.nextFrameActionId = raf(this.resizeTextarea);
};
resizeTextarea = () => {
const { autoSize } = this.props;
if (!autoSize || !this.textArea) {
return;
}
const { minRows, maxRows } = autoSize as AutoSizeType;
const textareaStyles = calculateNodeHeight(this.textArea, false, minRows, maxRows);
this.setState({ textareaStyles, resizeStatus: RESIZE_STATUS_RESIZING }, () => {
raf.cancel(this.resizeFrameId);
this.resizeFrameId = raf(() => {
this.setState({ resizeStatus: RESIZE_STATUS_RESIZED }, () => {
this.resizeFrameId = raf(() => {
this.setState({ resizeStatus: RESIZE_STATUS_NONE });
this.fixFirefoxAutoScroll();
});
});
});
});
};
componentWillUnmount() {
raf.cancel(this.nextFrameActionId);
raf.cancel(this.resizeFrameId);
}
// https://github.com/ant-design/ant-design/issues/21870
fixFirefoxAutoScroll() {
try {
if (document.activeElement === this.textArea) {
const currentStart = this.textArea.selectionStart;
const currentEnd = this.textArea.selectionEnd;
this.textArea.setSelectionRange(currentStart, currentEnd);
}
} catch (e) {
// Fix error in Chrome:
// Failed to read the 'selectionStart' property from 'HTMLInputElement'
// http://stackoverflow.com/q/21177489/3040605
}
}
renderTextArea = () => {
const { prefixCls, autoSize, onResize, className, disabled } = this.props;
const { textareaStyles, resizeStatus } = this.state;
const otherProps = omit(this.props, [
'prefixCls',
'onPressEnter',
'autoSize',
'defaultValue',
'allowClear',
'onResize',
]);
const cls = classNames(prefixCls, className, {
[`${prefixCls}-disabled`]: disabled,
});
// Fix https://github.com/ant-design/ant-design/issues/6776
// Make sure it could be reset when using form.getFieldDecorator
if ('value' in otherProps) {
otherProps.value = otherProps.value || '';
}
const style = {
...this.props.style,
...textareaStyles,
...(resizeStatus === RESIZE_STATUS_RESIZING
? // React will warning when mix `overflow` & `overflowY`.
// We need to define this separately.
{ overflowX: 'hidden', overflowY: 'hidden' }
: null),
};
return (
<ResizeObserver onResize={this.handleResize} disabled={!(autoSize || onResize)}>
<textarea {...otherProps} className={cls} style={style} ref={this.saveTextArea} />
</ResizeObserver>
);
};
render() {
return this.renderTextArea();
}
}
export default ResizableTextArea;

View File

@ -1,17 +1,12 @@
import * as React from 'react';
import RcTextArea, { TextAreaProps as RcTextAreaProps, ResizableTextArea } from 'rc-textarea';
import omit from 'omit.js';
import ClearableLabeledInput from './ClearableLabeledInput';
import ResizableTextArea, { AutoSizeType } from './ResizableTextArea';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { fixControlledValue, resolveOnChange } from './Input';
export type HTMLTextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
export interface TextAreaProps extends HTMLTextareaProps {
prefixCls?: string;
autoSize?: boolean | AutoSizeType;
onPressEnter?: React.KeyboardEventHandler<HTMLTextAreaElement>;
export interface TextAreaProps extends RcTextAreaProps {
allowClear?: boolean;
onResize?: (size: { width: number; height: number }) => void;
}
export interface TextAreaState {
@ -54,8 +49,8 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
this.resizableTextArea.textArea.blur();
}
saveTextArea = (resizableTextArea: ResizableTextArea) => {
this.resizableTextArea = resizableTextArea;
saveTextArea = (textarea: RcTextArea) => {
this.resizableTextArea = textarea?.resizableTextArea;
};
saveClearableInput = (clearableInput: ClearableLabeledInput) => {
@ -63,25 +58,12 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
};
handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this.setValue(e.target.value, () => {
this.resizableTextArea.resizeTextarea();
});
this.setValue(e.target.value);
resolveOnChange(this.resizableTextArea.textArea, e, this.props.onChange);
};
handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const { onPressEnter, onKeyDown } = this.props;
if (e.keyCode === 13 && onPressEnter) {
onPressEnter(e);
}
if (onKeyDown) {
onKeyDown(e);
}
};
handleReset = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
this.setValue('', () => {
this.resizableTextArea.renderTextArea();
this.focus();
});
resolveOnChange(this.resizableTextArea.textArea, e, this.props.onChange);
@ -89,10 +71,9 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
renderTextArea = (prefixCls: string) => {
return (
<ResizableTextArea
{...this.props}
<RcTextArea
{...omit(this.props, ['allowClear'])}
prefixCls={prefixCls}
onKeyDown={this.handleKeyDown}
onChange={this.handleChange}
ref={this.saveTextArea}
/>

View File

@ -252,6 +252,7 @@ Array [
style="width:100px"
>
<textarea
class="rc-textarea"
rows="1"
/>
</div>,

View File

@ -1,9 +1,8 @@
import React from 'react';
import { mount } from 'enzyme';
// eslint-disable-next-line import/no-unresolved
import RcTextArea from 'rc-textarea';
import Input from '..';
import focusTest from '../../../tests/shared/focusTest';
import calculateNodeHeight, { calculateNodeStyling } from '../calculateNodeHeight';
import { sleep } from '../../../tests/utils';
const { TextArea } = Input;
@ -78,56 +77,14 @@ describe('TextArea', () => {
expect(wrapper.render()).toMatchSnapshot();
});
it('calculateNodeStyling works correctly', () => {
const wrapper = document.createElement('textarea');
wrapper.id = 'test';
wrapper.wrap = 'wrap';
calculateNodeStyling(wrapper, true);
const value = calculateNodeStyling(wrapper, true);
expect(value).toEqual({
borderSize: 2,
boxSizing: 'border-box',
paddingSize: 4,
sizingStyle:
'letter-spacing:normal;line-height:normal;padding-top:2px;padding-bottom:2px;font-family:-webkit-small-control;font-weight:;font-size:;font-variant:;text-rendering:auto;text-transform:none;width:;text-indent:0;padding-left:2px;padding-right:2px;border-width:1px;box-sizing:border-box',
});
});
it('boxSizing === "border-box"', () => {
const wrapper = document.createElement('textarea');
wrapper.style.boxSizing = 'border-box';
const { height } = calculateNodeHeight(wrapper);
expect(height).toBe(2);
});
it('boxSizing === "content-box"', () => {
const wrapper = document.createElement('textarea');
wrapper.style.boxSizing = 'content-box';
const { height } = calculateNodeHeight(wrapper);
expect(height).toBe(-4);
});
it('minRows or maxRows is not null', () => {
const wrapper = document.createElement('textarea');
expect(calculateNodeHeight(wrapper, 1, 1)).toEqual({
height: 2,
maxHeight: 9007199254740991,
minHeight: 2,
overflowY: undefined,
});
wrapper.style.boxSizing = 'content-box';
expect(calculateNodeHeight(wrapper, 1, 1)).toEqual({
height: -4,
maxHeight: 9007199254740991,
minHeight: -4,
overflowY: undefined,
});
});
it('when prop value not in this.props, resizeTextarea should be called', () => {
it('when prop value not in this.props, resizeTextarea should be called', async () => {
const wrapper = mount(<TextArea aria-label="textarea" />);
const resizeTextarea = jest.spyOn(wrapper.instance().resizableTextArea, 'resizeTextarea');
wrapper.find('textarea').simulate('change', 'test');
wrapper.find('textarea').simulate('change', {
target: {
value: 'test',
},
});
expect(resizeTextarea).toHaveBeenCalled();
});
@ -137,7 +94,7 @@ describe('TextArea', () => {
const wrapper = mount(
<TextArea onPressEnter={onPressEnter} onKeyDown={onKeyDown} aria-label="textarea" />,
);
wrapper.instance().handleKeyDown({ keyCode: 13 });
wrapper.find(RcTextArea).instance().handleKeyDown({ keyCode: 13 });
expect(onPressEnter).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
});

View File

@ -1,156 +0,0 @@
// Thanks to https://github.com/andreypopp/react-textarea-autosize/
/**
* calculateNodeHeight(uiTextNode, useCache = false)
*/
const HIDDEN_TEXTAREA_STYLE = `
min-height:0 !important;
max-height:none !important;
height:0 !important;
visibility:hidden !important;
overflow:hidden !important;
position:absolute !important;
z-index:-1000 !important;
top:0 !important;
right:0 !important
`;
const SIZING_STYLE = [
'letter-spacing',
'line-height',
'padding-top',
'padding-bottom',
'font-family',
'font-weight',
'font-size',
'font-variant',
'text-rendering',
'text-transform',
'width',
'text-indent',
'padding-left',
'padding-right',
'border-width',
'box-sizing',
];
export interface NodeType {
sizingStyle: string;
paddingSize: number;
borderSize: number;
boxSizing: string;
}
const computedStyleCache: { [key: string]: NodeType } = {};
let hiddenTextarea: HTMLTextAreaElement;
export function calculateNodeStyling(node: HTMLElement, useCache = false) {
const nodeRef = (node.getAttribute('id') ||
node.getAttribute('data-reactid') ||
node.getAttribute('name')) as string;
if (useCache && computedStyleCache[nodeRef]) {
return computedStyleCache[nodeRef];
}
const style = window.getComputedStyle(node);
const boxSizing =
style.getPropertyValue('box-sizing') ||
style.getPropertyValue('-moz-box-sizing') ||
style.getPropertyValue('-webkit-box-sizing');
const paddingSize =
parseFloat(style.getPropertyValue('padding-bottom')) +
parseFloat(style.getPropertyValue('padding-top'));
const borderSize =
parseFloat(style.getPropertyValue('border-bottom-width')) +
parseFloat(style.getPropertyValue('border-top-width'));
const sizingStyle = SIZING_STYLE.map(name => `${name}:${style.getPropertyValue(name)}`).join(';');
const nodeInfo: NodeType = {
sizingStyle,
paddingSize,
borderSize,
boxSizing,
};
if (useCache && nodeRef) {
computedStyleCache[nodeRef] = nodeInfo;
}
return nodeInfo;
}
export default function calculateNodeHeight(
uiTextNode: HTMLTextAreaElement,
useCache = false,
minRows: number | null = null,
maxRows: number | null = null,
) {
if (!hiddenTextarea) {
hiddenTextarea = document.createElement('textarea');
hiddenTextarea.setAttribute('tab-index', '-1');
hiddenTextarea.setAttribute('aria-hidden', 'true');
document.body.appendChild(hiddenTextarea);
}
// Fix wrap="off" issue
// https://github.com/ant-design/ant-design/issues/6577
if (uiTextNode.getAttribute('wrap')) {
hiddenTextarea.setAttribute('wrap', uiTextNode.getAttribute('wrap') as string);
} else {
hiddenTextarea.removeAttribute('wrap');
}
// Copy all CSS properties that have an impact on the height of the content in
// the textbox
const { paddingSize, borderSize, boxSizing, sizingStyle } = calculateNodeStyling(
uiTextNode,
useCache,
);
// Need to have the overflow attribute to hide the scrollbar otherwise
// text-lines will not calculated properly as the shadow will technically be
// narrower for content
hiddenTextarea.setAttribute('style', `${sizingStyle};${HIDDEN_TEXTAREA_STYLE}`);
hiddenTextarea.value = uiTextNode.value || uiTextNode.placeholder || '';
let minHeight = Number.MIN_SAFE_INTEGER;
let maxHeight = Number.MAX_SAFE_INTEGER;
let height = hiddenTextarea.scrollHeight;
let overflowY: any;
if (boxSizing === 'border-box') {
// border-box: add border, since height = content + padding + border
height += borderSize;
} else if (boxSizing === 'content-box') {
// remove padding, since height = content
height -= paddingSize;
}
if (minRows !== null || maxRows !== null) {
// measure height of a textarea with a single row
hiddenTextarea.value = ' ';
const singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
if (minRows !== null) {
minHeight = singleRowHeight * minRows;
if (boxSizing === 'border-box') {
minHeight = minHeight + paddingSize + borderSize;
}
height = Math.max(minHeight, height);
}
if (maxRows !== null) {
maxHeight = singleRowHeight * maxRows;
if (boxSizing === 'border-box') {
maxHeight = maxHeight + paddingSize + borderSize;
}
overflowY = height > maxHeight ? '' : 'hidden';
height = Math.min(maxHeight, height);
}
}
return { height, minHeight, maxHeight, overflowY };
}

View File

@ -136,6 +136,7 @@ describe('List.pagination', () => {
expect(wrapper.find('Pagination').first().render()).toMatchSnapshot();
});
// https://github.com/ant-design/ant-design/issues/24913
// https://github.com/ant-design/ant-design/issues/24501
it('should onChange called when pageSize change', () => {
const handlePaginationChange = jest.fn();

View File

@ -105,11 +105,6 @@ function List<T>({
return (page: number, pageSize: number) => {
setPaginationCurrent(page);
setPaginationSize(pageSize);
if (eventName === 'onShowSizeChange') {
if (pagination) {
pagination?.onChange?.(page, pageSize);
}
}
if (pagination && (pagination as any)[eventName]) {
(pagination as any)[eventName](page, pageSize);
}

View File

@ -6,6 +6,19 @@ exports[`renders ./components/mentions/demo/async.md correctly 1`] = `
style="width:100%"
>
<textarea
class="rc-textarea"
rows="1"
/>
</div>
`;
exports[`renders ./components/mentions/demo/autoSize.md correctly 1`] = `
<div
class="ant-mentions"
style="width:100%"
>
<textarea
class="rc-textarea"
rows="1"
/>
</div>
@ -17,6 +30,7 @@ exports[`renders ./components/mentions/demo/basic.md correctly 1`] = `
style="width:100%"
>
<textarea
class="rc-textarea"
rows="1"
>
@afc163
@ -55,6 +69,7 @@ exports[`renders ./components/mentions/demo/form.md correctly 1`] = `
class="ant-mentions"
>
<textarea
class="rc-textarea"
id="coders"
rows="1"
/>
@ -90,6 +105,7 @@ exports[`renders ./components/mentions/demo/form.md correctly 1`] = `
class="ant-mentions"
>
<textarea
class="rc-textarea"
id="bio"
placeholder="You can use @ to ref user here"
rows="3"
@ -141,6 +157,7 @@ exports[`renders ./components/mentions/demo/placement.md correctly 1`] = `
style="width:100%"
>
<textarea
class="rc-textarea"
rows="1"
/>
</div>
@ -152,6 +169,7 @@ exports[`renders ./components/mentions/demo/prefix.md correctly 1`] = `
style="width:100%"
>
<textarea
class="rc-textarea"
placeholder="input @ to mention people, # to mention tag"
rows="1"
/>
@ -168,6 +186,7 @@ exports[`renders ./components/mentions/demo/readonly.md correctly 1`] = `
style="width:100%"
>
<textarea
class="rc-textarea rc-textarea-disabled"
disabled=""
placeholder="this is disabled Mentions"
rows="1"
@ -179,6 +198,7 @@ exports[`renders ./components/mentions/demo/readonly.md correctly 1`] = `
style="width:100%"
>
<textarea
class="rc-textarea"
placeholder="this is readOnly Mentions"
readonly=""
rows="1"

View File

@ -5,6 +5,7 @@ exports[`Mentions rtl render component should be rendered correctly in RTL direc
class="ant-mentions ant-mentions-rtl"
>
<textarea
class="rc-textarea"
rows="1"
/>
</div>

View File

@ -0,0 +1,29 @@
---
order: 6
title:
zh-CN: 自动大小
en-US: autoSize
---
## zh-CN
自适应内容高度。
## en-US
Height autoSize.
```jsx
import { Mentions } from 'antd';
const { Option } = Mentions;
ReactDOM.render(
<Mentions autoSize style={{ width: '100%' }}>
<Option value="afc163">afc163</Option>
<Option value="zombieJ">zombieJ</Option>
<Option value="yesmeck">yesmeck</Option>
</Mentions>,
mountNode,
);
```

View File

@ -7,7 +7,7 @@ title:
## zh-CN
基本使用
基本使用
## en-US

View File

@ -38,6 +38,8 @@ When need to mention someone or something.
| onFocus | Trigger when mentions get focus | () => void | |
| onBlur | Trigger when mentions lose focus | () => void | |
| getPopupContainer | Set the mount HTML node for suggestions | () => HTMLElement | |
| autoSize | Textarea height autosize feature, can be set to `true\|false` or an object `{ minRows: 2, maxRows: 6 }` | boolean \| object | false |
| onResize | The callback function that is triggered when textarea resize | function({ width, height }) | |
### Mention methods

View File

@ -39,6 +39,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/jPE-itMFM/Mentions.svg
| onFocus | 获得焦点时触发 | () => void | |
| onBlur | 失去焦点时触发 | () => void | |
| getPopupContainer | 指定建议框挂载的 HTML 节点 | () => HTMLElement | |
| autoSize | 自适应内容高度,可设置为 `true|false` 或对象:`{ minRows: 2, maxRows: 6 }`。 | boolean\|object | false |
| onResize | resize 回调 | function({ width, height }) | |
### Mentions 方法

View File

@ -20,7 +20,7 @@ When requiring users to interact with the application, but without jumping to a
| cancelText | Text of the Cancel button | string\|ReactNode | `Cancel` |
| centered | Centered Modal | boolean | false |
| closable | Whether a close (x) button is visible on top right of the modal dialog or not | boolean | true |
| closeIcon | custom close icon | ReactNode | - |
| closeIcon | custom close icon | ReactNode | `<CloseOutlined />` |
| confirmLoading | Whether to apply loading visual effect for OK button or not | boolean | false |
| destroyOnClose | Whether to unmount child components on onClose | boolean | false |
| footer | Footer content, set as `footer={null}` when you don't need default buttons | string\|ReactNode | OK and Cancel buttons |

View File

@ -23,7 +23,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3StSdUlSH/Modal.svg
| cancelText | 取消按钮文字 | string\|ReactNode | 取消 |
| centered | 垂直居中展示 Modal | boolean | false |
| closable | 是否显示右上角的关闭按钮 | boolean | true |
| closeIcon | 自定义关闭图标 | ReactNode | - |
| closeIcon | 自定义关闭图标 | ReactNode | `<CloseOutlined />` |
| confirmLoading | 确定按钮 loading | boolean | false |
| destroyOnClose | 关闭时销毁 Modal 里的子元素 | boolean | false |
| footer | 底部内容,当不需要默认底部按钮时,可以设为 `footer={null}` | string\|ReactNode | 确定取消按钮 |

View File

@ -27,8 +27,8 @@
margin: 0;
color: @modal-heading-color;
font-weight: 500;
font-size: @font-size-lg;
line-height: 22px;
font-size: @modal-header-title-font-size;
line-height: @modal-header-title-line-height;
word-wrap: break-word;
}
@ -81,7 +81,7 @@
padding: @modal-header-padding;
color: @text-color;
background: @modal-header-bg;
border-bottom: @border-width-base @border-style-base @modal-header-border-color-split;
border-bottom: @modal-header-border-width @modal-header-border-style @modal-header-border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0;
}
@ -96,7 +96,7 @@
padding: @modal-footer-padding-vertical @modal-footer-padding-horizontal;
text-align: right;
background: @modal-footer-bg;
border-top: @border-width-base @border-style-base @modal-footer-border-color-split;
border-top: @modal-footer-border-width @modal-footer-border-style @modal-footer-border-color-split;
border-radius: 0 0 @border-radius-base @border-radius-base;
button + button {

View File

@ -29,21 +29,30 @@ describe('Pagination', () => {
return originalElement;
}
const wrapper = mount(<Pagination defaultCurrent={1} total={50} itemRender={itemRender} />);
expect(
wrapper
.find('button')
.at(0)
.props().disabled,
).toBe(true);
expect(wrapper.find('button').at(0).props().disabled).toBe(true);
});
it('should autometically be small when size is not specified', async () => {
const wrapper = mount(<Pagination responsive />);
expect(
wrapper
.find('ul')
.at(0)
.hasClass('mini'),
).toBe(true);
expect(wrapper.find('ul').at(0).hasClass('mini')).toBe(true);
});
// https://github.com/ant-design/ant-design/issues/24913
// https://github.com/ant-design/ant-design/issues/24501
it('should onChange called when pageSize change', () => {
const onChange = jest.fn();
const onShowSizeChange = jest.fn();
const wrapper = mount(
<Pagination
defaultCurrent={1}
total={500}
onChange={onChange}
onShowSizeChange={onShowSizeChange}
/>,
);
wrapper.find('.ant-select-selector').simulate('mousedown');
expect(wrapper.find('.ant-select-item-option').length).toBe(4);
wrapper.find('.ant-select-item-option').at(1).simulate('click');
expect(onChange).toHaveBeenCalledWith(1, 20);
});
});

View File

@ -10,8 +10,11 @@ interface CircleProps extends ProgressProps {
progressStatus: string;
}
function getPercentage({ percent, successPercent }: CircleProps) {
function getPercentage({ percent, success, successPercent }: CircleProps) {
const ptg = validProgress(percent);
if (success && 'progress' in success) {
successPercent = success.progress;
}
if (!successPercent) {
return ptg;
}
@ -20,8 +23,11 @@ function getPercentage({ percent, successPercent }: CircleProps) {
return [successPercent, validProgress(ptg - successPtg)];
}
function getStrokeColor({ successPercent, strokeColor }: CircleProps) {
function getStrokeColor({ success, strokeColor, successPercent }: CircleProps) {
const color = strokeColor || null;
if (success && 'progress' in success) {
successPercent = success.progress;
}
if (!successPercent) {
return color;
}

View File

@ -61,14 +61,15 @@ const Line: React.FC<LineProps> = props => {
const {
prefixCls,
percent,
successPercent,
strokeWidth,
size,
strokeColor,
strokeLinecap,
children,
trailColor,
success,
} = props;
let backgroundProps;
if (strokeColor && typeof strokeColor !== 'string') {
backgroundProps = handleGradient(strokeColor);
@ -77,23 +78,45 @@ const Line: React.FC<LineProps> = props => {
background: strokeColor,
};
}
let trailStyle;
if (trailColor && typeof trailColor === 'string') {
trailStyle = {
backgroundColor: trailColor,
};
}
let successColor;
if (success && 'strokeColor' in success) {
successColor = success.strokeColor;
}
let successStyle;
if (successColor && typeof successColor === 'string') {
successStyle = {
backgroundColor: successColor,
};
}
const percentStyle = {
width: `${validProgress(percent)}%`,
height: strokeWidth || (size === 'small' ? 6 : 8),
borderRadius: strokeLinecap === 'square' ? 0 : '',
...backgroundProps,
};
const successPercentStyle = {
let { successPercent } = props;
if (success && 'progress' in success) {
successPercent = success.progress;
}
let successPercentStyle = {
width: `${validProgress(successPercent)}%`,
height: strokeWidth || (size === 'small' ? 6 : 8),
borderRadius: strokeLinecap === 'square' ? 0 : '',
};
if (successStyle) {
successPercentStyle = { ...successPercentStyle, ...successStyle };
}
const successSegment =
successPercent !== undefined ? (
<div className={`${prefixCls}-success-bg`} style={successPercentStyle} />

View File

@ -500,6 +500,35 @@ exports[`Progress render strokeColor 3`] = `
</Progress>
`;
exports[`Progress render successColor progress 1`] = `
<div
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
<div
class="ant-progress-outer"
>
<div
class="ant-progress-inner"
>
<div
class="ant-progress-bg"
style="width: 60%; height: 8px;"
/>
<div
class="ant-progress-success-bg"
style="width: 30%; height: 8px; background-color: rgb(255, 255, 255);"
/>
</div>
</div>
<span
class="ant-progress-text"
title="60%"
>
60%
</span>
</div>
`;
exports[`Progress render trailColor progress 1`] = `
<div
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"

View File

@ -10,13 +10,13 @@ describe('Progress', () => {
rtlTest(Progress);
it('successPercent should decide the progress status when it exists', () => {
const wrapper = mount(<Progress percent={100} successPercent={50} />);
const wrapper = mount(<Progress percent={100} success={{ progress: 50 }} />);
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(0);
wrapper.setProps({ percent: 50, successPercent: 100 });
wrapper.setProps({ percent: 50, success: { progress: 100 } });
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(1);
wrapper.setProps({ percent: 100, successPercent: 0 });
wrapper.setProps({ percent: 100, success: { progress: 0 } });
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(0);
});
@ -36,7 +36,7 @@ describe('Progress', () => {
});
it('render negative successPercent', () => {
const wrapper = mount(<Progress percent={50} successPercent={-20} />);
const wrapper = mount(<Progress percent={50} success={{ progress: -20 }} />);
expect(wrapper.render()).toMatchSnapshot();
});
@ -44,7 +44,7 @@ describe('Progress', () => {
const wrapper = mount(
<Progress
percent={50}
successPercent={10}
success={{ progress: 10 }}
format={(percent, successPercent) => `${percent} ${successPercent}`}
/>,
);
@ -81,6 +81,13 @@ describe('Progress', () => {
expect(wrapper.render()).toMatchSnapshot();
});
it('render successColor progress', () => {
const wrapper = mount(
<Progress percent={60} success={{ progress: 30, strokeColor: '#ffffff' }} />,
);
expect(wrapper.render()).toMatchSnapshot();
});
it('render dashboard zero gapDegree', () => {
const wrapper = mount(<Progress type="dashboard" gapDegree={0} />);
expect(wrapper.render()).toMatchSnapshot();

View File

@ -19,15 +19,15 @@ import { Tooltip, Progress } from 'antd';
ReactDOM.render(
<>
<Tooltip title="3 done / 3 in progress / 4 to do">
<Progress percent={60} successPercent={30} />
<Progress percent={60} success={{ progress: 30 }} />
</Tooltip>
<Tooltip title="3 done / 3 in progress / 4 to do">
<Progress percent={60} successPercent={30} type="circle" />
<Progress percent={60} success={{ progress: 30 }} type="circle" />
</Tooltip>
<Tooltip title="3 done / 3 in progress / 4 to do">
<Progress percent={60} successPercent={30} type="dashboard" />
<Progress percent={60} success={{ progress: 30 }} type="dashboard" />
</Tooltip>
</>,
mountNode,

View File

@ -27,8 +27,8 @@ Properties that shared by all types.
| status | to set the status of the Progress, options: `success` `exception` `normal` `active`(line only) | string | - |
| strokeLinecap | to set the style of the progress linecap | `round` \| `square` | `round` |
| strokeColor | color of progress bar | string | - |
| successPercent | segmented success percent | number | 0 |
| trailColor | color of unfilled part | string | - |
| success | configs of successfully progress bar | { progress: number, strokeColor: string } | - |
### `type="line"`

View File

@ -28,8 +28,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xqsDu4ZyR/Progress.svg
| status | 状态,可选:`success` `exception` `normal` `active`(仅限 line) | string | - |
| strokeLinecap | - | `round` \| `square` | `round` |
| strokeColor | 进度条的色彩 | string | - |
| successPercent | 已完成的分段百分比 | number | 0 |
| trailColor | 未完成的分段的颜色 | string | - |
| success | 成功进度条相关配置 | { progress: number, strokeColor: string } | - |
### `type="line"`

View File

@ -8,6 +8,7 @@ import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { tuple } from '../_util/type';
import devWarning from '../_util/devWarning';
import Line from './Line';
import Circle from './Circle';
import Steps from './Steps';
@ -20,12 +21,17 @@ export type ProgressSize = 'default' | 'small';
export type StringGradients = { [percentage: string]: string };
type FromToGradients = { from: string; to: string };
export type ProgressGradient = { direction?: string } & (StringGradients | FromToGradients);
export interface SuccessProps {
progress?: number;
strokeColor?: string;
}
export interface ProgressProps {
prefixCls?: string;
className?: string;
type?: ProgressType;
percent?: number;
successPercent?: number;
format?: (percent?: number, successPercent?: number) => React.ReactNode;
status?: typeof ProgressStatuses[number];
showInfo?: boolean;
@ -34,11 +40,14 @@ export interface ProgressProps {
strokeColor?: string | ProgressGradient;
trailColor?: string;
width?: number;
success?: SuccessProps;
style?: React.CSSProperties;
gapDegree?: number;
gapPosition?: 'top' | 'bottom' | 'left' | 'right';
size?: ProgressSize;
steps?: number;
/** @deprecated Use `success` instead */
successPercent?: number;
}
export default class Progress extends React.Component<ProgressProps> {
@ -54,7 +63,11 @@ export default class Progress extends React.Component<ProgressProps> {
};
getPercentNumber() {
const { successPercent, percent = 0 } = this.props;
const { percent = 0, success } = this.props;
let { successPercent } = this.props;
if (success && 'progress' in success) {
successPercent = success.progress;
}
return parseInt(
successPercent !== undefined ? successPercent.toString() : percent.toString(),
10,
@ -70,7 +83,11 @@ export default class Progress extends React.Component<ProgressProps> {
}
renderProcessInfo(prefixCls: string, progressStatus: typeof ProgressStatuses[number]) {
const { showInfo, format, type, percent, successPercent } = this.props;
const { showInfo, format, type, percent, success } = this.props;
let { successPercent } = this.props;
if (success && 'progress' in success) {
successPercent = success.progress;
}
if (!showInfo) return null;
let text;
@ -105,6 +122,13 @@ export default class Progress extends React.Component<ProgressProps> {
const prefixCls = getPrefixCls('progress', customizePrefixCls);
const progressStatus = this.getProgressStatus();
const progressInfo = this.renderProcessInfo(prefixCls, progressStatus);
devWarning(
'successPercent' in props,
'Progress',
'`successPercent` is deprecated. Please use `success` instead.',
);
let progress;
// Render progress shape
if (type === 'line') {
@ -148,7 +172,6 @@ export default class Progress extends React.Component<ProgressProps> {
'status',
'format',
'trailColor',
'successPercent',
'strokeWidth',
'width',
'gapDegree',
@ -157,6 +180,8 @@ export default class Progress extends React.Component<ProgressProps> {
'strokeLinecap',
'percent',
'steps',
'success',
'successPercent',
])}
className={classString}
>

View File

@ -1110,6 +1110,7 @@ Array [
</span>
</label>
</div>,
<br />,
<div
class="ant-radio-group ant-radio-group-outline"
>
@ -1153,13 +1154,14 @@ Array [
</span>
</label>
<label
class="ant-radio-wrapper"
class="ant-radio-wrapper ant-radio-wrapper-disabled"
>
<span
class="ant-radio"
class="ant-radio ant-radio-disabled"
>
<input
class="ant-radio-input"
disabled=""
type="radio"
value="Orange"
/>
@ -1172,23 +1174,25 @@ Array [
</span>
</label>
</div>,
<br />,
<br />,
<div
class="ant-radio-group ant-radio-group-outline"
>
<label
class="ant-radio-wrapper ant-radio-wrapper-checked"
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
>
<span
class="ant-radio ant-radio-checked"
class="ant-radio-button ant-radio-button-checked"
>
<input
checked=""
class="ant-radio-input"
class="ant-radio-button-input"
type="radio"
value="Apple"
/>
<span
class="ant-radio-inner"
class="ant-radio-button-inner"
/>
</span>
<span>
@ -1196,18 +1200,18 @@ Array [
</span>
</label>
<label
class="ant-radio-wrapper"
class="ant-radio-button-wrapper"
>
<span
class="ant-radio"
class="ant-radio-button"
>
<input
class="ant-radio-input"
class="ant-radio-button-input"
type="radio"
value="Pear"
/>
<span
class="ant-radio-inner"
class="ant-radio-button-inner"
/>
</span>
<span>
@ -1215,18 +1219,83 @@ Array [
</span>
</label>
<label
class="ant-radio-wrapper"
class="ant-radio-button-wrapper"
>
<span
class="ant-radio"
class="ant-radio-button"
>
<input
class="ant-radio-input"
class="ant-radio-button-input"
type="radio"
value="Orange"
/>
<span
class="ant-radio-inner"
class="ant-radio-button-inner"
/>
</span>
<span>
Orange
</span>
</label>
</div>,
<br />,
<br />,
<div
class="ant-radio-group ant-radio-group-solid"
>
<label
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
>
<span
class="ant-radio-button ant-radio-button-checked"
>
<input
checked=""
class="ant-radio-button-input"
type="radio"
value="Apple"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Apple
</span>
</label>
<label
class="ant-radio-button-wrapper"
>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="Pear"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Pear
</span>
</label>
<label
class="ant-radio-button-wrapper ant-radio-button-wrapper-disabled"
>
<span
class="ant-radio-button ant-radio-button-disabled"
>
<input
class="ant-radio-button-input"
disabled=""
type="radio"
value="Orange"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>

View File

@ -7,11 +7,11 @@ title:
## zh-CN
通过配置 `options` 参数来渲染单选框。
通过配置 `options` 参数来渲染单选框。也可通过 `optionType` 参数来设置 Radio 类型。
## en-US
Render radios by configuring `options`.
Render radios by configuring `options`. Radio type can also be set through the `optionType` parameter.
```jsx
import { Radio } from 'antd';
@ -25,7 +25,7 @@ const options = [
const optionsWithDisabled = [
{ label: 'Apple', value: 'Apple' },
{ label: 'Pear', value: 'Pear' },
{ label: 'Orange', value: 'Orange', disabled: false },
{ label: 'Orange', value: 'Orange', disabled: true },
];
class App extends React.Component {
@ -33,6 +33,7 @@ class App extends React.Component {
value1: 'Apple',
value2: 'Apple',
value3: 'Apple',
value4: 'Apple',
};
onChange1 = e => {
@ -56,16 +57,36 @@ class App extends React.Component {
});
};
onChange4 = e => {
console.log('radio4 checked', e.target.value);
this.setState({
value4: e.target.value,
});
};
render() {
const { value1, value2, value3 } = this.state;
const { value1, value2, value3, value4 } = this.state;
return (
<>
<Radio.Group options={plainOptions} onChange={this.onChange1} value={value1} />
<Radio.Group options={options} onChange={this.onChange2} value={value2} />
<br />
<Radio.Group options={optionsWithDisabled} onChange={this.onChange2} value={value2} />
<br />
<br />
<Radio.Group
options={optionsWithDisabled}
options={options}
onChange={this.onChange3}
value={value3}
optionType="button"
/>
<br />
<br />
<Radio.Group
options={optionsWithDisabled}
onChange={this.onChange4}
value={value4}
optionType="button"
buttonStyle="solid"
/>
</>
);

View File

@ -43,6 +43,7 @@ const RadioGroup: React.FC<RadioGroupProps> = props => {
prefixCls: customizePrefixCls,
className = '',
options,
optionType,
buttonStyle,
disabled,
children,
@ -57,13 +58,14 @@ const RadioGroup: React.FC<RadioGroupProps> = props => {
let childrenToRender = children;
// 如果存在 options, 优先使用
if (options && options.length > 0) {
const optionsPrefixCls = optionType === 'button' ? `${prefixCls}-button` : prefixCls;
childrenToRender = options.map(option => {
if (typeof option === 'string') {
// 此处类型自动推导为 string
return (
<Radio
key={option}
prefixCls={prefixCls}
prefixCls={optionsPrefixCls}
disabled={disabled}
value={option}
checked={value === option}
@ -76,7 +78,7 @@ const RadioGroup: React.FC<RadioGroupProps> = props => {
return (
<Radio
key={`radio-group-value-options-${option.value}`}
prefixCls={prefixCls}
prefixCls={optionsPrefixCls}
disabled={option.disabled || disabled}
value={option.value}
checked={value === option.value}

View File

@ -28,16 +28,17 @@ Radio.
Radio group can wrap a group of `Radio`
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| defaultValue | Default selected value | any | |
| disabled | Disable all radio buttons | boolean | false |
| name | The `name` property of all `input[type="radio"]` children | string | |
| options | set children optional | string\[] \| Array&lt;{ label: string value: string disabled?: boolean }> | |
| size | size for radio button style | `large` \| `middle` \| `small` | |
| value | Used for setting the currently selected value. | any | |
| onChange | The callback function that is triggered when the state changes. | Function(e:Event) | |
| buttonStyle | style type of radio button | `outline` \| `solid` | `outline` |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| defaultValue | Default selected value | any | | |
| disabled | Disable all radio buttons | boolean | false | |
| name | The `name` property of all `input[type="radio"]` children | string | | |
| options | set children optional | string\[] \| Array&lt;{ label: string value: string disabled?: boolean }> | | |
| size | size for radio button style | `large` \| `middle` \| `small` | | |
| value | Used for setting the currently selected value. | any | | |
| onChange | The callback function that is triggered when the state changes. | Function(e:Event) | | |
| optionType | set Radio optionType | `default` \| `button` | `default` | 4.4.0 |
| buttonStyle | style type of radio button | `outline` \| `solid` | `outline` | |
## Methods

View File

@ -29,16 +29,17 @@ cover: https://gw.alipayobjects.com/zos/alicdn/8cYb5seNB/Radio.svg
单选框组合,用于包裹一组 `Radio`
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| defaultValue | 默认选中的值 | any | |
| disabled | 禁选所有子单选器 | boolean | false |
| name | RadioGroup 下所有 `input[type="radio"]``name` 属性 | string | |
| options | 以配置形式设置子元素 | string\[] \| Array&lt;{ label: string value: string disabled?: boolean }> | |
| size | 大小,只对按钮样式生效 | `large` \| `middle` \| `small` | - |
| value | 用于设置当前选中的值 | any | |
| onChange | 选项变化时的回调函数 | Function(e:Event) | |
| buttonStyle | RadioButton 的风格样式,目前有描边和填色两种风格 | `outline` \| `solid` | `outline` |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| defaultValue | 默认选中的值 | any | | |
| disabled | 禁选所有子单选器 | boolean | false | | |
| name | RadioGroup 下所有 `input[type="radio"]``name` 属性 | string | | |
| options | 以配置形式设置子元素 | string\[] \| Array&lt;{ label: string value: string disabled?: boolean }> | | |
| size | 大小,只对按钮样式生效 | `large` \| `middle` \| `small` | - | |
| value | 用于设置当前选中的值 | any | | |
| onChange | 选项变化时的回调函数 | Function(e:Event) | | |
| optionType | 用于设置 Radio `options` 类型 | `default` \| `button` | `default` | 4.4.0 |
| buttonStyle | RadioButton 的风格样式,目前有描边和填色两种风格 | `outline` \| `solid` | `outline` | |
## 方法

View File

@ -4,6 +4,7 @@ import { AbstractCheckboxProps } from '../checkbox/Checkbox';
import { SizeType } from '../config-provider/SizeContext';
export type RadioGroupButtonStyle = 'outline' | 'solid';
export type RadioGroupOptionType = 'default' | 'button';
export interface RadioGroupProps extends AbstractCheckboxGroupProps {
defaultValue?: any;
@ -15,6 +16,7 @@ export interface RadioGroupProps extends AbstractCheckboxGroupProps {
name?: string;
children?: React.ReactNode;
id?: string;
optionType?: RadioGroupOptionType;
buttonStyle?: RadioGroupButtonStyle;
}

View File

@ -5,6 +5,7 @@ import { RadioProps, RadioChangeEvent } from './interface';
import { ConfigContext } from '../config-provider';
import RadioGroupContext from './context';
import { composeRef } from '../_util/ref';
import devWarning from '../_util/devWarning';
const InternalRadio: React.ForwardRefRenderFunction<unknown, RadioProps> = (props, ref) => {
const context = React.useContext(RadioGroupContext);
@ -12,6 +13,10 @@ const InternalRadio: React.ForwardRefRenderFunction<unknown, RadioProps> = (prop
const innerRef = React.useRef<HTMLElement>();
const mergedRef = composeRef(ref, innerRef);
React.useEffect(() => {
devWarning(!('optionType' in props), 'Radio', '`optionType` is only support in Radio.Group.');
}, []);
const onChange = (e: RadioChangeEvent) => {
if (props.onChange) {
props.onChange(e);

View File

@ -310,7 +310,7 @@ exports[`renders ./components/rate/demo/basic.md correctly 1`] = `
`;
exports[`renders ./components/rate/demo/character.md correctly 1`] = `
<div>
Array [
<ul
class="ant-rate"
role="radiogroup"
@ -616,8 +616,8 @@ exports[`renders ./components/rate/demo/character.md correctly 1`] = `
</div>
</div>
</li>
</ul>
<br />
</ul>,
<br />,
<ul
class="ant-rate"
role="radiogroup"
@ -734,8 +734,8 @@ exports[`renders ./components/rate/demo/character.md correctly 1`] = `
</div>
</div>
</li>
</ul>
<br />
</ul>,
<br />,
<ul
class="ant-rate"
role="radiogroup"
@ -851,12 +851,440 @@ exports[`renders ./components/rate/demo/character.md correctly 1`] = `
</div>
</div>
</li>
</ul>
</div>
</ul>,
]
`;
exports[`renders ./components/rate/demo/character-function.md correctly 1`] = `
Array [
<ul
class="ant-rate"
role="radiogroup"
tabindex="0"
>
<li
class="ant-rate-star ant-rate-star-full"
>
<div
aria-checked="true"
aria-posinset="1"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
1
</div>
<div
class="ant-rate-star-second"
>
1
</div>
</div>
</li>
<li
class="ant-rate-star ant-rate-star-full"
>
<div
aria-checked="true"
aria-posinset="2"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
2
</div>
<div
class="ant-rate-star-second"
>
2
</div>
</div>
</li>
<li
class="ant-rate-star ant-rate-star-zero"
>
<div
aria-checked="false"
aria-posinset="3"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
3
</div>
<div
class="ant-rate-star-second"
>
3
</div>
</div>
</li>
<li
class="ant-rate-star ant-rate-star-zero"
>
<div
aria-checked="false"
aria-posinset="4"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
4
</div>
<div
class="ant-rate-star-second"
>
4
</div>
</div>
</li>
<li
class="ant-rate-star ant-rate-star-zero"
>
<div
aria-checked="false"
aria-posinset="5"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
5
</div>
<div
class="ant-rate-star-second"
>
5
</div>
</div>
</li>
</ul>,
<br />,
<ul
class="ant-rate"
role="radiogroup"
tabindex="0"
>
<li
class="ant-rate-star ant-rate-star-full"
>
<div
aria-checked="true"
aria-posinset="1"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
<span
aria-label="frown"
class="anticon anticon-frown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="frown"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM512 533c-85.5 0-155.6 67.3-160 151.6a8 8 0 008 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c.3 4.2 3.9 7.4 8.1 7.4H664a8 8 0 008-8.4C667.6 600.3 597.5 533 512 533z"
/>
</svg>
</span>
</div>
<div
class="ant-rate-star-second"
>
<span
aria-label="frown"
class="anticon anticon-frown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="frown"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM512 533c-85.5 0-155.6 67.3-160 151.6a8 8 0 008 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c.3 4.2 3.9 7.4 8.1 7.4H664a8 8 0 008-8.4C667.6 600.3 597.5 533 512 533z"
/>
</svg>
</span>
</div>
</div>
</li>
<li
class="ant-rate-star ant-rate-star-full"
>
<div
aria-checked="true"
aria-posinset="2"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
<span
aria-label="frown"
class="anticon anticon-frown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="frown"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM512 533c-85.5 0-155.6 67.3-160 151.6a8 8 0 008 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c.3 4.2 3.9 7.4 8.1 7.4H664a8 8 0 008-8.4C667.6 600.3 597.5 533 512 533z"
/>
</svg>
</span>
</div>
<div
class="ant-rate-star-second"
>
<span
aria-label="frown"
class="anticon anticon-frown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="frown"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM512 533c-85.5 0-155.6 67.3-160 151.6a8 8 0 008 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c.3 4.2 3.9 7.4 8.1 7.4H664a8 8 0 008-8.4C667.6 600.3 597.5 533 512 533z"
/>
</svg>
</span>
</div>
</div>
</li>
<li
class="ant-rate-star ant-rate-star-full"
>
<div
aria-checked="true"
aria-posinset="3"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
<span
aria-label="meh"
class="anticon anticon-meh"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="meh"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 565H360c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h304c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</div>
<div
class="ant-rate-star-second"
>
<span
aria-label="meh"
class="anticon anticon-meh"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="meh"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 565H360c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h304c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</div>
</div>
</li>
<li
class="ant-rate-star ant-rate-star-zero"
>
<div
aria-checked="false"
aria-posinset="4"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
<span
aria-label="smile"
class="anticon anticon-smile"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="smile"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
/>
</svg>
</span>
</div>
<div
class="ant-rate-star-second"
>
<span
aria-label="smile"
class="anticon anticon-smile"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="smile"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
/>
</svg>
</span>
</div>
</div>
</li>
<li
class="ant-rate-star ant-rate-star-zero"
>
<div
aria-checked="false"
aria-posinset="5"
aria-setsize="5"
role="radio"
tabindex="0"
>
<div
class="ant-rate-star-first"
>
<span
aria-label="smile"
class="anticon anticon-smile"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="smile"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
/>
</svg>
</span>
</div>
<div
class="ant-rate-star-second"
>
<span
aria-label="smile"
class="anticon anticon-smile"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="smile"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
/>
</svg>
</span>
</div>
</div>
</li>
</ul>,
]
`;
exports[`renders ./components/rate/demo/clear.md correctly 1`] = `
<div>
Array [
<ul
class="ant-rate"
role="radiogroup"
@ -1162,13 +1590,13 @@ exports[`renders ./components/rate/demo/clear.md correctly 1`] = `
</div>
</div>
</li>
</ul>
</ul>,
<span
class="ant-rate-text"
>
allowClear: true
</span>
<br />
</span>,
<br />,
<ul
class="ant-rate"
role="radiogroup"
@ -1474,13 +1902,13 @@ exports[`renders ./components/rate/demo/clear.md correctly 1`] = `
</div>
</div>
</li>
</ul>
</ul>,
<span
class="ant-rate-text"
>
allowClear: false
</span>
</div>
</span>,
]
`;
exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
@ -1497,7 +1925,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
aria-posinset="1"
aria-setsize="5"
role="radio"
tabindex="0"
tabindex="-1"
>
<div
class="ant-rate-star-first"
@ -1557,7 +1985,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
aria-posinset="2"
aria-setsize="5"
role="radio"
tabindex="0"
tabindex="-1"
>
<div
class="ant-rate-star-first"
@ -1617,7 +2045,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
aria-posinset="3"
aria-setsize="5"
role="radio"
tabindex="0"
tabindex="-1"
>
<div
class="ant-rate-star-first"
@ -1677,7 +2105,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
aria-posinset="4"
aria-setsize="5"
role="radio"
tabindex="0"
tabindex="-1"
>
<div
class="ant-rate-star-first"
@ -1737,7 +2165,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
aria-posinset="5"
aria-setsize="5"
role="radio"
tabindex="0"
tabindex="-1"
>
<div
class="ant-rate-star-first"

View File

@ -0,0 +1,46 @@
---
order: 6
title:
zh-CN: 自定义字符
en-US: Customize character
---
## zh-CN
可以使用 `(RateProps) => ReactNode` 的方式自定义每一个字符。
## en-US
Can customize each character using `(RateProps) => ReactNode`.
```jsx
import { Rate } from 'antd';
import { FrownOutlined, MehOutlined, SmileOutlined } from '@ant-design/icons';
const customIcons = {
1: <FrownOutlined />,
2: <FrownOutlined />,
3: <MehOutlined />,
4: <SmileOutlined />,
5: <SmileOutlined />,
};
ReactDOM.render(
<>
<Rate
defaultValue={2}
character={({ index }) => {
return index + 1;
}}
/>
<br />
<Rate
defaultValue={3}
character={({ index }) => {
return customIcons[index + 1];
}}
/>
</>,
mountNode,
);
```

View File

@ -18,13 +18,13 @@ import { Rate } from 'antd';
import { HeartOutlined } from '@ant-design/icons';
ReactDOM.render(
<div>
<>
<Rate character={<HeartOutlined />} allowHalf />
<br />
<Rate character="A" allowHalf style={{ fontSize: 36 }} />
<br />
<Rate character="好" allowHalf />
</div>,
</>,
mountNode,
);
```

View File

@ -17,13 +17,13 @@ Support set allow to clear star when click again.
import { Rate } from 'antd';
ReactDOM.render(
<div>
<>
<Rate defaultValue={3} />
<span className="ant-rate-text">allowClear: true</span>
<br />
<Rate allowClear={false} defaultValue={3} />
<span className="ant-rate-text">allowClear: false</span>
</div>,
</>,
mountNode,
);
```

View File

@ -14,24 +14,24 @@ Rate component.
## API
| Property | Description | type | Default |
| --- | --- | --- | --- |
| allowClear | whether to allow clear when click again | boolean | true |
| allowHalf | whether to allow semi selection | boolean | false |
| autoFocus | get focus when component mounted | boolean | false |
| character | custom character of rate | ReactNode | [`<StarFilled />`](/components/icon/) |
| className | custom class name of rate | string | |
| count | star count | number | 5 |
| defaultValue | default value | number | 0 |
| disabled | read only, unable to interact | boolean | false |
| style | custom style object of rate | CSSProperties | |
| tooltips | Customize tooltip by each character | string\[] | |
| value | current value | number | |
| onBlur | callback when component lose focus | Function() | |
| onChange | callback when select value | Function(value: number) | |
| onFocus | callback when component get focus | Function() | |
| onHoverChange | callback when hover item | Function(value: number) | |
| onKeyDown | callback when keydown on component | Function(event) | |
| Property | Description | type | Default | Version |
| --- | --- | --- | --- | --- |
| allowClear | whether to allow clear when click again | boolean | true | |
| allowHalf | whether to allow semi selection | boolean | false | |
| autoFocus | get focus when component mounted | boolean | false | |
| character | custom character of rate | ReactNode \| (RateProps) => ReactNode | [`<StarFilled />`](/components/icon/) | Function(): 4.4.0 |
| className | custom class name of rate | string | | |
| count | star count | number | 5 | |
| defaultValue | default value | number | 0 | |
| disabled | read only, unable to interact | boolean | false | |
| style | custom style object of rate | CSSProperties | | |
| tooltips | Customize tooltip by each character | string\[] | | |
| value | current value | number | | |
| onBlur | callback when component lose focus | Function() | | |
| onChange | callback when select value | Function(value: number) | | |
| onFocus | callback when component get focus | Function() | | |
| onHoverChange | callback when hover item | Function(value: number) | | |
| onKeyDown | callback when keydown on component | Function(event) | | |
## Methods

View File

@ -15,24 +15,24 @@ cover: https://gw.alipayobjects.com/zos/alicdn/R5uiIWmxe/Rate.svg
## API
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| allowClear | 是否允许再次点击后清除 | boolean | true |
| allowHalf | 是否允许半选 | boolean | false |
| autoFocus | 自动获取焦点 | boolean | false |
| character | 自定义字符 | ReactNode | [`<StarFilled />`](/components/icon/) |
| className | 自定义样式类名 | string | |
| count | star 总数 | number | 5 |
| defaultValue | 默认值 | number | 0 |
| disabled | 只读,无法进行交互 | boolean | false |
| style | 自定义样式对象 | CSSProperties | |
| tooltips | 自定义每项的提示信息 | string\[] | |
| value | 当前数,受控值 | number | |
| onBlur | 失去焦点时的回调 | Function() | |
| onChange | 选择时的回调 | Function(value: number) | |
| onFocus | 获取焦点时的回调 | Function() | |
| onHoverChange | 鼠标经过时数值变化的回调 | Function(value: number) | |
| onKeyDown | 按键回调 | Function(event) | |
| 属性 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| allowClear | 是否允许再次点击后清除 | boolean | true | |
| allowHalf | 是否允许半选 | boolean | false | |
| autoFocus | 自动获取焦点 | boolean | false | |
| character | 自定义字符 | ReactNode \| (RateProps) => ReactNode | [`<StarFilled />`](/components/icon/) | Function(): 4.4.0 |
| className | 自定义样式类名 | string | | |
| count | star 总数 | number | 5 | |
| defaultValue | 默认值 | number | 0 | |
| disabled | 只读,无法进行交互 | boolean | false | |
| style | 自定义样式对象 | CSSProperties | | |
| tooltips | 自定义每项的提示信息 | string\[] | | |
| value | 当前数,受控值 | number | | |
| onBlur | 失去焦点时的回调 | Function() | | |
| onChange | 选择时的回调 | Function(value: number) | | |
| onFocus | 获取焦点时的回调 | Function() | | |
| onHoverChange | 鼠标经过时数值变化的回调 | Function(value: number) | | |
| onKeyDown | 按键回调 | Function(event) | | |
## 方法

View File

@ -1108,6 +1108,26 @@ exports[`renders ./components/result/demo/error.md correctly 1`] = `
>
Please check and modify the following information before resubmitting.
</div>
<div
class="ant-result-extra"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Go Console
</span>
</button>
<button
class="ant-btn"
type="button"
>
<span>
Buy Again
</span>
</button>
</div>
<div
class="ant-result-content"
>
@ -1190,26 +1210,6 @@ exports[`renders ./components/result/demo/error.md correctly 1`] = `
</div>
</div>
</div>
<div
class="ant-result-extra"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Go Console
</span>
</button>
<button
class="ant-btn"
type="button"
>
<span>
Buy Again
</span>
</button>
</div>
</div>
`;

View File

@ -104,8 +104,8 @@ const Result: ResultType = props => (
{renderIcon(prefixCls, props)}
<div className={`${prefixCls}-title`}>{title}</div>
{subTitle && <div className={`${prefixCls}-subtitle`}>{subTitle}</div>}
{children && <div className={`${prefixCls}-content`}>{children}</div>}
{renderExtra(prefixCls, props)}
{children && <div className={`${prefixCls}-content`}>{children}</div>}
</div>
);
}}

View File

@ -741,7 +741,7 @@ exports[`renders ./components/select/demo/custom-tag-render.md correctly 1`] = `
gold
<span
aria-label="close"
class="anticon anticon-close"
class="anticon anticon-close ant-tag-close-icon"
role="img"
tabindex="-1"
>
@ -770,7 +770,7 @@ exports[`renders ./components/select/demo/custom-tag-render.md correctly 1`] = `
cyan
<span
aria-label="close"
class="anticon anticon-close"
class="anticon anticon-close ant-tag-close-icon"
role="img"
tabindex="-1"
>

View File

@ -23,7 +23,7 @@ const SkeletonAvatar = (props: AvatarProps) => {
);
};
return <ConfigConsumer>{renderSkeletonAvatar}</ConfigConsumer>;
}
};
SkeletonAvatar.defaultProps = {
size: 'default',

View File

@ -27,11 +27,12 @@ const Element = (props: SkeletonElementProps) => {
const sizeStyle: React.CSSProperties =
typeof size === 'number'
? {
width: size,
height: size,
lineHeight: `${size}px`,
}
width: size,
height: size,
lineHeight: `${size}px`,
}
: {};
return (
<span
className={classNames(prefixCls, className, sizeCls, shapeCls)}
@ -40,5 +41,4 @@ const Element = (props: SkeletonElementProps) => {
);
};
export default Element;

View File

@ -0,0 +1,35 @@
import * as React from 'react';
import classNames from 'classnames';
import { SkeletonElementProps } from './Element';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
export interface SkeletonImageProps
extends Omit<SkeletonElementProps, 'size' | 'shape' | 'active'> {}
const path =
'M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z';
const SkeletonImage = (props: SkeletonImageProps) => {
const renderSkeletonImage = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className, style } = props;
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
const cls = classNames(prefixCls, className, `${prefixCls}-element`);
return (
<div className={cls}>
<div className={classNames(`${prefixCls}-image`, className)} style={style}>
<svg
viewBox="0 0 1098 1024"
xmlns="http://www.w3.org/2000/svg"
className={`${prefixCls}-image-svg`}
>
<path d={path} className={`${prefixCls}-image-path`} />
</svg>
</div>
</div>
);
};
return <ConfigConsumer>{renderSkeletonImage}</ConfigConsumer>;
};
export default SkeletonImage;

View File

@ -7,10 +7,10 @@ import Element from './Element';
import SkeletonAvatar, { AvatarProps } from './Avatar';
import SkeletonButton from './Button';
import SkeletonInput from './Input';
import SkeletonImage from './Image';
/* This only for skeleton internal. */
interface SkeletonAvatarProps extends Omit<AvatarProps, 'active'> {
}
interface SkeletonAvatarProps extends Omit<AvatarProps, 'active'> {}
export interface SkeletonProps {
active?: boolean;
@ -170,5 +170,6 @@ Skeleton.defaultProps = {
Skeleton.Button = SkeletonButton;
Skeleton.Avatar = SkeletonAvatar;
Skeleton.Input = SkeletonInput;
Skeleton.Image = SkeletonImage;
export default Skeleton;

View File

@ -686,6 +686,27 @@ exports[`renders ./components/skeleton/demo/element.md correctly 1`] = `
/>
</div>
</div>
<br />
<div>
<div
class="ant-skeleton ant-skeleton-element"
>
<div
class="ant-skeleton-image"
>
<svg
class="ant-skeleton-image-svg"
viewBox="0 0 1098 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
class="ant-skeleton-image-path"
d="M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z"
/>
</svg>
</div>
</div>
</div>
</div>
`;

View File

@ -310,6 +310,27 @@ exports[`Skeleton button element size 3`] = `
</div>
`;
exports[`Skeleton image element should render normal 1`] = `
<div
class="ant-skeleton ant-skeleton-element"
>
<div
class="ant-skeleton-image"
>
<svg
class="ant-skeleton-image-svg"
viewBox="0 0 1098 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
class="ant-skeleton-image-path"
d="M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z"
/>
</svg>
</div>
</div>
`;
exports[`Skeleton input element active 1`] = `
<div
class="ant-skeleton ant-skeleton-element ant-skeleton-active"

View File

@ -14,6 +14,7 @@ describe('Skeleton', () => {
const genSkeletonButton = props => mount(<Skeleton.Button {...props} />);
const genSkeletonAvatar = props => mount(<Skeleton.Avatar {...props} />);
const genSkeletonInput = props => mount(<Skeleton.Input {...props} />);
const genSkeletonImage = props => mount(<Skeleton.Image {...props} />);
mountTest(Skeleton);
rtlTest(Skeleton);
@ -31,7 +32,7 @@ describe('Skeleton', () => {
it('should round title and paragraph', () => {
const wrapperSmall = genSkeleton({ round: true, title: true, paragraph: true });
expect(wrapperSmall.render()).toMatchSnapshot();
})
});
describe('avatar', () => {
it('size', () => {
@ -135,4 +136,11 @@ describe('Skeleton', () => {
expect(wrapperLarge.render()).toMatchSnapshot();
});
});
describe('image element', () => {
it('should render normal', () => {
const wrapper = genSkeletonImage();
expect(wrapper.render()).toMatchSnapshot();
});
});
});

View File

@ -1,17 +1,17 @@
---
order: 2
title:
zh-CN: 骨架按钮、头像和输入框
en-US: Skeleton button, avatar and input.
zh-CN: 骨架按钮、头像、输入框和图像
en-US: Skeleton button, avatar, input and Image.
---
## zh-CN
骨架按钮、头像和输入框
骨架按钮、头像、输入框和图像
## en-US
Skeleton button, avatar and input.
Skeleton button, avatar, input and Image.
```jsx
import { Skeleton, Switch, Form, Radio } from 'antd';
@ -113,6 +113,10 @@ class Demo extends React.Component {
</Form>
<Skeleton.Input style={{ width: '300px' }} active={inputActive} size={inputSize} />
</div>
<br />
<div>
<Skeleton.Image />
</div>
</div>
);
}

View File

@ -7,6 +7,7 @@
@skeleton-paragraph-prefix-cls: ~'@{skeleton-prefix-cls}-paragraph';
@skeleton-button-prefix-cls: ~'@{skeleton-prefix-cls}-button';
@skeleton-input-prefix-cls: ~'@{skeleton-prefix-cls}-input';
@skeleton-image-prefix-cls: ~'@{skeleton-prefix-cls}-image';
.@{skeleton-prefix-cls} {
display: table;
@ -99,6 +100,10 @@
.@{skeleton-input-prefix-cls} {
.skeleton-color();
}
.@{skeleton-image-prefix-cls} {
.skeleton-color();
}
}
// Skeleton element
@ -115,6 +120,10 @@
.@{skeleton-input-prefix-cls} {
.skeleton-element-input();
}
.@{skeleton-image-prefix-cls} {
.skeleton-element-image();
}
}
}
// Button
@ -168,6 +177,27 @@
}
}
// Image
.skeleton-element-image() {
display: flex;
align-items: center;
justify-content: center;
vertical-align: top;
background: @skeleton-color;
.skeleton-element-image-size(@image-size-base*2);
&-path {
fill: #bfbfbf;
}
&-svg {
.skeleton-element-image-size(@image-size-base);
max-width: @image-size-base * 4;
max-height: @image-size-base * 4;
}
}
.skeleton-element-avatar-size(@size) {
width: @size;
.skeleton-element-common-size(@size);
@ -196,6 +226,15 @@
.skeleton-element-common-size(@size);
}
.skeleton-element-image-size(@size) {
width: @size;
.skeleton-element-common-size(@size);
&.@{skeleton-image-prefix-cls}-circle {
border-radius: 50%;
}
}
.skeleton-element-common-size(@size) {
height: @size;
line-height: @size;

View File

@ -47,9 +47,6 @@
}
}
}
&-content {
width: @steps-desciption-max-width;
}
&-process .@{steps-prefix-cls}-item-icon {
position: relative;
top: -1px;

View File

@ -186,7 +186,7 @@
// Modal
// --
@modal-header-padding: 11px @padding-lg;
@modal-header-padding: 11px @modal-header-padding-horizontal;
@modal-footer-padding-vertical: @padding-sm;
@modal-header-close-size: 44px;
@modal-confirm-body-padding: 24px 24px 16px;
@ -292,3 +292,8 @@
// Progress
// ---
@progress-circle-text-font-size: 0.833333em;
// Image
// ---
@image-size-base: 48px;
@image-font-size-base: 24px;

View File

@ -490,9 +490,15 @@
// Modal
// --
@modal-header-padding-vertical: @padding-md;
@modal-header-padding-horizontal: @padding-lg;
@modal-body-padding: @padding-lg;
@modal-header-bg: @component-background;
@modal-header-padding: @padding-md @padding-lg;
@modal-header-padding: @modal-header-padding-vertical @modal-header-padding-horizontal;
@modal-header-border-width: @border-width-base;
@modal-header-border-style: @border-style-base;
@modal-header-title-line-height: 22px;
@modal-header-title-font-size: @font-size-lg;
@modal-header-border-color-split: @border-color-split;
@modal-header-close-size: 56px;
@modal-content-bg: @component-background;
@ -500,8 +506,10 @@
@modal-close-color: @text-color-secondary;
@modal-footer-bg: transparent;
@modal-footer-border-color-split: @border-color-split;
@modal-footer-border-style: @border-style-base;
@modal-footer-padding-vertical: 10px;
@modal-footer-padding-horizontal: 16px;
@modal-footer-border-width: @border-width-base;
@modal-mask-bg: fade(@black, 45%);
@modal-confirm-body-padding: 32px 32px 24px;
@ -966,4 +974,11 @@
@result-title-font-size: 24px;
@result-subtitle-font-size: @font-size-base;
@result-icon-font-size: 72px;
@result-extra-margin: 32px 0 0 0;
@result-extra-margin: 24px 0 0 0;
// Image
// ---
@image-size-base: 48px;
@image-font-size-base: 24px;
@image-bg: #ccc;
@image-color: #fff;

View File

@ -24,6 +24,7 @@ import {
TablePaginationConfig,
SortOrder,
TableLocale,
TableAction,
} from './interface';
import useSelection, { SELECTION_ALL, SELECTION_INVERT } from './hooks/useSelection';
import useSorter, { getSortData, SortState } from './hooks/useSorter';
@ -184,7 +185,11 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
// ============================ Events =============================
const changeEventInfo: Partial<ChangeEventInfo<RecordType>> = {};
const triggerOnChange = (info: Partial<ChangeEventInfo<RecordType>>, reset: boolean = false) => {
const triggerOnChange = (
info: Partial<ChangeEventInfo<RecordType>>,
action: TableAction,
reset: boolean = false,
) => {
const changeInfo = {
...changeEventInfo,
...info,
@ -211,12 +216,18 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
}
if (onChange) {
onChange(changeInfo.pagination!, changeInfo.filters!, changeInfo.sorter!, {
currentDataSource: getFilterData(
getSortData(rawData, changeInfo.sorterStates!, childrenColumnName),
changeInfo.filterStates!,
),
});
onChange(
changeInfo.pagination!,
changeInfo.filters!,
changeInfo.sorter!,
{
currentDataSource: getFilterData(
getSortData(rawData, changeInfo.sorterStates!, childrenColumnName),
changeInfo.filterStates!,
),
action,
},
);
}
};
@ -237,6 +248,7 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
sorter,
sorterStates,
},
'sort',
false,
);
};
@ -266,6 +278,7 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
filters,
filterStates,
},
'filter',
true,
);
};
@ -294,9 +307,12 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
// ========================== Pagination ==========================
const onPaginationChange = (current: number, pageSize: number) => {
triggerOnChange({
pagination: { ...changeEventInfo.pagination, current, pageSize },
});
triggerOnChange(
{
pagination: { ...changeEventInfo.pagination, current, pageSize },
},
'paginate',
);
};
const [mergedPagination, resetPagination] = usePagination(

View File

@ -375,6 +375,7 @@ describe('Table.filter', () => {
{},
{
currentDataSource: [],
action: 'filter',
},
);
});
@ -954,6 +955,7 @@ describe('Table.filter', () => {
{},
{
currentDataSource: [],
action: 'filter',
},
);
expect(wrapper.find('.ant-pagination-item')).toHaveLength(0);
@ -985,6 +987,7 @@ describe('Table.filter', () => {
{},
{
currentDataSource: [],
action: 'filter',
},
);
});
@ -1057,7 +1060,10 @@ describe('Table.filter', () => {
title: 'Name',
},
}),
expect.anything(),
{
currentDataSource: expect.anything(),
action: 'sort',
},
);
// Filter it
@ -1077,7 +1083,10 @@ describe('Table.filter', () => {
title: 'Name',
},
}),
expect.anything(),
{
currentDataSource: expect.anything(),
action: 'filter',
},
);
});

View File

@ -100,7 +100,7 @@ describe('Table.pagination', () => {
wrapper.find('.ant-select-selector').simulate('mousedown');
wrapper.find('.ant-select-item').last().simulate('click');
expect(scrollTo).toHaveBeenCalledTimes(2);
expect(scrollTo).toHaveBeenCalledTimes(3);
});
it('fires change event', () => {
@ -131,6 +131,7 @@ describe('Table.pagination', () => {
{ key: 2, name: 'Tom' },
{ key: 3, name: 'Jerry' },
],
action: 'paginate',
},
);
@ -179,6 +180,27 @@ describe('Table.pagination', () => {
);
});
// https://github.com/ant-design/ant-design/issues/24913
it('should onChange called when pageSize change', () => {
const onChange = jest.fn();
const onShowSizeChange = jest.fn();
const wrapper = mount(
createTable({
pagination: {
current: 1,
pageSize: 10,
total: 200,
onChange,
onShowSizeChange,
},
}),
);
wrapper.find('.ant-select-selector').simulate('mousedown');
expect(wrapper.find('.ant-select-item-option').length).toBe(4);
wrapper.find('.ant-select-item-option').at(1).simulate('click');
expect(onChange).toHaveBeenCalledWith(1, 20);
});
it('should not change page when pagination current is specified', () => {
const wrapper = mount(createTable({ pagination: { current: 2, pageSize: 1 } }));
expect(wrapper.find('.ant-pagination-item-2').hasClass('ant-pagination-item-active')).toBe(
@ -244,6 +266,7 @@ describe('Table.pagination', () => {
{ key: 2, name: 'Tom' },
{ key: 3, name: 'Jerry' },
],
action: 'paginate',
},
);
expect(onPaginationChange).toHaveBeenCalledWith(2, 10);

View File

@ -787,27 +787,56 @@ describe('Table.rowSelection', () => {
expect(onChange.mock.calls[0][1]).toEqual([expect.objectContaining({ name: 'bamboo' })]);
});
it('do not cache selected keys', () => {
const onChange = jest.fn();
const wrapper = mount(
<Table
dataSource={[{ name: 'light' }, { name: 'bamboo' }]}
rowSelection={{ onChange }}
rowKey="name"
/>,
);
describe('cache with selected keys', () => {
it('default not cache', () => {
const onChange = jest.fn();
const wrapper = mount(
<Table
dataSource={[{ name: 'light' }, { name: 'bamboo' }]}
rowSelection={{ onChange }}
rowKey="name"
/>,
);
wrapper
.find('tbody input')
.first()
.simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenCalledWith(['light'], [{ name: 'light' }]);
wrapper
.find('tbody input')
.first()
.simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenCalledWith(['light'], [{ name: 'light' }]);
wrapper.setProps({ dataSource: [{ name: 'bamboo' }] });
wrapper
.find('tbody input')
.first()
.simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenCalledWith(['bamboo'], [{ name: 'bamboo' }]);
wrapper.setProps({ dataSource: [{ name: 'bamboo' }] });
wrapper
.find('tbody input')
.first()
.simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenCalledWith(['bamboo'], [{ name: 'bamboo' }]);
});
it('cache with preserveSelectedRowKeys', () => {
const onChange = jest.fn();
const wrapper = mount(
<Table
dataSource={[{ name: 'light' }, { name: 'bamboo' }]}
rowSelection={{ onChange, preserveSelectedRowKeys: true }}
rowKey="name"
/>,
);
wrapper
.find('tbody input')
.first()
.simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenCalledWith(['light'], [{ name: 'light' }]);
wrapper.setProps({ dataSource: [{ name: 'bamboo' }] });
wrapper
.find('tbody input')
.first()
.simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenCalledWith(
['light', 'bamboo'],
[{ name: 'light' }, { name: 'bamboo' }],
);
});
});
});

View File

@ -11,12 +11,16 @@ title:
另外,本例也展示了筛选排序功能如何交给服务端实现,列不需要指定具体的 `onFilter``sorter` 函数,而是在把筛选和排序的参数发到服务端来处理。
当使用 `rowSelection` 时,请设置 `rowSelection.preserveSelectedRowKeys` 属性以保留 `key`
**注意,此示例使用 [模拟接口](https://randomuser.me),展示数据可能不准确,请打开网络面板查看请求。**
## en-US
This example shows how to fetch and present data from a remote server, and how to implement filtering and sorting in server side by sending related parameters to server.
Setting `rowSelection.preserveSelectedRowKeys` to keep the `key` when enable selection.
**Note, this example use [Mock API](https://randomuser.me) that you can look up in Network Console.**
```jsx

Some files were not shown because too many files have changed in this diff Show More