merge feature into master-to-merge-feature

This commit is contained in:
afc163 2020-10-10 11:36:02 +08:00
commit 68a1b18232
80 changed files with 2736 additions and 785 deletions

View File

@ -1,6 +1,7 @@
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs'; export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type BreakpointMap = Partial<Record<Breakpoint, string>>; export type BreakpointMap = Partial<Record<Breakpoint, string>>;
export type ScreenMap = Partial<Record<Breakpoint, boolean>>; export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
export type ScreenSizeMap = Partial<Record<Breakpoint, number>>;
export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs']; export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];

View File

@ -138,6 +138,9 @@ exports[`renders ./components/auto-complete/demo/custom.md correctly 1`] = `
> >
<span <span
class="ant-select-selection-search" class="ant-select-selection-search"
>
<div
class="ant-input-textarea"
> >
<textarea <textarea
aria-activedescendant="undefined_list_0" aria-activedescendant="undefined_list_0"
@ -152,6 +155,7 @@ exports[`renders ./components/auto-complete/demo/custom.md correctly 1`] = `
style="height:50px" style="height:50px"
type="search" type="search"
/> />
</div>
</span> </span>
<span <span
class="ant-select-selection-placeholder" class="ant-select-selection-placeholder"

View File

@ -1,13 +1,19 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import Avatar from '..'; import Avatar from '..';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import useBreakpoint from '../../grid/hooks/useBreakpoint';
jest.mock('../../grid/hooks/useBreakpoint');
describe('Avatar Render', () => { describe('Avatar Render', () => {
mountTest(Avatar); mountTest(Avatar);
rtlTest(Avatar); rtlTest(Avatar);
const sizes = { xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 };
let originOffsetWidth; let originOffsetWidth;
beforeAll(() => { beforeAll(() => {
// Mock offsetHeight // Mock offsetHeight
@ -152,6 +158,19 @@ describe('Avatar Render', () => {
expect(wrapper).toMatchRenderedSnapshot(); expect(wrapper).toMatchRenderedSnapshot();
}); });
Object.entries(sizes).forEach(([key, value]) => {
it(`adjusts component size to ${value} when window size is ${key}`, () => {
const wrapper = global.document.createElement('div');
useBreakpoint.mockReturnValue({ [key]: true });
act(() => {
ReactDOM.render(<Avatar size={sizes} />, wrapper);
});
expect(wrapper).toMatchSnapshot();
});
});
it('support onMouseEnter', () => { it('support onMouseEnter', () => {
const onMouseEnter = jest.fn(); const onMouseEnter = jest.fn();
const wrapper = mount(<Avatar onMouseEnter={onMouseEnter}>TestString</Avatar>); const wrapper = mount(<Avatar onMouseEnter={onMouseEnter}>TestString</Avatar>);

View File

@ -1,5 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Avatar Render adjusts component size to 24 when window size is xs 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 24px; height: 24px; line-height: 24px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 32 when window size is sm 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 32px; height: 32px; line-height: 32px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 40 when window size is md 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 40px; height: 40px; line-height: 40px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 64 when window size is lg 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 64px; height: 64px; line-height: 64px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 80 when window size is xl 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 80px; height: 80px; line-height: 80px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 100 when window size is xxl 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 100px; height: 100px; line-height: 100px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render fallback 1`] = ` exports[`Avatar Render fallback 1`] = `
<span <span
class="ant-avatar ant-avatar-circle ant-avatar-image" class="ant-avatar ant-avatar-circle ant-avatar-image"

View File

@ -469,6 +469,25 @@ Array [
] ]
`; `;
exports[`renders ./components/avatar/demo/fallback.md correctly 1`] = `
Array [
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
src="http://abc.com/not-exist.jpg"
/>
</span>,
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
src="http://abc.com/not-exist.jpg"
/>
</span>,
]
`;
exports[`renders ./components/avatar/demo/group.md correctly 1`] = ` exports[`renders ./components/avatar/demo/group.md correctly 1`] = `
Array [ Array [
<div <div
@ -583,23 +602,31 @@ Array [
] ]
`; `;
exports[`renders ./components/avatar/demo/fallback.md correctly 1`] = ` exports[`renders ./components/avatar/demo/responsive.md correctly 1`] = `
Array [
<span <span
class="ant-avatar ant-avatar-circle ant-avatar-image" class="ant-avatar ant-avatar-circle ant-avatar-icon"
> >
<img
src="http://abc.com/not-exist.jpg"
/>
</span>,
<span <span
class="ant-avatar ant-avatar-circle ant-avatar-image" aria-label="ant-design"
class="anticon anticon-ant-design"
role="img"
> >
<img <svg
src="http://abc.com/not-exist.jpg" aria-hidden="true"
class=""
data-icon="ant-design"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M716.3 313.8c19-18.9 19-49.7 0-68.6l-69.9-69.9.1.1c-18.5-18.5-50.3-50.3-95.3-95.2-21.2-20.7-55.5-20.5-76.5.5L80.9 474.2a53.84 53.84 0 000 76.4L474.6 944a54.14 54.14 0 0076.5 0l165.1-165c19-18.9 19-49.7 0-68.6a48.7 48.7 0 00-68.7 0l-125 125.2c-5.2 5.2-13.3 5.2-18.5 0L189.5 521.4c-5.2-5.2-5.2-13.3 0-18.5l314.4-314.2c.4-.4.9-.7 1.3-1.1 5.2-4.1 12.4-3.7 17.2 1.1l125.2 125.1c19 19 49.8 19 68.7 0zM408.6 514.4a106.3 106.2 0 10212.6 0 106.3 106.2 0 10-212.6 0zm536.2-38.6L821.9 353.5c-19-18.9-49.8-18.9-68.7.1a48.4 48.4 0 000 68.6l83 82.9c5.2 5.2 5.2 13.3 0 18.5l-81.8 81.7a48.4 48.4 0 000 68.6 48.7 48.7 0 0068.7 0l121.8-121.7a53.93 53.93 0 00-.1-76.4z"
/> />
</span>, </svg>
] </span>
</span>
`; `;
exports[`renders ./components/avatar/demo/toggle-debug.md correctly 1`] = ` exports[`renders ./components/avatar/demo/toggle-debug.md correctly 1`] = `

View File

@ -5,6 +5,8 @@ import ResizeObserver from 'rc-resize-observer';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import { composeRef } from '../_util/ref'; import { composeRef } from '../_util/ref';
import { Breakpoint, responsiveArray, ScreenSizeMap } from '../_util/responsiveObserve';
import useBreakpoint from '../grid/hooks/useBreakpoint';
export interface AvatarProps { export interface AvatarProps {
/** Shape of avatar, options:`circle`, `square` */ /** Shape of avatar, options:`circle`, `square` */
@ -13,7 +15,7 @@ export interface AvatarProps {
* Size of avatar, options: `large`, `small`, `default` * Size of avatar, options: `large`, `small`, `default`
* or a custom number size * or a custom number size
* */ * */
size?: 'large' | 'small' | 'default' | number; size?: 'large' | 'small' | 'default' | number | ScreenSizeMap;
gap?: number; gap?: number;
/** Src of image avatar */ /** Src of image avatar */
src?: string; src?: string;
@ -94,6 +96,25 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
...others ...others
} = props; } = props;
const screens = useBreakpoint();
const responsiveSizeStyle: React.CSSProperties = React.useMemo(() => {
if (typeof size !== 'object') {
return {};
}
const currentBreakpoint: Breakpoint = responsiveArray.find(screen => screens[screen])!;
const currentSize = size[currentBreakpoint];
return currentSize
? {
width: currentSize,
height: currentSize,
lineHeight: `${currentSize}px`,
fontSize: icon ? currentSize / 2 : 18,
}
: {};
}, [screens, size]);
devWarning( devWarning(
!(typeof icon === 'string' && icon.length > 2), !(typeof icon === 'string' && icon.length > 2),
'Avatar', 'Avatar',
@ -185,7 +206,7 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
return ( return (
<span <span
{...others} {...others}
style={{ ...sizeStyle, ...others.style }} style={{ ...sizeStyle, ...responsiveSizeStyle, ...others.style }}
className={classString} className={classString}
ref={avatarNodeMergeRef as any} ref={avatarNodeMergeRef as any}
> >

View File

@ -0,0 +1,27 @@
---
order: 5
title:
zh-CN: 响应式尺寸
en-US: Responsive Size
---
## zh-CN
头像大小可以根据屏幕大小自动调整。
## en-US
Avatar size can be automatically adjusted based on the screen size.
```tsx
import { Avatar } from 'antd';
import { AntDesignOutlined } from '@ant-design/icons';
ReactDOM.render(
<Avatar
size={{ xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 }}
icon={<AntDesignOutlined />}
/>,
mountNode,
);
```

View File

@ -15,7 +15,7 @@ Avatars can be used to represent people or objects. It supports images, `Icon`s,
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| icon | Custom icon type for an icon avatar | ReactNode | - | | | icon | Custom icon type for an icon avatar | ReactNode | - | |
| shape | The shape of avatar | `circle` \| `square` | `circle` | | | shape | The shape of avatar | `circle` \| `square` | `circle` | |
| size | The size of the avatar | number \| `large` \| `small` \| `default` | `default` | | | size | The size of the avatar | number \| `large` \| `small` \| `default` \| `{ xs: number, sm: number, ...}` | `default` | 4.7.0 |
| src | The address of the image for an image avatar | string | - | | | src | The address of the image for an image avatar | string | - | |
| srcSet | A list of sources to use for different screen resolutions | string | - | | | srcSet | A list of sources to use for different screen resolutions | string | - | |
| alt | This attribute defines the alternative text describing the image | string | - | | | alt | This attribute defines the alternative text describing the image | string | - | |

View File

@ -20,7 +20,7 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/aBcnbw68hP/Avatar.svg
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| icon | 设置头像的自定义图标 | ReactNode | - | | | icon | 设置头像的自定义图标 | ReactNode | - | |
| shape | 指定头像的形状 | `circle` \| `square` | `circle` | | | shape | 指定头像的形状 | `circle` \| `square` | `circle` | |
| size | 设置头像的大小 | number \| `large` \| `small` \| `default` | `default` | | | size | 设置头像的大小 | number \| `large` \| `small` \| `default` \| `{ xs: number, sm: number, ...}` | `default` | 4.7.0 |
| src | 图片类头像的资源地址 | string | - | | | src | 图片类头像的资源地址 | string | - | |
| srcSet | 设置图片类头像响应式资源地址 | string | - | | | srcSet | 设置图片类头像响应式资源地址 | string | - | |
| alt | 图像无法显示时的替代文本 | string | - | | | alt | 图像无法显示时的替代文本 | string | - | |

View File

@ -2,4 +2,5 @@ import '../../style/index.less';
import './index.less'; import './index.less';
// style dependencies // style dependencies
// deps-lint-skip: grid
import '../../popover/style'; import '../../popover/style';

View File

@ -155,6 +155,9 @@ exports[`renders ./components/comment/demo/editor.md correctly 1`] = `
> >
<div <div
class="ant-form-item-control-input-content" class="ant-form-item-control-input-content"
>
<div
class="ant-input-textarea"
> >
<textarea <textarea
class="ant-input" class="ant-input"
@ -164,6 +167,7 @@ exports[`renders ./components/comment/demo/editor.md correctly 1`] = `
</div> </div>
</div> </div>
</div> </div>
</div>
<div <div
class="ant-row ant-form-item" class="ant-row ant-form-item"
> >

View File

@ -14190,7 +14190,7 @@ exports[`ConfigProvider components Form configProvider 1`] = `
</div> </div>
</div> </div>
<div <div
class="config-form-item-explain" class="config-form-item-explain config-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -14227,7 +14227,7 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
</div> </div>
</div> </div>
<div <div
class="config-form-item-explain" class="config-form-item-explain config-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -14264,7 +14264,7 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
</div> </div>
</div> </div>
<div <div
class="config-form-item-explain" class="config-form-item-explain config-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -14301,7 +14301,7 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -14338,7 +14338,7 @@ exports[`ConfigProvider components Form normal 1`] = `
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -14375,7 +14375,7 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
</div> </div>
</div> </div>
<div <div
class="prefix-Form-item-explain" class="prefix-Form-item-explain prefix-Form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -14531,10 +14531,14 @@ exports[`ConfigProvider components Input configProvider 1`] = `
</span> </span>
</span> </span>
</span> </span>
<div
class="config-input-textarea"
>
<textarea <textarea
class="config-input" class="config-input"
/> />
</div> </div>
</div>
`; `;
exports[`ConfigProvider components Input configProvider componentSize large 1`] = ` exports[`ConfigProvider components Input configProvider componentSize large 1`] = `
@ -14620,10 +14624,14 @@ exports[`ConfigProvider components Input configProvider componentSize large 1`]
</span> </span>
</span> </span>
</span> </span>
<div
class="config-input-textarea"
>
<textarea <textarea
class="config-input" class="config-input"
/> />
</div> </div>
</div>
`; `;
exports[`ConfigProvider components Input configProvider componentSize middle 1`] = ` exports[`ConfigProvider components Input configProvider componentSize middle 1`] = `
@ -14709,10 +14717,14 @@ exports[`ConfigProvider components Input configProvider componentSize middle 1`]
</span> </span>
</span> </span>
</span> </span>
<div
class="config-input-textarea"
>
<textarea <textarea
class="config-input" class="config-input"
/> />
</div> </div>
</div>
`; `;
exports[`ConfigProvider components Input configProvider virtual and dropdownMatchSelectWidth 1`] = ` exports[`ConfigProvider components Input configProvider virtual and dropdownMatchSelectWidth 1`] = `
@ -14798,10 +14810,14 @@ exports[`ConfigProvider components Input configProvider virtual and dropdownMatc
</span> </span>
</span> </span>
</span> </span>
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
/> />
</div> </div>
</div>
`; `;
exports[`ConfigProvider components Input normal 1`] = ` exports[`ConfigProvider components Input normal 1`] = `
@ -14887,10 +14903,14 @@ exports[`ConfigProvider components Input normal 1`] = `
</span> </span>
</span> </span>
</span> </span>
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
/> />
</div> </div>
</div>
`; `;
exports[`ConfigProvider components Input prefixCls 1`] = ` exports[`ConfigProvider components Input prefixCls 1`] = `
@ -14976,10 +14996,14 @@ exports[`ConfigProvider components Input prefixCls 1`] = `
</span> </span>
</span> </span>
</span> </span>
<div
class="prefix-Input-textarea"
>
<textarea <textarea
class="prefix-Input" class="prefix-Input"
/> />
</div> </div>
</div>
`; `;
exports[`ConfigProvider components InputNumber configProvider 1`] = ` exports[`ConfigProvider components InputNumber configProvider 1`] = `

View File

@ -2011,6 +2011,7 @@ exports[`renders ./components/date-picker/demo/format.md correctly 1`] = `
</div> </div>
<div <div
class="ant-space-item" class="ant-space-item"
style="margin-bottom:12px"
> >
<div <div
class="ant-picker ant-picker-range" class="ant-picker ant-picker-range"
@ -2120,6 +2121,74 @@ exports[`renders ./components/date-picker/demo/format.md correctly 1`] = `
</span> </span>
</div> </div>
</div> </div>
<div
class="ant-space-item"
>
<div
class="ant-picker"
>
<div
class="ant-picker-input"
>
<input
autocomplete="off"
placeholder="Select date"
readonly=""
size="27"
title=""
value=""
/>
<span
class="ant-picker-suffix"
>
<span
aria-label="calendar"
class="anticon anticon-calendar"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="calendar"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"
/>
</svg>
</span>
</span>
<span
class="ant-picker-clear"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
</div> </div>
`; `;

View File

@ -24,6 +24,10 @@ const monthFormat = 'YYYY/MM';
const dateFormatList = ['DD/MM/YYYY', 'DD/MM/YY']; const dateFormatList = ['DD/MM/YYYY', 'DD/MM/YY'];
const customFormat = value => {
return `custom format: ${value.format(dateFormat)}`;
};
ReactDOM.render( ReactDOM.render(
<Space direction="vertical" size={12}> <Space direction="vertical" size={12}>
<DatePicker defaultValue={moment('2015/01/01', dateFormat)} format={dateFormat} /> <DatePicker defaultValue={moment('2015/01/01', dateFormat)} format={dateFormat} />
@ -33,6 +37,7 @@ ReactDOM.render(
defaultValue={[moment('2015/01/01', dateFormat), moment('2015/01/01', dateFormat)]} defaultValue={[moment('2015/01/01', dateFormat), moment('2015/01/01', dateFormat)]}
format={dateFormat} format={dateFormat}
/> />
<DatePicker defaultValue={moment('2015/01/01', dateFormat)} format={customFormat} />
</Space>, </Space>,
mountNode, mountNode,
); );

View File

@ -36,7 +36,7 @@ export function getTimeProps<DateType>(
const firstFormat = toArray(format)[0]; const firstFormat = toArray(format)[0];
const showTimeObj: SharedTimeProps<DateType> = { ...props }; const showTimeObj: SharedTimeProps<DateType> = { ...props };
if (firstFormat) { if (firstFormat && typeof firstFormat === 'string') {
if (!firstFormat.includes('s') && showSecond === undefined) { if (!firstFormat.includes('s') && showSecond === undefined) {
showTimeObj.showSecond = false; showTimeObj.showSecond = false;
} }

View File

@ -89,7 +89,7 @@ The following APIs are shared by DatePicker, RangePicker.
| defaultValue | To set default date, if start time or end time is null or undefined, the date range will be an open interval | [moment](http://momentjs.com/) | - | | | defaultValue | To set default date, if start time or end time is null or undefined, the date range will be an open interval | [moment](http://momentjs.com/) | - | |
| defaultPickerValue | To set default picker date | [moment](http://momentjs.com/) | - | | | defaultPickerValue | To set default picker date | [moment](http://momentjs.com/) | - | |
| disabledTime | To specify the time that cannot be selected | function(date) | - | | | disabledTime | To specify the time that cannot be selected | function(date) | - | |
| format | To set the date format, refer to [moment.js](http://momentjs.com/). When an array is provided, all values are used for parsing and first value is used for formatting | string \| string[] | `YYYY-MM-DD` | | | format | To set the date format, refer to [moment.js](http://momentjs.com/). When an array is provided, all values are used for parsing and first value is used for formatting, support [Custom Format](#components-date-picker-demo-format) | string \| (value: moment) => string \| (string \| (value: moment) => string)[] | `YYYY-MM-DD` | |
| renderExtraFooter | Render extra footer in panel | (mode) => React.ReactNode | - | | | renderExtraFooter | Render extra footer in panel | (mode) => React.ReactNode | - | |
| showTime | To provide an additional time selection | object \| boolean | [TimePicker Options](/components/time-picker/#API) | | | showTime | To provide an additional time selection | object \| boolean | [TimePicker Options](/components/time-picker/#API) | |
| showTime.defaultValue | To set default time of selected date, [demo](#components-date-picker-demo-disabled-date) | [moment](http://momentjs.com/) | moment() | | | showTime.defaultValue | To set default time of selected date, [demo](#components-date-picker-demo-disabled-date) | [moment](http://momentjs.com/) | moment() | |

View File

@ -90,7 +90,7 @@ import locale from 'antd/es/locale/zh_CN';
| defaultValue | 默认日期,如果开始时间或结束时间为 `null` 或者 `undefined`,日期范围将是一个开区间 | [moment](http://momentjs.com/) | - | | | defaultValue | 默认日期,如果开始时间或结束时间为 `null` 或者 `undefined`,日期范围将是一个开区间 | [moment](http://momentjs.com/) | - | |
| defaultPickerValue | 默认面板日期 | [moment](http://momentjs.com/) | - | | | defaultPickerValue | 默认面板日期 | [moment](http://momentjs.com/) | - | |
| disabledTime | 不可选择的时间 | function(date) | - | | | disabledTime | 不可选择的时间 | function(date) | - | |
| format | 设置日期格式,为数组时支持多格式匹配,展示以第一个为准。配置参考 [moment.js](http://momentjs.com/) | string \| string[] | `YYYY-MM-DD` | | | format | 设置日期格式,为数组时支持多格式匹配,展示以第一个为准。配置参考 [moment.js](http://momentjs.com/),支持[自定义格式](#components-date-picker-demo-format) | string \| (value: moment) => string \| (string \| (value: moment) => string)[] | `YYYY-MM-DD` | |
| renderExtraFooter | 在面板中添加额外的页脚 | (mode) => React.ReactNode | - | | | renderExtraFooter | 在面板中添加额外的页脚 | (mode) => React.ReactNode | - | |
| showTime | 增加时间选择功能 | Object \| boolean | [TimePicker Options](/components/time-picker/#API) | | | showTime | 增加时间选择功能 | Object \| boolean | [TimePicker Options](/components/time-picker/#API) | |
| showTime.defaultValue | 设置用户选择日期时默认的时分秒,[例子](#components-date-picker-demo-disabled-date) | [moment](http://momentjs.com/) | moment() | | | showTime.defaultValue | 设置用户选择日期时默认的时分秒,[例子](#components-date-picker-demo-disabled-date) | [moment](http://momentjs.com/) | moment() | |

View File

@ -6,7 +6,14 @@ import { PickerLocale } from '../generatePicker';
const locale: PickerLocale = { const locale: PickerLocale = {
lang: { lang: {
placeholder: 'เลือกวันที่', placeholder: 'เลือกวันที่',
yearPlaceholder: 'เลือกปี',
quarterPlaceholder: 'เลือกไตรมาส',
monthPlaceholder: 'เลือกเดือน',
weekPlaceholder: 'เลือกสัปดาห์',
rangePlaceholder: ['วันเริ่มต้น', 'วันสิ้นสุด'], rangePlaceholder: ['วันเริ่มต้น', 'วันสิ้นสุด'],
rangeYearPlaceholder: ['ปีเริ่มต้น', 'ปีสิ้นสุด'],
rangeMonthPlaceholder: ['เดือนเริ่มต้น', 'เดือนสิ้นสุด'],
rangeWeekPlaceholder: ['สัปดาห์เริ่มต้น', 'สัปดาห์สิ้นสุด'],
...CalendarLocale, ...CalendarLocale,
}, },
timePickerLocale: { timePickerLocale: {

View File

@ -0,0 +1,95 @@
import * as React from 'react';
import classNames from 'classnames';
import CSSMotion from 'rc-motion';
import useMemo from 'rc-util/lib/hooks/useMemo';
import useCacheErrors from './hooks/useCacheErrors';
import useForceUpdate from '../_util/hooks/useForceUpdate';
import { FormItemPrefixContext } from './context';
const EMPTY_LIST: React.ReactNode[] = [];
export interface ErrorListProps {
errors?: React.ReactNode[];
/** @private Internal usage. Do not use in your production */
help?: React.ReactNode;
/** @private Internal usage. Do not use in your production */
onDomErrorVisibleChange?: (visible: boolean) => void;
}
export default function ErrorList({
errors = EMPTY_LIST,
help,
onDomErrorVisibleChange,
}: ErrorListProps) {
const forceUpdate = useForceUpdate();
const { prefixCls, status } = React.useContext(FormItemPrefixContext);
const [visible, cacheErrors] = useCacheErrors(
errors,
changedVisible => {
if (changedVisible) {
/**
* We trigger in sync to avoid dom shaking but this get warning in react 16.13.
* So use Promise to keep in micro async to handle this.
* https://github.com/ant-design/ant-design/issues/21698#issuecomment-593743485
*/
Promise.resolve().then(() => {
onDomErrorVisibleChange?.(true);
});
}
forceUpdate();
},
!!help,
);
const memoErrors = useMemo(
() => cacheErrors,
visible,
(_, nextVisible) => nextVisible,
);
// Memo status in same visible
const [innerStatus, setInnerStatus] = React.useState(status);
React.useEffect(() => {
if (visible && status) {
setInnerStatus(status);
}
}, [visible, status]);
const baseClassName = `${prefixCls}-item-explain`;
return (
<CSSMotion
motionDeadline={500}
visible={visible}
motionName="show-help"
onLeaveEnd={() => {
onDomErrorVisibleChange?.(false);
}}
motionAppear
removeOnLeave
>
{({ className: motionClassName }: { className: string }) => {
return (
<div
className={classNames(
baseClassName,
{
[`${baseClassName}-${innerStatus}`]: innerStatus,
},
motionClassName,
)}
key="help"
>
{memoErrors.map((error, index) => (
// eslint-disable-next-line react/no-array-index-key
<div key={index} role="alert">
{error}
</div>
))}
</div>
);
}}
</CSSMotion>
);
}

View File

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useContext, useRef } from 'react';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import classNames from 'classnames'; import classNames from 'classnames';
import { Field, FormInstance } from 'rc-field-form'; import { Field, FormInstance } from 'rc-field-form';
@ -11,7 +12,7 @@ import Row from '../grid/row';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import { tuple } from '../_util/type'; import { tuple } from '../_util/type';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel'; import FormItemLabel, { FormItemLabelProps, LabelTooltipType } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput'; import FormItemInput, { FormItemInputProps } from './FormItemInput';
import { FormContext, FormItemContext } from './context'; import { FormContext, FormItemContext } from './context';
import { toArray, getFieldId } from './util'; import { toArray, getFieldId } from './util';
@ -22,9 +23,9 @@ import useItemRef from './hooks/useItemRef';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type ValidateStatus = typeof ValidateStatuses[number]; export type ValidateStatus = typeof ValidateStatuses[number];
type RenderChildren = (form: FormInstance) => React.ReactNode; type RenderChildren<Values = any> = (form: FormInstance<Values>) => React.ReactNode;
type RcFieldProps = Omit<FieldProps, 'children'>; type RcFieldProps = Omit<FieldProps, 'children'>;
type ChildrenType = RenderChildren | React.ReactNode; type ChildrenType<Values = any> = RenderChildren<Values> | React.ReactNode;
interface MemoInputProps { interface MemoInputProps {
value: any; value: any;
@ -39,19 +40,23 @@ const MemoInput = React.memo(
}, },
); );
export interface FormItemProps extends FormItemLabelProps, FormItemInputProps, RcFieldProps { export interface FormItemProps<Values = any>
extends FormItemLabelProps,
FormItemInputProps,
RcFieldProps {
prefixCls?: string; prefixCls?: string;
noStyle?: boolean; noStyle?: boolean;
style?: React.CSSProperties; style?: React.CSSProperties;
className?: string; className?: string;
children?: ChildrenType; children?: ChildrenType<Values>;
id?: string; id?: string;
hasFeedback?: boolean; hasFeedback?: boolean;
validateStatus?: ValidateStatus; validateStatus?: ValidateStatus;
required?: boolean; required?: boolean;
hidden?: boolean; hidden?: boolean;
initialValue?: any; initialValue?: any;
messageVariables?: Record<string, string>;
tooltip?: LabelTooltipType;
/** Auto passed by List render props. User should not use this. */ /** Auto passed by List render props. User should not use this. */
fieldKey?: React.Key | React.Key[]; fieldKey?: React.Key | React.Key[];
} }
@ -63,7 +68,7 @@ function hasValidName(name?: NamePath): Boolean {
return !(name === undefined || name === null); return !(name === undefined || name === null);
} }
function FormItem(props: FormItemProps): React.ReactElement { function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElement {
const { const {
name, name,
fieldKey, fieldKey,
@ -80,20 +85,20 @@ function FormItem(props: FormItemProps): React.ReactElement {
children, children,
required, required,
label, label,
messageVariables,
trigger = 'onChange', trigger = 'onChange',
validateTrigger, validateTrigger,
hidden, hidden,
...restProps ...restProps
} = props; } = props;
const destroyRef = React.useRef(false); const destroyRef = useRef(false);
const { getPrefixCls } = React.useContext(ConfigContext); const { getPrefixCls } = useContext(ConfigContext);
const { name: formName, requiredMark } = React.useContext(FormContext); const { name: formName, requiredMark } = useContext(FormContext);
const { updateItemErrors } = React.useContext(FormItemContext); const { updateItemErrors } = useContext(FormItemContext);
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help); const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
const prevValidateStatusRef = React.useRef<ValidateStatus | undefined>(validateStatus);
const [inlineErrors, setInlineErrors] = useFrameState<Record<string, string[]>>({}); const [inlineErrors, setInlineErrors] = useFrameState<Record<string, string[]>>({});
const { validateTrigger: contextValidateTrigger } = React.useContext(FieldContext); const { validateTrigger: contextValidateTrigger } = useContext(FieldContext);
const mergedValidateTrigger = const mergedValidateTrigger =
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger; validateTrigger !== undefined ? validateTrigger : contextValidateTrigger;
@ -106,7 +111,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
const hasName = hasValidName(name); const hasName = hasValidName(name);
// Cache Field NamePath // Cache Field NamePath
const nameRef = React.useRef<(string | number)[]>([]); const nameRef = useRef<(string | number)[]>([]);
// Should clean up if Field removed // Should clean up if Field removed
React.useEffect(() => { React.useEffect(() => {
@ -175,10 +180,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
mergedValidateStatus = 'success'; mergedValidateStatus = 'success';
} }
if (domErrorVisible && help) {
prevValidateStatusRef.current = mergedValidateStatus;
}
const itemClassName = { const itemClassName = {
[`${prefixCls}-item`]: true, [`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: domErrorVisible || help, [`${prefixCls}-item-with-help`]: domErrorVisible || help,
@ -189,8 +190,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
[`${prefixCls}-item-has-success`]: mergedValidateStatus === 'success', [`${prefixCls}-item-has-success`]: mergedValidateStatus === 'success',
[`${prefixCls}-item-has-warning`]: mergedValidateStatus === 'warning', [`${prefixCls}-item-has-warning`]: mergedValidateStatus === 'warning',
[`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error', [`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error',
[`${prefixCls}-item-has-error-leave`]:
!help && domErrorVisible && prevValidateStatusRef.current === 'error',
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating', [`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
[`${prefixCls}-item-hidden`]: hidden, [`${prefixCls}-item-hidden`]: hidden,
}; };
@ -218,6 +217,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
'normalize', 'normalize',
'preserve', 'preserve',
'required', 'required',
'tooltip',
'validateFirst', 'validateFirst',
'validateStatus', 'validateStatus',
'valuePropName', 'valuePropName',
@ -238,6 +238,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
{...meta} {...meta}
errors={mergedErrors} errors={mergedErrors}
prefixCls={prefixCls} prefixCls={prefixCls}
status={mergedValidateStatus}
onDomErrorVisibleChange={setDomErrorVisible} onDomErrorVisibleChange={setDomErrorVisible}
validateStatus={mergedValidateStatus} validateStatus={mergedValidateStatus}
> >
@ -252,17 +253,20 @@ function FormItem(props: FormItemProps): React.ReactElement {
const isRenderProps = typeof children === 'function'; const isRenderProps = typeof children === 'function';
// Record for real component render // Record for real component render
const updateRef = React.useRef(0); const updateRef = useRef(0);
updateRef.current += 1; updateRef.current += 1;
if (!hasName && !isRenderProps && !dependencies) { if (!hasName && !isRenderProps && !dependencies) {
return renderLayout(children) as JSX.Element; return renderLayout(children) as JSX.Element;
} }
const variables: Record<string, string> = {}; let variables: Record<string, string> = {};
if (typeof label === 'string') { if (typeof label === 'string') {
variables.label = label; variables.label = label;
} }
if (messageVariables) {
variables = { ...variables, ...messageVariables };
}
return ( return (
<Field <Field

View File

@ -4,14 +4,11 @@ import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled'; import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled'; import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import useMemo from 'rc-util/lib/hooks/useMemo';
import CSSMotion from 'rc-motion';
import Col, { ColProps } from '../grid/col'; import Col, { ColProps } from '../grid/col';
import { ValidateStatus } from './FormItem'; import { ValidateStatus } from './FormItem';
import { FormContext } from './context'; import { FormContext, FormItemPrefixContext } from './context';
import useCacheErrors from './hooks/useCacheErrors'; import ErrorList from './ErrorList';
import useForceUpdate from '../_util/hooks/useForceUpdate';
interface FormItemInputMiscProps { interface FormItemInputMiscProps {
prefixCls: string; prefixCls: string;
@ -26,6 +23,7 @@ export interface FormItemInputProps {
wrapperCol?: ColProps; wrapperCol?: ColProps;
help?: React.ReactNode; help?: React.ReactNode;
extra?: React.ReactNode; extra?: React.ReactNode;
status?: ValidateStatus;
} }
const iconMap: { [key: string]: any } = { const iconMap: { [key: string]: any } = {
@ -37,6 +35,7 @@ const iconMap: { [key: string]: any } = {
const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
prefixCls, prefixCls,
status,
wrapperCol, wrapperCol,
children, children,
help, help,
@ -46,8 +45,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
validateStatus, validateStatus,
extra, extra,
}) => { }) => {
const forceUpdate = useForceUpdate();
const baseClassName = `${prefixCls}-item`; const baseClassName = `${prefixCls}-item`;
const formContext = React.useContext(FormContext); const formContext = React.useContext(FormContext);
@ -56,24 +53,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className); const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
const [visible, cacheErrors] = useCacheErrors(
errors,
changedVisible => {
if (changedVisible) {
/**
* We trigger in sync to avoid dom shaking but this get warning in react 16.13.
* So use Promise to keep in micro async to handle this.
* https://github.com/ant-design/ant-design/issues/21698#issuecomment-593743485
*/
Promise.resolve().then(() => {
onDomErrorVisibleChange(true);
});
}
forceUpdate();
},
!!help,
);
React.useEffect( React.useEffect(
() => () => { () => () => {
onDomErrorVisibleChange(false); onDomErrorVisibleChange(false);
@ -81,12 +60,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
[], [],
); );
const memoErrors = useMemo(
() => cacheErrors,
visible,
(_, nextVisible) => nextVisible,
);
// Should provides additional icon if `hasFeedback` // Should provides additional icon if `hasFeedback`
const IconNode = validateStatus && iconMap[validateStatus]; const IconNode = validateStatus && iconMap[validateStatus];
const icon = const icon =
@ -108,29 +81,13 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
<div className={`${baseClassName}-control-input-content`}>{children}</div> <div className={`${baseClassName}-control-input-content`}>{children}</div>
{icon} {icon}
</div> </div>
<CSSMotion <FormItemPrefixContext.Provider value={{ prefixCls, status }}>
motionDeadline={500} <ErrorList
visible={visible} errors={errors}
motionName="show-help" help={help}
onLeaveEnd={() => { onDomErrorVisibleChange={onDomErrorVisibleChange}
onDomErrorVisibleChange(false); />
}} </FormItemPrefixContext.Provider>
motionAppear
removeOnLeave
>
{({ className: motionClassName }: { className: string }) => {
return (
<div className={classNames(`${baseClassName}-explain`, motionClassName)} key="help">
{memoErrors.map((error, index) => (
// eslint-disable-next-line react/no-array-index-key
<div key={index} role="alert">
{error}
</div>
))}
</div>
);
}}
</CSSMotion>
{extra && <div className={`${baseClassName}-extra`}>{extra}</div>} {extra && <div className={`${baseClassName}-extra`}>{extra}</div>}
</Col> </Col>
</FormContext.Provider> </FormContext.Provider>

View File

@ -1,11 +1,33 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import QuestionCircleOutlined from '@ant-design/icons/QuestionCircleOutlined';
import Col, { ColProps } from '../grid/col'; import Col, { ColProps } from '../grid/col';
import { FormLabelAlign } from './interface'; import { FormLabelAlign } from './interface';
import { FormContext, FormContextProps } from './context'; import { FormContext, FormContextProps } from './context';
import { RequiredMark } from './Form'; import { RequiredMark } from './Form';
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver'; import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale/default'; import defaultLocale from '../locale/default';
import Tooltip, { TooltipProps } from '../tooltip';
export type WrapperTooltipProps = TooltipProps & {
icon?: React.ReactElement;
};
export type LabelTooltipType = WrapperTooltipProps | React.ReactNode;
function toTooltipProps(tooltip: LabelTooltipType): WrapperTooltipProps | null {
if (!tooltip) {
return null;
}
if (tooltip && typeof tooltip === 'object' && !React.isValidElement(tooltip)) {
return tooltip as WrapperTooltipProps;
}
return {
title: tooltip,
};
}
export interface FormItemLabelProps { export interface FormItemLabelProps {
colon?: boolean; colon?: boolean;
@ -14,6 +36,7 @@ export interface FormItemLabelProps {
labelAlign?: FormLabelAlign; labelAlign?: FormLabelAlign;
labelCol?: ColProps; labelCol?: ColProps;
requiredMark?: RequiredMark; requiredMark?: RequiredMark;
tooltip?: LabelTooltipType;
} }
const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixCls: string }> = ({ const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixCls: string }> = ({
@ -25,6 +48,7 @@ const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixC
colon, colon,
required, required,
requiredMark, requiredMark,
tooltip,
}) => { }) => {
const [formLocale] = useLocaleReceiver('Form'); const [formLocale] = useLocaleReceiver('Form');
@ -58,6 +82,24 @@ const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixC
labelChildren = (label as string).replace(/[:|]\s*$/, ''); labelChildren = (label as string).replace(/[:|]\s*$/, '');
} }
// Tooltip
const tooltipProps = toTooltipProps(tooltip);
if (tooltipProps) {
const { icon = <QuestionCircleOutlined />, ...restTooltipProps } = tooltipProps;
const tooltipNode = (
<Tooltip {...restTooltipProps}>
{React.cloneElement(icon, { className: `${prefixCls}-item-tooltip` })}
</Tooltip>
);
labelChildren = (
<>
{labelChildren}
{tooltipNode}
</>
);
}
// Add required mark if optional // Add required mark if optional
if (requiredMark === 'optional' && !required) { if (requiredMark === 'optional' && !required) {
labelChildren = ( labelChildren = (

View File

@ -2,6 +2,8 @@ import * as React from 'react';
import { List } from 'rc-field-form'; import { List } from 'rc-field-form';
import { StoreValue } from 'rc-field-form/lib/interface'; import { StoreValue } from 'rc-field-form/lib/interface';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import { ConfigContext } from '../config-provider';
import { FormItemPrefixContext } from './context';
export interface FormListFieldData { export interface FormListFieldData {
name: number; name: number;
@ -16,19 +18,38 @@ export interface FormListOperation {
} }
export interface FormListProps { export interface FormListProps {
prefixCls?: string;
name: string | number | (string | number)[]; name: string | number | (string | number)[];
children: (fields: FormListFieldData[], operation: FormListOperation) => React.ReactNode; children: (
fields: FormListFieldData[],
operation: FormListOperation,
meta: { errors: React.ReactNode[] },
) => React.ReactNode;
} }
const FormList: React.FC<FormListProps> = ({ children, ...props }) => { const FormList: React.FC<FormListProps> = ({
prefixCls: customizePrefixCls,
children,
...props
}) => {
devWarning(!!props.name, 'Form.List', 'Miss `name` prop.'); devWarning(!!props.name, 'Form.List', 'Miss `name` prop.');
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('form', customizePrefixCls);
return ( return (
<List {...props}> <List {...props}>
{(fields, operation) => { {(fields, operation, meta) => {
return children( return (
<FormItemPrefixContext.Provider value={{ prefixCls, status: 'error' }}>
{children(
fields.map(field => ({ ...field, fieldKey: field.key })), fields.map(field => ({ ...field, fieldKey: field.key })),
operation, operation,
{
errors: meta.errors,
},
)}
</FormItemPrefixContext.Provider>
); );
}} }}
</List> </List>

View File

@ -1078,7 +1078,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -1120,7 +1120,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -1193,7 +1193,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -1235,7 +1235,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -1334,7 +1334,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -1389,7 +1389,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -1480,7 +1480,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -1531,7 +1531,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -2787,6 +2787,9 @@ exports[`renders ./components/form/demo/nest-messages.md correctly 1`] = `
> >
<div <div
class="ant-form-item-control-input-content" class="ant-form-item-control-input-content"
>
<div
class="ant-input-textarea"
> >
<textarea <textarea
class="ant-input" class="ant-input"
@ -2796,6 +2799,7 @@ exports[`renders ./components/form/demo/nest-messages.md correctly 1`] = `
</div> </div>
</div> </div>
</div> </div>
</div>
<div <div
class="ant-row ant-form-item" class="ant-row ant-form-item"
> >
@ -3800,6 +3804,29 @@ exports[`renders ./components/form/demo/required-mark.md correctly 1`] = `
title="Field A" title="Field A"
> >
Field A Field A
<span
aria-label="question-circle"
class="anticon anticon-question-circle ant-form-item-tooltip"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="question-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.5-111.6 40.7C369.2 344 352 380.7 352 420v7.6c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V420c0-44.1 43.1-80 96-80s96 35.9 96 80c0 31.1-22 59.6-56.1 72.7-21.2 8.1-39.2 22.3-52.1 40.9-13.1 19-19.9 41.8-19.9 64.9V620c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-22.7a48.3 48.3 0 0130.9-44.8c59-22.7 97.1-74.7 97.1-132.5.1-39.3-17.1-76-48.3-103.3zM472 732a40 40 0 1080 0 40 40 0 10-80 0z"
/>
</svg>
</span>
</label> </label>
</div> </div>
<div <div
@ -3832,6 +3859,29 @@ exports[`renders ./components/form/demo/required-mark.md correctly 1`] = `
title="Field B" title="Field B"
> >
Field B Field B
<span
aria-label="info-circle"
class="anticon anticon-info-circle ant-form-item-tooltip"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="info-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
<span <span
class="ant-form-item-optional" class="ant-form-item-optional"
> >
@ -6395,7 +6445,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -6522,7 +6572,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span> </span>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-validating"
> >
<div <div
role="alert" role="alert"
@ -6702,7 +6752,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span> </span>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"
@ -7090,7 +7140,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span> </span>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-validating"
> >
<div <div
role="alert" role="alert"
@ -7179,7 +7229,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-error"
> >
<div <div
role="alert" role="alert"

View File

@ -560,6 +560,23 @@ describe('Form', () => {
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('Bamboo is good!'); expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('Bamboo is good!');
}); });
it('`messageVariables` support validate', async () => {
const wrapper = mount(
// eslint-disable-next-line no-template-curly-in-string
<Form validateMessages={{ required: '${label} is good!' }}>
<Form.Item name="test" messageVariables={{ label: 'Bamboo' }} rules={[{ required: true }]}>
<input />
</Form.Item>
</Form>,
);
wrapper.find('form').simulate('submit');
await sleep(100);
wrapper.update();
await sleep(100);
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('Bamboo is good!');
});
it('validation message should has alert role', async () => { it('validation message should has alert role', async () => {
// https://github.com/ant-design/ant-design/issues/25711 // https://github.com/ant-design/ant-design/issues/25711
const wrapper = mount( const wrapper = mount(
@ -754,4 +771,32 @@ describe('Form', () => {
expect(wrapper.find('form').hasClass('ant-form-hide-required-mark')).toBeTruthy(); expect(wrapper.find('form').hasClass('ant-form-hide-required-mark')).toBeTruthy();
}); });
describe('tooltip', () => {
it('ReactNode', () => {
const wrapper = mount(
<Form>
<Form.Item label="light" tooltip={<span>Bamboo</span>}>
<Input />
</Form.Item>
</Form>,
);
const tooltipProps = wrapper.find('Tooltip').props();
expect(tooltipProps.title).toEqual(<span>Bamboo</span>);
});
it('config', () => {
const wrapper = mount(
<Form>
<Form.Item label="light" tooltip={{ title: 'Bamboo' }}>
<Input />
</Form.Item>
</Form>,
);
const tooltipProps = wrapper.find('Tooltip').props();
expect(tooltipProps.title).toEqual('Bamboo');
});
});
}); });

View File

@ -164,4 +164,76 @@ describe('Form.List', () => {
await sleep(); await sleep();
expect(onFinish).toHaveBeenLastCalledWith({ list: ['input2', 'input3'] }); expect(onFinish).toHaveBeenLastCalledWith({ list: ['input2', 'input3'] });
}); });
it('list errors', async () => {
jest.useFakeTimers();
let operation;
const wrapper = mount(
<Form>
<Form.List
name="list"
rules={[
{
validator: async (_, value) => {
if (value.length < 2) {
return Promise.reject(new Error('At least 2'));
}
},
},
]}
>
{(_, opt, { errors }) => {
operation = opt;
return <Form.ErrorList errors={errors} />;
}}
</Form.List>
</Form>,
);
async function addItem() {
await act(async () => {
operation.add();
await sleep(100);
jest.runAllTimers();
wrapper.update();
});
}
await addItem();
expect(wrapper.find('.ant-form-item-explain div').text()).toEqual('At least 2');
await addItem();
expect(wrapper.find('.ant-form-item-explain div')).toHaveLength(0);
jest.useRealTimers();
});
describe('ErrorList component', () => {
it('should trigger onDomErrorVisibleChange by motion end', async () => {
jest.useFakeTimers();
const onDomErrorVisibleChange = jest.fn();
const wrapper = mount(
<Form.ErrorList
errors={['bamboo is light']}
onDomErrorVisibleChange={onDomErrorVisibleChange}
/>,
);
await act(async () => {
await sleep();
jest.runAllTimers();
wrapper.update();
});
act(() => {
wrapper.find('CSSMotion').props().onLeaveEnd();
});
expect(onDomErrorVisibleChange).toHaveBeenCalledWith(false);
jest.useRealTimers();
});
});
}); });

View File

@ -67,4 +67,20 @@ describe('Form.typescript', () => {
expect(Demo).toBeTruthy(); expect(Demo).toBeTruthy();
}); });
}); });
it('FormItem renderProps support generic', () => {
const Demo = () => (
<Form<FormValues>>
<Form.Item<FormValues>>
{({ getFieldsValue }) => {
const values: FormValues = getFieldsValue();
expect(values).toBeTruthy();
return null;
}}
</Form.Item>
</Form>
);
expect(Demo).toBeTruthy();
});
}); });

View File

@ -5,6 +5,7 @@ import { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/Form
import { ColProps } from '../grid/col'; import { ColProps } from '../grid/col';
import { FormLabelAlign } from './interface'; import { FormLabelAlign } from './interface';
import { RequiredMark } from './Form'; import { RequiredMark } from './Form';
import { ValidateStatus } from './FormItem';
/** /**
* Form Context * Form Context
@ -49,3 +50,15 @@ export const FormProvider: React.FC<FormProviderProps> = props => {
const providerProps = omit(props, ['prefixCls']); const providerProps = omit(props, ['prefixCls']);
return <RcFormProvider {...providerProps} />; return <RcFormProvider {...providerProps} />;
}; };
/**
* Used for ErrorList only
*/
export interface FormItemPrefixContextProps {
prefixCls: string;
status?: ValidateStatus;
}
export const FormItemPrefixContext = React.createContext<FormItemPrefixContextProps>({
prefixCls: '',
});

View File

@ -41,8 +41,19 @@ const DynamicFieldSet = () => {
return ( return (
<Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}> <Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}>
<Form.List name="names"> <Form.List
{(fields, { add, remove }) => { name="names"
rules={[
{
validator: async (_, names) => {
if (!names || names.length < 2) {
return Promise.reject(new Error('At least 2 passengers'));
}
},
},
]}
>
{(fields, { add, remove }, { errors }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@ -96,6 +107,8 @@ const DynamicFieldSet = () => {
> >
<PlusOutlined /> Add field at head <PlusOutlined /> Add field at head
</Button> </Button>
<Form.ErrorList errors={errors} />
</Form.Item> </Form.Item>
</div> </div>
); );

View File

@ -16,6 +16,7 @@ Switch required or optional style with `requiredMark`.
```tsx ```tsx
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Form, Input, Button, Radio } from 'antd'; import { Form, Input, Button, Radio } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
const FormLayoutDemo = () => { const FormLayoutDemo = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
@ -40,10 +41,13 @@ const FormLayoutDemo = () => {
<Radio.Button value={false}>Hidden</Radio.Button> <Radio.Button value={false}>Hidden</Radio.Button>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item label="Field A" required> <Form.Item label="Field A" required tooltip="This is a required field">
<Input placeholder="input placeholder" /> <Input placeholder="input placeholder" />
</Form.Item> </Form.Item>
<Form.Item label="Field B"> <Form.Item
label="Field B"
tooltip={{ title: 'Tooltip with customize icon', icon: <InfoCircleOutlined /> }}
>
<Input placeholder="input placeholder" /> <Input placeholder="input placeholder" />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>

View File

@ -84,20 +84,10 @@ Form field component for data bidirectional binding, validation, layout, and so
| noStyle | No style for `true`, used as a pure field control | boolean | false | | | noStyle | No style for `true`, used as a pure field control | boolean | false | |
| label | Label text | ReactNode | - | | | label | Label text | ReactNode | - | |
| labelAlign | The text align of label | `left` \| `right` | `right` | | | labelAlign | The text align of label | `left` \| `right` | `right` | |
| labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with `<Col>`. You can set `labelCol` on Form which will not affect nest Item. If both exists, use Item first | [object](/components/grid/#Col) | - | |
| name | Field name, support array | [NamePath](#NamePath) | - | | <<<<<<< HEAD | labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with `<Col>`. You can set `labelCol` on Form. If both exists, use Item first | [object](/components/grid/#Col) | - | | | messageVariables | default validate filed info | Record<string, string> | - | 4.7.0 | ======= | labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with `<Col>`. You can set `labelCol` on Form which will not affect nest Item. If both exists, use Item first | [object](/components/grid/#Col) | - | |
| normalize | Normalize value from component value before passing to Form instance | (value, prevValue, prevValues) => any | - | |
| preserve | Keep field value even when field removed | boolean | true | 4.4.0 | > > > > > > > origin/master | name | Field name, support array | [NamePath](#NamePath) | - | | | normalize | Normalize value from component value before passing to Form instance | (value, prevValue, prevValues) => any | - | | | preserve | Keep field value even when field removed | boolean | true | 4.4.0 | | required | Display required style. It will be generated by the validation rule | boolean | false | | | rules | Rules for field validation. Click [here](#components-form-demo-basic) to see an example | [Rule](#Rule)[] | - | | | shouldUpdate | Custom field update logic. See [below](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | | | tooltip | Config tooltip info | ReactNode \| [TooltipProps & { icon: ReactNode }](/components/tooltip#API) | - | 4.7.0 | | trigger | When to collect the value of children node | string | `onChange` | | | validateFirst | Whether stop validate on first rule of error for this field. Will parallel validate when `parallel` cofigured | boolean \| `parallel` | false | `parallel`: 4.5.0 | | validateStatus | The validation status. If not provided, it will be generated by validation rule. options: `success` `warning` `error` `validating` | string | - | | | validateTrigger | When to validate the value of children node | string \| string[] | `onChange` | | | valuePropName | Props of children node, for example, the prop of Switch is 'checked'. This prop is an encapsulation of `getValueProps`, which will be invalid after customizing `getValueProps` | string | `value` | | | wrapperCol | The layout for input controls, same as `labelCol`. You can set `wrapperCol` on Form which will not affect nest Item. If both exists, use Item first | [object](/components/grid/#Col) | - | | | hidden | Whether to hide Form.Item (still collect and validate value) | boolean | false | |
| required | Display required style. It will be generated by the validation rule | boolean | false | |
| rules | Rules for field validation. Click [here](#components-form-demo-basic) to see an example | [Rule](#Rule)[] | - | |
| shouldUpdate | Custom field update logic. See [below](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | |
| trigger | When to collect the value of children node | string | `onChange` | |
| validateFirst | Whether stop validate on first rule of error for this field. Will parallel validate when `parallel` cofigured | boolean \| `parallel` | false | `parallel`: 4.5.0 |
| validateStatus | The validation status. If not provided, it will be generated by validation rule. options: `success` `warning` `error` `validating` | string | - | |
| validateTrigger | When to validate the value of children node | string \| string[] | `onChange` | |
| valuePropName | Props of children node, for example, the prop of Switch is 'checked'. This prop is an encapsulation of `getValueProps`, which will be invalid after customizing `getValueProps` | string | `value` | |
| wrapperCol | The layout for input controls, same as `labelCol`. You can set `wrapperCol` on Form which will not affect nest Item. If both exists, use Item first | [object](/components/grid/#Col) | - | |
| hidden | Whether to hide Form.Item (still collect and validate value) | boolean | false | |
After wrapped by `Form.Item` with `name` property, `value`(or other property defined by `valuePropName`) `onChange`(or other property defined by `trigger`) props will be added to form controls, the flow of form data will be handled by Form which will cause: After wrapped by `Form.Item` with `name` property, `value`(or other property defined by `valuePropName`) `onChange`(or other property defined by `trigger`) props will be added to form controls, the flow of form data will be handled by Form which will cause:
@ -145,14 +135,30 @@ When `shouldUpdate` is a function, it will be called by form values update. Prov
You can ref [example](#components-form-demo-control-hooks) to see detail. You can ref [example](#components-form-demo-control-hooks) to see detail.
### messageVariables
You can modify the default verification information of Form.Item through `messageVariables`.
```jsx
<Form>
<Form.Item messageVariables={{ another: 'good' }} label="user">
<Input />
</Form.Item>
<Form.Item messageVariables={{ label: 'good' }} label={<span>user</span>}>
<Input />
</Form.Item>
</Form>
```
## Form.List ## Form.List
Provides array management for fields. Provides array management for fields.
| Property | Description | Type | Default | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| name | Field name, support array | [NamePath](#NamePath) | - | | name | Field name, support array | [NamePath](#NamePath) | - | |
| children | Render function | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - | | children | Render function | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - | |
| rules | Validate rules, only support customize validator. Should work with [ErrorList](#Form.ErrorList) | { validator, message }[] | - | 4.7.0 |
```tsx ```tsx
<Form.List> <Form.List>
@ -178,6 +184,14 @@ Some operator functions in render form of Form.List.
| remove | remove form item | (index: number \| number[]) => void | number[]: 4.5.0 | | remove | remove form item | (index: number \| number[]) => void | number[]: 4.5.0 |
| move | move form item | (from: number, to: number) => void | - | | move | move form item | (from: number, to: number) => void | - |
## Form.ErrorList
New in 4.7.0. Show error messages, should only work with `rules` of Form.List.
| Property | Description | Type | Default |
| -------- | ----------- | ----------- | ------- |
| errors | Error list | ReactNode[] | - |
## Form.Provider ## Form.Provider
Provide linkage between forms. If a sub form with `name` prop update, it will auto trigger Provider related events. See [example](#components-form-demo-form-context). Provide linkage between forms. If a sub form with `name` prop update, it will auto trigger Provider related events. See [example](#components-form-demo-form-context).
@ -363,6 +377,10 @@ Validating is also part of the value updating. It pass follow steps:
In each `onFieldsChange`, you will get `false` > `true` > `false` with `isFieldValidating`. In each `onFieldsChange`, you will get `false` > `true` > `false` with `isFieldValidating`.
### Why Form.List do not support `label` and need ErrorList to show errors?
Form.List use renderProps which mean internal structure is flexible. Thus `label` and `error` can not have best place. If you want to use antd `label`, you can wrap with Form.Item instead.
### Why Form.Item `dependencies` can not work on Form.List field? ### Why Form.Item `dependencies` can not work on Form.List field?
Your name path should also contain Form.List `name`: Your name path should also contain Form.List `name`:

View File

@ -1,6 +1,7 @@
import { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface'; import { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface';
import InternalForm, { useForm, FormInstance, FormProps } from './Form'; import InternalForm, { useForm, FormInstance, FormProps } from './Form';
import Item, { FormItemProps } from './FormItem'; import Item, { FormItemProps } from './FormItem';
import ErrorList, { ErrorListProps } from './ErrorList';
import List, { FormListProps } from './FormList'; import List, { FormListProps } from './FormList';
import { FormProvider } from './context'; import { FormProvider } from './context';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
@ -11,6 +12,7 @@ interface FormInterface extends InternalFormType {
useForm: typeof useForm; useForm: typeof useForm;
Item: typeof Item; Item: typeof Item;
List: typeof List; List: typeof List;
ErrorList: typeof ErrorList;
Provider: typeof FormProvider; Provider: typeof FormProvider;
/** @deprecated Only for warning usage. Do not use. */ /** @deprecated Only for warning usage. Do not use. */
@ -21,6 +23,7 @@ const Form = InternalForm as FormInterface;
Form.Item = Item; Form.Item = Item;
Form.List = List; Form.List = List;
Form.ErrorList = ErrorList;
Form.useForm = useForm; Form.useForm = useForm;
Form.Provider = FormProvider; Form.Provider = FormProvider;
Form.create = () => { Form.create = () => {
@ -31,6 +34,15 @@ Form.create = () => {
); );
}; };
export { FormInstance, FormProps, FormItemProps, FormListProps, Rule, RuleObject, RuleRender }; export {
FormInstance,
FormProps,
FormItemProps,
ErrorListProps,
Rule,
RuleObject,
RuleRender,
FormListProps,
};
export default Form; export default Form;

View File

@ -85,20 +85,10 @@ const validateMessages = {
| noStyle | 为 `true` 时不带样式,作为纯字段控件使用 | boolean | false | | | noStyle | 为 `true` 时不带样式,作为纯字段控件使用 | boolean | false | |
| label | `label` 标签的文本 | ReactNode | - | | | label | `label` 标签的文本 | ReactNode | - | |
| labelAlign | 标签文本对齐方式 | `left` \| `right` | `right` | | | labelAlign | 标签文本对齐方式 | `left` \| `right` | `right` | |
| labelCol | `label` 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}`。你可以通过 Form 的 `labelCol` 进行统一设置,不会作用于嵌套 Item。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid/#Col) | - | |
| name | 字段名,支持数组 | [NamePath](#NamePath) | - | | <<<<<<< HEAD | labelCol | `label` 标签布局 `<Col>` 组件设置 `span` `offset` `{span: 3, offset: 12}` `sm: {span: 3, offset: 12}`你可以通过 Form `labelCol` 进行统一设置当和 Form 同时设置时 Item 为准 | [object](/components/grid/#Col) | - | | | messageVariables | 默认验证字段的信息 | Record<string, string> | - | 4.7.0 | ======= | labelCol | `label` 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}`。你可以通过 Form 的 `labelCol` 进行统一设置,不会作用于嵌套 Item。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid/#Col) | - | |
| preserve | 当字段被删除时保留字段值 | boolean | true | 4.4.0 |
| normalize | 组件获取值后进行转换,再放入 Form 中 | (value, prevValue, prevValues) => any | - | | > > > > > > > origin/master | name | 字段名,支持数组 | [NamePath](#NamePath) | - | | | preserve | 当字段被删除时保留字段值 | boolean | true | 4.4.0 | | normalize | 组件获取值后进行转换,再放入 Form 中 | (value, prevValue, prevValues) => any | - | | | required | 必填样式设置。如不设置,则会根据校验规则自动生成 | boolean | false | | | rules | 校验规则,设置字段的校验逻辑。点击[此处](#components-form-demo-basic)查看示例 | [Rule](#Rule)[] | - | | | shouldUpdate | 自定义字段更新逻辑,说明[见下](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | | | tooltip | 配置提示信息 | ReactNode \| [TooltipProps & { icon: ReactNode }](/components/tooltip#API) | - | 4.7.0 | | trigger | 设置收集字段值变更的时机 | string | `onChange` | | | validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验。设置 `parallel` 时会并行校验 | boolean \| `parallel` | false | `parallel`: 4.5.0 | | validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | - | | | validateTrigger | 设置字段校验的时机 | string \| string[] | `onChange` | | | valuePropName | 子节点的值的属性,如 Switch 的是 'checked'。该属性为 `getValueProps` 的封装,自定义 `getValueProps` 后会失效 | string | `value` | | | wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 `labelCol`。你可以通过 Form 的 `wrapperCol` 进行统一设置,不会作用于嵌套 Item。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid/#Col) | - | | | hidden | 是否隐藏字段(依然会收集和校验字段) | boolean | false | |
| required | 必填样式设置。如不设置,则会根据校验规则自动生成 | boolean | false | |
| rules | 校验规则,设置字段的校验逻辑。点击[此处](#components-form-demo-basic)查看示例 | [Rule](#Rule)[] | - | |
| shouldUpdate | 自定义字段更新逻辑,说明[见下](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | |
| trigger | 设置收集字段值变更的时机 | string | `onChange` | |
| validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验。设置 `parallel` 时会并行校验 | boolean \| `parallel` | false | `parallel`: 4.5.0 |
| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | - | |
| validateTrigger | 设置字段校验的时机 | string \| string[] | `onChange` | |
| valuePropName | 子节点的值的属性,如 Switch 的是 'checked'。该属性为 `getValueProps` 的封装,自定义 `getValueProps` 后会失效 | string | `value` | |
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 `labelCol`。你可以通过 Form 的 `wrapperCol` 进行统一设置,不会作用于嵌套 Item。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid/#Col) | - | |
| hidden | 是否隐藏字段(依然会收集和校验字段) | boolean | false | |
被设置了 `name` 属性的 `Form.Item` 包装的控件,表单控件会自动添加 `value`(或 `valuePropName` 指定的其他属性) `onChange`(或 `trigger` 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果: 被设置了 `name` 属性的 `Form.Item` 包装的控件,表单控件会自动添加 `value`(或 `valuePropName` 指定的其他属性) `onChange`(或 `trigger` 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:
@ -146,14 +136,30 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
你可以参考[示例](#components-form-demo-control-hooks)查看具体使用场景。 你可以参考[示例](#components-form-demo-control-hooks)查看具体使用场景。
### messageVariables
你可以通过 `messageVariables` 修改 Form.Item 的默认验证信息。
```jsx
<Form>
<Form.Item messageVariables={{ another: 'good' }} label="user">
<Input />
</Form.Item>
<Form.Item messageVariables={{ label: 'good' }} label={<span>user</span>}>
<Input />
</Form.Item>
</Form>
```
## Form.List ## Form.List
为字段提供数组化管理。 为字段提供数组化管理。
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| name | 字段名,支持数组 | [NamePath](#NamePath) | - | | name | 字段名,支持数组 | [NamePath](#NamePath) | - | |
| children | 渲染函数 | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - | | children | 渲染函数 | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - | |
| rules | 校验规则,仅支持自定义规则。需要配合 [ErrorList](#Form.ErrorList) 一同使用。 | { validator, message }[] | - | 4.7.0 |
```tsx ```tsx
<Form.List> <Form.List>
@ -180,6 +186,14 @@ Form.List 渲染表单相关操作函数。
| remove | 删除表单项 | (index: number \| number[]) => void | number[]: 4.5.0 | | remove | 删除表单项 | (index: number \| number[]) => void | number[]: 4.5.0 |
| move | 移动表单项 | (from: number, to: number) => void | - | | move | 移动表单项 | (from: number, to: number) => void | - |
## Form.ErrorList
4.7.0 新增。错误展示组件,仅限配合 Form.List 的 rules 一同使用。
| 参数 | 说明 | 类型 | 默认值 |
| ------ | -------- | ----------- | ------ |
| errors | 错误列表 | ReactNode[] | - |
## Form.Provider ## Form.Provider
提供表单间联动功能,其下设置 `name` 的 Form 更新时,会自动触发对应事件。查看[示例](#components-form-demo-form-context)。 提供表单间联动功能,其下设置 `name` 的 Form 更新时,会自动触发对应事件。查看[示例](#components-form-demo-form-context)。
@ -365,6 +379,10 @@ validator(rule, value, callback) => {
在触发过程中,调用 `isFieldValidating` 会经历 `false` > `true` > `false` 的变化过程。 在触发过程中,调用 `isFieldValidating` 会经历 `false` > `true` > `false` 的变化过程。
### 为什么 Form.List 不支持 `label` 还需要使用 ErrorList 展示错误?
Form.List 本身是 renderProps内部样式非常自由。因而默认配置 `label``error` 节点很难与之配合。如果你需要 antd 样式的 `label`,可以通过外部包裹 Form.Item 来实现。
### 为什么 Form.Item 的 `dependencies` 对 Form.List 下的字段没有效果? ### 为什么 Form.Item 的 `dependencies` 对 Form.List 下的字段没有效果?
Form.List 下的字段需要包裹 Form.List 本身的 `name`,比如: Form.List 下的字段需要包裹 Form.List 本身的 `name`,比如:

View File

@ -127,6 +127,13 @@
} }
} }
// Optional mark
.@{form-item-prefix-cls}-tooltip {
writing-mode: horizontal-tb;
margin-inline-start: @margin-xss;
color: @text-color-secondary;
}
&::after { &::after {
& when (@form-item-trailing-colon=true) { & when (@form-item-trailing-colon=true) {
content: ':'; content: ':';

View File

@ -3,3 +3,4 @@ import './index.less';
// style dependencies // style dependencies
import '../../grid/style'; import '../../grid/style';
import '../../tooltip/style';

View File

@ -1,7 +1,6 @@
@import '../../input/style/mixin'; @import '../../input/style/mixin';
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) { .form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) {
.@{ant-prefix}-form-item-explain,
.@{ant-prefix}-form-item-split { .@{ant-prefix}-form-item-split {
color: @text-color; color: @text-color;
} }

View File

@ -6,6 +6,18 @@
// ================================================================ // ================================================================
/* Some non-status related component style is in `components.less` */ /* Some non-status related component style is in `components.less` */
// ========================= Explain =========================
/* To support leave along ErrorList. We add additional className to handle explain style */
&-explain {
&&-error {
color: @error-color;
}
&&-warning {
color: @warning-color;
}
}
&-has-feedback { &-has-feedback {
// ========================= Input ========================= // ========================= Input =========================
.@{ant-prefix}-input { .@{ant-prefix}-input {
@ -245,13 +257,6 @@
} }
} }
// Patch to keep error explain color
&-has-error-leave {
.@{form-item-prefix-cls}-explain {
color: @error-color;
}
}
// ====================== Validating ======================= // ====================== Validating =======================
&-is-validating { &-is-validating {
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon { &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {

View File

@ -21,8 +21,17 @@ Previewable image.
| fallback | Load failure fault-tolerant src | string | - | 4.6.0 | | fallback | Load failure fault-tolerant src | string | - | 4.6.0 |
| height | Image height | string \| number | - | 4.6.0 | | height | Image height | string \| number | - | 4.6.0 |
| placeholder | Load placeholder, use default placeholder when set `true` | ReactNode | - | 4.6.0 | | placeholder | Load placeholder, use default placeholder when set `true` | ReactNode | - | 4.6.0 |
| preview | Whether to enable the preview | boolean | true | 4.6.0 | | preview | preview config, disabled when `false` | boolean \| [previewType](#previewType) | true | 4.6.0 [previewType](#previewType):4.7.0 |
| src | Image path | string | - | 4.6.0 | | src | Image path | string | - | 4.6.0 |
| width | Image width | string \| number | - | 4.6.0 | | width | Image width | string \| number | - | 4.6.0 |
### previewType
```
{
visible: boolean,
onVisibleChange:function(value, prevValue)
}
```
Other attributes [<img\>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes) Other attributes [<img\>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes)

View File

@ -17,13 +17,22 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/D1dXz9PZqa/image.svg
## API ## API
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| ----------- | ---------------------------------- | ---------------- | ------ | ----- | | --- | --- | --- | --- | --- |
| alt | 图像描述 | string | - | 4.6.0 | | alt | 图像描述 | string | - | 4.6.0 |
| fallback | 加载失败容错地址 | string | - | 4.6.0 | | fallback | 加载失败容错地址 | string | - | 4.6.0 |
| height | 图像高度 | string \| number | - | 4.6.0 | | height | 图像高度 | string \| number | - | 4.6.0 |
| placeholder | 加载占位, 为 `true` 时使用默认占位 | ReactNode | - | 4.6.0 | | placeholder | 加载占位, 为 `true` 时使用默认占位 | ReactNode | - | 4.6.0 |
| preview | 是否开启预览 | boolean | true | 4.6.0 | | preview | 预览参数,为 `false` 时禁用 | boolean \| \| [previewType](#previewType) | true | 4.6.0 [previewType](#previewType):4.7.0 |
| src | 图片地址 | string | - | 4.6.0 | | src | 图片地址 | string | - | 4.6.0 |
| width | 图像宽度 | string \| number | - | 4.6.0 | | width | 图像宽度 | string \| number | - | 4.6.0 |
### previewType
```
{
visible: boolean,
onVisibleChange:function(value, prevValue)
}
```
其他属性见 [<img\>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes) 其他属性见 [<img\>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes)

View File

@ -9,6 +9,8 @@ import { fixControlledValue, resolveOnChange } from './Input';
export interface TextAreaProps extends RcTextAreaProps { export interface TextAreaProps extends RcTextAreaProps {
allowClear?: boolean; allowClear?: boolean;
bordered?: boolean; bordered?: boolean;
showCount?: boolean;
maxLength?: number;
} }
export interface TextAreaState { export interface TextAreaState {
@ -77,7 +79,7 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
renderTextArea = (prefixCls: string, bordered: boolean) => { renderTextArea = (prefixCls: string, bordered: boolean) => {
return ( return (
<RcTextArea <RcTextArea
{...omit(this.props, ['allowClear', 'bordered'])} {...omit(this.props, ['allowClear', 'bordered', 'showCount'])}
className={classNames( className={classNames(
{ {
[`${prefixCls}-borderless`]: !bordered, [`${prefixCls}-borderless`]: !bordered,
@ -92,22 +94,39 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
}; };
renderComponent = ({ getPrefixCls, direction }: ConfigConsumerProps) => { renderComponent = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const { value } = this.state; let value = fixControlledValue(this.state?.value);
const { prefixCls: customizePrefixCls, bordered = true } = this.props; const {
prefixCls: customizePrefixCls,
bordered = true,
showCount = false,
maxLength,
} = this.props;
const prefixCls = getPrefixCls('input', customizePrefixCls); const prefixCls = getPrefixCls('input', customizePrefixCls);
const hasMaxLength = Number(maxLength) > 0;
value = hasMaxLength ? value.slice(0, maxLength) : value;
const valueLength = [...value].length;
const dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
return ( return (
<div
className={classNames(`${prefixCls}-textarea`, {
[`${prefixCls}-textarea-show-count`]: showCount,
})}
{...(showCount ? { 'data-count': dataCount } : {})}
>
<ClearableLabeledInput <ClearableLabeledInput
{...this.props} {...this.props}
prefixCls={prefixCls} prefixCls={prefixCls}
direction={direction} direction={direction}
inputType="text" inputType="text"
value={fixControlledValue(value)} value={value}
element={this.renderTextArea(prefixCls, bordered)} element={this.renderTextArea(prefixCls, bordered)}
handleReset={this.handleReset} handleReset={this.handleReset}
ref={this.saveClearableInput} ref={this.saveClearableInput}
triggerFocus={this.focus} triggerFocus={this.focus}
bordered={bordered} bordered={bordered}
/> />
</div>
); );
}; };

View File

@ -260,11 +260,15 @@ Array [
rows="1" rows="1"
/> />
</div>, </div>,
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
rows="1" rows="1"
style="width:100px" style="width:100px"
/>, />
</div>,
<button <button
class="ant-btn ant-btn-primary" class="ant-btn ant-btn-primary"
type="button" type="button"
@ -1004,6 +1008,9 @@ Array [
</span>, </span>,
<br />, <br />,
<br />, <br />,
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -1032,30 +1039,43 @@ Array [
/> />
</svg> </svg>
</span> </span>
</span>, </span>
</div>,
] ]
`; `;
exports[`renders ./components/input/demo/autosize-textarea.md correctly 1`] = ` exports[`renders ./components/input/demo/autosize-textarea.md correctly 1`] = `
Array [ Array [
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
placeholder="Autosize height based on content lines" placeholder="Autosize height based on content lines"
/>, />
</div>,
<div <div
style="margin:24px 0" style="margin:24px 0"
/>, />,
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
placeholder="Autosize height with minimum and maximum number of lines" placeholder="Autosize height with minimum and maximum number of lines"
/>, />
</div>,
<div <div
style="margin:24px 0" style="margin:24px 0"
/>, />,
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
placeholder="Controlled autosize" placeholder="Controlled autosize"
/>, />
</div>,
] ]
`; `;
@ -1093,10 +1113,17 @@ exports[`renders ./components/input/demo/borderless-debug.md correctly 1`] = `
type="text" type="text"
value="" value=""
/> />
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input ant-input-borderless" class="ant-input ant-input-borderless"
placeholder="Unbordered" placeholder="Unbordered"
/> />
</div>
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn ant-input-affix-wrapper-borderless" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn ant-input-affix-wrapper-borderless"
> >
@ -1126,6 +1153,7 @@ exports[`renders ./components/input/demo/borderless-debug.md correctly 1`] = `
</svg> </svg>
</span> </span>
</span> </span>
</div>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-borderless" class="ant-input-affix-wrapper ant-input-affix-wrapper-borderless"
> >
@ -2651,10 +2679,14 @@ Array [
`; `;
exports[`renders ./components/input/demo/textarea.md correctly 1`] = ` exports[`renders ./components/input/demo/textarea.md correctly 1`] = `
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
rows="4" rows="4"
/> />
</div>
`; `;
exports[`renders ./components/input/demo/textarea-resize.md correctly 1`] = ` exports[`renders ./components/input/demo/textarea-resize.md correctly 1`] = `
@ -2668,15 +2700,31 @@ Array [
Auto Resize: false Auto Resize: false
</span> </span>
</button>, </button>,
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
rows="4" rows="4"
> >
The autoSize property applies to textarea nodes, and only the height changes automatically. In addition, autoSize can be set to an object, specifying the minimum number of rows and the maximum number of rows. The autoSize property applies to textarea nodes, and only the height changes automatically. In addition, autoSize can be set to an object, specifying the minimum number of rows and the maximum number of rows. The autoSize property applies to textarea nodes, and only the height changes automatically. In addition, autoSize can be set to an object, specifying the minimum number of rows and the maximum number of rows. The autoSize property applies to textarea nodes, and only the height changes automatically. In addition, autoSize can be set to an object, specifying the minimum number of rows and the maximum number of rows.
</textarea>, </textarea>
</div>,
] ]
`; `;
exports[`renders ./components/input/demo/textarea-show-count.md correctly 1`] = `
<div
class="ant-input-textarea ant-input-textarea-show-count"
data-count="0 / 100"
>
<textarea
class="ant-input"
maxlength="100"
/>
</div>
`;
exports[`renders ./components/input/demo/tooltip.md correctly 1`] = ` exports[`renders ./components/input/demo/tooltip.md correctly 1`] = `
<input <input
class="ant-input" class="ant-input"

View File

@ -1,6 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TextArea allowClear should change type when click 1`] = ` exports[`TextArea allowClear should change type when click 1`] = `
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -31,9 +34,13 @@ exports[`TextArea allowClear should change type when click 1`] = `
</svg> </svg>
</span> </span>
</span> </span>
</div>
`; `;
exports[`TextArea allowClear should change type when click 2`] = ` exports[`TextArea allowClear should change type when click 2`] = `
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -62,9 +69,13 @@ exports[`TextArea allowClear should change type when click 2`] = `
</svg> </svg>
</span> </span>
</span> </span>
</div>
`; `;
exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 1`] = ` exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 1`] = `
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -93,9 +104,13 @@ exports[`TextArea allowClear should not show icon if defaultValue is undefined,
</svg> </svg>
</span> </span>
</span> </span>
</div>
`; `;
exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 2`] = ` exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 2`] = `
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -124,9 +139,13 @@ exports[`TextArea allowClear should not show icon if defaultValue is undefined,
</svg> </svg>
</span> </span>
</span> </span>
</div>
`; `;
exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 3`] = ` exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 3`] = `
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -155,9 +174,13 @@ exports[`TextArea allowClear should not show icon if defaultValue is undefined,
</svg> </svg>
</span> </span>
</span> </span>
</div>
`; `;
exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 1`] = ` exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 1`] = `
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -186,9 +209,13 @@ exports[`TextArea allowClear should not show icon if value is undefined, null or
</svg> </svg>
</span> </span>
</span> </span>
</div>
`; `;
exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 2`] = ` exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 2`] = `
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -217,9 +244,13 @@ exports[`TextArea allowClear should not show icon if value is undefined, null or
</svg> </svg>
</span> </span>
</span> </span>
</div>
`; `;
exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 3`] = ` exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 3`] = `
<div
class="ant-input-textarea"
>
<span <span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn" class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
> >
@ -248,18 +279,27 @@ exports[`TextArea allowClear should not show icon if value is undefined, null or
</svg> </svg>
</span> </span>
</span> </span>
</div>
`; `;
exports[`TextArea should support disabled 1`] = ` exports[`TextArea should support disabled 1`] = `
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input ant-input-disabled" class="ant-input ant-input-disabled"
disabled="" disabled=""
/> />
</div>
`; `;
exports[`TextArea should support maxLength 1`] = ` exports[`TextArea should support maxLength 1`] = `
<div
class="ant-input-textarea"
>
<textarea <textarea
class="ant-input" class="ant-input"
maxlength="10" maxlength="10"
/> />
</div>
`; `;

View File

@ -130,7 +130,14 @@ describe('TextArea', () => {
const textarea = mount(<TextArea value="111" />); const textarea = mount(<TextArea value="111" />);
input.setProps({ value: undefined }); input.setProps({ value: undefined });
textarea.setProps({ value: undefined }); textarea.setProps({ value: undefined });
expect(textarea.getDOMNode().value).toBe(input.getDOMNode().value); expect(textarea.find('textarea').prop('value')).toBe(input.getDOMNode().value);
});
it('should support showCount', async () => {
const wrapper = mount(<TextArea maxLength={5} showCount 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');
}); });
}); });

View File

@ -0,0 +1,22 @@
---
order: 12
title:
zh-CN: 带字数提示的文本域
en-US: Textarea with character counting
---
## zh-CN
展示字数提示。
## en-US
Show character counting.
```jsx
import { Input } from 'antd';
const { TextArea } = Input;
ReactDOM.render(<TextArea showCount maxLength={100} />, mountNode);
```

View File

@ -49,6 +49,8 @@ The rest of the props of Input are exactly the same as the original [input](http
| allowClear | If allow to remove input content with clear icon | boolean | false | | | allowClear | If allow to remove input content with clear icon | boolean | false | |
| onResize | The callback function that is triggered when resize | function({ width, height }) | - | | | onResize | The callback function that is triggered when resize | function({ width, height }) | - | |
| bordered | Whether has border style | boolean | true | 4.5.0 | | bordered | Whether has border style | boolean | true | 4.5.0 |
| showCount | Whether show text count | boolean | false | |
| maxLength | The max length | number | - | |
The rest of the props of `Input.TextArea` are the same as the original [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea). The rest of the props of `Input.TextArea` are the same as the original [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).

View File

@ -50,6 +50,8 @@ Input 的其他属性和 React 自带的 [input](https://facebook.github.io/reac
| allowClear | 可以点击清除图标删除内容 | boolean | false | | | allowClear | 可以点击清除图标删除内容 | boolean | false | |
| onResize | resize 回调 | function({ width, height }) | - | | | onResize | resize 回调 | function({ width, height }) | - | |
| bordered | 是否有边框 | boolean | true | 4.5.0 | | bordered | 是否有边框 | boolean | true | 4.5.0 |
| showCount | 是否展示字数 | boolean | false | |
| maxLength | 内容最大长度 | number | - | |
`Input.TextArea` 的其他属性和浏览器自带的 [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) 一致。 `Input.TextArea` 的其他属性和浏览器自带的 [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) 一致。

View File

@ -43,6 +43,15 @@
padding-bottom: 3px; padding-bottom: 3px;
} }
} }
&-textarea {
&-show-count::after {
display: block;
color: @normal-color;
text-align: right;
content: attr(data-count);
}
}
} }
@import './search-input'; @import './search-input';

View File

@ -6810,7 +6810,7 @@ exports[`Locale Provider should display the text as ar 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle1" aria-labelledby="rcDialogTitle1"
@ -6819,7 +6819,7 @@ exports[`Locale Provider should display the text as ar 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -11851,7 +11851,7 @@ exports[`Locale Provider should display the text as az 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle0" aria-labelledby="rcDialogTitle0"
@ -11860,7 +11860,7 @@ exports[`Locale Provider should display the text as az 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -16892,7 +16892,7 @@ exports[`Locale Provider should display the text as bg 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle2" aria-labelledby="rcDialogTitle2"
@ -16901,7 +16901,7 @@ exports[`Locale Provider should display the text as bg 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -21933,7 +21933,7 @@ exports[`Locale Provider should display the text as by 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle3" aria-labelledby="rcDialogTitle3"
@ -21942,7 +21942,7 @@ exports[`Locale Provider should display the text as by 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -26974,7 +26974,7 @@ exports[`Locale Provider should display the text as ca 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle4" aria-labelledby="rcDialogTitle4"
@ -26983,7 +26983,7 @@ exports[`Locale Provider should display the text as ca 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -32015,7 +32015,7 @@ exports[`Locale Provider should display the text as cs 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle5" aria-labelledby="rcDialogTitle5"
@ -32024,7 +32024,7 @@ exports[`Locale Provider should display the text as cs 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -37056,7 +37056,7 @@ exports[`Locale Provider should display the text as da 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle6" aria-labelledby="rcDialogTitle6"
@ -37065,7 +37065,7 @@ exports[`Locale Provider should display the text as da 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -42097,7 +42097,7 @@ exports[`Locale Provider should display the text as de 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle7" aria-labelledby="rcDialogTitle7"
@ -42106,7 +42106,7 @@ exports[`Locale Provider should display the text as de 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -47138,7 +47138,7 @@ exports[`Locale Provider should display the text as el 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle8" aria-labelledby="rcDialogTitle8"
@ -47147,7 +47147,7 @@ exports[`Locale Provider should display the text as el 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -52179,7 +52179,7 @@ exports[`Locale Provider should display the text as en 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle10" aria-labelledby="rcDialogTitle10"
@ -52188,7 +52188,7 @@ exports[`Locale Provider should display the text as en 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -57220,7 +57220,7 @@ exports[`Locale Provider should display the text as en-gb 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle9" aria-labelledby="rcDialogTitle9"
@ -57229,7 +57229,7 @@ exports[`Locale Provider should display the text as en-gb 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -62261,7 +62261,7 @@ exports[`Locale Provider should display the text as es 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle11" aria-labelledby="rcDialogTitle11"
@ -62270,7 +62270,7 @@ exports[`Locale Provider should display the text as es 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -67302,7 +67302,7 @@ exports[`Locale Provider should display the text as et 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle12" aria-labelledby="rcDialogTitle12"
@ -67311,7 +67311,7 @@ exports[`Locale Provider should display the text as et 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -72343,7 +72343,7 @@ exports[`Locale Provider should display the text as fa 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle13" aria-labelledby="rcDialogTitle13"
@ -72352,7 +72352,7 @@ exports[`Locale Provider should display the text as fa 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -77384,7 +77384,7 @@ exports[`Locale Provider should display the text as fi 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle14" aria-labelledby="rcDialogTitle14"
@ -77393,7 +77393,7 @@ exports[`Locale Provider should display the text as fi 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -82425,7 +82425,7 @@ exports[`Locale Provider should display the text as fr 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle15" aria-labelledby="rcDialogTitle15"
@ -82434,7 +82434,7 @@ exports[`Locale Provider should display the text as fr 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -87466,7 +87466,7 @@ exports[`Locale Provider should display the text as fr 2`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle16" aria-labelledby="rcDialogTitle16"
@ -87475,7 +87475,7 @@ exports[`Locale Provider should display the text as fr 2`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -92507,7 +92507,7 @@ exports[`Locale Provider should display the text as ga 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle17" aria-labelledby="rcDialogTitle17"
@ -92516,7 +92516,7 @@ exports[`Locale Provider should display the text as ga 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -97548,7 +97548,7 @@ exports[`Locale Provider should display the text as gl 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle18" aria-labelledby="rcDialogTitle18"
@ -97557,7 +97557,7 @@ exports[`Locale Provider should display the text as gl 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -102589,7 +102589,7 @@ exports[`Locale Provider should display the text as he 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle19" aria-labelledby="rcDialogTitle19"
@ -102598,7 +102598,7 @@ exports[`Locale Provider should display the text as he 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -107630,7 +107630,7 @@ exports[`Locale Provider should display the text as hi 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle20" aria-labelledby="rcDialogTitle20"
@ -107639,7 +107639,7 @@ exports[`Locale Provider should display the text as hi 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -112671,7 +112671,7 @@ exports[`Locale Provider should display the text as hr 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle21" aria-labelledby="rcDialogTitle21"
@ -112680,7 +112680,7 @@ exports[`Locale Provider should display the text as hr 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -117712,7 +117712,7 @@ exports[`Locale Provider should display the text as hu 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle22" aria-labelledby="rcDialogTitle22"
@ -117721,7 +117721,7 @@ exports[`Locale Provider should display the text as hu 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -122753,7 +122753,7 @@ exports[`Locale Provider should display the text as hy-am 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle23" aria-labelledby="rcDialogTitle23"
@ -122762,7 +122762,7 @@ exports[`Locale Provider should display the text as hy-am 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -127794,7 +127794,7 @@ exports[`Locale Provider should display the text as id 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle53" aria-labelledby="rcDialogTitle53"
@ -127803,7 +127803,7 @@ exports[`Locale Provider should display the text as id 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -132835,7 +132835,7 @@ exports[`Locale Provider should display the text as is 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle24" aria-labelledby="rcDialogTitle24"
@ -132844,7 +132844,7 @@ exports[`Locale Provider should display the text as is 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -137876,7 +137876,7 @@ exports[`Locale Provider should display the text as it 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle25" aria-labelledby="rcDialogTitle25"
@ -137885,7 +137885,7 @@ exports[`Locale Provider should display the text as it 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -142917,7 +142917,7 @@ exports[`Locale Provider should display the text as ja 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle26" aria-labelledby="rcDialogTitle26"
@ -142926,7 +142926,7 @@ exports[`Locale Provider should display the text as ja 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -147958,7 +147958,7 @@ exports[`Locale Provider should display the text as kn 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle27" aria-labelledby="rcDialogTitle27"
@ -147967,7 +147967,7 @@ exports[`Locale Provider should display the text as kn 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -152999,7 +152999,7 @@ exports[`Locale Provider should display the text as ko 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle28" aria-labelledby="rcDialogTitle28"
@ -153008,7 +153008,7 @@ exports[`Locale Provider should display the text as ko 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -158040,7 +158040,7 @@ exports[`Locale Provider should display the text as ku 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle29" aria-labelledby="rcDialogTitle29"
@ -158049,7 +158049,7 @@ exports[`Locale Provider should display the text as ku 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -163081,7 +163081,7 @@ exports[`Locale Provider should display the text as ku-iq 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle30" aria-labelledby="rcDialogTitle30"
@ -163090,7 +163090,7 @@ exports[`Locale Provider should display the text as ku-iq 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -168122,7 +168122,7 @@ exports[`Locale Provider should display the text as lt 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle31" aria-labelledby="rcDialogTitle31"
@ -168131,7 +168131,7 @@ exports[`Locale Provider should display the text as lt 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -173163,7 +173163,7 @@ exports[`Locale Provider should display the text as lv 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle54" aria-labelledby="rcDialogTitle54"
@ -173172,7 +173172,7 @@ exports[`Locale Provider should display the text as lv 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -178204,7 +178204,7 @@ exports[`Locale Provider should display the text as mk 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle32" aria-labelledby="rcDialogTitle32"
@ -178213,7 +178213,7 @@ exports[`Locale Provider should display the text as mk 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -183245,7 +183245,7 @@ exports[`Locale Provider should display the text as mn-mn 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle34" aria-labelledby="rcDialogTitle34"
@ -183254,7 +183254,7 @@ exports[`Locale Provider should display the text as mn-mn 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -188286,7 +188286,7 @@ exports[`Locale Provider should display the text as ms-my 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle33" aria-labelledby="rcDialogTitle33"
@ -188295,7 +188295,7 @@ exports[`Locale Provider should display the text as ms-my 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -193327,7 +193327,7 @@ exports[`Locale Provider should display the text as nb 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle35" aria-labelledby="rcDialogTitle35"
@ -193336,7 +193336,7 @@ exports[`Locale Provider should display the text as nb 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -198368,7 +198368,7 @@ exports[`Locale Provider should display the text as ne-np 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle36" aria-labelledby="rcDialogTitle36"
@ -198377,7 +198377,7 @@ exports[`Locale Provider should display the text as ne-np 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -203409,7 +203409,7 @@ exports[`Locale Provider should display the text as nl 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle38" aria-labelledby="rcDialogTitle38"
@ -203418,7 +203418,7 @@ exports[`Locale Provider should display the text as nl 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -208450,7 +208450,7 @@ exports[`Locale Provider should display the text as nl-be 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle37" aria-labelledby="rcDialogTitle37"
@ -208459,7 +208459,7 @@ exports[`Locale Provider should display the text as nl-be 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -213491,7 +213491,7 @@ exports[`Locale Provider should display the text as pl 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle39" aria-labelledby="rcDialogTitle39"
@ -213500,7 +213500,7 @@ exports[`Locale Provider should display the text as pl 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -218532,7 +218532,7 @@ exports[`Locale Provider should display the text as pt 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle41" aria-labelledby="rcDialogTitle41"
@ -218541,7 +218541,7 @@ exports[`Locale Provider should display the text as pt 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -223573,7 +223573,7 @@ exports[`Locale Provider should display the text as pt-br 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle40" aria-labelledby="rcDialogTitle40"
@ -223582,7 +223582,7 @@ exports[`Locale Provider should display the text as pt-br 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -228614,7 +228614,7 @@ exports[`Locale Provider should display the text as ro 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle42" aria-labelledby="rcDialogTitle42"
@ -228623,7 +228623,7 @@ exports[`Locale Provider should display the text as ro 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -233655,7 +233655,7 @@ exports[`Locale Provider should display the text as ru 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle43" aria-labelledby="rcDialogTitle43"
@ -233664,7 +233664,7 @@ exports[`Locale Provider should display the text as ru 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -238696,7 +238696,7 @@ exports[`Locale Provider should display the text as sk 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle44" aria-labelledby="rcDialogTitle44"
@ -238705,7 +238705,7 @@ exports[`Locale Provider should display the text as sk 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -243737,7 +243737,7 @@ exports[`Locale Provider should display the text as sl 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle45" aria-labelledby="rcDialogTitle45"
@ -243746,7 +243746,7 @@ exports[`Locale Provider should display the text as sl 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -248778,7 +248778,7 @@ exports[`Locale Provider should display the text as sr 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle46" aria-labelledby="rcDialogTitle46"
@ -248787,7 +248787,7 @@ exports[`Locale Provider should display the text as sr 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -253819,7 +253819,7 @@ exports[`Locale Provider should display the text as sv 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle47" aria-labelledby="rcDialogTitle47"
@ -253828,7 +253828,7 @@ exports[`Locale Provider should display the text as sv 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -258860,7 +258860,7 @@ exports[`Locale Provider should display the text as ta 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle48" aria-labelledby="rcDialogTitle48"
@ -258869,7 +258869,7 @@ exports[`Locale Provider should display the text as ta 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -263901,7 +263901,7 @@ exports[`Locale Provider should display the text as th 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle49" aria-labelledby="rcDialogTitle49"
@ -263910,7 +263910,7 @@ exports[`Locale Provider should display the text as th 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -268942,7 +268942,7 @@ exports[`Locale Provider should display the text as tr 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle50" aria-labelledby="rcDialogTitle50"
@ -268951,7 +268951,7 @@ exports[`Locale Provider should display the text as tr 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -273983,7 +273983,7 @@ exports[`Locale Provider should display the text as uk 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle51" aria-labelledby="rcDialogTitle51"
@ -273992,7 +273992,7 @@ exports[`Locale Provider should display the text as uk 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -279024,7 +279024,7 @@ exports[`Locale Provider should display the text as vi 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle52" aria-labelledby="rcDialogTitle52"
@ -279033,7 +279033,7 @@ exports[`Locale Provider should display the text as vi 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -284065,7 +284065,7 @@ exports[`Locale Provider should display the text as zh-cn 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle55" aria-labelledby="rcDialogTitle55"
@ -284074,7 +284074,7 @@ exports[`Locale Provider should display the text as zh-cn 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -289106,7 +289106,7 @@ exports[`Locale Provider should display the text as zh-hk 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle56" aria-labelledby="rcDialogTitle56"
@ -289115,7 +289115,7 @@ exports[`Locale Provider should display the text as zh-hk 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -294147,7 +294147,7 @@ exports[`Locale Provider should display the text as zh-tw 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
aria-labelledby="rcDialogTitle57" aria-labelledby="rcDialogTitle57"
@ -294156,7 +294156,7 @@ exports[`Locale Provider should display the text as zh-tw 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >

View File

@ -1,9 +1,12 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/th_TH'; import Pagination from 'rc-pagination/lib/locale/th_TH';
import DatePicker from '../date-picker/locale/th_TH'; import DatePicker from '../date-picker/locale/th_TH';
import TimePicker from '../time-picker/locale/th_TH'; import TimePicker from '../time-picker/locale/th_TH';
import Calendar from '../calendar/locale/th_TH'; import Calendar from '../calendar/locale/th_TH';
import { Locale } from '../locale-provider'; import { Locale } from '../locale-provider';
const typeTemplate = '${label} ไม่ใช่ ${type} ที่ถูกต้อง';
const localeValues: Locale = { const localeValues: Locale = {
locale: 'th', locale: 'th',
Pagination, Pagination,
@ -17,11 +20,17 @@ const localeValues: Locale = {
filterTitle: 'ตัวกรอง', filterTitle: 'ตัวกรอง',
filterConfirm: 'ยืนยัน', filterConfirm: 'ยืนยัน',
filterReset: 'รีเซ็ต', filterReset: 'รีเซ็ต',
filterEmptyText: 'ไม่มีตัวกรอง',
emptyText: 'ไม่มีข้อมูล',
selectAll: 'เลือกทั้งหมดในหน้านี้', selectAll: 'เลือกทั้งหมดในหน้านี้',
selectInvert: 'เลือกสถานะตรงกันข้าม', selectInvert: 'กลับสถานะการเลือกในหน้านี้',
selectionAll: 'เลือกข้อมูลทั้งหมด',
sortTitle: 'เรียง', sortTitle: 'เรียง',
expand: 'แสดงแถวข้อมูล', expand: 'แสดงแถวข้อมูล',
collapse: 'ย่อแถวข้อมูล', collapse: 'ย่อแถวข้อมูล',
triggerDesc: 'คลิกเรียงจากมากไปน้อย',
triggerAsc: 'คลิกเรียงจากน้อยไปมาก',
cancelSort: 'คลิกเพื่อยกเลิกการเรียง',
}, },
Modal: { Modal: {
okText: 'ตกลง', okText: 'ตกลง',
@ -37,6 +46,12 @@ const localeValues: Locale = {
searchPlaceholder: 'ค้นหา', searchPlaceholder: 'ค้นหา',
itemUnit: 'ชิ้น', itemUnit: 'ชิ้น',
itemsUnit: 'ชิ้น', itemsUnit: 'ชิ้น',
remove: 'นำออก',
selectCurrent: 'เลือกทั้งหมดในหน้านี้',
removeCurrent: 'นำออกทั้งหมดในหน้านี้',
selectAll: 'เลือกข้อมูลทั้งหมด',
removeAll: 'นำข้อมูลออกทั้งหมด',
selectInvert: 'กลับสถานะการเลือกในหน้านี้',
}, },
Upload: { Upload: {
uploading: 'กำลังอัปโหลด...', uploading: 'กำลังอัปโหลด...',
@ -60,6 +75,56 @@ const localeValues: Locale = {
PageHeader: { PageHeader: {
back: 'ย้อนกลับ', back: 'ย้อนกลับ',
}, },
Form: {
optional: '(ไม่จำเป็น)',
defaultValidateMessages: {
default: 'ฟิลด์ ${label} ไม่ผ่านเงื่อนไขการตรวจสอบ',
required: 'กรุณากรอก ${label}',
enum: '${label} ต้องเป็นค่าใดค่าหนึ่งใน [${enum}]',
whitespace: '${label} ไม่สามารถเป็นช่องว่างได้',
date: {
format: 'รูปแบบวันที่ ${label} ไม่ถูกต้อง',
parse: '${label} ไม่สามารถแปลงเป็นวันที่ได้',
invalid: '${label} เป็นวันที่ที่ไม่ถูกต้อง',
},
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} ต้องมี ${len} ตัวอักษร',
min: '${label} ต้องมีอย่างน้อย ${min} ตัวอักษร',
max: '${label} มีได้สูงสุด ${max} ตัวอักษร',
range: '${label} ต้องมี ${min}-${max} ตัวอักษร',
},
number: {
len: '${label} ต้องมี ${len} ตัว',
min: 'ค่าต่ำสุด ${label} คือ ${min}',
max: 'ค่าสูงสุด ${label} คือ ${max}',
range: '${label} ต้องมีค่า ${min}-${max}',
},
array: {
len: 'ต้องมี ${len} ${label}',
min: 'ต้องมีอย่างน้อย ${min} ${label}',
max: 'มีได้สูงสุด ${max} ${label}',
range: 'จำนวน ${label} ต้องอยู่ในช่วง ${min}-${max}',
},
pattern: {
mismatch: '${label} ไม่ตรงกับรูปแบบ ${pattern}',
},
},
},
}; };
export default localeValues; export default localeValues;

View File

@ -8,7 +8,7 @@ exports[`Modal render correctly 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
class="ant-modal-wrap" class="ant-modal-wrap"
@ -16,7 +16,7 @@ exports[`Modal render correctly 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -105,7 +105,7 @@ exports[`Modal render without footer 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
class="ant-modal-wrap" class="ant-modal-wrap"
@ -113,7 +113,7 @@ exports[`Modal render without footer 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >
@ -181,7 +181,7 @@ exports[`Modal support closeIcon 1`] = `
class="ant-modal-root" class="ant-modal-root"
> >
<div <div
class="ant-modal-mask fade-appear" class="ant-modal-mask fade-appear fade-appear-start fade"
/> />
<div <div
class="ant-modal-wrap" class="ant-modal-wrap"
@ -189,7 +189,7 @@ exports[`Modal support closeIcon 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="ant-modal zoom-appear" class="ant-modal zoom-appear zoom-appear-prepare zoom"
role="document" role="document"
style="width: 520px;" style="width: 520px;"
> >

View File

@ -283,6 +283,17 @@ exports[`renders ./components/modal/demo/manual.md correctly 1`] = `
</button> </button>
`; `;
exports[`renders ./components/modal/demo/modal-render.md correctly 1`] = `
<button
class="ant-btn"
type="button"
>
<span>
Open Draggable Modal
</span>
</button>
`;
exports[`renders ./components/modal/demo/position.md correctly 1`] = ` exports[`renders ./components/modal/demo/position.md correctly 1`] = `
Array [ Array [
<button <button

View File

@ -1,12 +1,45 @@
import TestUtils from 'react-dom/test-utils'; import TestUtils, { act } from 'react-dom/test-utils';
import CSSMotion from 'rc-motion';
import { genCSSMotion } from 'rc-motion/lib/CSSMotion';
import KeyCode from 'rc-util/lib/KeyCode';
import Modal from '..'; import Modal from '..';
import { destroyFns } from '../Modal'; import { destroyFns } from '../Modal';
import { sleep } from '../../../tests/utils';
const { confirm } = Modal; const { confirm } = Modal;
jest.mock('rc-motion');
describe('Modal.confirm triggers callbacks correctly', () => { describe('Modal.confirm triggers callbacks correctly', () => {
// Inject CSSMotion to replace with No transition support
const MockCSSMotion = genCSSMotion(false);
Object.keys(MockCSSMotion).forEach(key => {
CSSMotion[key] = MockCSSMotion[key];
});
// Mock for rc-util raf
window.requestAnimationFrame = callback => {
return window.setTimeout(callback, 16);
};
window.cancelAnimationFrame = id => {
window.clearTimeout(id);
};
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
/* eslint-disable no-console */
// Hack error to remove act warning
const originError = console.error;
console.error = (...args) => {
const errorStr = String(args[0]);
if (errorStr.includes('was not wrapped in act(...)')) {
return;
}
originError(...args);
};
/* eslint-enable */
afterEach(() => { afterEach(() => {
errorSpy.mockReset(); errorSpy.mockReset();
document.body.innerHTML = ''; document.body.innerHTML = '';
@ -68,20 +101,39 @@ describe('Modal.confirm triggers callbacks correctly', () => {
expect(onOk.mock.calls.length).toBe(1); expect(onOk.mock.calls.length).toBe(1);
}); });
it('should allow Modal.comfirm without onCancel been set', () => { it('should allow Modal.confirm without onCancel been set', () => {
open(); open();
// Third Modal // Third Modal
$$('.ant-btn')[0].click(); $$('.ant-btn')[0].click();
expect(errorSpy).not.toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled();
}); });
it('should allow Modal.comfirm without onOk been set', () => { it('should allow Modal.confirm without onOk been set', () => {
open(); open();
// Fourth Modal // Fourth Modal
$$('.ant-btn-primary')[0].click(); $$('.ant-btn-primary')[0].click();
expect(errorSpy).not.toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled();
}); });
it('should close confirm modal when press ESC', () => {
jest.useFakeTimers();
const onCancel = jest.fn();
Modal.confirm({
title: 'title',
content: 'content',
onCancel,
});
jest.runAllTimers();
expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(1);
TestUtils.Simulate.keyDown($$('.ant-modal')[0], {
keyCode: KeyCode.ESC,
});
jest.runAllTimers();
expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(0);
expect(onCancel).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});
it('should not hide confirm when onOk return Promise.resolve', () => { it('should not hide confirm when onOk return Promise.resolve', () => {
open({ open({
onOk: () => Promise.resolve(''), onOk: () => Promise.resolve(''),
@ -90,23 +142,27 @@ describe('Modal.confirm triggers callbacks correctly', () => {
expect($$('.ant-modal-confirm')).toHaveLength(1); expect($$('.ant-modal-confirm')).toHaveLength(1);
}); });
it('should emit error when onOk return Promise.reject', () => { it('should emit error when onOk return Promise.reject', async () => {
const error = new Error('something wrong'); const error = new Error('something wrong');
open({ open({
onOk: () => Promise.reject(error), onOk: () => {
return Promise.reject(error);
},
}); });
$$('.ant-btn-primary')[0].click(); $$('.ant-btn-primary')[0].click();
// wait promise // wait promise
return Promise.resolve().then(() => { await sleep();
expect(errorSpy).toHaveBeenCalledWith(error); expect(errorSpy).toHaveBeenCalledWith(error);
}); });
});
it('shows animation when close', () => { it('shows animation when close', () => {
open(); open();
jest.useFakeTimers(); jest.useFakeTimers();
expect($$('.ant-modal-confirm')).toHaveLength(1); expect($$('.ant-modal-confirm')).toHaveLength(1);
$$('.ant-btn')[0].click(); $$('.ant-btn')[0].click();
jest.runAllTimers(); jest.runAllTimers();
expect($$('.ant-modal-confirm')).toHaveLength(0); expect($$('.ant-modal-confirm')).toHaveLength(0);
jest.useRealTimers(); jest.useRealTimers();
@ -158,25 +214,6 @@ describe('Modal.confirm triggers callbacks correctly', () => {
jest.useRealTimers(); jest.useRealTimers();
}); });
it('should close confirm modal when press ESC', () => {
jest.useFakeTimers();
const onCancel = jest.fn();
Modal.confirm({
title: 'title',
content: 'content',
onCancel,
});
jest.runAllTimers();
expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(1);
TestUtils.Simulate.keyDown($$('.ant-modal')[0], {
keyCode: 27,
});
jest.runAllTimers();
expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(0);
expect(onCancel).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});
it('should not close modals when click confirm button when onOk has argument', () => { it('should not close modals when click confirm button when onOk has argument', () => {
jest.useFakeTimers(); jest.useFakeTimers();
['info', 'success', 'warning', 'error'].forEach(type => { ['info', 'success', 'warning', 'error'].forEach(type => {
@ -268,23 +305,35 @@ describe('Modal.confirm triggers callbacks correctly', () => {
it('destroyFns should reduce when instance.destroy', () => { it('destroyFns should reduce when instance.destroy', () => {
jest.useFakeTimers(); jest.useFakeTimers();
Modal.destroyAll(); // clear destroyFns Modal.destroyAll(); // clear destroyFns
jest.runAllTimers(); jest.runAllTimers();
const instances = []; const instances = [];
['info', 'success', 'warning', 'error'].forEach(type => { ['info', 'success', 'warning', 'error'].forEach(type => {
const instance = Modal[type]({ const instance = Modal[type]({
title: 'title', title: 'title',
content: 'content', content: 'content',
}); });
// Render modal
act(() => {
jest.runAllTimers();
});
instances.push(instance); instances.push(instance);
}); });
const { length } = instances; const { length } = instances;
instances.forEach((instance, index) => { instances.forEach((instance, index) => {
expect(destroyFns.length).toBe(length - index); expect(destroyFns.length).toBe(length - index);
act(() => {
instance.destroy(); instance.destroy();
jest.runAllTimers(); jest.runAllTimers();
});
expect(destroyFns.length).toBe(length - index - 1); expect(destroyFns.length).toBe(length - index - 1);
}); });
jest.useRealTimers(); jest.useRealTimers();
}); });

View File

@ -1,11 +1,20 @@
import React from 'react'; import React from 'react';
import CSSMotion from 'rc-motion';
import { genCSSMotion } from 'rc-motion/lib/CSSMotion';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import Modal from '..'; import Modal from '..';
import Button from '../../button'; import Button from '../../button';
jest.mock('rc-util/lib/Portal'); jest.mock('rc-util/lib/Portal');
jest.mock('rc-motion');
describe('Modal.hook', () => { describe('Modal.hook', () => {
// Inject CSSMotion to replace with No transition support
const MockCSSMotion = genCSSMotion(false);
Object.keys(MockCSSMotion).forEach(key => {
CSSMotion[key] = MockCSSMotion[key];
});
it('hooks support context', () => { it('hooks support context', () => {
jest.useFakeTimers(); jest.useFakeTimers();
const Context = React.createContext('light'); const Context = React.createContext('light');

View File

@ -0,0 +1,98 @@
---
order: 13
title:
zh-CN: 自定义渲染对话框
en-US: Custom modal content render
---
## zh-CN
自定义渲染对话框, 可通过 `react-draggable` 来实现拖拽。
## en-US
Custom modal content render. use `react-draggable` implements draggable.
```jsx
import { Modal, Button } from 'antd';
import Draggable from 'react-draggable';
class App extends React.Component {
state = {
visible: false,
disabled: true,
};
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = e => {
console.log(e);
this.setState({
visible: false,
});
};
handleCancel = e => {
console.log(e);
this.setState({
visible: false,
});
};
render() {
return (
<>
<Button onClick={this.showModal}>
Open Draggable Modal
</Button>
<Modal
title={
<div
style={{
width: '100%',
cursor: 'move',
}}
onMouseOver={() => {
if (this.state.disabled) {
this.setState({
disabled: true,
});
}
}}
onMouseOut={() => {
this.setState({
disabled: false,
});
}}
// fix eslintjsx-a11y/mouse-events-have-key-events
// https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/mouse-events-have-key-events.md
onFocus={() => {}}
onBlur={() => {}}
// end
>
Draggable Modal
</div>
}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
modalRender={modal => <Draggable disabled={this.state.disabled}>{modal}</Draggable>}
>
<p>
Just don&apos;t learn physics at school and your life will be full of magic and
miracles.
</p>
<br />
<p>Day before yesterday I saw a rabbit, and yesterday a deer, and today, you.</p>
</Modal>
</>
);
}
}
ReactDOM.render(<App />, mountNode);
```

View File

@ -31,6 +31,7 @@ When requiring users to interact with the application, but without jumping to a
| mask | Whether show mask or not | boolean | true | | mask | Whether show mask or not | boolean | true |
| maskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | boolean | true | | maskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | boolean | true |
| maskStyle | Style for modal's mask element | object | {} | | maskStyle | Style for modal's mask element | object | {} |
| modalRender | Custom modal content render | (node: ReactNode) => ReactNode | - | 4.7.0 |
| okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - | | okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - |
| okText | Text of the OK button | ReactNode | `OK` | | okText | Text of the OK button | ReactNode | `OK` |
| okType | Button `type` of the OK button | string | `primary` | | okType | Button `type` of the OK button | string | `primary` |

View File

@ -34,6 +34,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3StSdUlSH/Modal.svg
| mask | 是否展示遮罩 | boolean | true | | mask | 是否展示遮罩 | boolean | true |
| maskClosable | 点击蒙层是否允许关闭 | boolean | true | | maskClosable | 点击蒙层是否允许关闭 | boolean | true |
| maskStyle | 遮罩样式 | object | {} | | maskStyle | 遮罩样式 | object | {} |
| modalRender | 自定义渲染对话框 | (node: ReactNode) => ReactNode | - | 4.7.0 |
| okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - | | okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - |
| okText | 确认按钮文字 | ReactNode | `确定` | | okText | 确认按钮文字 | ReactNode | `确定` |
| okType | 确认按钮类型 | string | `primary` | | okType | 确认按钮类型 | string | `primary` |

View File

@ -15,6 +15,7 @@ export interface ItemProps {
direction?: 'horizontal' | 'vertical'; direction?: 'horizontal' | 'vertical';
size?: SizeType | number; size?: SizeType | number;
marginDirection: 'marginLeft' | 'marginRight'; marginDirection: 'marginLeft' | 'marginRight';
split?: string | React.ReactNode;
} }
export default function Item({ export default function Item({
@ -24,6 +25,7 @@ export default function Item({
size, size,
marginDirection, marginDirection,
children, children,
split,
}: ItemProps) { }: ItemProps) {
const latestIndex = React.useContext(LastIndexContext); const latestIndex = React.useContext(LastIndexContext);
@ -31,19 +33,24 @@ export default function Item({
return null; return null;
} }
return ( const style =
<div
className={className}
style={
index >= latestIndex index >= latestIndex
? {} ? {}
: { : {
[direction === 'vertical' ? 'marginBottom' : marginDirection]: [direction === 'vertical' ? 'marginBottom' : marginDirection]:
typeof size === 'string' ? spaceSize[size] : size, ((typeof size === 'string' ? spaceSize[size] : size) ?? 0) / (split ? 2 : 1),
} };
}
> return (
<>
<div className={className} style={style}>
{children} {children}
</div> </div>
{index < latestIndex && split && (
<span className={`${className}-split`} style={style}>
{split}
</span>
)}
</>
); );
} }

View File

@ -533,6 +533,60 @@ Array [
] ]
`; `;
exports[`renders ./components/space/demo/split.md correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:4px"
>
<a
class="ant-typography"
>
Link
</a>
</div>
<span
class="ant-space-item-split"
style="margin-right:4px"
>
<div
class="ant-divider ant-divider-vertical"
role="separator"
/>
</span>
<div
class="ant-space-item"
style="margin-right:4px"
>
<a
class="ant-typography"
>
Link
</a>
</div>
<span
class="ant-space-item-split"
style="margin-right:4px"
>
<div
class="ant-divider ant-divider-vertical"
role="separator"
/>
</span>
<div
class="ant-space-item"
>
<a
class="ant-typography"
>
Link
</a>
</div>
</div>
`;
exports[`renders ./components/space/demo/vertical.md correctly 1`] = ` exports[`renders ./components/space/demo/vertical.md correctly 1`] = `
<div <div
class="ant-space ant-space-vertical" class="ant-space ant-space-vertical"

View File

@ -87,3 +87,41 @@ Array [
</div>, </div>,
] ]
`; `;
exports[`Space split 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:4px"
>
text1
</div>
<span
class="ant-space-item-split"
style="margin-right:4px"
>
-
</span>
<div
class="ant-space-item"
style="margin-right:4px"
>
<span>
text1
</span>
</div>
<span
class="ant-space-item-split"
style="margin-right:4px"
>
-
</span>
<div
class="ant-space-item"
>
text3
</div>
</div>
`;

View File

@ -125,4 +125,15 @@ describe('Space', () => {
expect(wrapper.find('#demo').text()).toBe('2'); expect(wrapper.find('#demo').text()).toBe('2');
}); });
it('split', () => {
const wrapper = mount(
<Space split="-">
text1<span>text1</span>
<>text3</>
</Space>,
);
expect(render(wrapper)).toMatchSnapshot();
});
}); });

View File

@ -0,0 +1,30 @@
---
order: 99
title:
zh-CN: 分隔符
en-US: Split
---
## zh-CN
相邻组件分隔符。
## en-US
Crowded components split.
```jsx
import { Space, Typography, Divider } from 'antd';
function SpaceSplit() {
return (
<Space split={<Divider type="vertical" />}>
<Typography.Link>Link</Typography.Link>
<Typography.Link>Link</Typography.Link>
<Typography.Link>Link</Typography.Link>
</Space>
);
}
ReactDOM.render(<SpaceSplit />, mountNode);
```

View File

@ -19,3 +19,4 @@ Avoid components clinging together and set a unified space.
| align | Align items | `start` \| `end` \|`center` \|`baseline` | - | 4.2.0 | | align | Align items | `start` \| `end` \|`center` \|`baseline` | - | 4.2.0 |
| direction | The space direction | `vertical` \| `horizontal` | `horizontal` | 4.1.0 | | direction | The space direction | `vertical` \| `horizontal` | `horizontal` | 4.1.0 |
| size | The space size | `small` \| `middle` \| `large` \| `number` | `small` | 4.1.0 | | size | The space size | `small` \| `middle` \| `large` \| `number` | `small` | 4.1.0 |
| split | Set split | ReactNode | - | 4.7.0 |

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray'; import toArray from 'rc-util/lib/Children/toArray';
import { ConfigConsumerProps, ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import { SizeType } from '../config-provider/SizeContext'; import { SizeType } from '../config-provider/SizeContext';
import Item from './Item'; import Item from './Item';
@ -15,12 +15,11 @@ export interface SpaceProps {
direction?: 'horizontal' | 'vertical'; direction?: 'horizontal' | 'vertical';
// No `stretch` since many components do not support that. // No `stretch` since many components do not support that.
align?: 'start' | 'end' | 'center' | 'baseline'; align?: 'start' | 'end' | 'center' | 'baseline';
split?: React.ReactNode;
} }
const Space: React.FC<SpaceProps> = props => { const Space: React.FC<SpaceProps> = props => {
const { getPrefixCls, space, direction: directionConfig }: ConfigConsumerProps = React.useContext( const { getPrefixCls, space, direction: directionConfig } = React.useContext(ConfigContext);
ConfigContext,
);
const { const {
size = space?.size || 'small', size = space?.size || 'small',
@ -29,6 +28,7 @@ const Space: React.FC<SpaceProps> = props => {
children, children,
direction = 'horizontal', direction = 'horizontal',
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
split,
...otherProps ...otherProps
} = props; } = props;
@ -70,6 +70,7 @@ const Space: React.FC<SpaceProps> = props => {
size={size} size={size}
index={i} index={i}
marginDirection={marginDirection} marginDirection={marginDirection}
split={split}
> >
{child} {child}
</Item> </Item>

View File

@ -23,3 +23,4 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/wc6%263gJ0Y8/Space.svg
| align | 对齐方式 | `start` \| `end` \|`center` \|`baseline` | - | 4.2.0 | | align | 对齐方式 | `start` \| `end` \|`center` \|`baseline` | - | 4.2.0 |
| direction | 间距方向 | `vertical` \| `horizontal` | `horizontal` | 4.1.0 | | direction | 间距方向 | `vertical` \| `horizontal` | `horizontal` | 4.1.0 |
| size | 间距大小 | `small` \| `middle` \| `large` \| `number` | `small` | 4.1.0 | | size | 间距大小 | `small` \| `middle` \| `large` \| `number` | `small` | 4.1.0 |
| split | 设置拆分 | ReactNode | - | 4.7.0 |

View File

@ -84,7 +84,7 @@ const columns = [
| getPopupContainer | The render container of dropdowns in table | (triggerNode) => HTMLElement | () => TableHtmlElement | | | getPopupContainer | The render container of dropdowns in table | (triggerNode) => HTMLElement | () => TableHtmlElement | |
| sortDirections | Supported sort way, could be `ascend`, `descend` | Array | \[`ascend`, `descend`] | | | sortDirections | Supported sort way, could be `ascend`, `descend` | Array | \[`ascend`, `descend`] | |
| showSorterTooltip | The header show next sorter direction tooltip | boolean | true | | | showSorterTooltip | The header show next sorter direction tooltip | boolean | true | |
| sticky | Set sticky header and scroll bar | boolean \| `{offsetHeader?: number, offsetScroll?: number}` | - | 4.6.0 | | sticky | Set sticky header and scroll bar | boolean \| `{offsetHeader?: number, offsetScroll?: number, getContainer?: () => HTMLElement}` | - | 4.6.0 (getContainer: 4.7.0) |
#### onRow usage #### onRow usage

View File

@ -91,7 +91,7 @@ const columns = [
| getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | () => TableHtmlElement | | | getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | () => TableHtmlElement | |
| sortDirections | 支持的排序方式,取值为 `ascend` `descend` | Array | \[`ascend`, `descend`] | | | sortDirections | 支持的排序方式,取值为 `ascend` `descend` | Array | \[`ascend`, `descend`] | |
| showSorterTooltip | 表头是否显示下一次排序的 tooltip 提示 | boolean | true | | | showSorterTooltip | 表头是否显示下一次排序的 tooltip 提示 | boolean | true | |
| sticky | 设置粘性头部和滚动条 | boolean \| `{offsetHeader?: number, offsetScroll?: number}` | - | 4.6.0 | | sticky | 设置粘性头部和滚动条 | boolean \| `{offsetHeader?: number, offsetScroll?: number, getContainer?: () => HTMLElement}` | - | 4.6.0 (getContainer: 4.7.0) |
#### onRow 用法 #### onRow 用法

View File

@ -607,7 +607,7 @@
z-index: @table-sticky-zindex; z-index: @table-sticky-zindex;
} }
&-scroll { &-scroll {
position: fixed; position: sticky;
bottom: 0; bottom: 0;
z-index: @table-sticky-zindex; z-index: @table-sticky-zindex;
display: flex; display: flex;

View File

@ -6,12 +6,11 @@ import copy from 'copy-to-clipboard';
import Title from '../Title'; import Title from '../Title';
import Link from '../Link'; import Link from '../Link';
import Paragraph from '../Paragraph'; import Paragraph from '../Paragraph';
import Base from '../Base'; // eslint-disable-line import/no-named-as-default import Base from '../Base';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import Typography from '../Typography'; import Typography from '../Typography';
import { sleep } from '../../../tests/utils'; import { sleep } from '../../../tests/utils';
import TextArea from '../../input/TextArea';
jest.mock('copy-to-clipboard'); jest.mock('copy-to-clipboard');
@ -354,11 +353,11 @@ describe('Typography', () => {
expect(onStart).toHaveBeenCalled(); expect(onStart).toHaveBeenCalled();
// Should have className // Should have className
const props = wrapper.find('div').props(); const props = wrapper.find('div').first().props();
expect(props.style).toEqual(style); expect(props.style).toEqual(style);
expect(props.className.includes(className)).toBeTruthy(); expect(props.className.includes(className)).toBeTruthy();
wrapper.find(TextArea).simulate('change', { wrapper.find('textarea').simulate('change', {
target: { value: 'Bamboo' }, target: { value: 'Bamboo' },
}); });
@ -379,21 +378,21 @@ describe('Typography', () => {
testStep({ name: 'by key up' }, wrapper => { testStep({ name: 'by key up' }, wrapper => {
// Not trigger when inComposition // Not trigger when inComposition
wrapper.find(TextArea).simulate('compositionStart'); wrapper.find('textarea').simulate('compositionStart');
wrapper.find(TextArea).simulate('keyDown', { keyCode: KeyCode.ENTER }); wrapper.find('textarea').simulate('keyDown', { keyCode: KeyCode.ENTER });
wrapper.find(TextArea).simulate('compositionEnd'); wrapper.find('textarea').simulate('compositionEnd');
wrapper.find(TextArea).simulate('keyUp', { keyCode: KeyCode.ENTER }); wrapper.find('textarea').simulate('keyUp', { keyCode: KeyCode.ENTER });
// Now trigger // Now trigger
wrapper.find(TextArea).simulate('keyDown', { keyCode: KeyCode.ENTER }); wrapper.find('textarea').simulate('keyDown', { keyCode: KeyCode.ENTER });
wrapper.find(TextArea).simulate('keyUp', { keyCode: KeyCode.ENTER }); wrapper.find('textarea').simulate('keyUp', { keyCode: KeyCode.ENTER });
}); });
testStep( testStep(
{ name: 'by esc key' }, { name: 'by esc key' },
wrapper => { wrapper => {
wrapper.find(TextArea).simulate('keyDown', { keyCode: KeyCode.ESC }); wrapper.find('textarea').simulate('keyDown', { keyCode: KeyCode.ESC });
wrapper.find(TextArea).simulate('keyUp', { keyCode: KeyCode.ESC }); wrapper.find('textarea').simulate('keyUp', { keyCode: KeyCode.ESC });
}, },
onChange => { onChange => {
// eslint-disable-next-line jest/no-standalone-expect // eslint-disable-next-line jest/no-standalone-expect
@ -402,7 +401,7 @@ describe('Typography', () => {
); );
testStep({ name: 'by blur' }, wrapper => { testStep({ name: 'by blur' }, wrapper => {
wrapper.find(TextArea).simulate('blur'); wrapper.find('textarea').simulate('blur');
}); });
testStep({ name: 'customize edit icon', icon: <HighlightOutlined /> }); testStep({ name: 'customize edit icon', icon: <HighlightOutlined /> });

View File

@ -43,6 +43,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
type, type,
children, children,
style, style,
itemRender,
} = props; } = props;
const [dragState, setDragState] = React.useState<string>('drop'); const [dragState, setDragState] = React.useState<string>('drop');
@ -261,6 +262,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
isImageUrl={isImageUrl} isImageUrl={isImageUrl}
progress={progress} progress={progress}
appendAction={button} appendAction={button}
itemRender={itemRender}
/> />
); );
}} }}

View File

@ -37,6 +37,7 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
downloadIcon: customDownloadIcon, downloadIcon: customDownloadIcon,
progress: progressProps, progress: progressProps,
appendAction, appendAction,
itemRender,
}, },
ref, ref,
) => { ) => {
@ -210,7 +211,9 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
const removeIcon = showRemoveIcon const removeIcon = showRemoveIcon
? handleActionIconRender( ? handleActionIconRender(
customRemoveIcon || <DeleteOutlined />, (typeof customRemoveIcon === 'function' ? customRemoveIcon(file) : customRemoveIcon) || (
<DeleteOutlined />
),
() => handleClose(file), () => handleClose(file),
prefixCls, prefixCls,
locale.removeFile, locale.removeFile,
@ -220,7 +223,9 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
const downloadIcon = const downloadIcon =
showDownloadIcon && file.status === 'done' showDownloadIcon && file.status === 'done'
? handleActionIconRender( ? handleActionIconRender(
customDownloadIcon || <DownloadOutlined />, (typeof customDownloadIcon === 'function'
? customDownloadIcon(file)
: customDownloadIcon) || <DownloadOutlined />,
() => handleDownload(file), () => handleDownload(file),
prefixCls, prefixCls,
locale.downloadFile, locale.downloadFile,
@ -317,15 +322,17 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
const listContainerNameClass = classNames({ const listContainerNameClass = classNames({
[`${prefixCls}-list-picture-card-container`]: listType === 'picture-card', [`${prefixCls}-list-picture-card-container`]: listType === 'picture-card',
}); });
return ( const item =
<div key={file.uid} className={listContainerNameClass}> file.status === 'error' ? (
{file.status === 'error' ? (
<Tooltip title={message} getPopupContainer={node => node.parentNode as HTMLElement}> <Tooltip title={message} getPopupContainer={node => node.parentNode as HTMLElement}>
{dom} {dom}
</Tooltip> </Tooltip>
) : ( ) : (
<span>{dom}</span> <span>{dom}</span>
)} );
return (
<div key={file.uid} className={listContainerNameClass}>
{itemRender ? itemRender(item, file, items) : item}
</div> </div>
); );
}); });

View File

@ -690,6 +690,480 @@ exports[`renders ./components/upload/demo/drag.md correctly 1`] = `
</span> </span>
`; `;
exports[`renders ./components/upload/demo/drag-sorting.md correctly 1`] = `
<span
class=""
>
<div
class="ant-upload ant-upload-select ant-upload-select-text"
>
<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"
class=""
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>
Click to Upload
</span>
</button>
</span>
</div>
<div
class="ant-upload-list ant-upload-list-text"
>
<div
class=""
>
<div
class="ant-upload-draggable-list-item "
style="cursor:move"
>
<span>
<div
class="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-text"
>
<div
class="ant-upload-list-item-info"
>
<span>
<div
class="ant-upload-text-icon"
>
<span
aria-label="paper-clip"
class="anticon anticon-paper-clip"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="paper-clip"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M779.3 196.6c-94.2-94.2-247.6-94.2-341.7 0l-261 260.8c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l261-260.8c32.4-32.4 75.5-50.2 121.3-50.2s88.9 17.8 121.2 50.2c32.4 32.4 50.2 75.5 50.2 121.2 0 45.8-17.8 88.8-50.2 121.2l-266 265.9-43.1 43.1c-40.3 40.3-105.8 40.3-146.1 0-19.5-19.5-30.2-45.4-30.2-73s10.7-53.5 30.2-73l263.9-263.8c6.7-6.6 15.5-10.3 24.9-10.3h.1c9.4 0 18.1 3.7 24.7 10.3 6.7 6.7 10.3 15.5 10.3 24.9 0 9.3-3.7 18.1-10.3 24.7L372.4 653c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l215.6-215.6c19.9-19.9 30.8-46.3 30.8-74.4s-11-54.6-30.8-74.4c-41.1-41.1-107.9-41-149 0L463 364 224.8 602.1A172.22 172.22 0 00174 724.8c0 46.3 18.1 89.8 50.8 122.5 33.9 33.8 78.3 50.7 122.7 50.7 44.4 0 88.8-16.9 122.6-50.7l309.2-309C824.8 492.7 850 432 850 367.5c.1-64.6-25.1-125.3-70.7-170.9z"
/>
</svg>
</span>
</div>
<a
class="ant-upload-list-item-name ant-upload-list-item-name-icon-count-1"
href="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
rel="noopener noreferrer"
target="_blank"
title="image1.png"
>
image1.png
</a>
<span
class="ant-upload-list-item-card-actions "
>
<button
class="ant-btn ant-btn-text ant-btn-sm ant-btn-icon-only ant-upload-list-item-card-actions-btn"
title="Remove file"
type="button"
>
<span
aria-label="delete"
class="anticon anticon-delete"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="delete"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z"
/>
</svg>
</span>
</button>
</span>
</span>
</div>
</div>
</span>
</div>
</div>
<div
class=""
>
<div
class="ant-upload-draggable-list-item "
style="cursor:move"
>
<span>
<div
class="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-text"
>
<div
class="ant-upload-list-item-info"
>
<span>
<div
class="ant-upload-text-icon"
>
<span
aria-label="paper-clip"
class="anticon anticon-paper-clip"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="paper-clip"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M779.3 196.6c-94.2-94.2-247.6-94.2-341.7 0l-261 260.8c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l261-260.8c32.4-32.4 75.5-50.2 121.3-50.2s88.9 17.8 121.2 50.2c32.4 32.4 50.2 75.5 50.2 121.2 0 45.8-17.8 88.8-50.2 121.2l-266 265.9-43.1 43.1c-40.3 40.3-105.8 40.3-146.1 0-19.5-19.5-30.2-45.4-30.2-73s10.7-53.5 30.2-73l263.9-263.8c6.7-6.6 15.5-10.3 24.9-10.3h.1c9.4 0 18.1 3.7 24.7 10.3 6.7 6.7 10.3 15.5 10.3 24.9 0 9.3-3.7 18.1-10.3 24.7L372.4 653c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l215.6-215.6c19.9-19.9 30.8-46.3 30.8-74.4s-11-54.6-30.8-74.4c-41.1-41.1-107.9-41-149 0L463 364 224.8 602.1A172.22 172.22 0 00174 724.8c0 46.3 18.1 89.8 50.8 122.5 33.9 33.8 78.3 50.7 122.7 50.7 44.4 0 88.8-16.9 122.6-50.7l309.2-309C824.8 492.7 850 432 850 367.5c.1-64.6-25.1-125.3-70.7-170.9z"
/>
</svg>
</span>
</div>
<a
class="ant-upload-list-item-name ant-upload-list-item-name-icon-count-1"
href="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
rel="noopener noreferrer"
target="_blank"
title="image2.png"
>
image2.png
</a>
<span
class="ant-upload-list-item-card-actions "
>
<button
class="ant-btn ant-btn-text ant-btn-sm ant-btn-icon-only ant-upload-list-item-card-actions-btn"
title="Remove file"
type="button"
>
<span
aria-label="delete"
class="anticon anticon-delete"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="delete"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z"
/>
</svg>
</span>
</button>
</span>
</span>
</div>
</div>
</span>
</div>
</div>
<div
class=""
>
<div
class="ant-upload-draggable-list-item "
style="cursor:move"
>
<span>
<div
class="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-text"
>
<div
class="ant-upload-list-item-info"
>
<span>
<div
class="ant-upload-text-icon"
>
<span
aria-label="paper-clip"
class="anticon anticon-paper-clip"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="paper-clip"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M779.3 196.6c-94.2-94.2-247.6-94.2-341.7 0l-261 260.8c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l261-260.8c32.4-32.4 75.5-50.2 121.3-50.2s88.9 17.8 121.2 50.2c32.4 32.4 50.2 75.5 50.2 121.2 0 45.8-17.8 88.8-50.2 121.2l-266 265.9-43.1 43.1c-40.3 40.3-105.8 40.3-146.1 0-19.5-19.5-30.2-45.4-30.2-73s10.7-53.5 30.2-73l263.9-263.8c6.7-6.6 15.5-10.3 24.9-10.3h.1c9.4 0 18.1 3.7 24.7 10.3 6.7 6.7 10.3 15.5 10.3 24.9 0 9.3-3.7 18.1-10.3 24.7L372.4 653c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l215.6-215.6c19.9-19.9 30.8-46.3 30.8-74.4s-11-54.6-30.8-74.4c-41.1-41.1-107.9-41-149 0L463 364 224.8 602.1A172.22 172.22 0 00174 724.8c0 46.3 18.1 89.8 50.8 122.5 33.9 33.8 78.3 50.7 122.7 50.7 44.4 0 88.8-16.9 122.6-50.7l309.2-309C824.8 492.7 850 432 850 367.5c.1-64.6-25.1-125.3-70.7-170.9z"
/>
</svg>
</span>
</div>
<a
class="ant-upload-list-item-name ant-upload-list-item-name-icon-count-1"
href="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
rel="noopener noreferrer"
target="_blank"
title="image3.png"
>
image3.png
</a>
<span
class="ant-upload-list-item-card-actions "
>
<button
class="ant-btn ant-btn-text ant-btn-sm ant-btn-icon-only ant-upload-list-item-card-actions-btn"
title="Remove file"
type="button"
>
<span
aria-label="delete"
class="anticon anticon-delete"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="delete"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z"
/>
</svg>
</span>
</button>
</span>
</span>
</div>
</div>
</span>
</div>
</div>
<div
class=""
>
<div
class="ant-upload-draggable-list-item "
style="cursor:move"
>
<span>
<div
class="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-text"
>
<div
class="ant-upload-list-item-info"
>
<span>
<div
class="ant-upload-text-icon"
>
<span
aria-label="paper-clip"
class="anticon anticon-paper-clip"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="paper-clip"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M779.3 196.6c-94.2-94.2-247.6-94.2-341.7 0l-261 260.8c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l261-260.8c32.4-32.4 75.5-50.2 121.3-50.2s88.9 17.8 121.2 50.2c32.4 32.4 50.2 75.5 50.2 121.2 0 45.8-17.8 88.8-50.2 121.2l-266 265.9-43.1 43.1c-40.3 40.3-105.8 40.3-146.1 0-19.5-19.5-30.2-45.4-30.2-73s10.7-53.5 30.2-73l263.9-263.8c6.7-6.6 15.5-10.3 24.9-10.3h.1c9.4 0 18.1 3.7 24.7 10.3 6.7 6.7 10.3 15.5 10.3 24.9 0 9.3-3.7 18.1-10.3 24.7L372.4 653c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l215.6-215.6c19.9-19.9 30.8-46.3 30.8-74.4s-11-54.6-30.8-74.4c-41.1-41.1-107.9-41-149 0L463 364 224.8 602.1A172.22 172.22 0 00174 724.8c0 46.3 18.1 89.8 50.8 122.5 33.9 33.8 78.3 50.7 122.7 50.7 44.4 0 88.8-16.9 122.6-50.7l309.2-309C824.8 492.7 850 432 850 367.5c.1-64.6-25.1-125.3-70.7-170.9z"
/>
</svg>
</span>
</div>
<a
class="ant-upload-list-item-name ant-upload-list-item-name-icon-count-1"
href="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
rel="noopener noreferrer"
target="_blank"
title="image4.png"
>
image4.png
</a>
<span
class="ant-upload-list-item-card-actions "
>
<button
class="ant-btn ant-btn-text ant-btn-sm ant-btn-icon-only ant-upload-list-item-card-actions-btn"
title="Remove file"
type="button"
>
<span
aria-label="delete"
class="anticon anticon-delete"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="delete"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z"
/>
</svg>
</span>
</button>
</span>
</span>
</div>
</div>
</span>
</div>
</div>
<div
class=""
>
<div
class="ant-upload-draggable-list-item "
style="cursor:move"
>
<div
class="ant-upload-list-item ant-upload-list-item-error ant-upload-list-item-list-type-text"
>
<div
class="ant-upload-list-item-info"
>
<span>
<div
class="ant-upload-text-icon"
>
<span
aria-label="paper-clip"
class="anticon anticon-paper-clip"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="paper-clip"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M779.3 196.6c-94.2-94.2-247.6-94.2-341.7 0l-261 260.8c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l261-260.8c32.4-32.4 75.5-50.2 121.3-50.2s88.9 17.8 121.2 50.2c32.4 32.4 50.2 75.5 50.2 121.2 0 45.8-17.8 88.8-50.2 121.2l-266 265.9-43.1 43.1c-40.3 40.3-105.8 40.3-146.1 0-19.5-19.5-30.2-45.4-30.2-73s10.7-53.5 30.2-73l263.9-263.8c6.7-6.6 15.5-10.3 24.9-10.3h.1c9.4 0 18.1 3.7 24.7 10.3 6.7 6.7 10.3 15.5 10.3 24.9 0 9.3-3.7 18.1-10.3 24.7L372.4 653c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l215.6-215.6c19.9-19.9 30.8-46.3 30.8-74.4s-11-54.6-30.8-74.4c-41.1-41.1-107.9-41-149 0L463 364 224.8 602.1A172.22 172.22 0 00174 724.8c0 46.3 18.1 89.8 50.8 122.5 33.9 33.8 78.3 50.7 122.7 50.7 44.4 0 88.8-16.9 122.6-50.7l309.2-309C824.8 492.7 850 432 850 367.5c.1-64.6-25.1-125.3-70.7-170.9z"
/>
</svg>
</span>
</div>
<span
class="ant-upload-list-item-name ant-upload-list-item-name-icon-count-1"
title="image.png"
>
image.png
</span>
<span
class="ant-upload-list-item-card-actions "
>
<button
class="ant-btn ant-btn-text ant-btn-sm ant-btn-icon-only ant-upload-list-item-card-actions-btn"
title="Remove file"
type="button"
>
<span
aria-label="delete"
class="anticon anticon-delete"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="delete"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z"
/>
</svg>
</span>
</button>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</span>
`;
exports[`renders ./components/upload/demo/file-type.md correctly 1`] = ` exports[`renders ./components/upload/demo/file-type.md correctly 1`] = `
<span <span
class="ant-upload-picture-card-wrapper" class="ant-upload-picture-card-wrapper"

View File

@ -106,6 +106,31 @@ exports[`Upload List handle error 1`] = `
</span> </span>
`; `;
exports[`Upload List itemRender 1`] = `
<div
class="ant-upload-list ant-upload-list-text"
>
<div
class=""
>
<span
class="custom-item-render"
>
uid:-1 name: xxx.png status: removed url: https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png 1/2
</span>
</div>
<div
class=""
>
<span
class="custom-item-render"
>
uid:-2 name: yyy.png status: removed url: https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png 2/2
</span>
</div>
</div>
`;
exports[`Upload List should be uploading when upload a file 1`] = ` exports[`Upload List should be uploading when upload a file 1`] = `
<span <span
class="" class=""

View File

@ -533,7 +533,7 @@ describe('Upload List', () => {
showUploadList={{ showUploadList={{
showRemoveIcon: true, showRemoveIcon: true,
showDownloadIcon: true, showDownloadIcon: true,
removeIcon: <i>RM</i>, removeIcon: () => <i>RM</i>,
downloadIcon: <i>DL</i>, downloadIcon: <i>DL</i>,
}} }}
> >
@ -993,4 +993,20 @@ describe('Upload List', () => {
jest.useRealTimers(); jest.useRealTimers();
}); });
it('itemRender', () => {
const itemRender = (originNode, file, currFileList) => {
const { name, status, uid, url } = file;
const index = currFileList.indexOf(file);
return (
<span className="custom-item-render">
{`uid:${uid} name: ${name} status: ${status} url: ${url} ${index + 1}/${
currFileList.length
}`}
</span>
);
};
const wrapper = mount(<UploadList locale={{}} items={fileList} itemRender={itemRender} />);
expect(wrapper.render()).toMatchSnapshot();
});
}); });

View File

@ -0,0 +1,164 @@
---
order: 13
title:
zh-CN: 上传列表拖拽排序
en-US: Drag sorting of uploadList
---
## zh-CN
使用 `itemRender` ,我们可以集成 react-dnd 来实现对上传列表拖拽排序。
## en-US
By using `itemRender`, we can integrate upload with react-dnd to implement drag sorting of uploadList.
```jsx
import React, { useState, useCallback, useRef } from 'react';
import { Upload, Button, Tooltip } from 'antd';
import { DndProvider, useDrag, useDrop, createDndContext } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import update from 'immutability-helper';
import { UploadOutlined } from '@ant-design/icons';
const RNDContext = createDndContext(HTML5Backend);
const type = 'DragableUploadList';
const DragableUploadListItem = ({ originNode, moveRow, file, fileList }) => {
const ref = React.useRef();
const index = fileList.indexOf(file);
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: monitor => {
const { index: dragIndex } = monitor.getItem() || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName: dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: item => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
item: { type, index },
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
const errorNode = (
<Tooltip title="Upload Error" getPopupContainer={() => document.body}>
{originNode.props.children}
</Tooltip>
);
return (
<div
ref={ref}
className={`ant-upload-draggable-list-item ${isOver ? dropClassName : ''}`}
style={{ cursor: 'move' }}
>
{file.status === 'error' ? errorNode : originNode}
</div>
);
};
const DragSortingUpload: React.FC = () => {
const [fileList, setFileList] = useState([
{
uid: '-1',
name: 'image1.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-2',
name: 'image2.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-3',
name: 'image3.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-4',
name: 'image4.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-5',
name: 'image.png',
status: 'error',
},
]);
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
const dragRow = fileList[dragIndex];
setFileList(
update(fileList, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragRow],
],
}),
);
},
[fileList],
);
const manager = useRef(RNDContext);
const onChange = ({ fileList: newFileList }) => {
setFileList(newFileList);
};
return (
<DndProvider manager={manager.current.dragDropManager}>
<Upload
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
fileList={fileList}
onChange={onChange}
itemRender={(originNode, file, currFileList) => {
return (
<DragableUploadListItem
originNode={originNode}
file={file}
fileList={currFileList}
moveRow={moveRow}
/>
);
}}
>
<Button>
<UploadOutlined /> Click to Upload
</Button>
</Upload>
</DndProvider>
);
};
ReactDOM.render(<DragSortingUpload />, mountNode);
```
```css
#components-upload-demo-drag-sorting .ant-upload-draggable-list-item {
border-top: 2px dashed rgba(0, 0, 0, 0);
border-bottom: 2px dashed rgba(0, 0, 0, 0);
}
#components-upload-demo-drag-sorting .ant-upload-draggable-list-item.drop-over-downward {
border-bottom-color: #1890ff;
}
#components-upload-demo-drag-sorting .ant-upload-draggable-list-item.drop-over-upward {
border-top-color: #1890ff;
}
```

View File

@ -35,7 +35,7 @@ Uploading is the process of publishing information (web pages, text, pictures, v
| name | The name of uploading file | string | `file` | | | name | The name of uploading file | string | `file` | |
| previewFile | Customize preview file logic | (file: File \| Blob) => Promise&lt;dataURL: string> | - | | | previewFile | Customize preview file logic | (file: File \| Blob) => Promise&lt;dataURL: string> | - | |
| isImageUrl | Customize if render &lt;img /> in thumbnail | (file: UploadFile) => boolean | [(inside implementation)](https://github.com/ant-design/ant-design/blob/4ad5830eecfb87471cd8ac588c5d992862b70770/components/upload/utils.tsx#L47-L68) | | | isImageUrl | Customize if render &lt;img /> in thumbnail | (file: UploadFile) => boolean | [(inside implementation)](https://github.com/ant-design/ant-design/blob/4ad5830eecfb87471cd8ac588c5d992862b70770/components/upload/utils.tsx#L47-L68) | |
| showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon` and `downloadIcon` individually | boolean \| { showPreviewIcon?: boolean, showDownloadIcon?: boolean, showRemoveIcon?: boolean, removeIcon?: React.ReactNode, downloadIcon?: React.ReactNode } | true | | | showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon` and `downloadIcon` individually | boolean \| { showPreviewIcon?: boolean, showDownloadIcon?: boolean, showRemoveIcon?: boolean, removeIcon?: ReactNode \| (file: UploadFile) => ReactNode, downloadIcon?: ReactNode \| (file: UploadFile) => ReactNode } | true | function: 4.7.0 |
| withCredentials | The ajax upload with cookie sent | boolean | false | | | withCredentials | The ajax upload with cookie sent | boolean | false | |
| openFileDialogOnClick | Click open file dialog | boolean | true | | | openFileDialogOnClick | Click open file dialog | boolean | true | |
| onChange | A callback function, can be executed when uploading state is changing, see [onChange](#onChange) | function | - | | | onChange | A callback function, can be executed when uploading state is changing, see [onChange](#onChange) | function | - | |
@ -43,8 +43,9 @@ Uploading is the process of publishing information (web pages, text, pictures, v
| onRemove | A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is false or a Promise which resolve(false) or reject | function(file): boolean \| Promise | - | | | onRemove | A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is false or a Promise which resolve(false) or reject | function(file): boolean \| Promise | - | |
| onDownload | Click the method to download the file, pass the method to perform the method logic, do not pass the default jump to the new TAB | function(file): void | (Jump to new TAB) | | | onDownload | Click the method to download the file, pass the method to perform the method logic, do not pass the default jump to the new TAB | function(file): void | (Jump to new TAB) | |
| transformFile   | Customize transform file before request | Function(file): string \| Blob \| File \| Promise&lt;string \| Blob \| File> | - | | | transformFile   | Customize transform file before request | Function(file): string \| Blob \| File \| Promise&lt;string \| Blob \| File> | - | |
| iconRender | Custom show icon | (file: UploadFile, listType?: UploadListType) => React.ReactNode | - | | | iconRender | Custom show icon | (file: UploadFile, listType?: UploadListType) => ReactNode | - | |
| progress | Custom progress bar | [ProgressProps](/components/progress/#API) (support `type="line"` only) | { strokeWidth: 2, showInfo: false } | 4.3.0 | | progress | Custom progress bar | [ProgressProps](/components/progress/#API) (support `type="line"` only) | { strokeWidth: 2, showInfo: false } | 4.3.0 |
| itemRender | Custom item of uploadList | (originNode: ReactElement, file: UploadFile, fileList?: object\[]) => React.ReactNode | - | 4.7.0 |
### onChange ### onChange

View File

@ -36,7 +36,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg
| name | 发到后台的文件参数名 | string | `file` | | | name | 发到后台的文件参数名 | string | `file` | |
| previewFile | 自定义文件预览逻辑 | (file: File \| Blob) => Promise<dataURL: string> | - | | | previewFile | 自定义文件预览逻辑 | (file: File \| Blob) => Promise<dataURL: string> | - | |
| isImageUrl | 自定义缩略图是否使用 &lt;img /> 标签进行显示 | (file: UploadFile) => boolean | [(内部实现)](https://github.com/ant-design/ant-design/blob/4ad5830eecfb87471cd8ac588c5d992862b70770/components/upload/utils.tsx#L47-L68) | | | isImageUrl | 自定义缩略图是否使用 &lt;img /> 标签进行显示 | (file: UploadFile) => boolean | [(内部实现)](https://github.com/ant-design/ant-design/blob/4ad5830eecfb87471cd8ac588c5d992862b70770/components/upload/utils.tsx#L47-L68) | |
| showUploadList | 是否展示文件列表, 可设为一个对象,用于单独设定 `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon``downloadIcon` | boolean \| { showPreviewIcon?: boolean, showRemoveIcon?: boolean, showDownloadIcon?: boolean, removeIcon?: React.ReactNode, downloadIcon?: React.ReactNode } | true | | | showUploadList | 是否展示文件列表, 可设为一个对象,用于单独设定 `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon``downloadIcon` | boolean \| { showPreviewIcon?: boolean, showRemoveIcon?: boolean, showDownloadIcon?: boolean, removeIcon?: ReactNode \| (file: UploadFile) => ReactNode, downloadIcon?: ReactNode \| (file: UploadFile) => ReactNode } | true | function: 4.7.0 |
| withCredentials | 上传请求时是否携带 cookie | boolean | false | | | withCredentials | 上传请求时是否携带 cookie | boolean | false | |
| openFileDialogOnClick | 点击打开文件对话框 | boolean | true | | | openFileDialogOnClick | 点击打开文件对话框 | boolean | true | |
| onChange | 上传文件改变时的状态,详见 [onChange](#onChange) | function | - | | | onChange | 上传文件改变时的状态,详见 [onChange](#onChange) | function | - | |
@ -44,8 +44,9 @@ cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg
| onRemove   | 点击移除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象Promise 对象 resolve(false) 或 reject 时不移除               | function(file): boolean \| Promise | -   | | | onRemove   | 点击移除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象Promise 对象 resolve(false) 或 reject 时不移除               | function(file): boolean \| Promise | -   | |
| onDownload | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页 | function(file): void | (跳转新标签页) | | | onDownload | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页 | function(file): void | (跳转新标签页) | |
| transformFile   | 在上传之前转换文件。支持返回一个 Promise 对象   | function(file): string \| Blob \| File \| Promise&lt;string \| Blob \| File> | -   | | | transformFile   | 在上传之前转换文件。支持返回一个 Promise 对象   | function(file): string \| Blob \| File \| Promise&lt;string \| Blob \| File> | -   | |
| iconRender | 自定义显示 icon | (file: UploadFile, listType?: UploadListType) => React.ReactNode | - | | | iconRender | 自定义显示 icon | (file: UploadFile, listType?: UploadListType) => ReactNode | - | |
| progress | 自定义进度条样式 | [ProgressProps](/components/progress/#API)(仅支持 `type="line"` | { strokeWidth: 2, showInfo: false } | 4.3.0 | | progress | 自定义进度条样式 | [ProgressProps](/components/progress/#API)(仅支持 `type="line"` | { strokeWidth: 2, showInfo: false } | 4.3.0 |
| itemRender | 自定义上传列表项 | (originNode: ReactElement, file: UploadFile, fileList?: object\[]) => React.ReactNode | - | 4.7.0 |
### onChange ### onChange

View File

@ -56,8 +56,8 @@ export interface ShowUploadListInterface {
showRemoveIcon?: boolean; showRemoveIcon?: boolean;
showPreviewIcon?: boolean; showPreviewIcon?: boolean;
showDownloadIcon?: boolean; showDownloadIcon?: boolean;
removeIcon?: React.ReactNode; removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
downloadIcon?: React.ReactNode; downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
} }
export interface UploadLocale { export interface UploadLocale {
@ -111,6 +111,11 @@ export interface UploadProps<T = any> {
iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode; iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode;
isImageUrl?: (file: UploadFile) => boolean; isImageUrl?: (file: UploadFile) => boolean;
progress?: UploadListProgressProps; progress?: UploadListProgressProps;
itemRender?: (
originNode: React.ReactElement,
file: UploadFile,
fileList?: Array<UploadFile<T>>,
) => React.ReactNode;
} }
export interface UploadState<T = any> { export interface UploadState<T = any> {
@ -129,11 +134,16 @@ export interface UploadListProps<T = any> {
showRemoveIcon?: boolean; showRemoveIcon?: boolean;
showDownloadIcon?: boolean; showDownloadIcon?: boolean;
showPreviewIcon?: boolean; showPreviewIcon?: boolean;
removeIcon?: React.ReactNode; removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
downloadIcon?: React.ReactNode; downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
locale: UploadLocale; locale: UploadLocale;
previewFile?: PreviewFileHandler; previewFile?: PreviewFileHandler;
iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode; iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode;
isImageUrl?: (file: UploadFile) => boolean; isImageUrl?: (file: UploadFile) => boolean;
appendAction?: React.ReactNode; appendAction?: React.ReactNode;
itemRender?: (
originNode: React.ReactElement,
file: UploadFile,
fileList?: Array<UploadFile<T>>,
) => React.ReactNode;
} }

View File

@ -97,6 +97,16 @@ During actual real-world project development, you will most likely need a develo
- [d2-admin](https://github.com/d2-projects/d2-admin) - [d2-admin](https://github.com/d2-projects/d2-admin)
- More scaffolds at [Scaffold Market](http://scaffold.ant.design/) - More scaffolds at [Scaffold Market](http://scaffold.ant.design/)
## Test with Jest
If you use `create-react-app` follow the instructions [here](/docs/react/use-with-create-react-app#Test-with-Jest) instead.
Jest does not support `esm` modules, and Ant Design uses them. In order to test your Ant Design application with Jest you have to add the following to your Jest config :
```json
"transform": { "^.+\\.(ts|tsx|js|jsx)?$": "ts-jest" }
```
## Import on Demand ## Import on Demand
`antd` supports tree shaking of ES modules, so using `import { Button } from 'antd';` would drop js code you didn't use. `antd` supports tree shaking of ES modules, so using `import { Button } from 'antd';` would drop js code you didn't use.

View File

@ -82,6 +82,18 @@ Ok, you should now see a blue primary button displayed on the page. Next you can
We are successfully running antd components now, go build your own application! We are successfully running antd components now, go build your own application!
## Test with Jest
`create-react-app` comes with `jest` built in. Jest does not support `esm` modules, and Ant Design uses them. In order to test your Ant Design application with Jest you have to add the following to your `package.json` :
```json
"jest": {
"transformIgnorePatterns": [
"/node_modules/(?!antd|@ant-design|rc-.+?|@babel/runtime).+(js|jsx)$"
]
}
```
## Advanced Guides ## Advanced Guides
In the real world, we usually have to modify default webpack config for custom needs such as themes. We can achieve that by using [craco](https://github.com/gsoft-inc/craco) which is one of create-react-app's custom config solutions. In the real world, we usually have to modify default webpack config for custom needs such as themes. We can achieve that by using [craco](https://github.com/gsoft-inc/craco) which is one of create-react-app's custom config solutions.

View File

@ -121,11 +121,11 @@
"rc-cascader": "~1.4.0", "rc-cascader": "~1.4.0",
"rc-checkbox": "~2.3.0", "rc-checkbox": "~2.3.0",
"rc-collapse": "~2.0.0", "rc-collapse": "~2.0.0",
"rc-dialog": "~8.2.1", "rc-dialog": "~8.4.0",
"rc-drawer": "~4.1.0", "rc-drawer": "~4.1.0",
"rc-dropdown": "~3.2.0", "rc-dropdown": "~3.2.0",
"rc-field-form": "~1.10.0", "rc-field-form": "~1.12.0",
"rc-image": "~3.0.6", "rc-image": "~3.2.1",
"rc-input-number": "~6.0.0", "rc-input-number": "~6.0.0",
"rc-mentions": "~1.5.0", "rc-mentions": "~1.5.0",
"rc-menu": "~8.7.1", "rc-menu": "~8.7.1",
@ -140,7 +140,7 @@
"rc-slider": "~9.5.2", "rc-slider": "~9.5.2",
"rc-steps": "~4.1.0", "rc-steps": "~4.1.0",
"rc-switch": "~3.2.0", "rc-switch": "~3.2.0",
"rc-table": "~7.9.2", "rc-table": "~7.10.0",
"rc-tabs": "~11.6.0", "rc-tabs": "~11.6.0",
"rc-textarea": "~0.3.0", "rc-textarea": "~0.3.0",
"rc-tooltip": "~5.0.0", "rc-tooltip": "~5.0.0",
@ -249,6 +249,7 @@
"react-dnd": "^11.1.1", "react-dnd": "^11.1.1",
"react-dnd-html5-backend": "^11.1.1", "react-dnd-html5-backend": "^11.1.1",
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-draggable": "^4.4.3",
"react-github-button": "^0.1.11", "react-github-button": "^0.1.11",
"react-helmet-async": "^1.0.4", "react-helmet-async": "^1.0.4",
"react-highlight-words": "^0.16.0", "react-highlight-words": "^0.16.0",