diff --git a/components/cascader/__tests__/__snapshots__/demo.test.js.snap b/components/cascader/__tests__/__snapshots__/demo.test.js.snap
index c1ce961fe7..185cec5e8e 100644
--- a/components/cascader/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/cascader/__tests__/__snapshots__/demo.test.js.snap
@@ -80,6 +80,46 @@ exports[`renders ./components/cascader/demo/change-on-select.md correctly 1`] =
`;
+exports[`renders ./components/cascader/demo/custom-dropdown.md correctly 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/cascader/demo/custom-render.md correctly 1`] = `
+Array [
-
+ ,
+ ,
+ ,
ab
-
-
+ ,
+ ,
+ ,
+
+
+
+
+
+
+
+
+ ,
+ ,
+ ,
+
+
+
+
+
+
+
+
+ ,
+]
`;
diff --git a/components/cascader/demo/custom-dropdown.md b/components/cascader/demo/custom-dropdown.md
new file mode 100644
index 0000000000..70c9e6b0aa
--- /dev/null
+++ b/components/cascader/demo/custom-dropdown.md
@@ -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 (
+
+ {menus}
+
+
The footer is not very short.
+
+ );
+}
+
+ReactDOM.render(
+ ,
+ mountNode,
+);
+```
diff --git a/components/cascader/demo/suffix.md b/components/cascader/demo/suffix.md
index ebbb61b17e..e426bc9f3f 100644
--- a/components/cascader/demo/suffix.md
+++ b/components/cascader/demo/suffix.md
@@ -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(
-
+ <>
}
options={options}
onChange={onChange}
placeholder="Please select"
/>
+
+
+
+
+
}
options={options}
onChange={onChange}
placeholder="Please select"
/>
-
,
+
+
+
+ >,
mountNode,
);
```
diff --git a/components/cascader/index.en-US.md b/components/cascader/index.en-US.md
index 5780557dc7..a22e9cfd75 100644
--- a/components/cascader/index.en-US.md
+++ b/components/cascader/index.en-US.md
@@ -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` | - | |
diff --git a/components/cascader/index.tsx b/components/cascader/index.tsx
index ede83d54ff..59ea5ae5cb 100644
--- a/components/cascader/index.tsx
+++ b/components/cascader/index.tsx
@@ -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 {
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 {
);
- let expandIcon = ;
- if (isRtlLayout) {
- expandIcon = ;
+ let expandIconNode;
+ if (expandIcon) {
+ expandIconNode = expandIcon;
+ } else {
+ expandIconNode = isRtlLayout ? : ;
}
const loadingIcon = (
@@ -621,10 +628,11 @@ class Cascader extends React.Component {
onPopupVisibleChange={this.handlePopupVisibleChange}
onChange={this.handleChange}
dropdownMenuColumnStyle={dropdownMenuColumnStyle}
- expandIcon={expandIcon}
+ expandIcon={expandIconNode}
loadingIcon={loadingIcon}
popupClassName={rcCascaderPopupClassName}
popupPlacement={this.getPopupPlacement(direction)}
+ dropdownRender={dropdownRender}
>
{input}
diff --git a/components/cascader/index.zh-CN.md b/components/cascader/index.zh-CN.md
index cf0bf66fb1..51e3d10833 100644
--- a/components/cascader/index.zh-CN.md
+++ b/components/cascader/index.zh-CN.md
@@ -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` | - | |
diff --git a/components/collapse/Collapse.tsx b/components/collapse/Collapse.tsx
index 3c08ae111b..85c2e4ce3e 100644
--- a/components/collapse/Collapse.tsx
+++ b/components/collapse/Collapse.tsx
@@ -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 {
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,
);
diff --git a/components/collapse/__tests__/__snapshots__/demo.test.js.snap b/components/collapse/__tests__/__snapshots__/demo.test.js.snap
index 55d8483d31..eca6fbf59d 100644
--- a/components/collapse/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/collapse/__tests__/__snapshots__/demo.test.js.snap
@@ -718,6 +718,125 @@ exports[`renders ./components/collapse/demo/extra.md correctly 1`] = `
`;
+exports[`renders ./components/collapse/demo/ghost.md correctly 1`] = `
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/collapse/demo/mix.md correctly 1`] = `
+
+ {text}
+
+
+ {text}
+
+
+ {text}
+
+ ,
+ mountNode,
+);
+```
diff --git a/components/collapse/index.en-US.md b/components/collapse/index.en-US.md
index e8bbbcbfd2..b107018b2e 100644
--- a/components/collapse/index.en-US.md
+++ b/components/collapse/index.en-US.md
@@ -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
diff --git a/components/collapse/index.zh-CN.md b/components/collapse/index.zh-CN.md
index 925cfb40ac..65f9f20253 100644
--- a/components/collapse/index.zh-CN.md
+++ b/components/collapse/index.zh-CN.md
@@ -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
diff --git a/components/collapse/style/index.less b/components/collapse/style/index.less
index 7be3c23aee..e854a9aed1 100644
--- a/components/collapse/style/index.less
+++ b/components/collapse/style/index.less
@@ -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 {
diff --git a/components/config-provider/__tests__/__snapshots__/components.test.js.snap b/components/config-provider/__tests__/__snapshots__/components.test.js.snap
index 43935822d6..3a40254eb8 100644
--- a/components/config-provider/__tests__/__snapshots__/components.test.js.snap
+++ b/components/config-provider/__tests__/__snapshots__/components.test.js.snap
@@ -16901,9 +16901,11 @@ exports[`ConfigProvider components Pagination configProvider 1`] = `
class="config-pagination-prev config-pagination-disabled"
title="Previous Page"
>
-
+
`;
+
+exports[`Drawer support closeIcon 1`] = `
+
+
+
+
+
+
+
+
+ Here is content of Drawer
+
+
+
+
+
+
+`;
diff --git a/components/drawer/index.en-US.md b/components/drawer/index.en-US.md
index ef2e2600d7..25e6da6157 100644
--- a/components/drawer/index.en-US.md
+++ b/components/drawer/index.en-US.md
@@ -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 | ` ` |
| 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' |
diff --git a/components/drawer/index.tsx b/components/drawer/index.tsx
index 309f5544eb..8f12376d9e 100644
--- a/components/drawer/index.tsx
+++ b/components/drawer/index.tsx
@@ -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 , prefixCls, onClose } = this.props;
return (
closable && (
// eslint-disable-next-line react/button-has-type
@@ -209,7 +210,7 @@ class Drawer extends React.Component
-
+ {closeIcon}
)
);
@@ -283,6 +284,7 @@ class Drawer extends React.Component ` |
| destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false |
| forceRender | 预渲染 Drawer 内元素 | boolean | false |
| getContainer | 指定 Drawer 挂载的 HTML 节点, false 为挂载在当前 dom | HTMLElement \| `() => HTMLElement` \| Selectors \| false | 'body' |
diff --git a/components/dropdown/__tests__/__snapshots__/demo.test.js.snap b/components/dropdown/__tests__/__snapshots__/demo.test.js.snap
index f880c6c956..cb88c19e00 100644
--- a/components/dropdown/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/dropdown/__tests__/__snapshots__/demo.test.js.snap
@@ -1,5 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`renders ./components/dropdown/demo/arrow.md correctly 1`] = `
+Array [
+
+
+ bottomLeft
+
+ ,
+
+
+ bottomCenter
+
+ ,
+
+
+ bottomRight
+
+ ,
+ ,
+
+
+ topLeft
+
+ ,
+
+
+ topCenter
+
+ ,
+
+
+ topRight
+
+ ,
+]
+`;
+
exports[`renders ./components/dropdown/demo/basic.md correctly 1`] = `
+
+
+ 1st menu item
+
+
+
+
+ 2nd menu item
+
+
+
+
+ 3rd menu item
+
+
+
+);
+
+ReactDOM.render(
+ <>
+
+ bottomLeft
+
+
+ bottomCenter
+
+
+ bottomRight
+
+
+
+ topLeft
+
+
+ topCenter
+
+
+ topRight
+
+ >,
+ 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;
+}
+```
diff --git a/components/dropdown/dropdown.tsx b/components/dropdown/dropdown.tsx
index 7c768577b5..5b972add30 100644
--- a/components/dropdown/dropdown.tsx
+++ b/components/dropdown/dropdown.tsx
@@ -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 (
document.body` | |
| overlay | The dropdown menu | [Menu](/components/menu) \| () => Menu | - | |
| overlayClassName | Class name of the dropdown root element | string | - | |
diff --git a/components/dropdown/index.zh-CN.md b/components/dropdown/index.zh-CN.md
index 82b5afbdb0..92a1caefb8 100644
--- a/components/dropdown/index.zh-CN.md
+++ b/components/dropdown/index.zh-CN.md
@@ -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 | - | |
diff --git a/components/dropdown/style/index.less b/components/dropdown/style/index.less
index cb47c6a945..b977cde04f 100644
--- a/components/dropdown/style/index.less
+++ b/components/dropdown/style/index.less
@@ -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;
diff --git a/components/form/Form.tsx b/components/form/Form.tsx
index 75f788750e..2f884d403c 100644
--- a/components/form/Form.tsx
+++ b/components/form/Form.tsx
@@ -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 = (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 = (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 = (props,
wrapperCol,
vertical: layout === 'vertical',
colon,
+ itemRef: __INTERNAL__.itemRef,
}),
[name, labelAlign, labelCol, wrapperCol, layout, colon],
);
@@ -100,12 +92,10 @@ const InternalForm: React.ForwardRefRenderFunction = (props,
return (
-
+
(validateStatus);
@@ -97,7 +102,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
}
}
- const { name: formName } = formContext;
const hasName = hasValidName(name);
// Cache Field NamePath
@@ -126,6 +130,9 @@ function FormItem(props: FormItemProps): React.ReactElement {
}
};
+ // ===================== Children Ref =====================
+ const getItemRef = useItemRef();
+
function renderLayout(
baseChildren: React.ReactNode,
fieldId?: string,
@@ -179,6 +186,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
[`${prefixCls}-item-has-error-leave`]:
!help && domErrorVisible && prevValidateStatusRef.current === 'error',
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
+ [`${prefixCls}-hidden`]: hidden,
};
// ======================= Children =======================
@@ -323,6 +331,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([
...toArray(trigger),
diff --git a/components/form/FormItemInput.tsx b/components/form/FormItemInput.tsx
index 4690b3e549..039c445f69 100644
--- a/components/form/FormItemInput.tsx
+++ b/components/form/FormItemInput.tsx
@@ -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;
diff --git a/components/form/__tests__/__snapshots__/demo.test.js.snap b/components/form/__tests__/__snapshots__/demo.test.js.snap
index cb29c5c3bf..f5c7fbc3d7 100644
--- a/components/form/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/form/__tests__/__snapshots__/demo.test.js.snap
@@ -2196,6 +2196,84 @@ exports[`renders ./components/form/demo/normal-login.md correctly 1`] = `
`;
+exports[`renders ./components/form/demo/ref-item.md correctly 1`] = `
+
+`;
+
exports[`renders ./components/form/demo/register.md correctly 1`] = `
+`;
+
exports[`Form Form.Item should support data-*、aria-* and custom attribute 1`] = `
+
+
+ ,
+ );
+ expect(wrapper).toMatchRenderedSnapshot();
+ });
});
diff --git a/components/form/__tests__/ref.test.tsx b/components/form/__tests__/ref.test.tsx
new file mode 100644
index 0000000000..1fee1ba93d
--- /dev/null
+++ b/components/form/__tests__/ref.test.tsx
@@ -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) => void;
+ show?: boolean;
+ }) => {
+ const [form] = Form.useForm();
+ const removeRef = React.useRef();
+ const testRef = React.useRef();
+ const listRef = React.useRef();
+
+ return (
+
+
+
+ )}
+
+
+
+
+
+
+ {fields =>
+ fields.map(field => (
+
+
+
+ ))
+ }
+
+
+ {
+ onRef(form.getFieldInstance('test'), testRef.current);
+ }}
+ >
+ Form.Item
+
+ {
+ onRef(form.getFieldInstance(['list', 0]), listRef.current);
+ }}
+ >
+ Form.List
+
+ {
+ onRef(form.getFieldInstance('remove'), removeRef.current);
+ }}
+ >
+ Removed
+
+
+ );
+ };
+
+ it('should ref work', () => {
+ const onRef = jest.fn();
+ const wrapper = mount( );
+
+ 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);
+ });
+});
diff --git a/components/form/context.tsx b/components/form/context.tsx
index 73dfb8c3a2..f87fc786c1 100644
--- a/components/form/context.tsx
+++ b/components/form/context.tsx
@@ -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({
labelAlign: 'right',
vertical: false,
+ itemRef: (() => {}) as any,
});
/**
diff --git a/components/form/demo/ref-item.md b/components/form/demo/ref-item.md
new file mode 100644
index 0000000000..dadd38ce13
--- /dev/null
+++ b/components/form/demo/ref-item.md
@@ -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 (
+
+
+
+
+
+ {fields =>
+ fields.map(field => (
+
+
+
+ ))
+ }
+
+
+ {
+ form.getFieldInstance('test').focus();
+ }}
+ >
+ Focus Form.Item
+
+ {
+ form.getFieldInstance(['list', 0]).focus();
+ }}
+ >
+ Focus Form.List
+
+
+ );
+};
+
+ReactDOM.render( , mountNode);
+```
diff --git a/components/form/hooks/useCacheErrors.ts b/components/form/hooks/useCacheErrors.ts
new file mode 100644
index 0000000000..149841f5e6
--- /dev/null
+++ b/components/form/hooks/useCacheErrors.ts
@@ -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];
+}
diff --git a/components/form/hooks/useForm.ts b/components/form/hooks/useForm.ts
new file mode 100644
index 0000000000..521d16eacd
--- /dev/null
+++ b/components/form/hooks/useForm.ts
@@ -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>({});
+
+ 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];
+}
diff --git a/components/form/hooks/useFrameState.ts b/components/form/hooks/useFrameState.ts
new file mode 100644
index 0000000000..10d2bd4af1
--- /dev/null
+++ b/components/form/hooks/useFrameState.ts
@@ -0,0 +1,48 @@
+import * as React from 'react';
+import { useRef } from 'react';
+import raf from 'raf';
+
+type Updater = (prev?: ValueType) => ValueType;
+
+export default function useFrameState(
+ defaultValue: ValueType,
+): [ValueType, (updater: Updater) => void] {
+ const [value, setValue] = React.useState(defaultValue);
+ const frameRef = useRef(null);
+ const batchRef = useRef[]>([]);
+ const destroyRef = useRef(false);
+
+ React.useEffect(
+ () => () => {
+ destroyRef.current = true;
+ raf.cancel(frameRef.current!);
+ },
+ [],
+ );
+
+ function setFrameValue(updater: Updater) {
+ 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];
+}
diff --git a/components/form/hooks/useItemRef.ts b/components/form/hooks/useItemRef.ts
new file mode 100644
index 0000000000..1e96ef8cb5
--- /dev/null
+++ b/components/form/hooks/useItemRef.ts
@@ -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;
+ ref?: React.Ref;
+ }>({});
+
+ function getRef(name: InternalNamePath, children: any) {
+ const childrenRef: React.Ref =
+ 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;
+}
diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md
index 269b48f832..6c84bf98de 100644
--- a/components/form/index.en-US.md
+++ b/components/form/index.en-US.md
@@ -29,6 +29,7 @@ High performance Form component with data scope management. Including data colle
| labelCol | label layout, like ` ` component. Set `span` `offset` value like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` | [object](/components/grid/#Col) | - | |
| layout | Form layout | `horizontal` \| `vertical` \| `inline` | `horizontal` | |
| name | Form name. Will be the prefix of Field `id` | string | - | |
+| preserve | Keep field value even when field removed | boolean | true | 4.4.0 |
| scrollToFirstError | Auto scroll to first failed field when submit | boolean | false | |
| size | Set field component size (antd components only) | `small` \| `middle` \| `large` | - | |
| validateMessages | Validation prompt template, description [see below](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - | |
@@ -86,6 +87,7 @@ Form field component for data bidirectional binding, validation, layout, and so
| labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with ` `. You can set `labelCol` on Form. If both exists, use Item first | [object](/components/grid/#Col) | - | |
| name | Field name, support array | [NamePath](#NamePath) | - | |
| normalize | Normalize value from component value before passing to Form instance | (value, prevValue, prevValues) => any | - | |
+| preserve | Keep field value even when field removed | boolean | true | 4.4.0 |
| required | Display required style. It will be generated by the validation rule | boolean | false | |
| rules | Rules for field validation. Click [here](#components-form-demo-basic) to see an example | [Rule](#Rule)[] | - | |
| shouldUpdate | Custom field update logic. See [below](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | |
@@ -95,6 +97,7 @@ Form field component for data bidirectional binding, validation, layout, and so
| validateTrigger | When to validate the value of children node | string \| string[] | `onChange` | |
| valuePropName | Props of children node, for example, the prop of Switch is 'checked'. This prop is an encapsulation of `getValueProps`, which will be invalid after customizing `getValueProps` | string | `value` | |
| wrapperCol | The layout for input controls, same as `labelCol`. You can set `wrapperCol` on Form. If both exists, use Item first | [object](/components/grid/#Col) | - | |
+| hidden | whether to hide Form.Item (still collect and validate value) | boolean | false | |
After wrapped by `Form.Item` with `name` property, `value`(or other property defined by `valuePropName`) `onChange`(or other property defined by `trigger`) props will be added to form controls, the flow of form data will be handled by Form which will cause:
@@ -185,21 +188,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
diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md
index ab2841a3f7..7d94e5f7b6 100644
--- a/components/form/index.zh-CN.md
+++ b/components/form/index.zh-CN.md
@@ -30,6 +30,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/ORmcdeaoO/Form.svg
| labelCol | label 标签布局,同 ` ` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}` 或 `sm: {span: 3, offset: 12}` | [object](/components/grid/#Col) | - | |
| layout | 表单布局 | `horizontal` \| `vertical` \| `inline` | `horizontal` | |
| name | 表单名称,会作为表单字段 `id` 前缀使用 | string | - | |
+| preserve | 当字段被删除时保留字段值 | boolean | true | 4.4.0 |
| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | boolean | false | |
| size | 设置字段组件的尺寸(仅限 antd 组件) | `small` \| `middle` \| `large` | - | |
| validateMessages | 验证提示模板,说明[见下](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - | |
@@ -86,6 +87,7 @@ const validateMessages = {
| labelAlign | 标签文本对齐方式 | `left` \| `right` | `right` | |
| labelCol | `label` 标签布局,同 ` ` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}` 或 `sm: {span: 3, offset: 12}`。你可以通过 Form 的 `labelCol` 进行统一设置。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid/#Col) | - | |
| name | 字段名,支持数组 | [NamePath](#NamePath) | - | |
+| preserve | 当字段被删除时保留字段值 | boolean | true | 4.4.0 |
| normalize | 组件获取值后进行转换,再放入 Form 中 | (value, prevValue, prevValues) => any | - | |
| required | 必填样式设置。如不设置,则会根据校验规则自动生成 | boolean | false | |
| rules | 校验规则,设置字段的校验逻辑。点击[此处](#components-form-demo-basic)查看示例 | [Rule](#Rule)[] | - | |
@@ -96,6 +98,7 @@ const validateMessages = {
| validateTrigger | 设置字段校验的时机 | string \| string[] | `onChange` | |
| valuePropName | 子节点的值的属性,如 Switch 的是 'checked'。该属性为 `getValueProps` 的封装,自定义 `getValueProps` 后会失效 | string | `value` | |
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 `labelCol`。你可以通过 Form 的 `wrapperCol` 进行统一设置。当和 Form 同时设置时,以 Item 为准。 | [object](/components/grid/#Col) | - | |
+| hidden | 是否隐藏字段(依然会收集和校验字段) | boolean | false | |
被设置了 `name` 属性的 `Form.Item` 包装的控件,表单控件会自动添加 `value`(或 `valuePropName` 指定的其他属性) `onChange`(或 `trigger` 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:
@@ -186,21 +189,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 返回示例
diff --git a/components/form/interface.ts b/components/form/interface.ts
index f9ec7a0e76..eae8bf49b5 100644
--- a/components/form/interface.ts
+++ b/components/form/interface.ts
@@ -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';
diff --git a/components/form/style/index.less b/components/form/style/index.less
index 51fa0a93ee..7bff52228a 100644
--- a/components/form/style/index.less
+++ b/components/form/style/index.less
@@ -66,6 +66,10 @@
margin-bottom: 0;
}
+ &-hidden {
+ display: none;
+ }
+
// ==============================================================
// = Label =
// ==============================================================
diff --git a/components/form/util.ts b/components/form/util.ts
index 93fd896890..d32067fb33 100644
--- a/components/form/util.ts
+++ b/components/form/util.ts
@@ -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(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 = (prev?: ValueType) => ValueType;
-
-export function useFrameState(
- defaultValue: ValueType,
-): [ValueType, (updater: Updater) => void] {
- const [value, setValue] = React.useState(defaultValue);
- const frameRef = React.useRef(null);
- const batchRef = React.useRef[]>([]);
- const destroyRef = React.useRef(false);
-
- React.useEffect(
- () => () => {
- destroyRef.current = true;
- raf.cancel(frameRef.current!);
- },
- [],
- );
-
- function setFrameValue(updater: Updater) {
- 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];
-}
diff --git a/components/grid/__tests__/__snapshots__/demo.test.js.snap b/components/grid/__tests__/__snapshots__/demo.test.js.snap
index a0f3b42096..4b6ca60339 100644
--- a/components/grid/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/grid/__tests__/__snapshots__/demo.test.js.snap
@@ -410,30 +410,78 @@ Array [
`;
exports[`renders ./components/grid/demo/flex-order.md correctly 1`] = `
-
+Array [
- 1 col-order-4
-
+
+ Normal
+
+
,
- 2 col-order-3
-
+
+ 1 col-order-4
+
+
+ 2 col-order-3
+
+
+ 3 col-order-2
+
+
+ 4 col-order-1
+
+ ,
- 3 col-order-2
-
+
+ Responsive
+
+ ,
- 4 col-order-1
-
-
+
+ 1 col-order-esponsive
+
+
+ 2 col-order-esponsive
+
+
+ 3 col-order-esponsive
+
+
+ 4 col-order-esponsive
+
+ ,
+]
`;
exports[`renders ./components/grid/demo/flex-stretch.md correctly 1`] = `
diff --git a/components/grid/demo/flex-order.md b/components/grid/demo/flex-order.md
index ab6a985c93..04e50c3eb4 100644
--- a/components/grid/demo/flex-order.md
+++ b/components/grid/demo/flex-order.md
@@ -14,23 +14,45 @@ title:
To change the element sort by order.
```jsx
-import { Row, Col } from 'antd';
+import { Row, Col, Divider } from 'antd';
ReactDOM.render(
-
-
- 1 col-order-4
-
-
- 2 col-order-3
-
-
- 3 col-order-2
-
-
- 4 col-order-1
-
-
,
+ <>
+
+ Normal
+
+
+
+ 1 col-order-4
+
+
+ 2 col-order-3
+
+
+ 3 col-order-2
+
+
+ 4 col-order-1
+
+
+
+ Responsive
+
+
+
+ 1 col-order-esponsive
+
+
+ 2 col-order-esponsive
+
+
+ 3 col-order-esponsive
+
+
+ 4 col-order-esponsive
+
+
+ >,
mountNode,
);
```
diff --git a/components/input/ResizableTextArea.tsx b/components/input/ResizableTextArea.tsx
deleted file mode 100644
index 66e4c8a873..0000000000
--- a/components/input/ResizableTextArea.tsx
+++ /dev/null
@@ -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 {
- 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 (
-
-
-
- );
- };
-
- render() {
- return this.renderTextArea();
- }
-}
-
-export default ResizableTextArea;
diff --git a/components/input/TextArea.tsx b/components/input/TextArea.tsx
index e824e89132..9d35d6f678 100644
--- a/components/input/TextArea.tsx
+++ b/components/input/TextArea.tsx
@@ -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;
-
-export interface TextAreaProps extends HTMLTextareaProps {
- prefixCls?: string;
- autoSize?: boolean | AutoSizeType;
- onPressEnter?: React.KeyboardEventHandler;
+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 {
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 {
};
handleChange = (e: React.ChangeEvent) => {
- 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) => {
- const { onPressEnter, onKeyDown } = this.props;
- if (e.keyCode === 13 && onPressEnter) {
- onPressEnter(e);
- }
- if (onKeyDown) {
- onKeyDown(e);
- }
- };
-
handleReset = (e: React.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 {
renderTextArea = (prefixCls: string) => {
return (
-
diff --git a/components/input/__tests__/__snapshots__/demo.test.js.snap b/components/input/__tests__/__snapshots__/demo.test.js.snap
index 592571f41f..10560f1d9c 100644
--- a/components/input/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/input/__tests__/__snapshots__/demo.test.js.snap
@@ -252,6 +252,7 @@ Array [
style="width:100px"
>
,
diff --git a/components/input/__tests__/textarea.test.js b/components/input/__tests__/textarea.test.js
index ad6e5301f5..0198cde007 100644
--- a/components/input/__tests__/textarea.test.js
+++ b/components/input/__tests__/textarea.test.js
@@ -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();
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(
,
);
- wrapper.instance().handleKeyDown({ keyCode: 13 });
+ wrapper.find(RcTextArea).instance().handleKeyDown({ keyCode: 13 });
expect(onPressEnter).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
});
diff --git a/components/input/calculateNodeHeight.tsx b/components/input/calculateNodeHeight.tsx
deleted file mode 100644
index 7b8fa9ca3c..0000000000
--- a/components/input/calculateNodeHeight.tsx
+++ /dev/null
@@ -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 };
-}
diff --git a/components/list/__tests__/__snapshots__/demo.test.js.snap b/components/list/__tests__/__snapshots__/demo.test.js.snap
index b5bd14b70b..42453a980b 100644
--- a/components/list/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/list/__tests__/__snapshots__/demo.test.js.snap
@@ -2142,9 +2142,11 @@ exports[`renders ./components/list/demo/vertical.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
diff --git a/components/list/__tests__/__snapshots__/pagination.test.js.snap b/components/list/__tests__/__snapshots__/pagination.test.js.snap
index a8a0b36797..91b7d01978 100644
--- a/components/list/__tests__/__snapshots__/pagination.test.js.snap
+++ b/components/list/__tests__/__snapshots__/pagination.test.js.snap
@@ -38,9 +38,11 @@ exports[`List.pagination renders pagination correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -128,9 +132,11 @@ exports[`List.pagination should change page size work 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
);
-
// change arrows direction in right-to-left direction
if (direction === 'rtl') {
- let temp: any;
- temp = prevIcon;
- prevIcon = nextIcon;
- nextIcon = temp;
-
- temp = jumpPrevIcon;
- jumpPrevIcon = jumpNextIcon;
- jumpNextIcon = temp;
+ [prevIcon, nextIcon] = [nextIcon, prevIcon];
+ [jumpPrevIcon, jumpNextIcon] = [jumpNextIcon, jumpPrevIcon];
}
return {
prevIcon,
diff --git a/components/pagination/__tests__/__snapshots__/demo.test.js.snap b/components/pagination/__tests__/__snapshots__/demo.test.js.snap
index 76a04f92b0..8ee0878f44 100644
--- a/components/pagination/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/pagination/__tests__/__snapshots__/demo.test.js.snap
@@ -16,9 +16,11 @@ exports[`renders ./components/pagination/demo/all.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
`;
@@ -368,8 +376,10 @@ exports[`renders ./components/pagination/demo/changer.md correctly 1`] = `
tabindex="0"
title="Previous Page"
>
-
+
`;
@@ -1119,8 +1139,10 @@ exports[`renders ./components/pagination/demo/jump.md correctly 1`] = `
tabindex="0"
title="Previous Page"
>
-
+
@@ -2006,8 +2046,10 @@ exports[`renders ./components/pagination/demo/more.md correctly 1`] = `
tabindex="0"
title="Previous Page"
>
-
+
`;
@@ -2369,9 +2417,11 @@ exports[`renders ./components/pagination/demo/total.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
`;
@@ -89,9 +93,11 @@ exports[`Pagination should be rendered correctly in RTL 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
`;
diff --git a/components/pagination/__tests__/index.test.js b/components/pagination/__tests__/index.test.js
index a046416ea5..70c7685154 100644
--- a/components/pagination/__tests__/index.test.js
+++ b/components/pagination/__tests__/index.test.js
@@ -29,21 +29,30 @@ describe('Pagination', () => {
return originalElement;
}
const wrapper = mount( );
- 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( );
- 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(
+ ,
+ );
+ 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);
});
});
diff --git a/components/pagination/style/index.less b/components/pagination/style/index.less
index a977be0355..4dfbee5a21 100644
--- a/components/pagination/style/index.less
+++ b/components/pagination/style/index.less
@@ -165,17 +165,18 @@
&-next {
outline: 0;
- a {
+ button {
color: @text-color;
user-select: none;
}
- &:hover a {
+ &:hover button {
border-color: @primary-5;
}
.@{pagination-prefix-cls}-item-link {
display: block;
+ width: 100%;
height: 100%;
font-size: 12px;
text-align: center;
@@ -198,7 +199,6 @@
&:hover,
&:focus {
cursor: not-allowed;
- a,
.@{pagination-prefix-cls}-item-link {
color: @disabled-color;
border-color: @border-color-base;
@@ -372,27 +372,18 @@
}
.@{pagination-prefix-cls}-item-link {
- &,
- &:hover,
- &:focus {
- color: @text-color-secondary;
- background: @disabled-bg;
- border-color: @border-color-base;
- cursor: not-allowed;
- }
+ color: @disabled-color;
+ background: @disabled-bg;
+ border-color: @border-color-base;
+ cursor: not-allowed;
}
- .@{pagination-prefix-cls}-jump-prev,
- .@{pagination-prefix-cls}-jump-next {
- &:focus,
- &:hover {
- .@{pagination-prefix-cls}-item-link-icon {
- opacity: 0;
- }
- .@{pagination-prefix-cls}-item-ellipsis {
- opacity: 1;
- }
- }
+ .@{pagination-prefix-cls}-item-link-icon {
+ opacity: 0;
+ }
+
+ .@{pagination-prefix-cls}-item-ellipsis {
+ opacity: 1;
}
}
}
diff --git a/components/progress/Circle.tsx b/components/progress/Circle.tsx
index d5f1d91fed..827f0fa670 100644
--- a/components/progress/Circle.tsx
+++ b/components/progress/Circle.tsx
@@ -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;
}
diff --git a/components/progress/Line.tsx b/components/progress/Line.tsx
index b36f8d3b17..22d5f4b43d 100644
--- a/components/progress/Line.tsx
+++ b/components/progress/Line.tsx
@@ -61,14 +61,15 @@ const Line: React.FC = 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 = 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 ? (
diff --git a/components/progress/__tests__/__snapshots__/index.test.js.snap b/components/progress/__tests__/__snapshots__/index.test.js.snap
index 9f2a41170a..af211c08e5 100644
--- a/components/progress/__tests__/__snapshots__/index.test.js.snap
+++ b/components/progress/__tests__/__snapshots__/index.test.js.snap
@@ -500,6 +500,35 @@ exports[`Progress render strokeColor 3`] = `
`;
+exports[`Progress render successColor progress 1`] = `
+
+`;
+
exports[`Progress render trailColor progress 1`] = `
{
rtlTest(Progress);
it('successPercent should decide the progress status when it exists', () => {
- const wrapper = mount(
);
+ const wrapper = mount(
);
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(
);
+ const wrapper = mount(
);
expect(wrapper.render()).toMatchSnapshot();
});
@@ -44,7 +44,7 @@ describe('Progress', () => {
const wrapper = mount(
`${percent} ${successPercent}`}
/>,
);
@@ -81,6 +81,13 @@ describe('Progress', () => {
expect(wrapper.render()).toMatchSnapshot();
});
+ it('render successColor progress', () => {
+ const wrapper = mount(
+ ,
+ );
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+
it('render dashboard zero gapDegree', () => {
const wrapper = mount( );
expect(wrapper.render()).toMatchSnapshot();
diff --git a/components/progress/demo/segment.md b/components/progress/demo/segment.md
index 9f9ab3b82e..a68e53c545 100644
--- a/components/progress/demo/segment.md
+++ b/components/progress/demo/segment.md
@@ -19,15 +19,15 @@ import { Tooltip, Progress } from 'antd';
ReactDOM.render(
<>
-
+
-
+
-
+
>,
mountNode,
diff --git a/components/progress/index.en-US.md b/components/progress/index.en-US.md
index 071781ec0f..1b70db5539 100644
--- a/components/progress/index.en-US.md
+++ b/components/progress/index.en-US.md
@@ -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"`
diff --git a/components/progress/index.zh-CN.md b/components/progress/index.zh-CN.md
index cb387f29be..d613b14a5b 100644
--- a/components/progress/index.zh-CN.md
+++ b/components/progress/index.zh-CN.md
@@ -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"`
diff --git a/components/progress/progress.tsx b/components/progress/progress.tsx
index 4382b75045..fef5698501 100644
--- a/components/progress/progress.tsx
+++ b/components/progress/progress.tsx
@@ -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 {
@@ -54,7 +63,11 @@ export default class Progress extends React.Component {
};
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 {
}
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 {
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 {
'status',
'format',
'trailColor',
- 'successPercent',
'strokeWidth',
'width',
'gapDegree',
@@ -157,6 +180,8 @@ export default class Progress extends React.Component {
'strokeLinecap',
'percent',
'steps',
+ 'success',
+ 'successPercent',
])}
className={classString}
>
diff --git a/components/radio/__tests__/__snapshots__/demo.test.js.snap b/components/radio/__tests__/__snapshots__/demo.test.js.snap
index 3aa3dbe8a9..77d5410566 100644
--- a/components/radio/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/radio/__tests__/__snapshots__/demo.test.js.snap
@@ -1110,6 +1110,7 @@ Array [
,
+ ,
@@ -1153,13 +1154,14 @@ Array [
@@ -1172,23 +1174,25 @@ Array [
,
+ ,
+ ,
@@ -1196,18 +1200,18 @@ Array [
@@ -1215,18 +1219,83 @@ Array [
+
+
+ Orange
+
+
+
,
+ ,
+ ,
+
+
+
+
+
+
+
+ Apple
+
+
+
+
+
+
+
+
+ Pear
+
+
+
+
+
+
diff --git a/components/radio/demo/radiogroup-options.md b/components/radio/demo/radiogroup-options.md
index 8dabc01900..929ce3275c 100644
--- a/components/radio/demo/radiogroup-options.md
+++ b/components/radio/demo/radiogroup-options.md
@@ -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 (
<>
-
+
+
+
+
+
+
+
>
);
diff --git a/components/radio/group.tsx b/components/radio/group.tsx
index ebf99c0465..40887df6c2 100644
--- a/components/radio/group.tsx
+++ b/components/radio/group.tsx
@@ -43,6 +43,7 @@ const RadioGroup: React.FC = props => {
prefixCls: customizePrefixCls,
className = '',
options,
+ optionType,
buttonStyle,
disabled,
children,
@@ -57,13 +58,14 @@ const RadioGroup: React.FC = 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 (
= props => {
return (
| |
-| 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<{ 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
diff --git a/components/radio/index.zh-CN.md b/components/radio/index.zh-CN.md
index e7903403e4..9924a9979e 100644
--- a/components/radio/index.zh-CN.md
+++ b/components/radio/index.zh-CN.md
@@ -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<{ 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<{ 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` | |
## 方法
diff --git a/components/radio/interface.tsx b/components/radio/interface.tsx
index d90e11c867..04a3109bb9 100644
--- a/components/radio/interface.tsx
+++ b/components/radio/interface.tsx
@@ -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;
}
diff --git a/components/radio/radio.tsx b/components/radio/radio.tsx
index a80ddfccd6..f9d0a2fca6 100644
--- a/components/radio/radio.tsx
+++ b/components/radio/radio.tsx
@@ -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 = (props, ref) => {
const context = React.useContext(RadioGroupContext);
@@ -12,6 +13,10 @@ const InternalRadio: React.ForwardRefRenderFunction = (prop
const innerRef = React.useRef();
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);
diff --git a/components/rate/__tests__/__snapshots__/demo.test.js.snap b/components/rate/__tests__/__snapshots__/demo.test.js.snap
index 74f1dc26ce..95e7968e77 100644
--- a/components/rate/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/rate/__tests__/__snapshots__/demo.test.js.snap
@@ -310,7 +310,7 @@ exports[`renders ./components/rate/demo/basic.md correctly 1`] = `
`;
exports[`renders ./components/rate/demo/character.md correctly 1`] = `
-
-
-
+ ,
+ ,
-
-
+ ,
+ ,
-
+ ,
+]
+`;
+
+exports[`renders ./components/rate/demo/character-function.md correctly 1`] = `
+Array [
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+]
`;
exports[`renders ./components/rate/demo/clear.md correctly 1`] = `
-
-
+ ,
allowClear: true
-
-
+ ,
+ ,
+ ,
allowClear: false
-
-
+ ,
+]
`;
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"
>
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:
,
+ 2:
,
+ 3:
,
+ 4:
,
+ 5:
,
+};
+
+ReactDOM.render(
+ <>
+
{
+ return index + 1;
+ }}
+ />
+
+ {
+ return customIcons[index + 1];
+ }}
+ />
+ >,
+ mountNode,
+);
+```
diff --git a/components/rate/demo/character.md b/components/rate/demo/character.md
index 2450176c57..72ecb45004 100644
--- a/components/rate/demo/character.md
+++ b/components/rate/demo/character.md
@@ -18,13 +18,13 @@ import { Rate } from 'antd';
import { HeartOutlined } from '@ant-design/icons';
ReactDOM.render(
-
+ <>
} allowHalf />
-
,
+ >,
mountNode,
);
```
diff --git a/components/rate/demo/clear.md b/components/rate/demo/clear.md
index 9111890087..4d26c3a204 100644
--- a/components/rate/demo/clear.md
+++ b/components/rate/demo/clear.md
@@ -17,13 +17,13 @@ Support set allow to clear star when click again.
import { Rate } from 'antd';
ReactDOM.render(
-
+ <>
allowClear: true
allowClear: false
-
,
+ >,
mountNode,
);
```
diff --git a/components/rate/index.en-US.md b/components/rate/index.en-US.md
index d9ecf67826..e72d21ac04 100644
--- a/components/rate/index.en-US.md
+++ b/components/rate/index.en-US.md
@@ -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 | [` `](/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 | [` `](/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
diff --git a/components/rate/index.zh-CN.md b/components/rate/index.zh-CN.md
index e15dacd9c5..c7a65d3839 100644
--- a/components/rate/index.zh-CN.md
+++ b/components/rate/index.zh-CN.md
@@ -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 | [` `](/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 | [` `](/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) | | |
## 方法
diff --git a/components/result/__tests__/__snapshots__/demo.test.js.snap b/components/result/__tests__/__snapshots__/demo.test.js.snap
index f2bd16d27c..d4e9c23291 100644
--- a/components/result/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/result/__tests__/__snapshots__/demo.test.js.snap
@@ -1108,6 +1108,26 @@ exports[`renders ./components/result/demo/error.md correctly 1`] = `
>
Please check and modify the following information before resubmitting.
+
@@ -1190,26 +1210,6 @@ exports[`renders ./components/result/demo/error.md correctly 1`] = `
-
`;
diff --git a/components/result/index.tsx b/components/result/index.tsx
index 151058fd25..86a49a9e46 100644
--- a/components/result/index.tsx
+++ b/components/result/index.tsx
@@ -104,8 +104,8 @@ const Result: ResultType = props => (
{renderIcon(prefixCls, props)}
{title}
{subTitle &&
{subTitle}
}
- {children &&
{children}
}
{renderExtra(prefixCls, props)}
+ {children &&
{children}
}
);
}}
diff --git a/components/select/__tests__/__snapshots__/demo.test.js.snap b/components/select/__tests__/__snapshots__/demo.test.js.snap
index 371315f8b9..1b85ebd754 100644
--- a/components/select/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/select/__tests__/__snapshots__/demo.test.js.snap
@@ -741,7 +741,7 @@ exports[`renders ./components/select/demo/custom-tag-render.md correctly 1`] = `
gold
@@ -770,7 +770,7 @@ exports[`renders ./components/select/demo/custom-tag-render.md correctly 1`] = `
cyan
diff --git a/components/skeleton/Avatar.tsx b/components/skeleton/Avatar.tsx
index 547b4ce1a2..b741c1501f 100644
--- a/components/skeleton/Avatar.tsx
+++ b/components/skeleton/Avatar.tsx
@@ -23,7 +23,7 @@ const SkeletonAvatar = (props: AvatarProps) => {
);
};
return {renderSkeletonAvatar} ;
-}
+};
SkeletonAvatar.defaultProps = {
size: 'default',
diff --git a/components/skeleton/Element.tsx b/components/skeleton/Element.tsx
index 836e94bff6..232ee919ba 100644
--- a/components/skeleton/Element.tsx
+++ b/components/skeleton/Element.tsx
@@ -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 (
{
);
};
-
export default Element;
diff --git a/components/skeleton/Image.tsx b/components/skeleton/Image.tsx
new file mode 100644
index 0000000000..a2075e6712
--- /dev/null
+++ b/components/skeleton/Image.tsx
@@ -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 {}
+
+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 (
+
+ );
+ };
+ return {renderSkeletonImage} ;
+};
+
+export default SkeletonImage;
diff --git a/components/skeleton/Skeleton.tsx b/components/skeleton/Skeleton.tsx
index 25a8ed8741..918f9f5c0a 100644
--- a/components/skeleton/Skeleton.tsx
+++ b/components/skeleton/Skeleton.tsx
@@ -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 {
-}
+interface SkeletonAvatarProps extends Omit {}
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;
diff --git a/components/skeleton/__tests__/__snapshots__/demo.test.js.snap b/components/skeleton/__tests__/__snapshots__/demo.test.js.snap
index dfd6321847..d10ec3c9f7 100644
--- a/components/skeleton/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/skeleton/__tests__/__snapshots__/demo.test.js.snap
@@ -686,6 +686,27 @@ exports[`renders ./components/skeleton/demo/element.md correctly 1`] = `
/>
+
+
`;
diff --git a/components/skeleton/__tests__/__snapshots__/index.test.js.snap b/components/skeleton/__tests__/__snapshots__/index.test.js.snap
index 0aa55a5bc3..e248c8ebdd 100644
--- a/components/skeleton/__tests__/__snapshots__/index.test.js.snap
+++ b/components/skeleton/__tests__/__snapshots__/index.test.js.snap
@@ -310,6 +310,27 @@ exports[`Skeleton button element size 3`] = `
`;
+exports[`Skeleton image element should render normal 1`] = `
+
+`;
+
exports[`Skeleton input element active 1`] = `
{
const genSkeletonButton = props => mount( );
const genSkeletonAvatar = props => mount( );
const genSkeletonInput = props => mount( );
+ const genSkeletonImage = props => mount( );
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();
+ });
+ });
});
diff --git a/components/skeleton/demo/element.md b/components/skeleton/demo/element.md
index 8ead48a05f..362c6bebea 100644
--- a/components/skeleton/demo/element.md
+++ b/components/skeleton/demo/element.md
@@ -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 {
+
+
+
+
);
}
diff --git a/components/skeleton/style/index.less b/components/skeleton/style/index.less
index cb3f9496d9..29be4e0910 100644
--- a/components/skeleton/style/index.less
+++ b/components/skeleton/style/index.less
@@ -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;
diff --git a/components/steps/style/progress-dot.less b/components/steps/style/progress-dot.less
index 34b76b8a11..534a2ef5aa 100644
--- a/components/steps/style/progress-dot.less
+++ b/components/steps/style/progress-dot.less
@@ -47,9 +47,6 @@
}
}
}
- &-content {
- width: @steps-desciption-max-width;
- }
&-process .@{steps-prefix-cls}-item-icon {
position: relative;
top: -1px;
diff --git a/components/style/themes/compact.less b/components/style/themes/compact.less
index a643b5d54b..841d77402c 100644
--- a/components/style/themes/compact.less
+++ b/components/style/themes/compact.less
@@ -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;
diff --git a/components/style/themes/default.less b/components/style/themes/default.less
index 90384c1749..fc1e180649 100644
--- a/components/style/themes/default.less
+++ b/components/style/themes/default.less
@@ -489,9 +489,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;
@@ -499,8 +505,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;
@@ -965,4 +973,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;
diff --git a/components/table/Table.tsx b/components/table/Table.tsx
index 80032beb00..cc9f57675a 100644
--- a/components/table/Table.tsx
+++ b/components/table/Table.tsx
@@ -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';
@@ -123,6 +124,12 @@ function Table(props: TableProps) {
showSorterTooltip = true,
} = props;
+ devWarning(
+ !(typeof rowKey === 'function' && rowKey.length > 1),
+ 'Table',
+ '`index` parameter of `rowKey` function is deprecated. There is no guarantee that it will work as expected.',
+ );
+
const screens = useBreakpoint();
const mergedColumns = React.useMemo(() => {
const matched = new Set(Object.keys(screens).filter((m: Breakpoint) => screens[m]));
@@ -184,7 +191,11 @@ function Table(props: TableProps) {
// ============================ Events =============================
const changeEventInfo: Partial> = {};
- const triggerOnChange = (info: Partial>, reset: boolean = false) => {
+ const triggerOnChange = (
+ info: Partial>,
+ action: TableAction,
+ reset: boolean = false,
+ ) => {
const changeInfo = {
...changeEventInfo,
...info,
@@ -211,12 +222,18 @@ function Table(props: TableProps) {
}
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 +254,7 @@ function Table(props: TableProps) {
sorter,
sorterStates,
},
+ 'sort',
false,
);
};
@@ -266,6 +284,7 @@ function Table(props: TableProps) {
filters,
filterStates,
},
+ 'filter',
true,
);
};
@@ -294,9 +313,12 @@ function Table(props: TableProps) {
// ========================== Pagination ==========================
const onPaginationChange = (current: number, pageSize: number) => {
- triggerOnChange({
- pagination: { ...changeEventInfo.pagination, current, pageSize },
- });
+ triggerOnChange(
+ {
+ pagination: { ...changeEventInfo.pagination, current, pageSize },
+ },
+ 'paginate',
+ );
};
const [mergedPagination, resetPagination] = usePagination(
diff --git a/components/table/__tests__/Table.filter.test.js b/components/table/__tests__/Table.filter.test.js
index 527d8d6bc1..8a4990d9a5 100644
--- a/components/table/__tests__/Table.filter.test.js
+++ b/components/table/__tests__/Table.filter.test.js
@@ -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',
+ },
);
});
diff --git a/components/table/__tests__/Table.pagination.test.js b/components/table/__tests__/Table.pagination.test.js
index 21285c755b..776223dd00 100644
--- a/components/table/__tests__/Table.pagination.test.js
+++ b/components/table/__tests__/Table.pagination.test.js
@@ -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);
diff --git a/components/table/__tests__/Table.rowSelection.test.js b/components/table/__tests__/Table.rowSelection.test.js
index 2bf5fec6ad..e7fa2fd8b6 100644
--- a/components/table/__tests__/Table.rowSelection.test.js
+++ b/components/table/__tests__/Table.rowSelection.test.js
@@ -43,7 +43,21 @@ describe('Table.rowSelection', () => {
.find('BodyRow')
.map(row => {
const { key } = row.props().record;
- if (!row.find('input').props().checked) {
+ if (!row.find('input').at(0).props().checked) {
+ return null;
+ }
+
+ return key;
+ })
+ .filter(key => key !== null);
+ }
+
+ function getIndeterminateSelection(wrapper) {
+ return wrapper
+ .find('BodyRow')
+ .map(row => {
+ const { key } = row.props().record;
+ if (!row.find('Checkbox').at(0).props().indeterminate) {
return null;
}
@@ -229,6 +243,19 @@ describe('Table.rowSelection', () => {
expect(handleSelectAll).toHaveBeenCalledWith(false, [], data);
});
+ it('works with selectAll option inside selection menu', () => {
+ const handleChange = jest.fn();
+ const rowSelection = {
+ onChange: handleChange,
+ selections: true,
+ };
+ const wrapper = mount(createTable({ rowSelection }));
+
+ const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
+ dropdownWrapper.find('.ant-dropdown-menu-item').first().simulate('click');
+ expect(handleChange.mock.calls[0][0]).toEqual([0, 1, 2, 3]);
+ });
+
it('render with default selection correctly', () => {
const rowSelection = {
selections: true,
@@ -787,27 +814,296 @@ 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(
- ,
- );
+ describe('supports children', () => {
+ const dataWithChildren = [
+ { key: 0, name: 'Jack' },
+ { key: 1, name: 'Lucy' },
+ { key: 2, name: 'Tom' },
+ {
+ key: 3,
+ name: 'Jerry',
+ children: [
+ {
+ key: 4,
+ name: 'Jerry Jack',
+ },
+ {
+ key: 5,
+ name: 'Jerry Lucy',
+ },
+ {
+ key: 6,
+ name: 'Jerry Tom',
+ children: [
+ {
+ key: 7,
+ name: 'Jerry Tom Jack',
+ },
+ {
+ key: 8,
+ name: 'Jerry Tom Lucy',
+ },
+ {
+ key: 9,
+ name: 'Jerry Tom Tom',
+ },
+ ],
+ },
+ ],
+ },
+ ];
+ describe('supports checkStrictly', () => {
+ it('use data entity key', () => {
+ const onChange = jest.fn();
- wrapper
- .find('tbody input')
- .first()
- .simulate('change', { target: { checked: true } });
- expect(onChange).toHaveBeenCalledWith(['light'], [{ name: 'light' }]);
+ const table = createTable({
+ dataSource: dataWithChildren,
+ defaultExpandAllRows: true,
+ rowSelection: {
+ checkStrictly: false,
+ onChange,
+ },
+ });
+ const wrapper = mount(table);
+ const checkboxes = wrapper.find('input');
- wrapper.setProps({ dataSource: [{ name: 'bamboo' }] });
- wrapper
- .find('tbody input')
- .first()
- .simulate('change', { target: { checked: true } });
- expect(onChange).toHaveBeenCalledWith(['bamboo'], [{ name: 'bamboo' }]);
+ checkboxes.at(4).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper)).toEqual([3, 4, 5, 6, 7, 8, 9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(onChange.mock.calls[0][0]).toEqual([3, 4, 5, 6, 7, 8, 9]);
+ checkboxes.at(7).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper)).toEqual([4, 5]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([3]);
+ expect(onChange.mock.calls[1][0]).toEqual([4, 5]);
+ });
+ it('use function rowkey', () => {
+ const onChange = jest.fn();
+ const table = createTable({
+ dataSource: dataWithChildren,
+ defaultExpandAllRows: true,
+ rowSelection: {
+ checkStrictly: false,
+ onChange,
+ },
+ rowKey: entity => entity.name,
+ });
+ const wrapper = mount(table);
+ const checkboxes = wrapper.find('input');
+
+ checkboxes.at(4).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper)).toEqual([3, 4, 5, 6, 7, 8, 9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(onChange.mock.calls[0][0]).toEqual([
+ 'Jerry',
+ 'Jerry Jack',
+ 'Jerry Lucy',
+ 'Jerry Tom',
+ 'Jerry Tom Jack',
+ 'Jerry Tom Lucy',
+ 'Jerry Tom Tom',
+ ]);
+ checkboxes.at(7).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper)).toEqual([4, 5]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([3]);
+ expect(onChange.mock.calls[1][0]).toEqual(['Jerry Jack', 'Jerry Lucy']);
+ });
+ it('use string rowkey', () => {
+ const onChange = jest.fn();
+ const table = createTable({
+ dataSource: dataWithChildren,
+ defaultExpandAllRows: true,
+ rowSelection: {
+ checkStrictly: false,
+ onChange,
+ },
+ rowKey: 'name',
+ });
+ const wrapper = mount(table);
+ const checkboxes = wrapper.find('input');
+
+ checkboxes.at(4).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper)).toEqual([3, 4, 5, 6, 7, 8, 9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(onChange.mock.calls[0][0]).toEqual([
+ 'Jerry',
+ 'Jerry Jack',
+ 'Jerry Lucy',
+ 'Jerry Tom',
+ 'Jerry Tom Jack',
+ 'Jerry Tom Lucy',
+ 'Jerry Tom Tom',
+ ]);
+ checkboxes.at(7).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper)).toEqual([4, 5]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([3]);
+ expect(onChange.mock.calls[1][0]).toEqual(['Jerry Jack', 'Jerry Lucy']);
+ });
+ it('initialized correctly', () => {
+ const table = createTable({
+ dataSource: dataWithChildren,
+ defaultExpandAllRows: true,
+ rowSelection: {
+ checkStrictly: false,
+ selectedRowKeys: [7, 8, 9],
+ },
+ rowKey: 'key',
+ });
+ const wrapper = mount(table);
+ expect(getSelections(wrapper)).toEqual([6, 7, 8, 9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([3]);
+ });
+ it('works with disabled checkbox', () => {
+ const onChange = jest.fn();
+
+ const table = createTable({
+ dataSource: dataWithChildren,
+ defaultExpandAllRows: true,
+ rowSelection: {
+ checkStrictly: false,
+ onChange,
+ getCheckboxProps(record) {
+ return {
+ disabled: record.name === 'Jerry Tom',
+ };
+ },
+ },
+ });
+ const wrapper = mount(table);
+ const checkboxes = wrapper.find('input');
+
+ checkboxes.at(10).simulate('change', { target: { checked: true } });
+ checkboxes.at(4).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper).sort()).toEqual([3, 4, 5, 9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(Array.from(onChange.mock.calls[1][0]).sort()).toEqual([3, 4, 5, 9]);
+ checkboxes.at(4).simulate('change', { target: { checked: false } });
+ expect(getSelections(wrapper)).toEqual([9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(onChange.mock.calls[2][0]).toEqual([9]);
+ });
+ it('works with disabled checkbox and function rowkey', () => {
+ const onChange = jest.fn();
+
+ const table = createTable({
+ dataSource: dataWithChildren,
+ defaultExpandAllRows: true,
+ rowSelection: {
+ checkStrictly: false,
+ onChange,
+ getCheckboxProps(record) {
+ return {
+ disabled: record.name === 'Jerry Tom',
+ };
+ },
+ },
+ rowKey: entity => entity.name,
+ });
+ const wrapper = mount(table);
+ const checkboxes = wrapper.find('input');
+
+ checkboxes.at(10).simulate('change', { target: { checked: true } });
+ checkboxes.at(4).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper).sort()).toEqual([3, 4, 5, 9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(Array.from(onChange.mock.calls[1][0]).sort()).toEqual([
+ 'Jerry',
+ 'Jerry Jack',
+ 'Jerry Lucy',
+ 'Jerry Tom Tom',
+ ]);
+ checkboxes.at(4).simulate('change', { target: { checked: false } });
+ expect(getSelections(wrapper)).toEqual([9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(onChange.mock.calls[2][0]).toEqual(['Jerry Tom Tom']);
+ });
+ it('works with disabled checkbox and string rowkey', () => {
+ const onChange = jest.fn();
+
+ const table = createTable({
+ dataSource: dataWithChildren,
+ defaultExpandAllRows: true,
+ rowSelection: {
+ checkStrictly: false,
+ onChange,
+ getCheckboxProps(record) {
+ return {
+ disabled: record.name === 'Jerry Tom',
+ };
+ },
+ },
+ rowKey: 'name',
+ });
+ const wrapper = mount(table);
+ const checkboxes = wrapper.find('input');
+
+ checkboxes.at(10).simulate('change', { target: { checked: true } });
+ checkboxes.at(4).simulate('change', { target: { checked: true } });
+ expect(getSelections(wrapper).sort()).toEqual([3, 4, 5, 9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(Array.from(onChange.mock.calls[1][0]).sort()).toEqual([
+ 'Jerry',
+ 'Jerry Jack',
+ 'Jerry Lucy',
+ 'Jerry Tom Tom',
+ ]);
+ checkboxes.at(4).simulate('change', { target: { checked: false } });
+ expect(getSelections(wrapper)).toEqual([9]);
+ expect(getIndeterminateSelection(wrapper)).toEqual([]);
+ expect(onChange.mock.calls[2][0]).toEqual(['Jerry Tom Tom']);
+ });
+ });
+ });
+
+ describe('cache with selected keys', () => {
+ it('default not cache', () => {
+ const onChange = jest.fn();
+ const wrapper = mount(
+ ,
+ );
+
+ 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' }]);
+ });
+
+ it('cache with preserveSelectedRowKeys', () => {
+ const onChange = jest.fn();
+ const wrapper = mount(
+ ,
+ );
+
+ 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' }],
+ );
+ });
});
});
diff --git a/components/table/__tests__/Table.test.js b/components/table/__tests__/Table.test.js
index 44f4b2c564..6d0e55d366 100644
--- a/components/table/__tests__/Table.test.js
+++ b/components/table/__tests__/Table.test.js
@@ -221,4 +221,31 @@ describe('Table', () => {
expect(td.getDOMNode().attributes.getNamedItem('title')).toBeFalsy();
});
});
+
+ it('warn about rowKey when using index parameter', () => {
+ warnSpy.mockReset();
+ const columns = [
+ {
+ title: 'Name',
+ key: 'name',
+ dataIndex: 'name',
+ },
+ ];
+ mount( record + index} />);
+ expect(warnSpy).toBeCalledWith(
+ 'Warning: [antd: Table] `index` parameter of `rowKey` function is deprecated. There is no guarantee that it will work as expected.',
+ );
+ });
+ it('not warn about rowKey', () => {
+ warnSpy.mockReset();
+ const columns = [
+ {
+ title: 'Name',
+ key: 'name',
+ dataIndex: 'name',
+ },
+ ];
+ mount( record.key} />);
+ expect(warnSpy).not.toBeCalled();
+ });
});
diff --git a/components/table/__tests__/__snapshots__/Table.expand.test.js.snap b/components/table/__tests__/__snapshots__/Table.expand.test.js.snap
index 05b358fcbf..9972e0b7a6 100644
--- a/components/table/__tests__/__snapshots__/Table.expand.test.js.snap
+++ b/components/table/__tests__/__snapshots__/Table.expand.test.js.snap
@@ -87,9 +87,11 @@ exports[`Table.expand click to expand 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
diff --git a/components/table/__tests__/__snapshots__/Table.pagination.test.js.snap b/components/table/__tests__/__snapshots__/Table.pagination.test.js.snap
index ca104b8e85..dd73bc0bec 100644
--- a/components/table/__tests__/__snapshots__/Table.pagination.test.js.snap
+++ b/components/table/__tests__/__snapshots__/Table.pagination.test.js.snap
@@ -91,9 +91,11 @@ exports[`Table.pagination Accepts pagination as true 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -234,9 +238,11 @@ exports[`Table.pagination renders pagination correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -406,9 +414,11 @@ exports[`Table.pagination renders pagination topLeft and bottomRight 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
diff --git a/components/table/__tests__/__snapshots__/Table.rowSelection.test.js.snap b/components/table/__tests__/__snapshots__/Table.rowSelection.test.js.snap
index ada679addc..150e7d393c 100644
--- a/components/table/__tests__/__snapshots__/Table.rowSelection.test.js.snap
+++ b/components/table/__tests__/__snapshots__/Table.rowSelection.test.js.snap
@@ -262,9 +262,11 @@ exports[`Table.rowSelection fix expand on th left when selection column fixed on
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -546,9 +550,11 @@ exports[`Table.rowSelection fix selection column on the left 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -835,9 +843,11 @@ exports[`Table.rowSelection fix selection column on the left when any other colu
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -1189,9 +1201,11 @@ exports[`Table.rowSelection should support getPopupContainer 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -1516,9 +1532,11 @@ exports[`Table.rowSelection should support getPopupContainer from ConfigProvider
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -1782,9 +1802,11 @@ exports[`Table.rowSelection use column as selection column when key is \`selecti
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
diff --git a/components/table/__tests__/__snapshots__/Table.sorter.test.js.snap b/components/table/__tests__/__snapshots__/Table.sorter.test.js.snap
index b1c37ca2e2..ff356298f6 100644
--- a/components/table/__tests__/__snapshots__/Table.sorter.test.js.snap
+++ b/components/table/__tests__/__snapshots__/Table.sorter.test.js.snap
@@ -191,9 +191,11 @@ exports[`Table.sorter should support defaultOrder in Column 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
diff --git a/components/table/__tests__/__snapshots__/demo.test.js.snap b/components/table/__tests__/__snapshots__/demo.test.js.snap
index ddd4a445b7..6c74690ae2 100644
--- a/components/table/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/table/__tests__/__snapshots__/demo.test.js.snap
@@ -458,9 +458,11 @@ exports[`renders ./components/table/demo/basic.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -671,9 +675,11 @@ exports[`renders ./components/table/demo/bordered.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -947,9 +955,11 @@ exports[`renders ./components/table/demo/colspan-rowspan.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -1281,9 +1293,11 @@ exports[`renders ./components/table/demo/custom-filter-panel.md correctly 1`] =
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -1477,9 +1493,11 @@ exports[`renders ./components/table/demo/drag-sorting.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -3698,9 +3718,11 @@ Array [
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -3915,9 +3939,11 @@ exports[`renders ./components/table/demo/edit-cell.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -4334,9 +4362,11 @@ exports[`renders ./components/table/demo/edit-row.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -5056,9 +5092,11 @@ exports[`renders ./components/table/demo/ellipsis-custom-tooltip.md correctly 1`
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -5345,9 +5385,11 @@ exports[`renders ./components/table/demo/expand.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -5418,48 +5462,121 @@ exports[`renders ./components/table/demo/expand.md correctly 1`] = `
`;
exports[`renders ./components/table/demo/expand-children.md correctly 1`] = `
-
+Array [
-
,
+
+
+
+
-
+ class="ant-table-row-indent indent-level-0"
+ style="padding-left:0px"
+ />
+
+ Joe Black
+
+
+ 32
+
+
+ Sidney No. 1 Lake Park
+
+
+
+
+
+
-
-
-
+ ,
+]
`;
exports[`renders ./components/table/demo/fixed-columns.md correctly 1`] = `
@@ -5972,9 +6050,11 @@ exports[`renders ./components/table/demo/fixed-columns.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -6880,9 +6962,11 @@ exports[`renders ./components/table/demo/fixed-columns-header.md correctly 1`] =
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -10019,9 +10117,11 @@ exports[`renders ./components/table/demo/jsx.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -10431,9 +10533,11 @@ exports[`renders ./components/table/demo/multiple-sorter.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -10838,9 +10944,11 @@ Array [
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -11152,9 +11262,11 @@ exports[`renders ./components/table/demo/nested-table.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -11410,9 +11524,11 @@ exports[`renders ./components/table/demo/pagination.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -12209,9 +12331,11 @@ Array [
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -12536,9 +12662,11 @@ exports[`renders ./components/table/demo/resizable-column.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -12671,9 +12801,11 @@ exports[`renders ./components/table/demo/responsive.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -13048,9 +13182,11 @@ exports[`renders ./components/table/demo/row-selection.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -13616,9 +13754,11 @@ exports[`renders ./components/table/demo/row-selection-and-operation.md correctl
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -14231,9 +14373,11 @@ exports[`renders ./components/table/demo/row-selection-custom.md correctly 1`] =
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -14622,9 +14768,11 @@ exports[`renders ./components/table/demo/row-selection-custom-debug.md correctly
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -14855,9 +15005,11 @@ exports[`renders ./components/table/demo/size.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
@@ -15048,9 +15202,11 @@ exports[`renders ./components/table/demo/size.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
diff --git a/components/table/demo/ajax.md b/components/table/demo/ajax.md
index a040e4bfb5..e1149c9d94 100644
--- a/components/table/demo/ajax.md
+++ b/components/table/demo/ajax.md
@@ -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
diff --git a/components/table/demo/expand-children.md b/components/table/demo/expand-children.md
index 410ba1ba27..b99dbdf6ef 100644
--- a/components/table/demo/expand-children.md
+++ b/components/table/demo/expand-children.md
@@ -11,18 +11,14 @@ title:
可以通过设置 `indentSize` 以控制每一层的缩进宽度。
-> 注:暂不支持父子数据递归关联选择。
-
## en-US
Display tree structure data in Table when there is field key `children` in dataSource, try to customize `childrenColumnName` property to avoid tree table structure.
You can control the indent width by setting `indentSize`.
-> Note, no support for recursive selection of tree structure data table yet.
-
```jsx
-import { Table } from 'antd';
+import { Table, Switch, Space } from 'antd';
const columns = [
{
@@ -122,8 +118,21 @@ const rowSelection = {
},
};
-ReactDOM.render(
- ,
- mountNode,
-);
+function TreeData() {
+ const [checkStrictly, setCheckStrictly] = React.useState(false);
+ return (
+ <>
+
+ CheckStrictly:
+
+
+ >
+ );
+}
+
+ReactDOM.render( , mountNode);
```
diff --git a/components/table/hooks/useSelection.tsx b/components/table/hooks/useSelection.tsx
index f31ba919e7..3dc2c81428 100644
--- a/components/table/hooks/useSelection.tsx
+++ b/components/table/hooks/useSelection.tsx
@@ -1,7 +1,13 @@
import * as React from 'react';
+import { useState, useCallback, useMemo } from 'react';
import DownOutlined from '@ant-design/icons/DownOutlined';
+import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil';
+import { conductCheck } from 'rc-tree/lib/utils/conductUtil';
+import { arrAdd, arrDel } from 'rc-tree/lib/util';
+import { DataNode, GetCheckDisabled } from 'rc-tree/lib/interface';
import { INTERNAL_COL_DEFINE } from 'rc-table';
import { FixedType } from 'rc-table/lib/interface';
+import useMergedState from 'rc-util/lib/hooks/useMergedState';
import Checkbox, { CheckboxProps } from '../../checkbox';
import Dropdown from '../../dropdown';
import Menu from '../../menu';
@@ -19,8 +25,6 @@ import {
GetPopupContainer,
} from '../interface';
-const EMPTY_LIST: any[] = [];
-
// TODO: warning if use ajax!!!
export const SELECTION_ALL = 'SELECT_ALL' as const;
export const SELECTION_INVERT = 'SELECT_INVERT' as const;
@@ -71,6 +75,7 @@ export default function useSelection(
config: UseSelectionConfig,
): [TransformColumns, Set] {
const {
+ preserveSelectedRowKeys,
selectedRowKeys,
getCheckboxProps,
onChange: onSelectionChange,
@@ -84,6 +89,7 @@ export default function useSelection(
fixed,
renderCell: customizeRenderCell,
hideSelectAll,
+ checkStrictly = true,
} = rowSelection || {};
const {
@@ -99,47 +105,137 @@ export default function useSelection(
getPopupContainer,
} = config;
- const [innerSelectedKeys, setInnerSelectedKeys] = React.useState();
- const mergedSelectedKeys = selectedRowKeys || innerSelectedKeys || EMPTY_LIST;
- const mergedSelectedKeySet = React.useMemo(() => {
- const keys = selectionType === 'radio' ? mergedSelectedKeys.slice(0, 1) : mergedSelectedKeys;
+ // ======================== Caches ========================
+ const preserveRecordsRef = React.useRef(new Map());
+
+ // ========================= Keys =========================
+ const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState(selectedRowKeys || [], {
+ value: selectedRowKeys,
+ });
+
+ const { keyEntities } = useMemo(
+ () =>
+ checkStrictly
+ ? { keyEntities: null }
+ : convertDataToEntities((data as unknown) as DataNode[], undefined, getRowKey as any),
+ [data, getRowKey, checkStrictly],
+ );
+
+ // Get flatten data
+ const flattedData = useMemo(() => flattenData(pageData, childrenColumnName), [
+ pageData,
+ childrenColumnName,
+ ]);
+
+ // Get all checkbox props
+ const checkboxPropsMap = useMemo(() => {
+ const map = new Map>();
+ flattedData.forEach((record, index) => {
+ const key = getRowKey(record, index);
+ const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {};
+ map.set(key, checkboxProps);
+
+ if (
+ process.env.NODE_ENV !== 'production' &&
+ ('checked' in checkboxProps || 'defaultChecked' in checkboxProps)
+ ) {
+ devWarning(
+ false,
+ 'Table',
+ 'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
+ );
+ }
+ });
+ return map;
+ }, [flattedData, getRowKey, getCheckboxProps]);
+
+ const isCheckboxDisabled: GetCheckDisabled = useCallback(
+ (r: RecordType) => {
+ return !!checkboxPropsMap.get(getRowKey(r))?.disabled;
+ },
+ [checkboxPropsMap, getRowKey],
+ );
+
+ const [derivedSelectedKeys, derivedHalfSelectedKeys] = useMemo(() => {
+ if (checkStrictly) {
+ return [mergedSelectedKeys, []];
+ }
+ const { checkedKeys, halfCheckedKeys } = conductCheck(
+ mergedSelectedKeys,
+ true,
+ keyEntities as any,
+ isCheckboxDisabled as any,
+ );
+ return [checkedKeys, halfCheckedKeys];
+ }, [mergedSelectedKeys, checkStrictly, keyEntities, isCheckboxDisabled]);
+
+ const derivedSelectedKeySet: Set = useMemo(() => {
+ const keys = selectionType === 'radio' ? derivedSelectedKeys.slice(0, 1) : derivedSelectedKeys;
return new Set(keys);
- }, [mergedSelectedKeys, selectionType]);
+ }, [derivedSelectedKeys, selectionType]);
+ const derivedHalfSelectedKeySet = useMemo(() => {
+ return selectionType === 'radio' ? new Set() : new Set(derivedHalfSelectedKeys);
+ }, [derivedHalfSelectedKeys, selectionType]);
// Save last selected key to enable range selection
- const [lastSelectedKey, setLastSelectedKey] = React.useState(null);
+ const [lastSelectedKey, setLastSelectedKey] = useState(null);
// Reset if rowSelection reset
React.useEffect(() => {
if (!rowSelection) {
- setInnerSelectedKeys([]);
+ setMergedSelectedKeys([]);
}
}, [!!rowSelection]);
- const setSelectedKeys = React.useCallback(
+ const setSelectedKeys = useCallback(
(keys: Key[]) => {
- const availableKeys: Key[] = [];
- const records: RecordType[] = [];
+ let availableKeys: Key[];
+ let records: RecordType[];
- keys.forEach(key => {
- const record = getRecordByKey(key);
- if (record !== undefined) {
- availableKeys.push(key);
- records.push(record);
- }
- });
+ if (preserveSelectedRowKeys) {
+ // Keep key if mark as preserveSelectedRowKeys
+ const newCache = new Map();
+ availableKeys = keys;
+ records = keys.map(key => {
+ let record = getRecordByKey(key);
- setInnerSelectedKeys(availableKeys);
+ if (!record && preserveRecordsRef.current.has(key)) {
+ record = preserveRecordsRef.current.get(key)!;
+ }
+
+ newCache.set(key, record);
+
+ return record;
+ });
+
+ // Refresh to new cache
+ preserveRecordsRef.current = newCache;
+ } else {
+ // Filter key which not exist in the `dataSource`
+ availableKeys = [];
+ records = [];
+
+ keys.forEach(key => {
+ const record = getRecordByKey(key);
+ if (record !== undefined) {
+ availableKeys.push(key);
+ records.push(record);
+ }
+ });
+ }
+
+ setMergedSelectedKeys(availableKeys);
if (onSelectionChange) {
onSelectionChange(availableKeys, records);
}
},
- [setInnerSelectedKeys, getRecordByKey, onSelectionChange],
+ [setMergedSelectedKeys, getRecordByKey, onSelectionChange, preserveSelectedRowKeys],
);
+ // ====================== Selections ======================
// Trigger single `onSelect` event
- const triggerSingleSelection = React.useCallback(
+ const triggerSingleSelection = useCallback(
(key: Key, selected: boolean, keys: Key[], event: Event) => {
if (onSelect) {
const rows = keys.map(k => getRecordByKey(k));
@@ -151,7 +247,7 @@ export default function useSelection(
[onSelect, getRecordByKey, setSelectedKeys],
);
- const mergedSelections = React.useMemo(() => {
+ const mergedSelections = useMemo(() => {
if (!selections || hideSelectAll) {
return null;
}
@@ -174,7 +270,7 @@ export default function useSelection(
key: 'invert',
text: tableLocale.selectInvert,
onSelect() {
- const keySet = new Set(mergedSelectedKeySet);
+ const keySet = new Set(derivedSelectedKeySet);
pageData.forEach((record, index) => {
const key = getRowKey(record, index);
@@ -200,38 +296,17 @@ export default function useSelection(
}
return selection as SelectionItem;
});
- }, [selections, mergedSelectedKeySet, pageData, getRowKey]);
+ }, [selections, derivedSelectedKeySet, pageData, getRowKey, onSelectInvert, setSelectedKeys]);
- const transformColumns = React.useCallback(
+ // ======================= Columns ========================
+ const transformColumns = useCallback(
(columns: ColumnsType): ColumnsType => {
if (!rowSelection) {
return columns;
}
- // Get flatten data
- const flattedData = flattenData(pageData, childrenColumnName);
-
// Support selection
- const keySet = new Set(mergedSelectedKeySet);
-
- // Get all checkbox props
- const checkboxPropsMap = new Map>();
- flattedData.forEach((record, index) => {
- const key = getRowKey(record, index);
- const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {};
- checkboxPropsMap.set(key, checkboxProps);
-
- if (
- process.env.NODE_ENV !== 'production' &&
- ('checked' in checkboxProps || 'defaultChecked' in checkboxProps)
- ) {
- devWarning(
- false,
- 'Table',
- 'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
- );
- }
- });
+ const keySet = new Set(derivedSelectedKeySet);
// Record key only need check with enabled
const recordKeys = flattedData
@@ -250,8 +325,10 @@ export default function useSelection(
});
} else {
recordKeys.forEach(key => {
- keySet.add(key);
- changeKeys.push(key);
+ if (!keySet.has(key)) {
+ keySet.add(key);
+ changeKeys.push(key);
+ }
});
}
@@ -353,6 +430,7 @@ export default function useSelection(
renderCell = (_, record, index) => {
const key = getRowKey(record, index);
const checked = keySet.has(key);
+ const indeterminate = derivedHalfSelectedKeySet.has(key);
// Record checked
return {
@@ -360,6 +438,7 @@ export default function useSelection(
e.stopPropagation()}
onChange={({ nativeEvent }) => {
const { shiftKey } = nativeEvent;
@@ -368,7 +447,7 @@ export default function useSelection(
let endIndex: number = -1;
// Get range of this
- if (shiftKey) {
+ if (shiftKey && checkStrictly) {
const pointKeys = new Set([lastSelectedKey, key]);
recordKeys.some((recordKey, recordIndex) => {
@@ -385,7 +464,7 @@ export default function useSelection(
});
}
- if (endIndex !== -1 && startIndex !== endIndex) {
+ if (endIndex !== -1 && startIndex !== endIndex && checkStrictly) {
// Batch update selections
const rangeKeys = recordKeys.slice(startIndex, endIndex + 1);
const changedKeys: Key[] = [];
@@ -417,13 +496,37 @@ export default function useSelection(
}
} else {
// Single record selected
- if (checked) {
- keySet.delete(key);
+ const originCheckedKeys = derivedSelectedKeys;
+ if (checkStrictly) {
+ const checkedKeys = checked
+ ? arrDel(originCheckedKeys, key)
+ : arrAdd(originCheckedKeys, key);
+ triggerSingleSelection(key, !checked, checkedKeys, nativeEvent);
} else {
- keySet.add(key);
- }
+ // Always fill first
+ const result = conductCheck(
+ [...originCheckedKeys, key],
+ true,
+ keyEntities as any,
+ isCheckboxDisabled as any,
+ );
+ const { checkedKeys, halfCheckedKeys } = result;
+ let nextCheckedKeys = checkedKeys;
- triggerSingleSelection(key, !checked, Array.from(keySet), nativeEvent);
+ // If remove, we do it again to correction
+ if (checked) {
+ const tempKeySet = new Set(checkedKeys);
+ tempKeySet.delete(key);
+ nextCheckedKeys = conductCheck(
+ Array.from(tempKeySet),
+ { checked: false, halfCheckedKeys },
+ keyEntities as any,
+ isCheckboxDisabled as any,
+ ).checkedKeys;
+ }
+
+ triggerSingleSelection(key, !checked, nextCheckedKeys, nativeEvent);
+ }
}
setLastSelectedKey(key);
@@ -468,18 +571,21 @@ export default function useSelection(
},
[
getRowKey,
- pageData,
+ flattedData,
rowSelection,
- innerSelectedKeys,
- mergedSelectedKeys,
+ derivedSelectedKeys,
+ derivedSelectedKeySet,
+ derivedHalfSelectedKeySet,
selectionColWidth,
mergedSelections,
expandType,
lastSelectedKey,
+ checkboxPropsMap,
onSelectMultiple,
triggerSingleSelection,
+ isCheckboxDisabled,
],
);
- return [transformColumns, mergedSelectedKeySet];
+ return [transformColumns, derivedSelectedKeySet];
}
diff --git a/components/table/index.en-US.md b/components/table/index.en-US.md
index 63f81de250..9571621d94 100644
--- a/components/table/index.en-US.md
+++ b/components/table/index.en-US.md
@@ -78,7 +78,7 @@ const columns = [
| size | Size of table | `default` \| `middle` \| `small` | `default` |
| summary | Summary content | (currentData) => ReactNode | - |
| title | Table title renderer | Function(currentPageData) | - |
-| onChange | Callback executed when pagination, filters or sorter is changed | Function(pagination, filters, sorter, extra: { currentDataSource: [] }) | - |
+| onChange | Callback executed when pagination, filters or sorter is changed | Function(pagination, filters, sorter, extra: { currentDataSource: [], action: `paginate` \| `sort` \| `filter` }) | - |
| onHeaderRow | Set props on per header row | Function(column, index) | - |
| onRow | Set props on per row | Function(record, index) | - |
| getPopupContainer | the render container of dropdowns in table | (triggerNode) => HTMLElement | `() => TableHtmlElement` |
@@ -121,7 +121,7 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| dataIndex | Display field of the data record, support nest path by string array | string \| string\[] | - | |
| defaultFilteredValue | Default filtered values | string\[] | - | | |
| defaultSortOrder | Default order of sorted values | `ascend` \| `descend` | - | |
-| filterDropdown | Customized filter overlay | React.ReactNode \| (props: [FilterDropdownProps](https://git.io/fjP5h)) => React.ReactNode | - | |
+| filterDropdown | Customized filter overlay | ReactNode \| (props: [FilterDropdownProps](https://git.io/fjP5h)) => ReactNode | - | |
| filterDropdownVisible | Whether `filterDropdown` is visible | boolean | - | |
| filtered | Whether the `dataSource` is filtered | boolean | false | |
| filteredValue | Controlled filtered value, filter icon will highlight | string\[] | - | |
@@ -185,19 +185,21 @@ Properties for row selection.
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
-| columnWidth | Set the width of the selection column | string\|number | `60px` | 4.0 |
-| columnTitle | Set the title of the selection column | string\|React.ReactNode | - | 4.0 |
-| fixed | Fixed selection column on the left | boolean | - | 4.0 |
-| getCheckboxProps | Get Checkbox or Radio props | Function(record) | - | 4.0 |
-| hideSelectAll | Hide the selectAll checkbox and custom selection | boolean | false | 4.3 |
+| checkStrictly | Check table row precisely; parent row and children rows are not associated | boolean | true | 4.4.0 |
+| columnWidth | Set the width of the selection column | string\|number | `60px` | |
+| columnTitle | Set the title of the selection column | string\|ReactNode | - | |
+| fixed | Fixed selection column on the left | boolean | - | |
+| getCheckboxProps | Get Checkbox or Radio props | Function(record) | - | |
+| hideSelectAll | Hide the selectAll checkbox and custom selection | boolean | `false` | 4.3 |
+| preserveSelectedRowKeys | Keep selection `key` even when it removed from `dataSource` | boolean | - | 4.4 |
| renderCell | Renderer of the table cell. Same as `render` in column | Function(checked, record, index, originNode) {} | - | 4.1 |
-| selectedRowKeys | Controlled selected row keys | string\[]\|number[] | \[] | 4.0 |
-| selections | Custom selection [config](#rowSelection), only displays default selections when set to `true` | object\[]\|boolean | - | 4.0 |
-| type | `checkbox` or `radio` | `checkbox` \| `radio` | `checkbox` | 4.0 |
-| onChange | Callback executed when selected rows change | Function(selectedRowKeys, selectedRows) | - | 4.0 |
-| onSelect | Callback executed when select/deselect one row | Function(record, selected, selectedRows, nativeEvent) | - | 4.0 |
-| onSelectAll | Callback executed when select/deselect all rows | Function(selected, selectedRows, changeRows) | - | 4.0 |
-| onSelectInvert | Callback executed when row selection is inverted | Function(selectedRowKeys) | - | 4.0 |
+| selectedRowKeys | Controlled selected row keys | string\[]\|number[] | \[] | |
+| selections | Custom selection [config](#rowSelection), only displays default selections when set to `true` | object\[]\|boolean | - | |
+| type | `checkbox` or `radio` | `checkbox` \| `radio` | `checkbox` | |
+| onChange | Callback executed when selected rows change | Function(selectedRowKeys, selectedRows) | - | |
+| onSelect | Callback executed when select/deselect one row | Function(record, selected, selectedRows, nativeEvent) | - | |
+| onSelectAll | Callback executed when select/deselect all rows | Function(selected, selectedRows, changeRows) | - | |
+| onSelectInvert | Callback executed when row selection is inverted | Function(selectedRowKeys) | - | |
### scroll
@@ -212,7 +214,7 @@ Properties for row selection.
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| key | Unique key of this selection | string | - |
-| text | Display text of this selection | string\|React.ReactNode | - |
+| text | Display text of this selection | string\|ReactNode | - |
| onSelect | Callback executed when this selection is clicked | Function(changeableRowKeys) | - |
## Using in TypeScript
@@ -289,6 +291,8 @@ Table total page count usually reduce after filter data, we defaultly return to
You may need to keep current page after filtering when fetch data from remote service, please check [this demo](https://codesandbox.io/s/yuanchengjiazaishuju-ant-design-demo-7y2uf) as workaround.
+Also you can use the action from extra param to determine when return to first page.
+
### Why Table pagination show size changer?
In order to improve user experience, Pagination show size changer by default when `total >= 50` since `4.1.0`. You can set `showSizeChanger=false` to disable this feature.
diff --git a/components/table/index.zh-CN.md b/components/table/index.zh-CN.md
index 7ba578b70d..63a5815212 100644
--- a/components/table/index.zh-CN.md
+++ b/components/table/index.zh-CN.md
@@ -85,7 +85,7 @@ const columns = [
| size | 表格大小 | `default` \| `middle` \| `small` | default |
| summary | 总结栏 | (currentData) => ReactNode | - |
| title | 表格标题 | Function(currentPageData) | - |
-| onChange | 分页、排序、筛选变化时触发 | Function(pagination, filters, sorter, extra: { currentDataSource: [] }) | - |
+| onChange | 分页、排序、筛选变化时触发 | Function(pagination, filters, sorter, extra: { currentDataSource: [], action: `paginate` \| `sort` \| `filter` }) | - |
| onHeaderRow | 设置头部行属性 | Function(column, index) | - |
| onRow | 设置行属性 | Function(record, index) | - |
| getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | `() => TableHtmlElement` |
@@ -128,7 +128,7 @@ const columns = [
| dataIndex | 列数据在数据项中对应的路径,支持通过数组查询嵌套路径 | string \| string\[] | - | |
| defaultFilteredValue | 默认筛选值 | string\[] | - | |
| defaultSortOrder | 默认排序顺序 | `ascend` \| `descend` | - | |
-| filterDropdown | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互 | React.ReactNode \| (props: [FilterDropdownProps](https://git.io/fjP5h)) => React.ReactNode | - | |
+| filterDropdown | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互 | ReactNode \| (props: [FilterDropdownProps](https://git.io/fjP5h)) => ReactNode | - | |
| filterDropdownVisible | 用于控制自定义筛选菜单是否可见 | boolean | - | |
| filtered | 标识数据是否经过过滤,筛选图标会高亮 | boolean | false | |
| filteredValue | 筛选的受控属性,外界可用此控制列的筛选状态,值为已筛选的 value 数组 | string\[] | - | |
@@ -192,19 +192,21 @@ const columns = [
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
-| columnWidth | 自定义列表选择框宽度 | string\|number | `60px` | 4.0 |
-| columnTitle | 自定义列表选择框标题 | string\|React.ReactNode | - | 4.0 |
-| fixed | 把选择框列固定在左边 | boolean | - | 4.0 |
-| getCheckboxProps | 选择框的默认属性配置 | Function(record) | - | 4.0 |
+| checkStrictly | checkable 状态下节点选择完全受控(父子数据选中状态不再关联) | boolean | true | 4.4.0 |
+| columnWidth | 自定义列表选择框宽度 | string\|number | `60px` | |
+| columnTitle | 自定义列表选择框标题 | string\|ReactNode | - | |
+| fixed | 把选择框列固定在左边 | boolean | - | |
+| getCheckboxProps | 选择框的默认属性配置 | Function(record) | - | |
| hideSelectAll | 隐藏全选勾选框与自定义选择项 | boolean | false | 4.3 |
+| preserveSelectedRowKeys | 当数据被删除时仍然保留选项的 `key` | boolean | - | 4.4 |
| renderCell | 渲染勾选框,用法与 Column 的 `render` 相同 | Function(checked, record, index, originNode) {} | - | 4.1 |
-| selectedRowKeys | 指定选中项的 key 数组,需要和 onChange 进行配合 | string\[]\|number[] | \[] | 4.0 |
-| selections | 自定义选择项 [配置项](#selection), 设为 `true` 时使用默认选择项 | object\[]\|boolean | true | 4.0 |
-| type | 多选/单选,`checkbox` or `radio` | string | `checkbox` | 4.0 |
-| onChange | 选中项发生变化时的回调 | Function(selectedRowKeys, selectedRows) | - | 4.0 |
-| onSelect | 用户手动选择/取消选择某行的回调 | Function(record, selected, selectedRows, nativeEvent) | - | 4.0 |
-| onSelectAll | 用户手动选择/取消选择所有行的回调 | Function(selected, selectedRows, changeRows) | - | 4.0 |
-| onSelectInvert | 用户手动选择反选的回调 | Function(selectedRowKeys) | - | 4.0 |
+| selectedRowKeys | 指定选中项的 key 数组,需要和 onChange 进行配合 | string\[]\|number[] | \[] | |
+| selections | 自定义选择项 [配置项](#selection), 设为 `true` 时使用默认选择项 | object\[]\|boolean | true | |
+| type | 多选/单选,`checkbox` or `radio` | string | `checkbox` | |
+| onChange | 选中项发生变化时的回调 | Function(selectedRowKeys, selectedRows) | - | |
+| onSelect | 用户手动选择/取消选择某行的回调 | Function(record, selected, selectedRows, nativeEvent) | - | |
+| onSelectAll | 用户手动选择/取消选择所有行的回调 | Function(selected, selectedRows, changeRows) | - | |
+| onSelectInvert | 用户手动选择反选的回调 | Function(selectedRowKeys) | - | |
### scroll
@@ -219,7 +221,7 @@ const columns = [
| 参数 | 说明 | 类型 | 默认值 |
| -------- | -------------------------- | --------------------------- | ------ |
| key | React 需要的 key,建议设置 | string | - |
-| text | 选择项显示的文字 | string\|React.ReactNode | - |
+| text | 选择项显示的文字 | string\|ReactNode | - |
| onSelect | 选择项点击回调 | Function(changeableRowKeys) | - |
## 在 TypeScript 中使用
diff --git a/components/table/interface.tsx b/components/table/interface.tsx
index 3054385aa3..1478bfc05e 100644
--- a/components/table/interface.tsx
+++ b/components/table/interface.tsx
@@ -8,6 +8,8 @@ import { CheckboxProps } from '../checkbox';
import { PaginationProps } from '../pagination';
import { Breakpoint } from '../_util/responsiveObserve';
import { INTERNAL_SELECTION_ITEM } from './hooks/useSelection';
+import { tuple } from '../_util/type';
+// import { TableAction } from './Table';
export { GetRowKey, ExpandableConfig };
@@ -38,6 +40,9 @@ export interface TableLocale {
export type SortOrder = 'descend' | 'ascend' | null;
+const TableActions = tuple('paginate', 'sort', 'filter');
+export type TableAction = typeof TableActions[number];
+
export type CompareFn = (a: T, b: T, sortOrder?: SortOrder) => number;
export interface ColumnFilterItem {
@@ -126,6 +131,8 @@ export type SelectionSelectFn = (
) => void;
export interface TableRowSelection {
+ /** Keep the selection keys in list even the key not exist in `dataSource` anymore */
+ preserveSelectedRowKeys?: boolean;
type?: RowSelectionType;
selectedRowKeys?: Key[];
onChange?: (selectedRowKeys: Key[], selectedRows: T[]) => void;
@@ -141,6 +148,7 @@ export interface TableRowSelection {
fixed?: boolean;
columnWidth?: string | number;
columnTitle?: string | React.ReactNode;
+ checkStrictly?: boolean;
renderCell?: (
value: boolean,
record: T,
@@ -155,6 +163,7 @@ export type TransformColumns = (
export interface TableCurrentDataSource {
currentDataSource: RecordType[];
+ action: TableAction;
}
export interface SorterResult {
diff --git a/components/table/style/index.less b/components/table/style/index.less
index b79c3b66bb..9128c24d9f 100644
--- a/components/table/style/index.less
+++ b/components/table/style/index.less
@@ -471,7 +471,7 @@
}
background: transparent;
border: 0;
- pointer-events: none;
+ visibility: hidden;
}
.@{table-prefix-cls}-row-indent + & {
diff --git a/components/tabs/__tests__/__snapshots__/demo.test.js.snap b/components/tabs/__tests__/__snapshots__/demo.test.js.snap
index 9c0e3049f4..9011eb2e0d 100644
--- a/components/tabs/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/tabs/__tests__/__snapshots__/demo.test.js.snap
@@ -390,6 +390,132 @@ exports[`renders ./components/tabs/demo/card-top.md correctly 1`] = `
`;
+exports[`renders ./components/tabs/demo/centered.md correctly 1`] = `
+
+
+
+
+
+ Content of Tab Pane 1
+
+
+
+
+
+
+`;
+
exports[`renders ./components/tabs/demo/custom-add-trigger.md correctly 1`] = `
(
+
+
+ Content of Tab Pane 1
+
+
+ Content of Tab Pane 2
+
+
+ Content of Tab Pane 3
+
+
+);
+
+ReactDOM.render(
, mountNode);
+```
diff --git a/components/tabs/index.en-US.md b/components/tabs/index.en-US.md
index 4ef08fa567..a254b65aed 100644
--- a/components/tabs/index.en-US.md
+++ b/components/tabs/index.en-US.md
@@ -22,12 +22,14 @@ Ant Design has 3 types of Tabs for different situations.
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
+| addIcon | Customize add icon | ReactNode | - | 4.4.0 |
| activeKey | Current TabPane's key | string | - | |
| animated | Whether to change tabs with animation. Only works while `tabPosition="top"\|"bottom"` | boolean \| {inkBar:boolean, tabPane:boolean} | true, false when `type="card"` | |
| renderTabBar | replace the TabBar | (props: DefaultTabBarProps, DefaultTabBar: React.ComponentClass) => React.ReactElement | - | |
| defaultActiveKey | Initial active TabPane's key, if `activeKey` is not set. | string | - | |
| hideAdd | Hide plus icon or not. Only works while `type="editable-card"` | boolean | false | |
| size | preset tab bar size | `large` \| `default` \| `small` | `default` | |
+| centered | Centers tabs | boolean | false | 4.4.0 |
| tabBarExtraContent | Extra content in tab bar | React.ReactNode | - | |
| tabBarGutter | The gap between tabs | number | - | |
| tabBarStyle | Tab bar style object | object | - | |
diff --git a/components/tabs/index.tsx b/components/tabs/index.tsx
index 7fe7b3fba6..17418b7c46 100755
--- a/components/tabs/index.tsx
+++ b/components/tabs/index.tsx
@@ -19,10 +19,12 @@ export interface TabsProps extends Omit
{
type?: TabsType;
size?: SizeType;
hideAdd?: boolean;
+ centered?: boolean;
+ addIcon?: React.ReactNode;
onEdit?: (e: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => void;
}
-function Tabs({ type, className, size, onEdit, hideAdd, ...props }: TabsProps) {
+function Tabs({ type, className, size, onEdit, hideAdd, centered, addIcon, ...props }: TabsProps) {
const { prefixCls: customizePrefixCls } = props;
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('tabs', customizePrefixCls);
@@ -34,7 +36,7 @@ function Tabs({ type, className, size, onEdit, hideAdd, ...props }: TabsProps) {
onEdit?.(editType === 'add' ? event : key!, editType);
},
removeIcon: ,
- addIcon: ,
+ addIcon: addIcon || ,
showAdd: hideAdd !== true,
};
}
@@ -54,6 +56,7 @@ function Tabs({ type, className, size, onEdit, hideAdd, ...props }: TabsProps) {
[`${prefixCls}-${size}`]: size,
[`${prefixCls}-card`]: ['card', 'editable-card'].includes(type as string),
[`${prefixCls}-editable-card`]: type === 'editable-card',
+ [`${prefixCls}-centered`]: centered,
})}
editable={editable}
moreIcon={ }
diff --git a/components/tabs/index.zh-CN.md b/components/tabs/index.zh-CN.md
index b6518c98af..ae33289b0d 100644
--- a/components/tabs/index.zh-CN.md
+++ b/components/tabs/index.zh-CN.md
@@ -25,12 +25,14 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
+| addIcon | 自定义添加按钮 | ReactNode | - | 4.4.0 |
| activeKey | 当前激活 tab 面板的 key | string | - | |
| animated | 是否使用动画切换 Tabs,在 `tabPosition=top|bottom` 时有效 | boolean \| {inkBar:boolean, tabPane:boolean} | true, 当 type="card" 时为 false | |
| renderTabBar | 替换 TabBar,用于二次封装标签头 | (props: DefaultTabBarProps, DefaultTabBar: React.ComponentClass) => React.ReactElement | - | |
| defaultActiveKey | 初始化选中面板的 key,如果没有设置 activeKey | string | 第一个面板 | |
| hideAdd | 是否隐藏加号图标,在 `type="editable-card"` 时有效 | boolean | false | |
| size | 大小,提供 `large` `default` 和 `small` 三种大小 | string | `default` | |
+| centered | 标签居中展示 | boolean | false | 4.4.0 |
| tabBarExtraContent | tab bar 上额外的元素 | React.ReactNode | - | |
| tabBarGutter | tabs 之间的间隙 | number | - | |
| tabBarStyle | tab bar 的样式对象 | object | - | |
diff --git a/components/tabs/style/index.less b/components/tabs/style/index.less
index 4ccb35611f..4beb3b9c7f 100644
--- a/components/tabs/style/index.less
+++ b/components/tabs/style/index.less
@@ -80,6 +80,7 @@
}
.@{tab-prefix-cls}-nav-add {
+ min-width: @tabs-card-height;
padding: 0 @padding-xs;
background: @tabs-card-head-background;
border: @border-width-base @border-style-base @border-color-split;
@@ -103,6 +104,17 @@
flex: none;
}
+ &-centered {
+ > .@{tab-prefix-cls}-nav,
+ > div > .@{tab-prefix-cls}-nav {
+ .@{tab-prefix-cls}-nav-wrap {
+ &:not([class*='@{tab-prefix-cls}-nav-wrap-ping']) {
+ justify-content: center;
+ }
+ }
+ }
+ }
+
// ============================ InkBar ============================
&-ink-bar {
position: absolute;
diff --git a/components/tag/__tests__/__snapshots__/demo.test.js.snap b/components/tag/__tests__/__snapshots__/demo.test.js.snap
index 5a56ef73e0..9ad2ce6325 100644
--- a/components/tag/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/tag/__tests__/__snapshots__/demo.test.js.snap
@@ -15,7 +15,7 @@ Array [
Tag 1
@@ -45,7 +45,7 @@ Array [
Tag 2
@@ -75,7 +75,7 @@ Array [
Tag 3
@@ -152,7 +152,7 @@ Array [
Tag 2
@@ -178,7 +178,7 @@ Array [
Prevent Default
@@ -356,7 +356,7 @@ Array [
@@ -384,7 +384,7 @@ Array [
@@ -444,7 +444,7 @@ Array [
Movies
@@ -476,6 +476,53 @@ Array [
]
`;
+exports[`renders ./components/tag/demo/customize.md correctly 1`] = `
+Array [
+
+ Tag1
+
+ 关 闭
+
+ ,
+
+ Tag2
+
+ ,
+]
+`;
+
exports[`renders ./components/tag/demo/icon.md correctly 1`] = `
Array [
+
+ Tag1
+
+ }>
+ Tag2
+
+ >,
+ mountNode,
+);
+```
diff --git a/components/tag/index.en-US.md b/components/tag/index.en-US.md
index 7a7532a294..38323d2cc0 100644
--- a/components/tag/index.en-US.md
+++ b/components/tag/index.en-US.md
@@ -17,13 +17,14 @@ Tag for categorizing or markup.
### Tag
-| Property | Description | Type | Default |
-| -------- | ------------------------------------ | ----------- | ------- |
-| closable | Whether the Tag can be closed | boolean | false |
-| color | Color of the Tag | string | - |
-| onClose | Callback executed when tag is closed | (e) => void | - |
-| visible | Whether the Tag is closed or not | boolean | true |
-| icon | Set the icon of tag | ReactNode | - | |
+| Property | Description | Type | Default | Version |
+| --------- | ------------------------------------ | ----------- | ------- | ------- |
+| closable | Whether the Tag can be closed | boolean | false | |
+| color | Color of the Tag | string | - | |
+| closeIcon | custom close icon | ReactNode | - | 4.4.0 |
+| onClose | Callback executed when tag is closed | (e) => void | - | |
+| visible | Whether the Tag is closed or not | boolean | true | |
+| icon | Set the icon of tag | ReactNode | - | |
### Tag.CheckableTag
diff --git a/components/tag/index.tsx b/components/tag/index.tsx
index 4505c8bebf..def8dfd440 100644
--- a/components/tag/index.tsx
+++ b/components/tag/index.tsx
@@ -21,6 +21,7 @@ export interface TagProps extends React.HTMLAttributes {
className?: string;
color?: LiteralUnion;
closable?: boolean;
+ closeIcon?: React.ReactNode;
visible?: boolean;
onClose?: Function;
style?: React.CSSProperties;
@@ -44,6 +45,7 @@ const InternalTag: React.ForwardRefRenderFunction = (
icon,
color,
onClose,
+ closeIcon,
closable = false,
...props
},
@@ -98,7 +100,16 @@ const InternalTag: React.ForwardRefRenderFunction = (
};
const renderCloseIcon = () => {
- return closable ? : null;
+ if (closable) {
+ return closeIcon ? (
+
+ {closeIcon}
+
+ ) : (
+
+ );
+ }
+ return null;
};
const isNeedWave =
diff --git a/components/tag/index.zh-CN.md b/components/tag/index.zh-CN.md
index 27676d1140..92544f82bc 100644
--- a/components/tag/index.zh-CN.md
+++ b/components/tag/index.zh-CN.md
@@ -17,13 +17,14 @@ cover: https://gw.alipayobjects.com/zos/alicdn/cH1BOLfxC/Tag.svg
### Tag
-| 参数 | 说明 | 类型 | 默认值 |
-| -------- | ---------------- | ----------- | ------ |
-| closable | 标签是否可以关闭 | boolean | false |
-| color | 标签色 | string | - |
-| onClose | 关闭时的回调 | (e) => void | - |
-| visible | 是否显示标签 | boolean | true |
-| icon | 设置图标 | ReactNode | - | |
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --------- | ---------------- | ----------- | ------ | ----- |
+| closable | 标签是否可以关闭 | boolean | false | |
+| color | 标签色 | string | - | |
+| closeIcon | 自定义关闭按钮 | ReactNode | - | 4.4.0 |
+| onClose | 关闭时的回调 | (e) => void | - | |
+| visible | 是否显示标签 | boolean | true | |
+| icon | 设置图标 | ReactNode | - | |
### Tag.CheckableTag
diff --git a/components/tag/style/index.less b/components/tag/style/index.less
index 76fe4c4f10..a11b3382d3 100644
--- a/components/tag/style/index.less
+++ b/components/tag/style/index.less
@@ -36,12 +36,11 @@
padding: 0 8px;
}
- .@{iconfont-css-prefix}-close {
+ &-close-icon {
.iconfont-size-under-12px(10px);
margin-left: 3px;
color: @text-color-secondary;
- font-weight: bold;
cursor: pointer;
transition: all 0.3s @ease-in-out-circ;
diff --git a/components/tag/style/rtl.less b/components/tag/style/rtl.less
index 33c31617e6..46d128babf 100644
--- a/components/tag/style/rtl.less
+++ b/components/tag/style/rtl.less
@@ -11,8 +11,8 @@
text-align: right;
}
- .@{iconfont-css-prefix}-close {
- .@{tag-prefix-cls}-rtl& {
+ &-close-icon {
+ .@{tag-prefix-cls}-rtl & {
margin-right: 3px;
margin-left: 0;
}
diff --git a/components/time-picker/index.en-US.md b/components/time-picker/index.en-US.md
index 8761cd4a09..9f2d62da38 100644
--- a/components/time-picker/index.en-US.md
+++ b/components/time-picker/index.en-US.md
@@ -53,6 +53,7 @@ import moment from 'moment';
| value | to set time | [moment](http://momentjs.com/) | - | |
| onChange | a callback function, can be executed when the selected time is changing | function(time: moment, timeString: string): void | - | |
| onOpenChange | a callback function which will be called while panel opening/closing | (open: boolean): void | - | |
+| showNow | Whether to show 'Now' button on panel | boolean | - | 4.4.0 |
## Methods
diff --git a/components/time-picker/index.zh-CN.md b/components/time-picker/index.zh-CN.md
index 19e302f183..4f5fea0593 100644
--- a/components/time-picker/index.zh-CN.md
+++ b/components/time-picker/index.zh-CN.md
@@ -50,9 +50,10 @@ import moment from 'moment';
| clearIcon | 自定义的清除图标 | ReactNode | - | |
| renderExtraFooter | 选择框底部显示自定义的内容 | () => ReactNode | - | |
| use12Hours | 使用 12 小时制,为 true 时 `format` 默认为 `h:mm:ss a` | boolean | false | |
-| value | 当前时间 | [moment](http://momentjs.com/) | - | |
+| value | 当前时间 | [moment](http://momentjs.com/) | 无 | |
| onChange | 时间发生变化的回调 | function(time: moment, timeString: string): void | - | |
| onOpenChange | 面板打开/关闭时的回调 | (open: boolean): void | - | |
+| showNow | 面板是否显示“此刻”按钮 | boolean | - | 4.4.0 |
## 方法
diff --git a/components/transfer/__tests__/__snapshots__/demo.test.js.snap b/components/transfer/__tests__/__snapshots__/demo.test.js.snap
index 2d65516d12..46fe15dd92 100644
--- a/components/transfer/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/transfer/__tests__/__snapshots__/demo.test.js.snap
@@ -3730,9 +3730,11 @@ exports[`renders ./components/transfer/demo/table-transfer.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
-
+
-
+
@@ -4161,9 +4165,11 @@ exports[`renders ./components/transfer/demo/table-transfer.md correctly 1`] = `
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
-
+
diff --git a/components/typography/__tests__/index.test.js b/components/typography/__tests__/index.test.js
index 6275f38369..c7900f081b 100644
--- a/components/typography/__tests__/index.test.js
+++ b/components/typography/__tests__/index.test.js
@@ -10,6 +10,7 @@ import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import Typography from '../Typography';
import { sleep } from '../../../tests/utils';
+import TextArea from '../../input/TextArea';
jest.mock('copy-to-clipboard');
@@ -258,7 +259,7 @@ describe('Typography', () => {
expect(props.style).toEqual(style);
expect(props.className.includes(className)).toBeTruthy();
- wrapper.find('TextArea').simulate('change', {
+ wrapper.find(TextArea).simulate('change', {
target: { value: 'Bamboo' },
});
@@ -275,21 +276,21 @@ describe('Typography', () => {
testStep('by key up', wrapper => {
// Not trigger when inComposition
- wrapper.find('TextArea').simulate('compositionStart');
- wrapper.find('TextArea').simulate('keyDown', { keyCode: KeyCode.ENTER });
- wrapper.find('TextArea').simulate('compositionEnd');
- wrapper.find('TextArea').simulate('keyUp', { keyCode: KeyCode.ENTER });
+ wrapper.find(TextArea).simulate('compositionStart');
+ wrapper.find(TextArea).simulate('keyDown', { keyCode: KeyCode.ENTER });
+ wrapper.find(TextArea).simulate('compositionEnd');
+ wrapper.find(TextArea).simulate('keyUp', { keyCode: KeyCode.ENTER });
// Now trigger
- wrapper.find('TextArea').simulate('keyDown', { keyCode: KeyCode.ENTER });
- wrapper.find('TextArea').simulate('keyUp', { keyCode: KeyCode.ENTER });
+ wrapper.find(TextArea).simulate('keyDown', { keyCode: KeyCode.ENTER });
+ wrapper.find(TextArea).simulate('keyUp', { keyCode: KeyCode.ENTER });
});
testStep(
'by esc key',
wrapper => {
- wrapper.find('TextArea').simulate('keyDown', { keyCode: KeyCode.ESC });
- wrapper.find('TextArea').simulate('keyUp', { keyCode: KeyCode.ESC });
+ wrapper.find(TextArea).simulate('keyDown', { keyCode: KeyCode.ESC });
+ wrapper.find(TextArea).simulate('keyUp', { keyCode: KeyCode.ESC });
},
onChange => {
// eslint-disable-next-line jest/no-standalone-expect
@@ -298,7 +299,7 @@ describe('Typography', () => {
);
testStep('by blur', wrapper => {
- wrapper.find('TextArea').simulate('blur');
+ wrapper.find(TextArea).simulate('blur');
});
});
diff --git a/package.json b/package.json
index 227db4afba..e4676cecb3 100644
--- a/package.json
+++ b/package.json
@@ -115,21 +115,21 @@
"omit.js": "^1.0.2",
"raf": "^3.4.1",
"rc-animate": "~3.1.0",
- "rc-cascader": "~1.2.0",
+ "rc-cascader": "~1.3.0",
"rc-checkbox": "~2.2.0",
"rc-collapse": "~2.0.0",
"rc-dialog": "~8.0.0",
"rc-drawer": "~4.0.0",
"rc-dropdown": "~3.1.2",
- "rc-field-form": "~1.4.1",
+ "rc-field-form": "~1.5.0",
"rc-input-number": "~5.0.0",
- "rc-mentions": "~1.2.0",
+ "rc-mentions": "~1.3.0",
"rc-menu": "~8.3.0",
"rc-notification": "~4.4.0",
- "rc-pagination": "~2.2.5",
- "rc-picker": "~1.6.1",
+ "rc-pagination": "~2.4.1",
+ "rc-picker": "~1.10.0",
"rc-progress": "~3.0.0",
- "rc-rate": "~2.7.0",
+ "rc-rate": "~2.8.2",
"rc-resize-observer": "^0.2.3",
"rc-select": "~11.0.0",
"rc-slider": "~9.3.0",
@@ -137,8 +137,9 @@
"rc-switch": "~3.2.0",
"rc-table": "~7.8.0",
"rc-tabs": "~11.5.0",
+ "rc-textarea": "~0.2.2",
"rc-tooltip": "~4.2.0",
- "rc-tree": "~3.3.0",
+ "rc-tree": "~3.5.0",
"rc-tree-select": "~4.0.0",
"rc-trigger": "~4.3.0",
"rc-upload": "~3.2.0",
diff --git a/site/theme/static/dark.less b/site/theme/static/dark.less
index 91a227000d..cf5d558ae8 100644
--- a/site/theme/static/dark.less
+++ b/site/theme/static/dark.less
@@ -70,22 +70,12 @@
border-bottom: 1px solid @border-color-split;
}
- &-codepen {
- background: transparent
- url('https://gw.alipayobjects.com/zos/antfincdn/1B3MOCiI5F/OtZslpOjYXijshDERXwc.svg') center /
- 14px no-repeat;
- }
+ &-actions > &-code-action {
+ color: @site-text-color-secondary;
- &-riddle {
- background: transparent
- url('https://gw.alipayobjects.com/zos/antfincdn/NByOhhT9rO/DlHbxMCyeuyOrqOdbgik.svg') center /
- 14px no-repeat;
- }
-
- &-codesandbox {
- background: transparent
- url('https://gw.alipayobjects.com/zos/antfincdn/hNEf2p1ZnS/aaYmtdDyHSCkXyLZVgGK.svg') center /
- 14px no-repeat;
+ &:hover {
+ color: @icon-color-hover;
+ }
}
}
diff --git a/site/theme/static/demo.less b/site/theme/static/demo.less
index 81d82cf33a..704ad17599 100644
--- a/site/theme/static/demo.less
+++ b/site/theme/static/demo.less
@@ -232,8 +232,9 @@
}
&-actions {
- padding-top: 12px;
- text-align: center;
+ display: flex;
+ justify-content: center;
+ padding: 12px 0;
border-top: 1px dashed @site-border-color-split;
opacity: 0.7;
transition: opacity 0.3s;
@@ -241,63 +242,47 @@
&:hover {
opacity: 1;
}
-
- > i,
- > form,
- > span {
- position: relative;
- display: inline-block;
- width: 16px;
- height: 16px;
- margin-left: 16px;
- vertical-align: top;
-
- .ant-row-rtl & {
- margin-right: 16px;
- margin-left: 0;
- }
-
- &:first-child {
- margin-left: 0;
-
- .ant-row-rtl & {
- margin-right: 0;
- }
- }
- }
-
- > form {
- top: -2px;
- }
}
- &-code-action {
- width: 20px;
- height: 20px;
+ &-actions > &-code-action {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 16px;
+ height: 16px;
+ margin-left: 16px;
color: @site-text-color-secondary;
- font-size: 16px;
- line-height: 18px;
cursor: pointer;
transition: all 0.24s;
+
+ .ant-row-rtl & {
+ margin-right: 16px;
+ margin-left: 0;
+ }
+
+ &:first-child {
+ margin-left: 0;
+
+ .ant-row-rtl & {
+ margin-right: 0;
+ }
+ }
+
&:hover {
color: @icon-color-hover;
}
}
&-code-copy {
- width: 20px;
- height: 20px;
- color: @site-text-color-secondary;
+ width: 14px;
+ height: 14px;
font-size: 14px;
- line-height: 20px;
text-align: center;
background: @component-background;
- border-radius: 20px;
cursor: pointer;
- transition: all 0.24s;
+ transition: transform 0.24s;
&:hover {
- color: @icon-color-hover;
transform: scale(1.2);
}
&.anticon-check {
@@ -307,52 +292,27 @@
}
&-codepen {
- width: 20px;
- height: 20px;
+ width: 14px;
+ height: 14px;
overflow: hidden;
- text-indent: -9999px;
- background: transparent
- url('https://gw.alipayobjects.com/zos/rmsportal/OtZslpOjYXijshDERXwc.svg') center / 14px
- no-repeat;
border: 0;
cursor: pointer;
- opacity: 0.65;
- transition: all 0.3s;
- &:hover {
- opacity: 1;
- }
}
&-riddle {
- display: none;
- width: 20px;
- height: 20px;
+ width: 14px;
+ height: 14px;
overflow: hidden;
- text-indent: -9999px;
- background: transparent
- url('https://gw.alipayobjects.com/zos/rmsportal/DlHbxMCyeuyOrqOdbgik.svg') center / 14px
- no-repeat;
border: 0;
cursor: pointer;
- opacity: 0.65;
- transition: all 0.3s;
- &:hover {
- opacity: 1;
- }
}
&-codesandbox {
- width: 20px;
- height: 20px;
+ width: 16px;
+ height: 16px;
overflow: hidden;
- text-indent: -9999px;
- background: transparent
- url('https://gw.alipayobjects.com/zos/rmsportal/aaYmtdDyHSCkXyLZVgGK.svg') center / 14px
- no-repeat;
border: 0;
cursor: pointer;
- opacity: 0.65;
- transition: all 0.3s;
&:hover {
opacity: 1;
}
@@ -383,10 +343,6 @@
}
}
-.show-riddle-button .code-box-riddle {
- display: block;
-}
-
.all-code-box-controls {
float: right;
diff --git a/site/theme/template/Content/ComponentDoc.jsx b/site/theme/template/Content/ComponentDoc.jsx
index be352867bf..9fafe6f291 100644
--- a/site/theme/template/Content/ComponentDoc.jsx
+++ b/site/theme/template/Content/ComponentDoc.jsx
@@ -107,6 +107,7 @@ class ComponentDoc extends React.Component {
const demoElem = (
+
{helmetTitle && {helmetTitle} }
{helmetTitle && }
diff --git a/site/theme/template/Content/Demo/CodePenIcon.jsx b/site/theme/template/Content/Demo/CodePenIcon.jsx
new file mode 100644
index 0000000000..7a26b5809d
--- /dev/null
+++ b/site/theme/template/Content/Demo/CodePenIcon.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import Icon from '@ant-design/icons';
+
+const CodePenIcon = props => {
+ const SVGIcon = () => (
+
+
+
+ );
+ return ;
+};
+
+export default CodePenIcon;
diff --git a/site/theme/template/Content/Demo/CodeSandboxIcon.jsx b/site/theme/template/Content/Demo/CodeSandboxIcon.jsx
new file mode 100644
index 0000000000..7857f98109
--- /dev/null
+++ b/site/theme/template/Content/Demo/CodeSandboxIcon.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import Icon from '@ant-design/icons';
+
+const CodeSandboxIcon = props => {
+ const SVGIcon = () => (
+
+
+
+ );
+ return ;
+};
+
+export default CodeSandboxIcon;
diff --git a/site/theme/template/Content/Demo/RiddleIcon.jsx b/site/theme/template/Content/Demo/RiddleIcon.jsx
new file mode 100644
index 0000000000..044e7d5ea2
--- /dev/null
+++ b/site/theme/template/Content/Demo/RiddleIcon.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import Icon from '@ant-design/icons';
+
+const RiddleIcon = props => {
+ const SVGIcon = () => (
+
+
+
+ );
+ return ;
+};
+
+export default RiddleIcon;
diff --git a/site/theme/template/Content/Demo/index.jsx b/site/theme/template/Content/Demo/index.jsx
index f6e6ad4c0a..5c54b1ff48 100644
--- a/site/theme/template/Content/Demo/index.jsx
+++ b/site/theme/template/Content/Demo/index.jsx
@@ -11,6 +11,9 @@ import stackblitzSdk from '@stackblitz/sdk';
import CodePreview from './CodePreview';
import EditButton from '../EditButton';
import BrowserFrame from '../../BrowserFrame';
+import CodeSandboxIcon from './CodeSandboxIcon';
+import CodePenIcon from './CodePenIcon';
+import RiddleIcon from './RiddleIcon';
const { ErrorBoundary } = Alert;
@@ -24,6 +27,12 @@ function compress(string) {
class Demo extends React.Component {
iframeRef = React.createRef();
+ codeSandboxIconRef = React.createRef();
+
+ riddleIconRef = React.createRef();
+
+ codepenIconRef = React.createRef();
+
state = {
codeExpand: false,
copied: false,
@@ -125,6 +134,7 @@ class Demo extends React.Component {
utils,
intl: { locale },
theme,
+ showRiddleButton,
} = props;
const { copied, copyTooltipVisible } = state;
if (!this.liveDemo) {
@@ -316,26 +326,34 @@ ${parsedSourceCode.replace('mountNode', "document.getElementById('container')")}
{introChildren}
+ {showRiddleButton ? (
+
+ ) : null}
-
}>
@@ -376,7 +391,7 @@ ${parsedSourceCode.replace('mountNode', "document.getElementById('container')")}
stackblitzSdk.openProject(stackblitzPrefillConfig);
}}
>
-
+
this.handleCodeCopied(meta.id)}>
@@ -388,7 +403,7 @@ ${parsedSourceCode.replace('mountNode', "document.getElementById('container')")}
{React.createElement(
copied && copyTooltipVisible ? CheckOutlined : SnippetsOutlined,
{
- className: 'code-box-code-copy',
+ className: 'code-box-code-copy code-box-code-action',
},
)}
@@ -396,7 +411,7 @@ ${parsedSourceCode.replace('mountNode', "document.getElementById('container')")}
}
>
-
+