diff --git a/components/form/__tests__/index.test.js b/components/form/__tests__/index.test.js
index 24fa633ebc..fbf9db983f 100644
--- a/components/form/__tests__/index.test.js
+++ b/components/form/__tests__/index.test.js
@@ -227,7 +227,7 @@ describe('Form', () => {
const onFinishFailed = jest.fn();
const wrapper = mount(
-
@@ -238,7 +238,11 @@ describe('Form', () => {
expect(scrollIntoView).not.toHaveBeenCalled();
wrapper.find('form').simulate('submit');
await sleep(50);
- expect(scrollIntoView).toHaveBeenCalled();
+ const inputNode = document.getElementById('test');
+ expect(scrollIntoView).toHaveBeenCalledWith(inputNode, {
+ block: 'center',
+ scrollMode: 'if-needed',
+ });
expect(onFinishFailed).toHaveBeenCalled();
wrapper.unmount();
diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md
index 2d5dbc2886..7ce216069a 100644
--- a/components/form/index.en-US.md
+++ b/components/form/index.en-US.md
@@ -30,7 +30,7 @@ High performance Form component with data scope management. Including data colle
| name | Form name. Will be the prefix of Field `id` | string | - | |
| preserve | Keep field value even when field removed | boolean | true | 4.4.0 |
| requiredMark | Required mark style. Can use required mark or optional mark. You can not config to single Form.Item since this is a Form level config | boolean \| `optional` | true | 4.6.0 |
-| scrollToFirstError | Auto scroll to first failed field when submit | boolean | false | |
+| scrollToFirstError | Auto scroll to first failed field when submit | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) | 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) | - | |
| validateTrigger | Config field validate trigger | string \| string\[] | `onChange` | 4.3.0 |
diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md
index 08cb91b0fd..e45937d7dc 100644
--- a/components/form/index.zh-CN.md
+++ b/components/form/index.zh-CN.md
@@ -31,7 +31,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/ORmcdeaoO/Form.svg
| name | 表单名称,会作为表单字段 `id` 前缀使用 | string | - | |
| preserve | 当字段被删除时保留字段值 | boolean | true | 4.4.0 |
| requiredMark | 必选样式,可以切换为必选或者可选展示样式。此为 Form 配置,Form.Item 无法单独配置 | boolean \| `optional` | true | 4.6.0 |
-| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | boolean | false | |
+| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) | false | |
| size | 设置字段组件的尺寸(仅限 antd 组件) | `small` \| `middle` \| `large` | - | |
| validateMessages | 验证提示模板,说明[见下](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - | |
| validateTrigger | 统一设置字段校验规则 | string \| string\[] | `onChange` | 4.3.0 |
diff --git a/components/image/__tests__/__snapshots__/demo.test.js.snap b/components/image/__tests__/__snapshots__/demo.test.js.snap
index 240b0b8aea..4aa80bad8e 100644
--- a/components/image/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/image/__tests__/__snapshots__/demo.test.js.snap
@@ -9,6 +9,34 @@ exports[`renders ./components/image/demo/basic.md correctly 1`] = `
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
/>
+
`;
@@ -22,6 +50,34 @@ exports[`renders ./components/image/demo/fallback.md correctly 1`] = `
src="error"
style="height:200px"
/>
+
`;
@@ -55,6 +111,34 @@ exports[`renders ./components/image/demo/placeholder.md correctly 1`] = `
/>
+
+
,
+
,
]
`;
diff --git a/components/image/__tests__/__snapshots__/index.test.js.snap b/components/image/__tests__/__snapshots__/index.test.js.snap
index 1e06c758b4..1be5935f37 100644
--- a/components/image/__tests__/__snapshots__/index.test.js.snap
+++ b/components/image/__tests__/__snapshots__/index.test.js.snap
@@ -7,5 +7,33 @@ exports[`Image rtl render component should be rendered correctly in RTL directio
+
`;
diff --git a/components/image/index.tsx b/components/image/index.tsx
index 8811192f67..24da2bca9c 100644
--- a/components/image/index.tsx
+++ b/components/image/index.tsx
@@ -1,5 +1,8 @@
import * as React from 'react';
+import { useContext } from 'react';
+import EyeOutlined from '@ant-design/icons/EyeOutlined';
import RcImage, { ImageProps } from 'rc-image';
+import defaultLocale from '../locale/en_US';
import PreviewGroup from './PreviewGroup';
import { ConfigContext } from '../config-provider';
@@ -7,11 +10,34 @@ export interface CompositionImage extends React.FC
{
PreviewGroup: typeof PreviewGroup;
}
-const Image: CompositionImage = ({ prefixCls: customizePrefixCls, ...otherProps }) => {
- const { getPrefixCls } = React.useContext(ConfigContext);
+const Image: CompositionImage = ({
+ prefixCls: customizePrefixCls,
+ preview,
+ ...otherProps
+}) => {
+ const { getPrefixCls } = useContext(ConfigContext);
const prefixCls = getPrefixCls('image', customizePrefixCls);
- return ;
+ const { locale: contextLocale = defaultLocale } = useContext(ConfigContext);
+ const imageLocale = contextLocale.Image || defaultLocale.Image;
+
+ const mergedPreview = React.useMemo(() => {
+ if (preview === false) {
+ return preview;
+ }
+
+ return {
+ mask: (
+
+
+ {imageLocale?.preview}
+
+ ),
+ ...(typeof preview === 'object' ? preview : null),
+ };
+ }, [preview, imageLocale]);
+
+ return ;
};
export { ImageProps };
diff --git a/components/image/style/index.less b/components/image/style/index.less
index 45f8460526..03c019a45b 100644
--- a/components/image/style/index.less
+++ b/components/image/style/index.less
@@ -20,6 +20,32 @@
}
}
+ &-mask {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: @text-color-inverse;
+ background: fade(@black, 50%);
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity @animation-duration-slow;
+
+ &-info {
+ .@{iconfont-css-prefix} {
+ margin-inline-end: @margin-xss;
+ }
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
&-placeholder {
.box();
}
diff --git a/components/input/Input.tsx b/components/input/Input.tsx
index e8b1655f52..bf96da2638 100644
--- a/components/input/Input.tsx
+++ b/components/input/Input.tsx
@@ -11,6 +11,10 @@ import { ConfigConsumer, ConfigConsumerProps, DirectionType } from '../config-pr
import SizeContext, { SizeType } from '../config-provider/SizeContext';
import devWarning from '../_util/devWarning';
+export interface InputFocusOptions extends FocusOptions {
+ cursor?: 'start' | 'end' | 'all';
+}
+
export interface InputProps
extends Omit, 'size' | 'prefix' | 'type'> {
prefixCls?: string;
@@ -99,6 +103,34 @@ export function getInputClassName(
});
}
+export function triggerFocus(
+ element?: HTMLInputElement | HTMLTextAreaElement,
+ option?: InputFocusOptions,
+) {
+ if (!element) return;
+
+ element.focus(option);
+
+ // Selection content
+ const { cursor } = option || {};
+ if (cursor) {
+ const len = element.value.length;
+
+ switch (cursor) {
+ case 'start':
+ element.setSelectionRange(0, 0);
+ break;
+
+ case 'end':
+ element.setSelectionRange(len, len);
+ break;
+
+ default:
+ element.setSelectionRange(0, len);
+ }
+ }
+}
+
export interface InputState {
value: any;
focused: boolean;
@@ -171,8 +203,8 @@ class Input extends React.Component {
}
}
- focus = () => {
- this.input.focus();
+ focus = (option?: InputFocusOptions) => {
+ triggerFocus(this.input, option);
};
blur() {
diff --git a/components/input/TextArea.tsx b/components/input/TextArea.tsx
index e99e8701ba..9953c95c0b 100644
--- a/components/input/TextArea.tsx
+++ b/components/input/TextArea.tsx
@@ -1,24 +1,30 @@
import * as React from 'react';
import RcTextArea, { TextAreaProps as RcTextAreaProps } from 'rc-textarea';
+import ResizableTextArea from 'rc-textarea/lib/ResizableTextArea';
import omit from 'omit.js';
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
-import { composeRef } from 'rc-util/lib/ref';
import ClearableLabeledInput from './ClearableLabeledInput';
import { ConfigContext } from '../config-provider';
-import { fixControlledValue, resolveOnChange } from './Input';
+import { fixControlledValue, resolveOnChange, triggerFocus, InputFocusOptions } from './Input';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
+interface ShowCountProps {
+ formatter: (args: { count: number; maxLength?: number }) => string;
+}
+
export interface TextAreaProps extends RcTextAreaProps {
allowClear?: boolean;
bordered?: boolean;
- showCount?: boolean;
+ showCount?: boolean | ShowCountProps;
maxLength?: number;
size?: SizeType;
}
-export interface TextAreaRef extends HTMLTextAreaElement {
- resizableTextArea: any;
+export interface TextAreaRef {
+ focus: (options?: InputFocusOptions) => void;
+ blur: () => void;
+ resizableTextArea?: ResizableTextArea;
}
const TextArea = React.forwardRef(
@@ -38,7 +44,7 @@ const TextArea = React.forwardRef(
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const size = React.useContext(SizeContext);
- const innerRef = React.useRef();
+ const innerRef = React.useRef();
const clearableInputRef = React.useRef(null);
const [value, setValue] = useMergedState(props.defaultValue, {
@@ -63,18 +69,26 @@ const TextArea = React.forwardRef(
const handleChange = (e: React.ChangeEvent) => {
handleSetValue(e.target.value);
- resolveOnChange(innerRef.current!, e, props.onChange);
+ resolveOnChange(innerRef.current as any, e, props.onChange);
};
const handleReset = (e: React.MouseEvent) => {
handleSetValue('', () => {
innerRef.current?.focus();
});
- resolveOnChange(innerRef.current!, e, props.onChange);
+ resolveOnChange(innerRef.current as any, e, props.onChange);
};
const prefixCls = getPrefixCls('input', customizePrefixCls);
+ React.useImperativeHandle(ref, () => ({
+ resizableTextArea: innerRef.current?.resizableTextArea,
+ focus: (option?: InputFocusOptions) => {
+ triggerFocus(innerRef.current?.resizableTextArea?.textArea, option);
+ },
+ blur: () => innerRef.current?.blur(),
+ }));
+
const textArea = (
(
style={showCount ? null : style}
prefixCls={prefixCls}
onChange={handleChange}
- ref={composeRef(ref, innerRef)}
+ ref={innerRef}
/>
);
- const val = fixControlledValue(value) as string;
+ let val = fixControlledValue(value) as string;
// Max length value
const hasMaxLength = Number(maxLength) > 0;
+ // fix #27612 将value转为数组进行截取,解决 '😂'.length === 2 等emoji表情导致的截取乱码的问题
+ val = hasMaxLength ? [...val].slice(0, maxLength).join('') : val;
// TextArea
const textareaNode = (
@@ -114,10 +130,14 @@ const TextArea = React.forwardRef(
// Only show text area wrapper when needed
if (showCount) {
- const valueLength = hasMaxLength
- ? Math.min(Number(maxLength), [...val].length)
- : [...val].length;
- const dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
+ const valueLength = [...val].length;
+
+ let dataCount = '';
+ if (typeof showCount === 'object') {
+ dataCount = showCount.formatter({ count: valueLength, maxLength });
+ } else {
+ dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
+ }
return (
`;
+exports[`renders ./components/input/demo/focus.md correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/input/demo/group.md correctly 1`] = `
),
onClose: args.onClose,
+ onClick: args.onClick,
};
}
diff --git a/components/message/index.zh-CN.md b/components/message/index.zh-CN.md
index 60d1b4b5d3..213c00fd17 100644
--- a/components/message/index.zh-CN.md
+++ b/components/message/index.zh-CN.md
@@ -59,6 +59,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/hAkKTIW0K/Message.svg
| key | 当前提示的唯一标志 | string \| number | - |
| style | 自定义内联样式 | [CSSProperties](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e434515761b36830c3e58a970abf5186f005adac/types/react/index.d.ts#L794) | - |
| onClose | 关闭时触发的回调函数 | function | - |
+| onClick | 点击 message 时触发的回调函数 | function | - |
### 全局方法
diff --git a/components/modal/Modal.tsx b/components/modal/Modal.tsx
index 5d43d01feb..713ed053ac 100644
--- a/components/modal/Modal.tsx
+++ b/components/modal/Modal.tsx
@@ -95,6 +95,7 @@ export interface ModalFuncProps {
// TODO: find out exact types
onOk?: (...args: any[]) => any;
onCancel?: (...args: any[]) => any;
+ afterClose?: () => void;
okButtonProps?: ButtonProps;
cancelButtonProps?: ButtonProps;
centered?: boolean;
diff --git a/components/modal/__tests__/confirm.test.js b/components/modal/__tests__/confirm.test.js
index 25137b25d9..8c8df3e248 100644
--- a/components/modal/__tests__/confirm.test.js
+++ b/components/modal/__tests__/confirm.test.js
@@ -497,4 +497,28 @@ describe('Modal.confirm triggers callbacks correctly', () => {
});
jest.useRealTimers();
});
+
+ it('trigger afterClose once when click on cancel button', async () => {
+ const afterClose = jest.fn();
+ open({
+ afterClose,
+ });
+ // first Modal
+ $$('.ant-btn')[0].click();
+ expect(afterClose).not.toHaveBeenCalled();
+ await sleep(500);
+ expect(afterClose).toHaveBeenCalled();
+ });
+
+ it('trigger afterClose once when click on ok button', async () => {
+ const afterClose = jest.fn();
+ open({
+ afterClose,
+ });
+ // second Modal
+ $$('.ant-btn-primary')[0].click();
+ expect(afterClose).not.toHaveBeenCalled();
+ await sleep(500);
+ expect(afterClose).toHaveBeenCalled();
+ });
});
diff --git a/components/modal/confirm.tsx b/components/modal/confirm.tsx
index 8e50217246..9b9c64f379 100644
--- a/components/modal/confirm.tsx
+++ b/components/modal/confirm.tsx
@@ -82,7 +82,12 @@ export default function confirm(config: ModalFuncProps) {
currentConfig = {
...currentConfig,
visible: false,
- afterClose: destroy.bind(this, ...args),
+ afterClose: () => {
+ if (typeof config.afterClose === 'function') {
+ config.afterClose();
+ }
+ destroy.apply(this, args);
+ },
};
render(currentConfig);
}
diff --git a/components/modal/index.en-US.md b/components/modal/index.en-US.md
index 24992af162..2c81842ca3 100644
--- a/components/modal/index.en-US.md
+++ b/components/modal/index.en-US.md
@@ -65,6 +65,7 @@ The items listed above are all functions, expecting a settings object as paramet
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
+| afterClose | Specify a function that will be called when modal is closed completely | function | - | 4.9.0 |
| autoFocusButton | Specify which button to autofocus | null \| `ok` \| `cancel` | `ok` | |
| bodyStyle | Body style for modal body element. Such as height, padding etc | CSSProperties | | 4.8.0 |
| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#API) | - | |
diff --git a/components/modal/index.zh-CN.md b/components/modal/index.zh-CN.md
index 8f666e1c57..f12b847ab0 100644
--- a/components/modal/index.zh-CN.md
+++ b/components/modal/index.zh-CN.md
@@ -68,6 +68,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3StSdUlSH/Modal.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
+| afterClose | Modal 完全关闭后的回调 | function | - | 4.9.0 |
| autoFocusButton | 指定自动获得焦点的按钮 | null \| `ok` \| `cancel` | `ok` | |
| bodyStyle | Modal body 样式 | CSSProperties | | 4.8.0 |
| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button/#API) | - | |
diff --git a/components/select/__tests__/__snapshots__/demo.test.js.snap b/components/select/__tests__/__snapshots__/demo.test.js.snap
index de20d375ac..c539d93907 100644
--- a/components/select/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/select/__tests__/__snapshots__/demo.test.js.snap
@@ -8,29 +8,38 @@ exports[`renders ./components/select/demo/automatic-tokenization.md correctly 1`
+
@@ -330,76 +339,95 @@ Array [
-
-
- a10
-
-
-
-
- c12
-
-
-
+ a10
+
-
-
-
-
-
+
-
-
-
+
+
+ c12
+
+
+
+
+
+
+
+
+
+
,
-
-
- gold
-
-
-
-
-
-
-
- cyan
-
-
-
-
-
-
-
-
-
-
-
+
+
+ gold
+
+
+
+
+
+
+
+
+
+ cyan
+
+
+
+
+
+
+
+
-
-
- Lucy
-
-
-
+ Lucy
+
+
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -1120,102 +1190,121 @@ Array [
-
-
- a10
-
-
-
+ a10
+
+
+
+
+
+
-
-
-
-
- c12
-
-
+
-
-
- a10
-
-
-
-
+
+ a10
+
+
+
+
- c12
-
-
-
-
-
+
+ c12
+
+
+
+
+
,
]
@@ -1374,67 +1482,216 @@ exports[`renders ./components/select/demo/option-label-prop.md correctly 1`] = `
+
+
+`;
+
+exports[`renders ./components/select/demo/responsive.md correctly 1`] = `
+
+
+
+
+
+
+
+
+ + 4 ...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 4 ...
+
+
+
+
+
+
+
`;
@@ -1598,32 +1855,41 @@ exports[`renders ./components/select/demo/select-users.md correctly 1`] = `
+
@@ -1767,102 +2033,121 @@ Array [
-
-
- a10
-
-
-
+ a10
+
+
+
+
+
+
-
-
-
-
- c12
-
-
+
,
,
@@ -1873,99 +2158,118 @@ Array [
-
-
- a10
-
-
-
+ a10
+
+
+
+
+
+
-
-
-
-
- c12
-
-
+
,
]
@@ -2106,29 +2410,38 @@ exports[`renders ./components/select/demo/tags.md correctly 1`] = `
+
diff --git a/components/select/demo/big-data.md b/components/select/demo/big-data.md
index 83c3134f88..4352586c2d 100644
--- a/components/select/demo/big-data.md
+++ b/components/select/demo/big-data.md
@@ -1,5 +1,5 @@
---
-order: 23
+order: 25
title:
zh-CN: 大数据
en-US: Big Data
diff --git a/components/select/demo/responsive.md b/components/select/demo/responsive.md
new file mode 100644
index 0000000000..4cafa74edc
--- /dev/null
+++ b/components/select/demo/responsive.md
@@ -0,0 +1,58 @@
+---
+order: 24
+title:
+ zh-CN: 响应式 maxTagCount
+ en-US: Responsive maxTagCount
+---
+
+## zh-CN
+
+多选下通过响应式布局让选项自动收缩。该功能对性能有所消耗,不推荐在大表单场景下使用。
+
+## en-US
+
+Auto collapse to tag with responsive case. Not recommend use in large form case since responsive calculation has a perf cost.
+
+```tsx
+import { Select, Space } from 'antd';
+
+interface ItemProps {
+ label: string;
+ value: string;
+}
+
+const options: ItemProps[] = [];
+
+for (let i = 10; i < 36; i++) {
+ const value = i.toString(36) + i;
+ options.push({
+ label: `Long Label: ${value}`,
+ value,
+ });
+}
+
+const Demo = () => {
+ const [value, setValue] = React.useState(['a10', 'c12', 'h17', 'j19', 'k20']);
+
+ const selectProps = {
+ mode: 'multiple' as const,
+ style: { width: '100%' },
+ value,
+ options,
+ onChange: (newValue: string[]) => {
+ setValue(newValue);
+ },
+ placeholder: 'Select Item...',
+ maxTagCount: 'responsive' as const,
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+ReactDOM.render(, mountNode);
+```
diff --git a/components/select/index.en-US.md b/components/select/index.en-US.md
index 7aa10ccac2..b877f62198 100644
--- a/components/select/index.en-US.md
+++ b/components/select/index.en-US.md
@@ -43,7 +43,7 @@ Select component to select value from options.
| labelInValue | Whether to embed label in value, turn the format of value from `string` to { value: string, label: ReactNode } | boolean | false | |
| listHeight | Config popup height | number | 256 | |
| loading | Indicate loading state | boolean | false | |
-| maxTagCount | Max tag count to show | number | - | |
+| maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | responsive: 4.10 |
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | |
| maxTagTextLength | Max tag text length to show | number | - | |
| menuItemSelectedIcon | The custom menuItemSelected icon with multiple options | ReactNode | - | |
diff --git a/components/select/index.zh-CN.md b/components/select/index.zh-CN.md
index fabea0922f..94d0829999 100644
--- a/components/select/index.zh-CN.md
+++ b/components/select/index.zh-CN.md
@@ -44,7 +44,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 Select 的 value 类型从 `string` 变为 { value: string, label: ReactNode } 的格式 | boolean | false | |
| listHeight | 设置弹窗滚动高度 | number | 256 | |
| loading | 加载中状态 | boolean | false | |
-| maxTagCount | 最多显示多少个 tag | number | - | |
+| maxTagCount | 最多显示多少个 tag,响应式模式会对性能产生损耗 | number \| `responsive` | - | responsive: 4.10 |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | |
| menuItemSelectedIcon | 自定义多选时当前选中的条目图标 | ReactNode | - | |
diff --git a/components/select/style/multiple.less b/components/select/style/multiple.less
index a3a79b9af4..92b32fb963 100644
--- a/components/select/style/multiple.less
+++ b/components/select/style/multiple.less
@@ -1,5 +1,6 @@
@import './index';
+@select-overflow-prefix-cls: ~'@{select-prefix-cls}-selection-overflow';
@select-multiple-item-border-width: 1px;
@select-multiple-padding: max(
@@ -13,6 +14,20 @@
* since chrome may update to redesign with its align logic.
*/
+// =========================== Overflow ===========================
+.@{select-overflow-prefix-cls} {
+ position: relative;
+ display: flex;
+ flex: auto;
+ flex-wrap: wrap;
+ max-width: 100%;
+
+ &-item {
+ flex: none;
+ max-width: 100%;
+ }
+}
+
.@{select-prefix-cls} {
&-multiple {
// ========================= Selector =========================
@@ -56,9 +71,10 @@
height: @select-multiple-item-height;
margin-top: @select-multiple-item-spacing-half;
- margin-right: @input-padding-vertical-base;
+ margin-inline-end: @input-padding-vertical-base;
margin-bottom: @select-multiple-item-spacing-half;
- padding: 0 (@padding-xs / 2) 0 @padding-xs;
+ padding-inline-start: @padding-xs;
+ padding-inline-end: (@padding-xs / 2);
line-height: @select-multiple-item-height - @select-multiple-item-border-width * 2;
background: @select-selection-item-bg;
border: 1px solid @select-selection-item-border-color;
@@ -102,14 +118,24 @@
}
// ========================== Input ==========================
+ .@{select-overflow-prefix-cls}-item + .@{select-overflow-prefix-cls}-item {
+ .@{select-prefix-cls}-selection-search {
+ margin-inline-start: 0;
+ }
+ }
+
.@{select-prefix-cls}-selection-search {
position: relative;
- margin-left: (@select-multiple-padding / 2);
+ max-width: 100%;
+ margin-top: @select-multiple-item-spacing-half;
+ margin-bottom: @select-multiple-item-spacing-half;
+ margin-inline-start: @input-padding-horizontal-base - @input-padding-vertical-base;
&-input,
&-mirror {
+ height: @select-multiple-item-height;
font-family: @font-family;
- line-height: @line-height-base;
+ line-height: @select-multiple-item-height;
transition: all 0.3s;
}
@@ -126,11 +152,6 @@
white-space: pre; // fix whitespace wrapping caused width calculation bug
visibility: hidden;
}
-
- // https://github.com/ant-design/ant-design/issues/22906
- &:first-child > .@{select-prefix-cls}-selection-search-input {
- margin-left: 6.5px !important;
- }
}
// ======================= Placeholder =======================
diff --git a/components/select/style/rtl.less b/components/select/style/rtl.less
index 86847296aa..56038a8630 100644
--- a/components/select/style/rtl.less
+++ b/components/select/style/rtl.less
@@ -66,9 +66,6 @@
// ======================== Selections ========================
.@{select-prefix-cls}-selection-item {
.@{select-prefix-cls}-rtl& {
- margin-right: 0;
- margin-left: @input-padding-vertical-base;
- padding: 0 @padding-xs 0 (@padding-xs / 2);
text-align: right;
}
// It's ok not to do this, but 24px makes bottom narrow in view should adjust
@@ -83,11 +80,6 @@
// ========================== Input ==========================
.@{select-prefix-cls}-selection-search {
- .@{select-prefix-cls}-rtl& {
- margin-right: (@select-multiple-padding / 2);
- margin-left: @input-padding-vertical-base;
- }
-
&-mirror {
.@{select-prefix-cls}-rtl& {
right: 0;
diff --git a/components/slider/__tests__/__snapshots__/demo.test.js.snap b/components/slider/__tests__/__snapshots__/demo.test.js.snap
index e7b0ec8870..2a6713d39c 100644
--- a/components/slider/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/slider/__tests__/__snapshots__/demo.test.js.snap
@@ -83,6 +83,46 @@ Array [
]
`;
+exports[`renders ./components/slider/demo/dragableTrack.md correctly 1`] = `
+
+`;
+
exports[`renders ./components/slider/demo/event.md correctly 1`] = `
Array [
, mountNode);
+```
diff --git a/components/slider/index.en-US.md b/components/slider/index.en-US.md
index e8c8fe77db..a98d5f543e 100644
--- a/components/slider/index.en-US.md
+++ b/components/slider/index.en-US.md
@@ -35,6 +35,12 @@ To input a value in a range.
| onAfterChange | Fire when onmouseup is fired | (value) => void | - | |
| onChange | Callback function that is fired when the user changes the slider's value | (value) => void | - | |
+### range
+
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| draggableTrack | Whether range track can be drag | boolean | false | 4.10.0 |
+
## Methods
| Name | Description | Version |
diff --git a/components/slider/index.tsx b/components/slider/index.tsx
index c86894228a..e72d4b7f23 100644
--- a/components/slider/index.tsx
+++ b/components/slider/index.tsx
@@ -59,7 +59,7 @@ export interface SliderSingleProps extends SliderBaseProps {
}
export interface SliderRangeProps extends SliderBaseProps {
- range: true;
+ range: true | SliderRange;
value?: [number, number];
defaultValue?: [number, number];
onChange?: (value: [number, number]) => void;
@@ -68,6 +68,10 @@ export interface SliderRangeProps extends SliderBaseProps {
trackStyle?: React.CSSProperties[];
}
+interface SliderRange {
+ draggableTrack?: boolean;
+}
+
export type Visibles = { [index: number]: boolean };
const Slider = React.forwardRef(
@@ -136,15 +140,24 @@ const Slider = React.forwardRef(
const cls = classNames(className, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
+
// make reverse default on rtl direction
if (direction === 'rtl' && !restProps.vertical) {
restProps.reverse = !restProps.reverse;
}
+
+ // extrack draggableTrack from range={{ ... }}
+ let draggableTrack: boolean | undefined;
+ if (typeof range === 'object') {
+ draggableTrack = range.draggableTrack;
+ }
+
if (range) {
return (
diff --git a/components/slider/index.zh-CN.md b/components/slider/index.zh-CN.md
index 5cfe349980..20d362fe6e 100644
--- a/components/slider/index.zh-CN.md
+++ b/components/slider/index.zh-CN.md
@@ -25,7 +25,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/HZ3meFc6W/Silder.svg
| marks | 刻度标记,key 的类型必须为 `number` 且取值在闭区间 \[min, max] 内,每个标签可以单独设置样式 | object | { number: ReactNode } or { number: { style: object, label: ReactNode } } | |
| max | 最大值 | number | 100 | |
| min | 最小值 | number | 0 | |
-| range | 双滑块模式 | boolean | false | |
+| range | 双滑块模式 | boolean \| [range](#range) | false | |
| reverse | 反向坐标轴 | boolean | false | |
| step | 步长,取值必须大于 0,并且可被 (max - min) 整除。当 `marks` 不为空对象时,可以设置 `step` 为 null,此时 Slider 的可选值仅有 marks 标出来的部分 | number \| null | 1 | |
| tipFormatter | Slider 会把当前值传给 `tipFormatter`,并在 Tooltip 中显示 `tipFormatter` 的返回值,若为 null,则隐藏 Tooltip | value => ReactNode \| null | IDENTITY | |
@@ -36,6 +36,12 @@ cover: https://gw.alipayobjects.com/zos/alicdn/HZ3meFc6W/Silder.svg
| onAfterChange | 与 `onmouseup` 触发时机一致,把当前值作为参数传入 | (value) => void | - | |
| onChange | 当 Slider 的值发生改变时,会触发 onChange 事件,并把改变后的值作为参数传入 | (value) => void | - | |
+### range
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| draggableTrack | 范围刻度是否可被拖拽 | boolean | false | 4.10.0 |
+
## 方法
| 名称 | 描述 | 版本 |
diff --git a/components/steps/index.en-US.md b/components/steps/index.en-US.md
index f96beeac5d..593bf2b343 100644
--- a/components/steps/index.en-US.md
+++ b/components/steps/index.en-US.md
@@ -35,6 +35,7 @@ The whole of the step bar.
| labelPlacement | Place title and description with `horizontal` or `vertical` direction | string | `horizontal` | |
| percent | Progress circle percentage of current step in `process` status (only works on basic Steps) | number | - | 4.5.0 |
| progressDot | Steps with progress dot style, customize the progress dot by setting it to a function. labelPlacement will be `vertical` | boolean \| (iconDot, {index, status, title, description}) => ReactNode | false | |
+| responsive | change to vertical direction when screen width smaller than `532px` | boolean | - | true |
| size | To specify the size of the step bar, `default` and `small` are currently supported | string | `default` | |
| status | To specify the status of current step, can be set to one of the following values: `wait` `process` `finish` `error` | string | `process` | |
| type | Type of steps, can be set to one of the following values: `default`, `navigation` | string | `default` | |
diff --git a/components/steps/index.tsx b/components/steps/index.tsx
index ab8c4a43ab..8ac2f99e16 100644
--- a/components/steps/index.tsx
+++ b/components/steps/index.tsx
@@ -18,6 +18,7 @@ export interface StepsProps {
labelPlacement?: 'horizontal' | 'vertical';
prefixCls?: string;
progressDot?: boolean | Function;
+ responsive?: boolean;
size?: 'default' | 'small';
status?: 'wait' | 'process' | 'finish' | 'error';
style?: React.CSSProperties;
@@ -42,11 +43,14 @@ interface StepsType extends React.FC {
}
const Steps: StepsType = props => {
- const { percent, size, className, direction } = props;
+ const { percent, size, className, direction, responsive } = props;
const { xs } = useBreakpoint();
const { getPrefixCls, direction: rtlDirection } = React.useContext(ConfigContext);
- const getDirection = React.useCallback(() => (xs ? 'vertical' : direction), [xs, direction]);
+ const getDirection = React.useCallback(() => (responsive && xs ? 'vertical' : direction), [
+ xs,
+ direction,
+ ]);
const prefixCls = getPrefixCls('steps', props.prefixCls);
const iconPrefix = getPrefixCls('', props.iconPrefix);
diff --git a/components/steps/index.zh-CN.md b/components/steps/index.zh-CN.md
index 956d302530..007b79781a 100644
--- a/components/steps/index.zh-CN.md
+++ b/components/steps/index.zh-CN.md
@@ -36,6 +36,7 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/UZYqMizXHaj/Steps.svg
| labelPlacement | 指定标签放置位置,默认水平放图标右侧,可选 `vertical` 放图标下方 | string | `horizontal` | |
| percent | 当前 `process` 步骤显示的进度条进度(只对基本类型的 Steps 生效) | number | - | 4.5.0 |
| progressDot | 点状步骤条,可以设置为一个 function,labelPlacement 将强制为 `vertical` | boolean \| (iconDot, {index, status, title, description}) => ReactNode | false | |
+| responsive | 当屏幕宽度小于 532px 时自动变为垂直模式 | boolean | - | true |
| size | 指定大小,目前支持普通(`default`)和迷你(`small`) | string | `default` | |
| status | 指定当前步骤的状态,可选 `wait` `process` `finish` `error` | string | `process` | |
| type | 步骤条类型,有 `default` 和 `navigation` 两种 | string | `default` | |
diff --git a/components/steps/style/nav.less b/components/steps/style/nav.less
index dd1ea63753..d72039a680 100644
--- a/components/steps/style/nav.less
+++ b/components/steps/style/nav.less
@@ -90,35 +90,33 @@
}
}
-@media (max-width: @screen-xs) {
- .@{steps-prefix-cls}-navigation {
- > .@{steps-prefix-cls}-item {
- margin-right: 0 !important;
- &::before {
- display: none;
- }
- &.@{steps-prefix-cls}-item-active::before {
- top: 0;
- right: 0;
- left: unset;
- display: block;
- width: 3px;
- height: calc(100% - 24px);
- }
- &::after {
- position: relative;
- top: -2px;
- left: 50%;
- display: block;
- width: 8px;
- height: 8px;
- margin-bottom: 8px;
- text-align: center;
- transform: rotate(135deg);
- }
- > .@{steps-prefix-cls}-item-container > .@{steps-prefix-cls}-item-tail {
- visibility: hidden;
- }
+.@{steps-prefix-cls}-navigation.@{steps-prefix-cls}-vertical {
+ > .@{steps-prefix-cls}-item {
+ margin-right: 0 !important;
+ &::before {
+ display: none;
+ }
+ &.@{steps-prefix-cls}-item-active::before {
+ top: 0;
+ right: 0;
+ left: unset;
+ display: block;
+ width: 3px;
+ height: calc(100% - 24px);
+ }
+ &::after {
+ position: relative;
+ top: -2px;
+ left: 50%;
+ display: block;
+ width: 8px;
+ height: 8px;
+ margin-bottom: 8px;
+ text-align: center;
+ transform: rotate(135deg);
+ }
+ > .@{steps-prefix-cls}-item-container > .@{steps-prefix-cls}-item-tail {
+ visibility: hidden;
}
}
}
diff --git a/components/style/themes/default.less b/components/style/themes/default.less
index d61ac7aeef..5d743c5336 100644
--- a/components/style/themes/default.less
+++ b/components/style/themes/default.less
@@ -1006,6 +1006,7 @@
@image-font-size-base: 24px;
@image-bg: #f5f5f5;
@image-color: #fff;
+@image-mask-font-size: 16px;
@image-preview-operation-size: 18px;
@image-preview-operation-color: @text-color-dark;
@image-preview-operation-disabled-color: fade(@image-preview-operation-color, 25%);
diff --git a/components/table/Table.tsx b/components/table/Table.tsx
index 5136a097d7..5639b088df 100644
--- a/components/table/Table.tsx
+++ b/components/table/Table.tsx
@@ -26,7 +26,11 @@ import {
TableLocale,
TableAction,
} from './interface';
-import useSelection, { SELECTION_ALL, SELECTION_INVERT } from './hooks/useSelection';
+import useSelection, {
+ SELECTION_ALL,
+ SELECTION_INVERT,
+ SELECTION_NONE,
+} from './hooks/useSelection';
import useSorter, { getSortData, SortState } from './hooks/useSorter';
import useFilter, { getFilterData, FilterState } from './hooks/useFilter';
import useTitleColumns from './hooks/useTitleColumns';
@@ -509,6 +513,7 @@ Table.defaultProps = {
Table.SELECTION_ALL = SELECTION_ALL;
Table.SELECTION_INVERT = SELECTION_INVERT;
+Table.SELECTION_NONE = SELECTION_NONE;
Table.Column = Column;
Table.ColumnGroup = ColumnGroup;
Table.Summary = Summary;
diff --git a/components/table/__tests__/Table.rowSelection.test.js b/components/table/__tests__/Table.rowSelection.test.js
index fa5a51d95d..9c356d1a1a 100644
--- a/components/table/__tests__/Table.rowSelection.test.js
+++ b/components/table/__tests__/Table.rowSelection.test.js
@@ -294,11 +294,28 @@ describe('Table.rowSelection', () => {
checkboxes.at(1).simulate('change', { target: { checked: true } });
const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
- dropdownWrapper.find('.ant-dropdown-menu-item').last().simulate('click');
+ dropdownWrapper.find('.ant-dropdown-menu-item').at(1).simulate('click');
expect(handleSelectInvert).toHaveBeenCalledWith([1, 2, 3]);
});
+ it('fires selectNone event', () => {
+ const handleSelectNone = jest.fn();
+ const rowSelection = {
+ onSelectNone: handleSelectNone,
+ selections: true,
+ };
+ const wrapper = mount(createTable({ rowSelection }));
+ const checkboxes = wrapper.find('input');
+
+ checkboxes.at(1).simulate('change', { target: { checked: true } });
+
+ const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
+ dropdownWrapper.find('.ant-dropdown-menu-item').last().simulate('click');
+
+ expect(handleSelectNone).toHaveBeenCalled();
+ });
+
it('fires selection event', () => {
const handleSelectOdd = jest.fn();
const handleSelectEven = jest.fn();
diff --git a/components/table/__tests__/__snapshots__/Table.rowSelection.test.js.snap b/components/table/__tests__/__snapshots__/Table.rowSelection.test.js.snap
index 221eec1058..adb6ad8554 100644
--- a/components/table/__tests__/__snapshots__/Table.rowSelection.test.js.snap
+++ b/components/table/__tests__/__snapshots__/Table.rowSelection.test.js.snap
@@ -984,6 +984,12 @@ exports[`Table.rowSelection render with default selection correctly 1`] = `
>
Invert current page
+
@@ -1093,6 +1099,12 @@ exports[`Table.rowSelection should support getPopupContainer 1`] = `
>
Invert current page
+
@@ -1420,6 +1432,12 @@ exports[`Table.rowSelection should support getPopupContainer from ConfigProvider
>
Invert current page
+
diff --git a/components/table/demo/row-selection-custom.md b/components/table/demo/row-selection-custom.md
index 4e150e1389..490f353e9f 100644
--- a/components/table/demo/row-selection-custom.md
+++ b/components/table/demo/row-selection-custom.md
@@ -59,6 +59,7 @@ class App extends React.Component {
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
+ Table.SELECTION_NONE,
{
key: 'odd',
text: 'Select Odd Row',
diff --git a/components/table/hooks/useSelection.tsx b/components/table/hooks/useSelection.tsx
index 9afa79fc11..17cfdb9c81 100644
--- a/components/table/hooks/useSelection.tsx
+++ b/components/table/hooks/useSelection.tsx
@@ -28,6 +28,7 @@ import {
// TODO: warning if use ajax!!!
export const SELECTION_ALL = 'SELECT_ALL' as const;
export const SELECTION_INVERT = 'SELECT_INVERT' as const;
+export const SELECTION_NONE = 'SELECT_NONE' as const;
function getFixedType(column: ColumnsType[number]): FixedType | undefined {
return column && column.fixed;
@@ -49,7 +50,8 @@ interface UseSelectionConfig {
export type INTERNAL_SELECTION_ITEM =
| SelectionItem
| typeof SELECTION_ALL
- | typeof SELECTION_INVERT;
+ | typeof SELECTION_INVERT
+ | typeof SELECTION_NONE;
function flattenData(
data: RecordType[] | undefined,
@@ -82,6 +84,7 @@ export default function useSelection(
onSelect,
onSelectAll,
onSelectInvert,
+ onSelectNone,
onSelectMultiple,
columnWidth: selectionColWidth,
type: selectionType,
@@ -255,7 +258,7 @@ export default function useSelection(
}
const selectionList: INTERNAL_SELECTION_ITEM[] =
- selections === true ? [SELECTION_ALL, SELECTION_INVERT] : selections;
+ selections === true ? [SELECTION_ALL, SELECTION_INVERT, SELECTION_NONE] : selections;
return selectionList.map((selection: INTERNAL_SELECTION_ITEM) => {
if (selection === SELECTION_ALL) {
@@ -296,6 +299,18 @@ export default function useSelection(
},
};
}
+ if (selection === SELECTION_NONE) {
+ return {
+ key: 'none',
+ text: tableLocale.selectNone,
+ onSelect() {
+ setSelectedKeys([]);
+ if (onSelectNone) {
+ onSelectNone();
+ }
+ },
+ };
+ }
return selection as SelectionItem;
});
}, [selections, derivedSelectedKeySet, pageData, getRowKey, onSelectInvert, setSelectedKeys]);
diff --git a/components/table/index.en-US.md b/components/table/index.en-US.md
index e6594a2955..a5daf86e62 100644
--- a/components/table/index.en-US.md
+++ b/components/table/index.en-US.md
@@ -166,7 +166,8 @@ More about pagination, please check [`Pagination`](/components/pagination/).
Properties for expandable.
| Property | Description | Type | Default |
-| --- | --- | --- | --- |
+| --- | --- | --- | --- | --- |
+| columnWidth | Set the width of the expand column | string \| number | - | |
| childrenColumnName | The column contains children to display | string | children |
| defaultExpandAllRows | Expand all rows initially | boolean | false |
| defaultExpandedRowKeys | Initial expanded row keys | string\[] | - |
@@ -202,6 +203,7 @@ Properties for row selection.
| 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) | - | |
+| onSelectNone | Callback executed when row selection is cleared | function() | - | |
### scroll
diff --git a/components/table/index.zh-CN.md b/components/table/index.zh-CN.md
index 28270cd0b4..390360b1f1 100644
--- a/components/table/index.zh-CN.md
+++ b/components/table/index.zh-CN.md
@@ -173,7 +173,8 @@ const columns = [
展开功能的配置。
| 参数 | 说明 | 类型 | 默认值 |
-| --- | --- | --- | --- |
+| --- | --- | --- | --- | --- |
+| columnWidth | 自定义展开列宽度 | string \| number | - | |
| childrenColumnName | 指定树形结构的列名 | string | children |
| defaultExpandAllRows | 初始时,是否展开所有行 | boolean | false |
| defaultExpandedRowKeys | 默认展开的行 | string\[] | - |
@@ -209,6 +210,7 @@ const columns = [
| onSelect | 用户手动选择/取消选择某行的回调 | function(record, selected, selectedRows, nativeEvent) | - | |
| onSelectAll | 用户手动选择/取消选择所有行的回调 | function(selected, selectedRows, changeRows) | - | |
| onSelectInvert | 用户手动选择反选的回调 | function(selectedRowKeys) | - | |
+| onSelectNone | 用户清空选择的回调 | function() | - | |
### scroll
diff --git a/components/table/interface.tsx b/components/table/interface.tsx
index b3f7cb2de0..75e4804b67 100644
--- a/components/table/interface.tsx
+++ b/components/table/interface.tsx
@@ -29,6 +29,7 @@ export interface TableLocale {
filterEmptyText?: React.ReactNode;
emptyText?: React.ReactNode | (() => React.ReactNode);
selectAll?: React.ReactNode;
+ selectNone?: React.ReactNode;
selectInvert?: React.ReactNode;
selectionAll?: React.ReactNode;
sortTitle?: string;
@@ -143,6 +144,7 @@ export interface TableRowSelection {
onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
/** @deprecated This function is meaningless and should use `onChange` instead */
onSelectInvert?: (selectedRowKeys: Key[]) => void;
+ onSelectNone?: () => void;
selections?: INTERNAL_SELECTION_ITEM[] | boolean;
hideSelectAll?: boolean;
fixed?: boolean;
diff --git a/components/time-picker/locale/hr_HR.tsx b/components/time-picker/locale/hr_HR.tsx
index 647fef34c8..d565f6f250 100644
--- a/components/time-picker/locale/hr_HR.tsx
+++ b/components/time-picker/locale/hr_HR.tsx
@@ -2,6 +2,7 @@ import { TimePickerLocale } from '../index';
const locale: TimePickerLocale = {
placeholder: 'Odaberite vrijeme',
+ rangePlaceholder: ['Vrijeme početka', 'Vrijeme završetka'],
};
export default locale;
diff --git a/components/tree-select/__tests__/__snapshots__/demo.test.js.snap b/components/tree-select/__tests__/__snapshots__/demo.test.js.snap
index 88279693f0..ec3fd447e2 100644
--- a/components/tree-select/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/tree-select/__tests__/__snapshots__/demo.test.js.snap
@@ -129,67 +129,81 @@ exports[`renders ./components/tree-select/demo/checkable.md correctly 1`] = `
`;
@@ -203,32 +217,41 @@ exports[`renders ./components/tree-select/demo/multiple.md correctly 1`] = `
+
diff --git a/components/tree-select/__tests__/__snapshots__/index.test.js.snap b/components/tree-select/__tests__/__snapshots__/index.test.js.snap
index aa28e38ab2..d87d493d38 100644
--- a/components/tree-select/__tests__/__snapshots__/index.test.js.snap
+++ b/components/tree-select/__tests__/__snapshots__/index.test.js.snap
@@ -150,71 +150,90 @@ exports[`TreeSelect TreeSelect Custom Icons should support customized icons 1`]
-
-
- my leaf
-
-
-
- remove
+
+
+ my leaf
+
+
+
+ remove
+
+
-
-
-
-
+
= (pr
listType,
onPreview,
onDownload,
+ onChange,
previewFile,
disabled,
locale: propLocale,
@@ -44,6 +45,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr
children,
style,
itemRender,
+ maxCount,
} = props;
const [dragState, setDragState] = React.useState('drop');
@@ -71,16 +73,22 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr
);
}, []);
- const onChange = (info: UploadChangeParam) => {
- setFileList(info.fileList);
+ const onInternalChange = (info: UploadChangeParam) => {
+ let cloneList = [...info.fileList];
- const { onChange: onChangeProp } = props;
- if (onChangeProp) {
- onChangeProp({
- ...info,
- fileList: [...info.fileList],
- });
+ // Cut to match count
+ if (maxCount === 1) {
+ cloneList = cloneList.slice(-1);
+ } else if (maxCount) {
+ cloneList = cloneList.slice(0, maxCount);
}
+
+ setFileList(cloneList);
+
+ onChange?.({
+ ...info,
+ fileList: cloneList,
+ });
};
const onStart = (file: RcFile) => {
@@ -96,7 +104,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr
nextFileList[fileIndex] = targetItem;
}
- onChange({
+ onInternalChange({
file: targetItem,
fileList: nextFileList,
});
@@ -118,7 +126,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr
targetItem.status = 'done';
targetItem.response = response;
targetItem.xhr = xhr;
- onChange({
+ onInternalChange({
file: { ...targetItem },
fileList: getFileList().concat(),
});
@@ -131,7 +139,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr
return;
}
targetItem.percent = e.percent;
- onChange({
+ onInternalChange({
event: e,
file: { ...targetItem },
fileList: getFileList().concat(),
@@ -147,7 +155,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr
targetItem.error = error;
targetItem.response = response;
targetItem.status = 'error';
- onChange({
+ onInternalChange({
file: { ...targetItem },
fileList: getFileList().concat(),
});
@@ -168,7 +176,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr
upload.current.abort(file);
}
- onChange({
+ onInternalChange({
file,
fileList: removedFileList,
});
@@ -197,7 +205,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr
}
});
- onChange({
+ onInternalChange({
file,
fileList: uniqueList,
});
diff --git a/components/upload/__tests__/__snapshots__/demo.test.js.snap b/components/upload/__tests__/__snapshots__/demo.test.js.snap
index 1cfdfba476..78143368fc 100644
--- a/components/upload/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/upload/__tests__/__snapshots__/demo.test.js.snap
@@ -1917,6 +1917,122 @@ exports[`renders ./components/upload/demo/fileList.md correctly 1`] = `
`;
+exports[`renders ./components/upload/demo/max-count.md correctly 1`] = `
+
+`;
+
exports[`renders ./components/upload/demo/picture-card.md correctly 1`] = `
{
mountTest(Upload);
@@ -629,4 +630,96 @@ describe('Upload', () => {
});
});
});
+
+ describe('maxCount', () => {
+ it('replace when only 1', async () => {
+ const onChange = jest.fn();
+ const fileList = [
+ {
+ uid: 'bar',
+ name: 'bar.png',
+ },
+ ];
+
+ const props = {
+ action: 'http://upload.com',
+ fileList,
+ onChange,
+ maxCount: 1,
+ };
+
+ const wrapper = mount(
+
+
+ ,
+ );
+
+ wrapper.find('input').simulate('change', {
+ target: {
+ files: [
+ new File(['foo'], 'foo.png', {
+ type: 'image/png',
+ }),
+ ],
+ },
+ });
+
+ await sleep(20);
+
+ expect(onChange.mock.calls[0][0].fileList).toHaveLength(1);
+ expect(onChange.mock.calls[0][0].fileList[0]).toEqual(
+ expect.objectContaining({
+ name: 'foo.png',
+ }),
+ );
+ });
+
+ it('maxCount > 1', async () => {
+ const onChange = jest.fn();
+ const fileList = [
+ {
+ uid: 'bar',
+ name: 'bar.png',
+ },
+ ];
+
+ const props = {
+ action: 'http://upload.com',
+ fileList,
+ onChange,
+ maxCount: 2,
+ };
+
+ const wrapper = mount(
+
+
+ ,
+ );
+
+ wrapper.find('input').simulate('change', {
+ target: {
+ files: [
+ new File(['foo'], 'foo.png', {
+ type: 'image/png',
+ }),
+ new File(['invisible'], 'invisible.png', {
+ type: 'image/png',
+ }),
+ ],
+ },
+ });
+
+ await sleep(20);
+
+ expect(onChange.mock.calls[0][0].fileList).toHaveLength(2);
+ expect(onChange.mock.calls[0][0].fileList).toEqual([
+ expect.objectContaining({
+ name: 'bar.png',
+ }),
+ expect.objectContaining({
+ name: 'foo.png',
+ }),
+ ]);
+ });
+ });
});
diff --git a/components/upload/demo/max-count.md b/components/upload/demo/max-count.md
new file mode 100644
index 0000000000..5a284066fd
--- /dev/null
+++ b/components/upload/demo/max-count.md
@@ -0,0 +1,40 @@
+---
+order: 10
+title:
+ zh-CN: 限制数量
+ en-US: Max Count
+---
+
+## zh-CN
+
+通过 `maxCount` 限制上传数量。当为 `1` 时,始终用最新上传的代替当前。
+
+## en-US
+
+Limit files with `maxCount`. Will replace current one when `maxCount` is `1`.
+
+```jsx
+import { Upload, Button, Space } from 'antd';
+import { UploadOutlined } from '@ant-design/icons';
+
+ReactDOM.render(
+
+
+ }>Upload (Max: 1)
+
+
+ }>Upload (Max: 3)
+
+ ,
+ mountNode,
+);
+```
diff --git a/components/upload/interface.tsx b/components/upload/interface.tsx
index 0a5ff4a57e..3898a2dfa9 100755
--- a/components/upload/interface.tsx
+++ b/components/upload/interface.tsx
@@ -108,6 +108,8 @@ export interface UploadProps {
isImageUrl?: (file: UploadFile) => boolean;
progress?: UploadListProgressProps;
itemRender?: ItemRender;
+ /** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */
+ maxCount?: number;
}
export interface UploadState {
diff --git a/package.json b/package.json
index 144fbf91d3..7180750f3b 100644
--- a/package.json
+++ b/package.json
@@ -124,27 +124,27 @@
"rc-drawer": "~4.1.0",
"rc-dropdown": "~3.2.0",
"rc-field-form": "~1.17.3",
- "rc-image": "~4.2.0",
+ "rc-image": "~4.4.0",
"rc-input-number": "~6.1.0",
"rc-mentions": "~1.5.0",
"rc-menu": "~8.10.0",
"rc-motion": "^2.4.0",
"rc-notification": "~4.5.2",
"rc-pagination": "~3.1.2",
- "rc-picker": "~2.4.1",
+ "rc-picker": "~2.5.1",
"rc-progress": "~3.1.0",
"rc-rate": "~2.9.0",
- "rc-resize-observer": "^0.2.3",
- "rc-select": "~11.5.3",
- "rc-slider": "~9.6.1",
+ "rc-resize-observer": "^1.0.0",
+ "rc-select": "~12.0.0",
+ "rc-slider": "~9.7.1",
"rc-steps": "~4.1.0",
"rc-switch": "~3.2.0",
- "rc-table": "~7.11.0",
+ "rc-table": "~7.12.0",
"rc-tabs": "~11.7.0",
"rc-textarea": "~0.3.0",
"rc-tooltip": "~5.0.0",
"rc-tree": "~4.1.0",
- "rc-tree-select": "~4.2.0",
+ "rc-tree-select": "~4.3.0",
"rc-upload": "~3.3.4",
"rc-util": "^5.1.0",
"scroll-into-view-if-needed": "^2.2.25",
@@ -242,7 +242,7 @@
"rc-scroll-anim": "^2.5.8",
"rc-trigger": "^5.1.2",
"rc-tween-one": "^2.4.1",
- "rc-virtual-list": "^3.0.3",
+ "rc-virtual-list": "^3.2.4",
"react": "^17.0.1",
"react-color": "^2.17.3",
"react-copy-to-clipboard": "^5.0.1",
@@ -295,7 +295,7 @@
"bundlesize": [
{
"path": "./dist/antd.min.js",
- "maxSize": "270 kB"
+ "maxSize": "275 kB"
},
{
"path": "./dist/antd.min.css",