Merge pull request #28619 from ant-design/feature

This commit is contained in:
Ant Design GitHub Bot 2020-12-31 17:41:15 +08:00 committed by GitHub
commit c47f81ed58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 2761 additions and 968 deletions

View File

@ -793,6 +793,34 @@ Array [
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>
</span>,
<span

View File

@ -6,7 +6,14 @@ import { PickerLocale } from '../generatePicker';
const locale: PickerLocale = {
lang: {
placeholder: 'Odaberite datum',
yearPlaceholder: 'Odaberite godinu',
quarterPlaceholder: 'Odaberite četvrtinu',
monthPlaceholder: 'Odaberite mjesec',
weekPlaceholder: 'Odaberite tjedan',
rangePlaceholder: ['Početni datum', 'Završni datum'],
rangeYearPlaceholder: ['Početna godina', 'Završna godina'],
rangeMonthPlaceholder: ['Početni mjesec', 'Završni mjesec'],
rangeWeekPlaceholder: ['Početni tjedan', 'Završni tjedan'],
...CalendarLocale,
},
timePickerLocale: {

View File

@ -1,6 +1,7 @@
import * as React from 'react';
import { DescriptionsItemProps } from './Item';
import Cell from './Cell';
import { DescriptionsContext, DescriptionsContextProps } from '.';
interface CellConfig {
component: string | [string, string];
@ -12,7 +13,14 @@ interface CellConfig {
function renderCells(
items: React.ReactElement<DescriptionsItemProps>[],
{ colon, prefixCls, bordered }: RowProps,
{ component, type, showLabel, showContent }: CellConfig,
{
component,
type,
showLabel,
showContent,
labelStyle: rootLabelStyle,
contentStyle: rootContentStyle,
}: CellConfig & DescriptionsContextProps,
) {
return items.map(
(
@ -37,8 +45,8 @@ function renderCells(
key={`${type}-${key || index}`}
className={className}
style={style}
labelStyle={labelStyle}
contentStyle={contentStyle}
labelStyle={{ ...rootLabelStyle, ...labelStyle }}
contentStyle={{ ...rootContentStyle, ...contentStyle }}
span={span}
colon={colon}
component={component}
@ -54,7 +62,7 @@ function renderCells(
<Cell
key={`label-${key || index}`}
className={className}
style={{ ...style, ...labelStyle }}
style={{ ...rootLabelStyle, ...style, ...labelStyle }}
span={1}
colon={colon}
component={component[0]}
@ -65,7 +73,7 @@ function renderCells(
<Cell
key={`content-${key || index}`}
className={className}
style={{ ...style, ...contentStyle }}
style={{ ...rootContentStyle, ...style, ...contentStyle }}
span={span * 2 - 1}
component={component[1]}
itemPrefixCls={itemPrefixCls}
@ -87,18 +95,26 @@ export interface RowProps {
}
const Row: React.FC<RowProps> = props => {
const descContext = React.useContext(DescriptionsContext);
const { prefixCls, vertical, row, index, bordered } = props;
if (vertical) {
return (
<>
<tr key={`label-${index}`} className={`${prefixCls}-row`}>
{renderCells(row, props, { component: 'th', type: 'label', showLabel: true })}
{renderCells(row, props, {
component: 'th',
type: 'label',
showLabel: true,
...descContext,
})}
</tr>
<tr key={`content-${index}`} className={`${prefixCls}-row`}>
{renderCells(row, props, {
component: 'td',
type: 'content',
showContent: true,
...descContext,
})}
</tr>
</>
@ -112,6 +128,7 @@ const Row: React.FC<RowProps> = props => {
type: 'item',
showLabel: true,
showContent: true,
...descContext,
})}
</tr>
);

View File

@ -960,6 +960,165 @@ Array [
</table>
</div>
</div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-descriptions"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
Root style
</div>
</div>
<div
class="ant-descriptions-view"
>
<table>
<tbody>
<tr
class="ant-descriptions-row"
>
<td
class="ant-descriptions-item"
colspan="1"
>
<div
class="ant-descriptions-item-container"
>
<span
class="ant-descriptions-item-label"
style="background:red"
>
Product
</span>
<span
class="ant-descriptions-item-content"
style="background:green"
>
Cloud Database
</span>
</div>
</td>
<td
class="ant-descriptions-item"
colspan="1"
>
<div
class="ant-descriptions-item-container"
>
<span
class="ant-descriptions-item-label"
style="background:red"
>
Billing Mode
</span>
<span
class="ant-descriptions-item-content"
style="background:green"
>
Prepaid
</span>
</div>
</td>
<td
class="ant-descriptions-item"
colspan="1"
>
<div
class="ant-descriptions-item-container"
>
<span
class="ant-descriptions-item-label"
style="background:red;color:orange"
>
Automatic Renewal
</span>
<span
class="ant-descriptions-item-content"
style="background:green;color:blue"
>
YES
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>,
<div
class="ant-descriptions ant-descriptions-bordered"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
Root style
</div>
</div>
<div
class="ant-descriptions-view"
>
<table>
<tbody>
<tr
class="ant-descriptions-row"
>
<th
class="ant-descriptions-item-label"
colspan="1"
style="background:red"
>
Product
</th>
<td
class="ant-descriptions-item-content"
colspan="1"
style="background:green"
>
Cloud Database
</td>
<th
class="ant-descriptions-item-label"
colspan="1"
style="background:red"
>
Billing Mode
</th>
<td
class="ant-descriptions-item-content"
colspan="1"
style="background:green"
>
Prepaid
</td>
<th
class="ant-descriptions-item-label"
colspan="1"
style="background:red;color:orange"
>
Automatic Renewal
</th>
<td
class="ant-descriptions-item-content"
colspan="1"
style="background:green;color:blue"
>
YES
</td>
</tr>
</tbody>
</table>
</div>
</div>,
]
`;

View File

@ -15,28 +15,53 @@ debug: true
Customize label & wrapper style
```tsx
import { Descriptions } from 'antd';
import { Descriptions, Divider } from 'antd';
const labelStyle: React.CSSProperties = { background: 'red' };
const contentStyle: React.CSSProperties = { background: 'green' };
function renderCelledDesc(bordered?: boolean) {
return (
<Descriptions title="User Info" bordered={bordered}>
<Descriptions.Item label="Product" labelStyle={labelStyle} contentStyle={contentStyle}>
Cloud Database
</Descriptions.Item>
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
<Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
</Descriptions>
);
}
function renderRootDesc(bordered?: boolean) {
return (
<Descriptions
title="Root style"
labelStyle={labelStyle}
contentStyle={contentStyle}
bordered={bordered}
>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
<Descriptions.Item
label="Automatic Renewal"
labelStyle={{ color: 'orange' }}
contentStyle={{ color: 'blue' }}
>
YES
</Descriptions.Item>
</Descriptions>
);
}
ReactDOM.render(
<>
<Descriptions title="User Info">
<Descriptions.Item label="Product" labelStyle={labelStyle} contentStyle={contentStyle}>
Cloud Database
</Descriptions.Item>
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
<Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
</Descriptions>
{renderCelledDesc()}
{renderCelledDesc(true)}
<Descriptions title="User Info" bordered>
<Descriptions.Item label="Product" labelStyle={labelStyle} contentStyle={contentStyle}>
Cloud Database
</Descriptions.Item>
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
<Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
</Descriptions>
<Divider />
{renderRootDesc()}
{renderRootDesc(true)}
</>,
mountNode,
);

View File

@ -21,7 +21,9 @@ Commonly displayed on the details page.
| bordered | Whether to display the border | boolean | false | |
| colon | Change default props `colon` value of Descriptions.Item | boolean | true | |
| column | The number of `DescriptionItems` in a row,could be a number or a object like `{ xs: 8, sm: 16, md: 24}`,(Only set `bordered={true}` to take effect) | number | 3 | |
| contentStyle | Customize label style | CSSProperties | - | 4.10.0 |
| extra | The action area of the description list, placed at the top-right | ReactNode | - | 4.5.0 |
| labelStyle | Customize label style | CSSProperties | - | 4.10.0 |
| layout | Define description layout | `horizontal` \| `vertical` | `horizontal` | |
| size | Set the size of the list. Can be set to `middle`,`small`, or not filled | `default` \| `middle` \| `small` | - | |
| title | The title of the description list, placed at the top | ReactNode | - | |

View File

@ -13,6 +13,13 @@ import Row from './Row';
import DescriptionsItem from './Item';
import { cloneElement } from '../_util/reactNode';
export interface DescriptionsContextProps {
labelStyle?: React.CSSProperties;
contentStyle?: React.CSSProperties;
}
export const DescriptionsContext = React.createContext<DescriptionsContextProps>({});
const DEFAULT_COLUMN_MAP: Record<Breakpoint, number> = {
xxl: 3,
xl: 3,
@ -104,6 +111,8 @@ export interface DescriptionsProps {
column?: number | Partial<Record<Breakpoint, number>>;
layout?: 'horizontal' | 'vertical';
colon?: boolean;
labelStyle?: React.CSSProperties;
contentStyle?: React.CSSProperties;
}
function Descriptions({
@ -118,6 +127,8 @@ function Descriptions({
className,
style,
size,
labelStyle,
contentStyle,
}: DescriptionsProps) {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('descriptions', customizePrefixCls);
@ -142,43 +153,45 @@ function Descriptions({
const rows = getRows(children, mergedColumn);
return (
<div
className={classNames(
prefixCls,
{
[`${prefixCls}-${size}`]: size && size !== 'default',
[`${prefixCls}-bordered`]: !!bordered,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
)}
style={style}
>
{(title || extra) && (
<div className={`${prefixCls}-header`}>
{title && <div className={`${prefixCls}-title`}>{title}</div>}
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
</div>
)}
<DescriptionsContext.Provider value={{ labelStyle, contentStyle }}>
<div
className={classNames(
prefixCls,
{
[`${prefixCls}-${size}`]: size && size !== 'default',
[`${prefixCls}-bordered`]: !!bordered,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
)}
style={style}
>
{(title || extra) && (
<div className={`${prefixCls}-header`}>
{title && <div className={`${prefixCls}-title`}>{title}</div>}
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
</div>
)}
<div className={`${prefixCls}-view`}>
<table>
<tbody>
{rows.map((row, index) => (
<Row
key={index}
index={index}
colon={colon}
prefixCls={prefixCls}
vertical={layout === 'vertical'}
bordered={bordered}
row={row}
/>
))}
</tbody>
</table>
<div className={`${prefixCls}-view`}>
<table>
<tbody>
{rows.map((row, index) => (
<Row
key={index}
index={index}
colon={colon}
prefixCls={prefixCls}
vertical={layout === 'vertical'}
bordered={bordered}
row={row}
/>
))}
</tbody>
</table>
</div>
</div>
</div>
</DescriptionsContext.Provider>
);
}

View File

@ -22,7 +22,9 @@ cover: https://gw.alipayobjects.com/zos/alicdn/MjtG9_FOI/Descriptions.svg
| bordered | 是否展示边框 | boolean | false | |
| colon | 配置 `Descriptions.Item``colon` 的默认值 | boolean | true | |
| column | 一行的 `DescriptionItems` 数量,可以写成像素值或支持响应式的对象写法 `{ xs: 8, sm: 16, md: 24}` | number | 3 | |
| contentStyle | 自定义内容样式 | CSSProperties | - | 4.10.0 |
| extra | 描述列表的操作区域,显示在右上方 | ReactNode | - | 4.5.0 |
| labelStyle | 自定义标签样式 | CSSProperties | - | 4.10.0 |
| layout | 描述布局 | `horizontal` \| `vertical` | `horizontal` | |
| size | 设置列表的大小。可以设置为 `middle` 、`small`, 或不填(只有设置 `bordered={true}` 生效) | `default` \| `middle` \| `small` | - | |
| title | 描述列表的标题,显示在最顶部 | ReactNode | - | |

View File

@ -4,6 +4,7 @@ import classNames from 'classnames';
import FieldForm, { List } from 'rc-field-form';
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
import { ValidateErrorEntity } from 'rc-field-form/lib/interface';
import { Options } from 'scroll-into-view-if-needed';
import { ColProps } from '../grid/col';
import { ConfigContext } from '../config-provider';
import { FormContext, FormContextProps } from './context';
@ -24,7 +25,7 @@ export interface FormProps<Values = any> extends Omit<RcFormProps<Values>, 'form
wrapperCol?: ColProps;
form?: FormInstance<Values>;
size?: SizeType;
scrollToFirstError?: boolean;
scrollToFirstError?: Options | boolean;
requiredMark?: RequiredMark;
/** @deprecated Will warning in future branch. Pls use `requiredMark` instead. */
hideRequiredMark?: boolean;
@ -106,8 +107,13 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
onFinishFailed(errorInfo);
}
let defaultScrollToFirstError: Options = { block: 'nearest' };
if (scrollToFirstError && errorInfo.errorFields.length) {
wrapForm.scrollToField(errorInfo.errorFields[0].name);
if (typeof scrollToFirstError === 'object') {
defaultScrollToFirstError = scrollToFirstError;
}
wrapForm.scrollToField(errorInfo.errorFields[0].name, defaultScrollToFirstError);
}
};

View File

@ -5167,33 +5167,42 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
style="width:0"
<div
class="ant-select-selection-overflow"
>
<input
aria-activedescendant="validate_other_select-multiple_list_0"
aria-autocomplete="list"
aria-controls="validate_other_select-multiple_list"
aria-haspopup="listbox"
aria-owns="validate_other_select-multiple_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="validate_other_select-multiple"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
<div
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
style="opacity:1"
>
 
</span>
</span>
<div
class="ant-select-selection-search"
style="width:0"
>
<input
aria-activedescendant="validate_other_select-multiple_list_0"
aria-autocomplete="list"
aria-controls="validate_other_select-multiple_list"
aria-haspopup="listbox"
aria-owns="validate_other_select-multiple_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="validate_other_select-multiple"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
>
 
</span>
</div>
</div>
</div>
<span
class="ant-select-selection-placeholder"
>

View File

@ -227,7 +227,7 @@ describe('Form', () => {
const onFinishFailed = jest.fn();
const wrapper = mount(
<Form scrollToFirstError onFinishFailed={onFinishFailed}>
<Form scrollToFirstError={{ block: 'center' }} onFinishFailed={onFinishFailed}>
<Form.Item name="test" rules={[{ required: true }]}>
<input />
</Form.Item>
@ -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();

View File

@ -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 |

View File

@ -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 |

View File

@ -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"
/>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>
`;
@ -22,6 +50,34 @@ exports[`renders ./components/image/demo/fallback.md correctly 1`] = `
src="error"
style="height:200px"
/>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>
`;
@ -55,6 +111,34 @@ exports[`renders ./components/image/demo/placeholder.md correctly 1`] = `
/>
</div>
</div>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>
</div>
<div
@ -82,6 +166,34 @@ Array [
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
/>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>,
<div
class="ant-image"
@ -91,6 +203,34 @@ Array [
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg"
/>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>,
]
`;

View File

@ -7,5 +7,33 @@ exports[`Image rtl render component should be rendered correctly in RTL directio
<img
class="ant-image-img"
/>
<div
class="ant-image-mask"
>
<div
class="ant-image-mask-info"
>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
/>
</svg>
</span>
Preview
</div>
</div>
</div>
`;

View File

@ -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<P> extends React.FC<P> {
PreviewGroup: typeof PreviewGroup;
}
const Image: CompositionImage<ImageProps> = ({ prefixCls: customizePrefixCls, ...otherProps }) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const Image: CompositionImage<ImageProps> = ({
prefixCls: customizePrefixCls,
preview,
...otherProps
}) => {
const { getPrefixCls } = useContext(ConfigContext);
const prefixCls = getPrefixCls('image', customizePrefixCls);
return <RcImage prefixCls={prefixCls} {...otherProps} />;
const { locale: contextLocale = defaultLocale } = useContext(ConfigContext);
const imageLocale = contextLocale.Image || defaultLocale.Image;
const mergedPreview = React.useMemo(() => {
if (preview === false) {
return preview;
}
return {
mask: (
<div className={`${prefixCls}-mask-info`}>
<EyeOutlined />
{imageLocale?.preview}
</div>
),
...(typeof preview === 'object' ? preview : null),
};
}, [preview, imageLocale]);
return <RcImage prefixCls={prefixCls} preview={mergedPreview} {...otherProps} />;
};
export { ImageProps };

View File

@ -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();
}

View File

@ -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<React.InputHTMLAttributes<HTMLInputElement>, '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<InputProps, InputState> {
}
}
focus = () => {
this.input.focus();
focus = (option?: InputFocusOptions) => {
triggerFocus(this.input, option);
};
blur() {

View File

@ -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<TextAreaRef, TextAreaProps>(
@ -38,7 +44,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const size = React.useContext(SizeContext);
const innerRef = React.useRef<TextAreaRef>();
const innerRef = React.useRef<RcTextArea>();
const clearableInputRef = React.useRef<ClearableLabeledInput>(null);
const [value, setValue] = useMergedState(props.defaultValue, {
@ -63,18 +69,26 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
handleSetValue(e.target.value);
resolveOnChange(innerRef.current!, e, props.onChange);
resolveOnChange(innerRef.current as any, e, props.onChange);
};
const handleReset = (e: React.MouseEvent<HTMLElement, 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 = (
<RcTextArea
{...omit(props, ['allowClear'])}
@ -88,14 +102,16 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
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<TextAreaRef, TextAreaProps>(
// 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 (
<div

View File

@ -1184,6 +1184,104 @@ exports[`renders ./components/input/demo/borderless-debug.md correctly 1`] = `
</div>
`;
exports[`renders ./components/input/demo/focus.md correctly 1`] = `
<div
class="ant-space ant-space-vertical"
style="width:100%"
>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn"
type="button"
>
<span>
Focus at first
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn"
type="button"
>
<span>
Focus at last
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn"
type="button"
>
<span>
Focus to select all
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn"
type="button"
>
<span>
Focus prevent scroll
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
aria-checked="true"
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
Input
</span>
</button>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<input
class="ant-input"
style="width:100%"
type="text"
value="Ant Design love you!"
/>
</div>
</div>
`;
exports[`renders ./components/input/demo/group.md correctly 1`] = `
<div
class="site-input-group-wrapper"

View File

@ -0,0 +1,58 @@
import React from 'react';
import { mount } from 'enzyme';
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import Input from '..';
const { TextArea } = Input;
describe('Input.Focus', () => {
let inputSpy: ReturnType<typeof spyElementPrototypes>;
let textareaSpy: ReturnType<typeof spyElementPrototypes>;
let focus: ReturnType<typeof jest.fn>;
let setSelectionRange: ReturnType<typeof jest.fn>;
beforeEach(() => {
focus = jest.fn();
setSelectionRange = jest.fn();
inputSpy = spyElementPrototypes(HTMLInputElement, {
focus,
setSelectionRange,
});
textareaSpy = spyElementPrototypes(HTMLTextAreaElement, {
focus,
setSelectionRange,
});
});
afterEach(() => {
inputSpy.mockRestore();
textareaSpy.mockRestore();
});
it('start', () => {
const ref = React.createRef<Input>();
mount(<Input ref={ref} defaultValue="light" />);
ref.current!.focus({ cursor: 'start' });
expect(focus).toHaveBeenCalled();
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 0);
});
it('end', () => {
const ref = React.createRef<Input>();
mount(<Input ref={ref} defaultValue="light" />);
ref.current!.focus({ cursor: 'end' });
expect(focus).toHaveBeenCalled();
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 5, 5);
});
it('all', () => {
const ref = React.createRef<any>();
mount(<TextArea ref={ref} defaultValue="light" />);
ref.current!.focus({ cursor: 'all' });
expect(focus).toHaveBeenCalled();
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 5);
});
});

View File

@ -140,7 +140,7 @@ describe('TextArea', () => {
it('maxLength', () => {
const wrapper = mount(<TextArea maxLength={5} showCount value="12345678" />);
const textarea = wrapper.find('.ant-input-textarea');
expect(wrapper.find('textarea').prop('value')).toBe('12345678');
expect(wrapper.find('textarea').prop('value')).toBe('12345');
expect(textarea.prop('data-count')).toBe('5 / 5');
});
@ -165,6 +165,19 @@ describe('TextArea', () => {
expect(wrapper.find('.ant-input').hasClass('bamboo')).toBeFalsy();
expect(wrapper.find('.ant-input').props().style.background).toBeFalsy();
});
it('count formatter', () => {
const wrapper = mount(
<TextArea
maxLength={5}
showCount={{ formatter: ({ count, maxLength }) => `${count}, ${maxLength}` }}
value="12345678"
/>,
);
const textarea = wrapper.find('.ant-input-textarea');
expect(wrapper.find('textarea').prop('value')).toBe('12345');
expect(textarea.prop('data-count')).toBe('5, 5');
});
});
it('should support size', async () => {

View File

@ -0,0 +1,84 @@
---
order: 21
title:
zh-CN: 聚焦
en-US: Focus
---
## zh-CN
聚焦额外配置属性。
## en-US
Focus with additional option.
```tsx
import { Input, Space, Button, Switch } from 'antd';
const Demo = () => {
const inputRef = React.useRef<any>(null);
const [input, setInput] = React.useState(true);
const sharedProps = {
style: { width: '100%' },
defaultValue: 'Ant Design love you!',
ref: inputRef,
};
return (
<Space direction="vertical" style={{ width: '100%' }}>
<Space>
<Button
onClick={() => {
inputRef.current!.focus({
cursor: 'start',
});
}}
>
Focus at first
</Button>
<Button
onClick={() => {
inputRef.current!.focus({
cursor: 'end',
});
}}
>
Focus at last
</Button>
<Button
onClick={() => {
inputRef.current!.focus({
cursor: 'all',
});
}}
>
Focus to select all
</Button>
<Button
onClick={() => {
inputRef.current!.focus({
preventScroll: true,
});
}}
>
Focus prevent scroll
</Button>
<Switch
checked={input}
checkedChildren="Input"
unCheckedChildren="TextArea"
onChange={() => {
setInput(!input);
}}
/>
</Space>
{input ? <Input {...sharedProps} /> : <Input.TextArea {...sharedProps} />}
</Space>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -47,7 +47,7 @@ The rest of the props of Input are exactly the same as the original [input](http
| bordered | Whether has border style | boolean | true | 4.5.0 |
| defaultValue | The initial input content | string | - | |
| maxLength | The max length | number | - | 4.7.0 |
| showCount | Whether show text count | boolean | false | 4.7.0 |
| showCount | Whether show text count | boolean \| { formatter: ({ count: number, maxLength?: number }) => string } | false | 4.7.0 (formatter: 4.10.0) |
| value | The input content value | string | - | |
| onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - | |
| onResize | The callback function that is triggered when resize | function({ width, height }) | - | |
@ -85,6 +85,13 @@ Supports all props of `Input`.
| iconRender | Custom toggle button | (visible) => ReactNode | (visible) => (visible ? &lt;EyeOutlined /> : &lt;EyeInvisibleOutlined />) | 4.3.0 |
| visibilityToggle | Whether show toggle button | boolean | true | |
#### Input Methods
| Name | Description | Parameters | Version |
| --- | --- | --- | --- |
| blur | Remove focus | - | |
| focus | Get focus | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | option - 4.10.0 |
## FAQ
### Why Input lose focus when change `prefix/suffix`

View File

@ -46,9 +46,10 @@ Input 的其他属性和 React 自带的 [input](https://facebook.github.io/reac
| allowClear | 可以点击清除图标删除内容 | boolean | false | |
| autoSize | 自适应内容高度,可设置为 true \| false 或对象:{ minRows: 2, maxRows: 6 } | boolean \| object | false | |
| bordered | 是否有边框 | boolean | true | 4.5.0 |
| countFormatter | 指定字数展示的格式 | (count: number, maxLength?: number) => string | - | 4.10.0 |
| defaultValue | 输入框默认内容 | string | - | |
| maxLength | 内容最大长度 | number | - | 4.7.0 |
| showCount | 是否展示字数 | boolean | false | 4.7.0 |
| showCount | 是否展示字数 | boolean \| { formatter: ({ count: number, maxLength?: number }) => string } | false | 4.7.0 (formatter: 4.10.0) |
| value | 输入框内容 | string | - | |
| onPressEnter | 按下回车的回调 | function(e) | - | |
| onResize | resize 回调 | function({ width, height }) | - | |
@ -86,6 +87,13 @@ Input 的其他属性和 React 自带的 [input](https://facebook.github.io/reac
| iconRender | 自定义切换按钮 | (visible) => ReactNode | (visible) => (visible ? &lt;EyeOutlined /> : &lt;EyeInvisibleOutlined />) | 4.3.0 |
| visibilityToggle | 是否显示切换按钮 | boolean | true | |
#### Input Methods
| 名称 | 说明 | 参数 | 版本 |
| --- | --- | --- | --- |
| blur | 取消焦点 | - | |
| focus | 获取焦点 | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | option - 4.10.0 |
## FAQ
### 为什么我动态改变 `prefix/suffix`Input 会失去焦点?

View File

@ -35,6 +35,9 @@ export interface Locale {
optional?: string;
defaultValidateMessages: ValidateMessages;
};
Image?: {
preview: string;
};
}
export interface LocaleProviderProps {

View File

@ -24,6 +24,7 @@ const localeValues: Locale = {
emptyText: 'No data',
selectAll: 'Select current page',
selectInvert: 'Invert current page',
selectNone: 'Clear all data',
selectionAll: 'Select all data',
sortTitle: 'Sort',
expand: 'Expand row',
@ -125,6 +126,9 @@ const localeValues: Locale = {
},
},
},
Image: {
preview: 'Preview',
},
};
export default localeValues;

View File

@ -1,9 +1,12 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/hr_HR';
import DatePicker from '../date-picker/locale/hr_HR';
import TimePicker from '../time-picker/locale/hr_HR';
import Calendar from '../calendar/locale/hr_HR';
import { Locale } from '../locale-provider';
const typeTemplate = '${label} nije valjan ${type}';
const localeValues: Locale = {
locale: 'hr',
Pagination,
@ -17,9 +20,17 @@ const localeValues: Locale = {
filterTitle: 'Filter meni',
filterConfirm: 'OK',
filterReset: 'Reset',
filterEmptyText: 'Nema filtera',
emptyText: 'Nema podataka',
selectAll: 'Označi trenutnu stranicu',
selectInvert: 'Invertiraj trenutnu stranicu',
selectionAll: 'Odaberite sve podatke',
sortTitle: 'Sortiraj',
expand: 'Proširi redak',
collapse: 'Sažmi redak',
triggerDesc: 'Kliknite za sortiranje silazno',
triggerAsc: 'Kliknite za sortiranje uzlazno',
cancelSort: 'Kliknite da biste otkazali sortiranje',
},
Modal: {
okText: 'OK',
@ -35,6 +46,12 @@ const localeValues: Locale = {
searchPlaceholder: 'Pretraži ovdje',
itemUnit: 'stavka',
itemsUnit: 'stavke',
remove: 'Ukloniti',
selectCurrent: 'Odaberite trenutnu stranicu',
removeCurrent: 'Ukloni trenutnu stranicu',
selectAll: 'Odaberite sve podatke',
removeAll: 'Uklonite sve podatke',
selectInvert: 'Obrni trenutnu stranicu',
},
Upload: {
uploading: 'Upload u tijeku...',
@ -50,10 +67,66 @@ const localeValues: Locale = {
icon: 'ikona',
},
Text: {
edit: 'uredi',
copy: 'kopiraj',
copied: 'kopiranje uspješno',
expand: 'proširi',
edit: 'Uredi',
copy: 'Kopiraj',
copied: 'Kopiranje uspješno',
expand: 'Proširi',
},
PageHeader: {
back: 'Natrag',
},
Form: {
optional: '(neobavezno)',
defaultValidateMessages: {
default: 'Pogreška provjere valjanosti polja za ${label}',
required: 'Molimo unesite ${label}',
enum: '${label} mora biti jedan od [${enum}]',
whitespace: '${label} ne može biti prazan znak',
date: {
format: '${label} format datuma je nevažeći',
parse: '${label} ne može se pretvoriti u datum',
invalid: '${label} je nevažeći datum',
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate,
},
string: {
len: '${label} mora biti ${len} slova',
min: '${label} mora biti najmanje ${min} slova',
max: '${label} mora biti do ${max} slova',
range: '${label} mora biti između ${min}-${max} slova',
},
number: {
len: '${label} mora biti jednak ${len}',
min: '${label} mora biti minimalano ${min}',
max: '${label} mora biti maksimalano ${max}',
range: '${label} mora biti između ${min}-${max}',
},
array: {
len: 'Mora biti ${len} ${label}',
min: 'Najmanje ${min} ${label}',
max: 'Najviše ${max} ${label}',
range: 'Količina ${label} mora biti između ${min}-${max}',
},
pattern: {
mismatch: '${label} ne odgovara obrascu ${pattern}',
},
},
},
Image: {
preview: 'Pregled',
},
};

View File

@ -13,7 +13,7 @@ const localeValues: Locale = {
DatePicker,
TimePicker,
Calendar,
// locales for all comoponents
// locales for all components
global: {
placeholder: '请选择',
},
@ -24,6 +24,7 @@ const localeValues: Locale = {
filterEmptyText: '无筛选项',
selectAll: '全选当页',
selectInvert: '反选当页',
selectNone: '清空所有',
selectionAll: '全选所有',
sortTitle: '排序',
expand: '展开行',
@ -124,6 +125,9 @@ const localeValues: Locale = {
},
},
},
Image: {
preview: '预览',
},
};
export default localeValues;

View File

@ -23,6 +23,7 @@ const localeValues: Locale = {
filterEmptyText: '無篩選項',
selectAll: '全部選取',
selectInvert: '反向選取',
selectNone: '清空所有',
selectionAll: '全選所有',
sortTitle: '排序',
expand: '展開行',

View File

@ -23,6 +23,7 @@ const localeValues: Locale = {
filterEmptyText: '無篩選項',
selectAll: '全部選取',
selectInvert: '反向選取',
selectNone: '清空所有',
selectionAll: '全選所有',
sortTitle: '排序',
expand: '展開行',

View File

@ -65,6 +65,27 @@ describe('message', () => {
});
});
it('trigger onClick method', () => {
const onClick = jest.fn();
class Test extends React.Component {
componentDidMount() {
message.info({
onClick,
duration: 0,
content: 'message info',
});
}
render() {
return <div>test message onClick method</div>;
}
}
mount(<Test />);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
document.querySelectorAll('.ant-message-notice')[0].click();
expect(onClick).toHaveBeenCalled();
});
it('should be called like promise', done => {
jest.useRealTimers();
const defaultDuration = 3;

View File

@ -58,6 +58,7 @@ The properties of config are as follows:
| key | The unique identifier of the Message | string \| number | - |
| style | Customized inline style | [CSSProperties](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e434515761b36830c3e58a970abf5186f005adac/types/react/index.d.ts#L794) | - |
| onClose | Specify a function that will be called when the message is closed | function | - |
| onClick | Specify a function that will be called when the message is clicked | function | - |
### Global static methods

View File

@ -127,6 +127,7 @@ export interface ArgsProps {
key?: string | number;
style?: React.CSSProperties;
className?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent {
@ -148,6 +149,7 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent {
</div>
),
onClose: args.onClose,
onClick: args.onClick,
};
}

View File

@ -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 | - |
### 全局方法

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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);
}

View File

@ -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) | - | |

View File

@ -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) | - | |

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
---
order: 23
order: 25
title:
zh-CN: 大数据
en-US: Big Data

View File

@ -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 (
<Space direction="vertical" style={{ width: '100%' }}>
<Select {...selectProps} />
<Select {...selectProps} disabled />
</Space>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -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 | - | |

View File

@ -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 | - | |

View File

@ -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 =======================

View File

@ -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;

View File

@ -83,6 +83,46 @@ Array [
]
`;
exports[`renders ./components/slider/demo/dragableTrack.md correctly 1`] = `
<div
class="ant-slider"
>
<div
class="ant-slider-rail"
/>
<div
class="ant-slider-track ant-slider-track-1"
style="left:20%;right:auto;width:30%"
/>
<div
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="20"
class="ant-slider-handle ant-slider-handle-1"
role="slider"
style="left:20%;right:auto;transform:translateX(-50%)"
tabindex="0"
/>
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="50"
class="ant-slider-handle ant-slider-handle-2"
role="slider"
style="left:50%;right:auto;transform:translateX(-50%)"
tabindex="0"
/>
<div
class="ant-slider-mark"
/>
</div>
`;
exports[`renders ./components/slider/demo/event.md correctly 1`] = `
Array [
<div

View File

@ -0,0 +1,20 @@
---
order: 9
title:
zh-CN: 范围可拖拽
en-US: Draggable track
---
## zh-CN
可以设置 `range.draggableTrack`,使得范围刻度整体可拖拽。
## en-US
Make range track draggable when set `range.draggableTrack`.
```jsx
import { Slider } from 'antd';
ReactDOM.render(<Slider range={{ draggableTrack: true }} defaultValue={[20, 50]} />, mountNode);
```

View File

@ -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 |

View File

@ -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<unknown, SliderSingleProps | SliderRangeProps>(
@ -136,15 +140,24 @@ const Slider = React.forwardRef<unknown, SliderSingleProps | SliderRangeProps>(
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 (
<RcRange
{...(restProps as SliderRangeProps)}
step={restProps.step!}
draggableTrack={draggableTrack}
className={cls}
ref={ref}
handle={(info: HandleGeneratorInfo) =>

View File

@ -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 |
## 方法
| 名称 | 描述 | 版本 |

View File

@ -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` | |

View File

@ -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<StepsProps> {
}
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);

View File

@ -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 | 点状步骤条,可以设置为一个 functionlabelPlacement 将强制为 `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` | |

View File

@ -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;
}
}
}

View File

@ -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%);

View File

@ -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;

View File

@ -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();

View File

@ -984,6 +984,12 @@ exports[`Table.rowSelection render with default selection correctly 1`] = `
>
Invert current page
</li>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
role="menuitem"
>
Clear all data
</li>
</ul>
</div>
</div>
@ -1093,6 +1099,12 @@ exports[`Table.rowSelection should support getPopupContainer 1`] = `
>
Invert current page
</li>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
role="menuitem"
>
Clear all data
</li>
</ul>
</div>
</div>
@ -1420,6 +1432,12 @@ exports[`Table.rowSelection should support getPopupContainer from ConfigProvider
>
Invert current page
</li>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
role="menuitem"
>
Clear all data
</li>
</ul>
</div>
</div>

View File

@ -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',

View File

@ -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<RecordType>(column: ColumnsType<RecordType>[number]): FixedType | undefined {
return column && column.fixed;
@ -49,7 +50,8 @@ interface UseSelectionConfig<RecordType> {
export type INTERNAL_SELECTION_ITEM =
| SelectionItem
| typeof SELECTION_ALL
| typeof SELECTION_INVERT;
| typeof SELECTION_INVERT
| typeof SELECTION_NONE;
function flattenData<RecordType>(
data: RecordType[] | undefined,
@ -82,6 +84,7 @@ export default function useSelection<RecordType>(
onSelect,
onSelectAll,
onSelectInvert,
onSelectNone,
onSelectMultiple,
columnWidth: selectionColWidth,
type: selectionType,
@ -255,7 +258,7 @@ export default function useSelection<RecordType>(
}
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<RecordType>(
},
};
}
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]);

View File

@ -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

View File

@ -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

View File

@ -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<T> {
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;

View File

@ -2,6 +2,7 @@ import { TimePickerLocale } from '../index';
const locale: TimePickerLocale = {
placeholder: 'Odaberite vrijeme',
rangePlaceholder: ['Vrijeme početka', 'Vrijeme završetka'],
};
export default locale;

View File

@ -129,67 +129,81 @@ exports[`renders ./components/tree-select/demo/checkable.md correctly 1`] = `
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-item"
<div
class="ant-select-selection-overflow"
>
<span
class="ant-select-selection-item-content"
>
Node1
</span>
<span
aria-hidden="true"
class="ant-select-selection-item-remove"
style="user-select:none;-webkit-user-select:none"
unselectable="on"
<div
class="ant-select-selection-overflow-item"
style="opacity:1"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
class="ant-select-selection-item"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
class="ant-select-selection-item-content"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
Node1
</span>
<span
aria-hidden="true"
class="ant-select-selection-item-remove"
style="user-select:none;-webkit-user-select:none"
unselectable="on"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
<span
class="ant-select-selection-search"
style="width:0"
>
<input
aria-activedescendant="undefined_list_0"
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-select-selection-search-input"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
</div>
<div
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
style="opacity:1"
>
 
</span>
</span>
<div
class="ant-select-selection-search"
style="width:0"
>
<input
aria-activedescendant="undefined_list_0"
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-select-selection-search-input"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
>
 
</span>
</div>
</div>
</div>
</div>
</div>
`;
@ -203,32 +217,41 @@ exports[`renders ./components/tree-select/demo/multiple.md correctly 1`] = `
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
style="width:0"
<div
class="ant-select-selection-overflow"
>
<input
aria-activedescendant="undefined_list_0"
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-select-selection-search-input"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
<div
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
style="opacity:1"
>
 
</span>
</span>
<div
class="ant-select-selection-search"
style="width:0"
>
<input
aria-activedescendant="undefined_list_0"
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-select-selection-search-input"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
>
 
</span>
</div>
</div>
</div>
<span
class="ant-select-selection-placeholder"
>

View File

@ -150,71 +150,90 @@ exports[`TreeSelect TreeSelect Custom Icons should support customized icons 1`]
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-item"
<div
class="ant-select-selection-overflow"
>
<span
class="ant-select-selection-item-content"
<div
class="ant-select-selection-overflow-item"
style="opacity: 1;"
>
my leaf
</span>
<span
aria-hidden="true"
class="ant-select-selection-item-remove"
style="user-select: none;"
unselectable="on"
>
<span>
remove
<span
class="ant-select-selection-item"
>
<span
class="ant-select-selection-item-content"
>
my leaf
</span>
<span
aria-hidden="true"
class="ant-select-selection-item-remove"
style="user-select: none;"
unselectable="on"
>
<span>
remove
</span>
</span>
</span>
</span>
</span>
<span
class="ant-select-selection-item"
>
<span
class="ant-select-selection-item-content"
</div>
<div
class="ant-select-selection-overflow-item"
style="opacity: 1;"
>
your leaf
</span>
<span
aria-hidden="true"
class="ant-select-selection-item-remove"
style="user-select: none;"
unselectable="on"
>
<span>
remove
<span
class="ant-select-selection-item"
>
<span
class="ant-select-selection-item-content"
>
your leaf
</span>
<span
aria-hidden="true"
class="ant-select-selection-item-remove"
style="user-select: none;"
unselectable="on"
>
<span>
remove
</span>
</span>
</span>
</span>
</span>
<span
class="ant-select-selection-search"
style="width: 0px;"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="rc_select_TEST_OR_SSR"
readonly=""
role="combobox"
style="opacity: 0;"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
</div>
<div
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
style="opacity: 1;"
>
 
</span>
</span>
<div
class="ant-select-selection-search"
style="width: 0px;"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="rc_select_TEST_OR_SSR"
readonly=""
role="combobox"
style="opacity: 0;"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
>
 
</span>
</div>
</div>
</div>
</div>
<span
aria-hidden="true"

View File

@ -31,7 +31,7 @@ Tree selection control.
| labelInValue | Whether to embed label in value, turn the format of value from `string` to {value: string, label: ReactNode, halfChecked: string\[]} | boolean | false | |
| listHeight | Config popup height | number | 256 | |
| loadData | Load data asynchronously | function(node) | - | |
| 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) | - | |
| multiple | Support multiple or not, will be `true` when enable `treeCheckable` | boolean | false | |
| placeholder | Placeholder of the select input | string | - | |

View File

@ -32,7 +32,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 value 类型从 `string` 变为 {value: string, label: ReactNode, halfChecked(treeCheckStrictly 时有效): string\[] } 的格式 | boolean | false | |
| listHeight | 设置弹窗滚动高度 | number | 256 | |
| loadData | 异步加载数据 | function(node) | - | |
| maxTagCount | 最多显示多少个 tag | number | - | |
| maxTagCount | 最多显示多少个 tag,响应式模式会对性能产生损耗 | number \| `responsive` | - | responsive: 4.10 |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | |
| multiple | 支持多选(当设置 treeCheckable 时自动变为 true | boolean | false | |
| placeholder | 选择框默认文字 | string | - | |

View File

@ -32,6 +32,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
listType,
onPreview,
onDownload,
onChange,
previewFile,
disabled,
locale: propLocale,
@ -44,6 +45,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
children,
style,
itemRender,
maxCount,
} = props;
const [dragState, setDragState] = React.useState<string>('drop');
@ -71,16 +73,22 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (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<unknown, UploadProps> = (pr
nextFileList[fileIndex] = targetItem;
}
onChange({
onInternalChange({
file: targetItem,
fileList: nextFileList,
});
@ -118,7 +126,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (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<unknown, UploadProps> = (pr
return;
}
targetItem.percent = e.percent;
onChange({
onInternalChange({
event: e,
file: { ...targetItem },
fileList: getFileList().concat(),
@ -147,7 +155,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (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<unknown, UploadProps> = (pr
upload.current.abort(file);
}
onChange({
onInternalChange({
file,
fileList: removedFileList,
});
@ -197,7 +205,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
}
});
onChange({
onInternalChange({
file,
fileList: uniqueList,
});

View File

@ -1917,6 +1917,122 @@ exports[`renders ./components/upload/demo/fileList.md correctly 1`] = `
</span>
`;
exports[`renders ./components/upload/demo/max-count.md correctly 1`] = `
<div
class="ant-space ant-space-vertical"
style="width:100%"
>
<div
class="ant-space-item"
style="margin-bottom:24px"
>
<span
class=""
>
<div
class="ant-upload ant-upload-select ant-upload-select-picture"
>
<span
class="ant-upload"
role="button"
tabindex="0"
>
<input
accept=""
style="display:none"
type="file"
/>
<button
class="ant-btn"
type="button"
>
<span
aria-label="upload"
class="anticon anticon-upload"
role="img"
>
<svg
aria-hidden="true"
data-icon="upload"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 00-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
<span>
Upload (Max: 1)
</span>
</button>
</span>
</div>
<div
class="ant-upload-list ant-upload-list-picture"
/>
</span>
</div>
<div
class="ant-space-item"
>
<span
class=""
>
<div
class="ant-upload ant-upload-select ant-upload-select-picture"
>
<span
class="ant-upload"
role="button"
tabindex="0"
>
<input
accept=""
multiple=""
style="display:none"
type="file"
/>
<button
class="ant-btn"
type="button"
>
<span
aria-label="upload"
class="anticon anticon-upload"
role="img"
>
<svg
aria-hidden="true"
data-icon="upload"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 00-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
<span>
Upload (Max: 3)
</span>
</button>
</span>
</div>
<div
class="ant-upload-list ant-upload-list-picture"
/>
</span>
</div>
</div>
`;
exports[`renders ./components/upload/demo/picture-card.md correctly 1`] = `
<span
class="ant-upload-picture-card-wrapper"

View File

@ -9,6 +9,7 @@ import { setup, teardown } from './mock';
import { resetWarned } from '../../_util/devWarning';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep } from '../../../tests/utils';
describe('Upload', () => {
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(
<Upload {...props}>
<button type="button">upload</button>
</Upload>,
);
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(
<Upload {...props}>
<button type="button">upload</button>
</Upload>,
);
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',
}),
]);
});
});
});

View File

@ -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(
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Upload
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
listType="picture"
maxCount={1}
>
<Button icon={<UploadOutlined />}>Upload (Max: 1)</Button>
</Upload>
<Upload
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
listType="picture"
maxCount={3}
multiple
>
<Button icon={<UploadOutlined />}>Upload (Max: 3)</Button>
</Upload>
</Space>,
mountNode,
);
```

View File

@ -108,6 +108,8 @@ export interface UploadProps<T = any> {
isImageUrl?: (file: UploadFile) => boolean;
progress?: UploadListProgressProps;
itemRender?: ItemRender<T>;
/** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */
maxCount?: number;
}
export interface UploadState<T = any> {

View File

@ -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",