Merge pull request #27064 from ant-design/feature

chore: merge feature into master
This commit is contained in:
偏右 2020-10-10 14:13:11 +08:00 committed by GitHub
commit 0fec3aa602
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 2916 additions and 785 deletions

View File

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

View File

@ -139,19 +139,23 @@ exports[`renders ./components/auto-complete/demo/custom.md correctly 1`] = `
<span
class="ant-select-selection-search"
>
<textarea
aria-activedescendant="undefined_list_0"
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-input ant-select-selection-search-input"
placeholder="input here"
role="combobox"
style="height:50px"
type="search"
/>
<div
class="ant-input-textarea"
>
<textarea
aria-activedescendant="undefined_list_0"
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-input ant-select-selection-search-input"
placeholder="input here"
role="combobox"
style="height:50px"
type="search"
/>
</div>
</span>
<span
class="ant-select-selection-placeholder"

View File

@ -1,13 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import Avatar from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import useBreakpoint from '../../grid/hooks/useBreakpoint';
jest.mock('../../grid/hooks/useBreakpoint');
describe('Avatar Render', () => {
mountTest(Avatar);
rtlTest(Avatar);
const sizes = { xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 };
let originOffsetWidth;
beforeAll(() => {
// Mock offsetHeight
@ -152,6 +158,19 @@ describe('Avatar Render', () => {
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', () => {
const onMouseEnter = jest.fn();
const wrapper = mount(<Avatar onMouseEnter={onMouseEnter}>TestString</Avatar>);

View File

@ -1,5 +1,89 @@
// 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`] = `
<span
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`] = `
Array [
<div
@ -583,23 +602,31 @@ Array [
]
`;
exports[`renders ./components/avatar/demo/fallback.md correctly 1`] = `
Array [
exports[`renders ./components/avatar/demo/responsive.md correctly 1`] = `
<span
class="ant-avatar ant-avatar-circle ant-avatar-icon"
>
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
aria-label="ant-design"
class="anticon anticon-ant-design"
role="img"
>
<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>,
]
<svg
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"
/>
</svg>
</span>
</span>
`;
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 devWarning from '../_util/devWarning';
import { composeRef } from '../_util/ref';
import { Breakpoint, responsiveArray, ScreenSizeMap } from '../_util/responsiveObserve';
import useBreakpoint from '../grid/hooks/useBreakpoint';
export interface AvatarProps {
/** Shape of avatar, options:`circle`, `square` */
@ -13,7 +15,7 @@ export interface AvatarProps {
* Size of avatar, options: `large`, `small`, `default`
* or a custom number size
* */
size?: 'large' | 'small' | 'default' | number;
size?: 'large' | 'small' | 'default' | number | ScreenSizeMap;
gap?: number;
/** Src of image avatar */
src?: string;
@ -94,6 +96,25 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
...others
} = 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(
!(typeof icon === 'string' && icon.length > 2),
'Avatar',
@ -185,7 +206,7 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
return (
<span
{...others}
style={{ ...sizeStyle, ...others.style }}
style={{ ...sizeStyle, ...responsiveSizeStyle, ...others.style }}
className={classString}
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 | - | |
| 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 | - | |
| srcSet | A list of sources to use for different screen resolutions | 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 | - | |
| 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 | - | |
| srcSet | 设置图片类头像响应式资源地址 | string | - | |
| alt | 图像无法显示时的替代文本 | string | - | |

View File

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

View File

@ -156,10 +156,14 @@ exports[`renders ./components/comment/demo/editor.md correctly 1`] = `
<div
class="ant-form-item-control-input-content"
>
<textarea
class="ant-input"
rows="4"
/>
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
rows="4"
/>
</div>
</div>
</div>
</div>

View File

@ -14190,7 +14190,7 @@ exports[`ConfigProvider components Form configProvider 1`] = `
</div>
</div>
<div
class="config-form-item-explain"
class="config-form-item-explain config-form-item-explain-error"
>
<div
role="alert"
@ -14227,7 +14227,7 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
</div>
</div>
<div
class="config-form-item-explain"
class="config-form-item-explain config-form-item-explain-error"
>
<div
role="alert"
@ -14264,7 +14264,7 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
</div>
</div>
<div
class="config-form-item-explain"
class="config-form-item-explain config-form-item-explain-error"
>
<div
role="alert"
@ -14301,7 +14301,7 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -14338,7 +14338,7 @@ exports[`ConfigProvider components Form normal 1`] = `
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -14375,7 +14375,7 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
</div>
</div>
<div
class="prefix-Form-item-explain"
class="prefix-Form-item-explain prefix-Form-item-explain-error"
>
<div
role="alert"
@ -14531,9 +14531,13 @@ exports[`ConfigProvider components Input configProvider 1`] = `
</span>
</span>
</span>
<textarea
class="config-input"
/>
<div
class="config-input-textarea"
>
<textarea
class="config-input"
/>
</div>
</div>
`;
@ -14620,9 +14624,13 @@ exports[`ConfigProvider components Input configProvider componentSize large 1`]
</span>
</span>
</span>
<textarea
class="config-input"
/>
<div
class="config-input-textarea"
>
<textarea
class="config-input"
/>
</div>
</div>
`;
@ -14709,9 +14717,13 @@ exports[`ConfigProvider components Input configProvider componentSize middle 1`]
</span>
</span>
</span>
<textarea
class="config-input"
/>
<div
class="config-input-textarea"
>
<textarea
class="config-input"
/>
</div>
</div>
`;
@ -14798,9 +14810,13 @@ exports[`ConfigProvider components Input configProvider virtual and dropdownMatc
</span>
</span>
</span>
<textarea
class="ant-input"
/>
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
</div>
</div>
`;
@ -14887,9 +14903,13 @@ exports[`ConfigProvider components Input normal 1`] = `
</span>
</span>
</span>
<textarea
class="ant-input"
/>
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
</div>
</div>
`;
@ -14976,9 +14996,13 @@ exports[`ConfigProvider components Input prefixCls 1`] = `
</span>
</span>
</span>
<textarea
class="prefix-Input"
/>
<div
class="prefix-Input-textarea"
>
<textarea
class="prefix-Input"
/>
</div>
</div>
`;
@ -16275,12 +16299,12 @@ exports[`ConfigProvider components Modal configProvider 1`] = `
class="config-modal-mask"
/>
<div
class="config-modal-wrap "
class="config-modal-wrap"
role="dialog"
tabindex="-1"
>
<div
class="config-modal "
class="config-modal"
role="document"
style="width:520px"
>
@ -16368,12 +16392,12 @@ exports[`ConfigProvider components Modal configProvider componentSize large 1`]
class="config-modal-mask"
/>
<div
class="config-modal-wrap "
class="config-modal-wrap"
role="dialog"
tabindex="-1"
>
<div
class="config-modal "
class="config-modal"
role="document"
style="width:520px"
>
@ -16461,12 +16485,12 @@ exports[`ConfigProvider components Modal configProvider componentSize middle 1`]
class="config-modal-mask"
/>
<div
class="config-modal-wrap "
class="config-modal-wrap"
role="dialog"
tabindex="-1"
>
<div
class="config-modal "
class="config-modal"
role="document"
style="width:520px"
>
@ -16554,12 +16578,12 @@ exports[`ConfigProvider components Modal configProvider virtual and dropdownMatc
class="ant-modal-mask"
/>
<div
class="ant-modal-wrap "
class="ant-modal-wrap"
role="dialog"
tabindex="-1"
>
<div
class="ant-modal "
class="ant-modal"
role="document"
style="width:520px"
>
@ -16647,12 +16671,12 @@ exports[`ConfigProvider components Modal normal 1`] = `
class="ant-modal-mask"
/>
<div
class="ant-modal-wrap "
class="ant-modal-wrap"
role="dialog"
tabindex="-1"
>
<div
class="ant-modal "
class="ant-modal"
role="document"
style="width:520px"
>
@ -16740,12 +16764,12 @@ exports[`ConfigProvider components Modal prefixCls 1`] = `
class="prefix-Modal-mask"
/>
<div
class="prefix-Modal-wrap "
class="prefix-Modal-wrap"
role="dialog"
tabindex="-1"
>
<div
class="prefix-Modal "
class="prefix-Modal"
role="document"
style="width:520px"
>

View File

@ -2011,6 +2011,7 @@ exports[`renders ./components/date-picker/demo/format.md correctly 1`] = `
</div>
<div
class="ant-space-item"
style="margin-bottom:12px"
>
<div
class="ant-picker ant-picker-range"
@ -2120,6 +2121,74 @@ exports[`renders ./components/date-picker/demo/format.md correctly 1`] = `
</span>
</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>
`;

View File

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

View File

@ -36,7 +36,7 @@ export function getTimeProps<DateType>(
const firstFormat = toArray(format)[0];
const showTimeObj: SharedTimeProps<DateType> = { ...props };
if (firstFormat) {
if (firstFormat && typeof firstFormat === 'string') {
if (!firstFormat.includes('s') && showSecond === undefined) {
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/) | - | |
| defaultPickerValue | To set default picker date | [moment](http://momentjs.com/) | - | |
| 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 | - | |
| 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() | |

View File

@ -90,7 +90,7 @@ import locale from 'antd/es/locale/zh_CN';
| defaultValue | 默认日期,如果开始时间或结束时间为 `null` 或者 `undefined`,日期范围将是一个开区间 | [moment](http://momentjs.com/) | - | |
| defaultPickerValue | 默认面板日期 | [moment](http://momentjs.com/) | - | |
| 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 | - | |
| showTime | 增加时间选择功能 | Object \| boolean | [TimePicker Options](/components/time-picker/#API) | |
| 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 = {
lang: {
placeholder: 'เลือกวันที่',
yearPlaceholder: 'เลือกปี',
quarterPlaceholder: 'เลือกไตรมาส',
monthPlaceholder: 'เลือกเดือน',
weekPlaceholder: 'เลือกสัปดาห์',
rangePlaceholder: ['วันเริ่มต้น', 'วันสิ้นสุด'],
rangeYearPlaceholder: ['ปีเริ่มต้น', 'ปีสิ้นสุด'],
rangeMonthPlaceholder: ['เดือนเริ่มต้น', 'เดือนสิ้นสุด'],
rangeWeekPlaceholder: ['สัปดาห์เริ่มต้น', 'สัปดาห์สิ้นสุด'],
...CalendarLocale,
},
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 { useContext, useRef } from 'react';
import isEqual from 'lodash/isEqual';
import classNames from 'classnames';
import { Field, FormInstance } from 'rc-field-form';
@ -11,7 +12,7 @@ import Row from '../grid/row';
import { ConfigContext } from '../config-provider';
import { tuple } from '../_util/type';
import devWarning from '../_util/devWarning';
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel';
import FormItemLabel, { FormItemLabelProps, LabelTooltipType } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput';
import { FormContext, FormItemContext } from './context';
import { toArray, getFieldId } from './util';
@ -22,9 +23,9 @@ import useItemRef from './hooks/useItemRef';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
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 ChildrenType = RenderChildren | React.ReactNode;
type ChildrenType<Values = any> = RenderChildren<Values> | React.ReactNode;
interface MemoInputProps {
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;
noStyle?: boolean;
style?: React.CSSProperties;
className?: string;
children?: ChildrenType;
children?: ChildrenType<Values>;
id?: string;
hasFeedback?: boolean;
validateStatus?: ValidateStatus;
required?: boolean;
hidden?: boolean;
initialValue?: any;
messageVariables?: Record<string, string>;
tooltip?: LabelTooltipType;
/** Auto passed by List render props. User should not use this. */
fieldKey?: React.Key | React.Key[];
}
@ -63,7 +68,7 @@ function hasValidName(name?: NamePath): Boolean {
return !(name === undefined || name === null);
}
function FormItem(props: FormItemProps): React.ReactElement {
function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElement {
const {
name,
fieldKey,
@ -80,20 +85,20 @@ function FormItem(props: FormItemProps): React.ReactElement {
children,
required,
label,
messageVariables,
trigger = 'onChange',
validateTrigger,
hidden,
...restProps
} = props;
const destroyRef = React.useRef(false);
const { getPrefixCls } = React.useContext(ConfigContext);
const { name: formName, requiredMark } = React.useContext(FormContext);
const { updateItemErrors } = React.useContext(FormItemContext);
const destroyRef = useRef(false);
const { getPrefixCls } = useContext(ConfigContext);
const { name: formName, requiredMark } = useContext(FormContext);
const { updateItemErrors } = useContext(FormItemContext);
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
const prevValidateStatusRef = React.useRef<ValidateStatus | undefined>(validateStatus);
const [inlineErrors, setInlineErrors] = useFrameState<Record<string, string[]>>({});
const { validateTrigger: contextValidateTrigger } = React.useContext(FieldContext);
const { validateTrigger: contextValidateTrigger } = useContext(FieldContext);
const mergedValidateTrigger =
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger;
@ -106,7 +111,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
const hasName = hasValidName(name);
// Cache Field NamePath
const nameRef = React.useRef<(string | number)[]>([]);
const nameRef = useRef<(string | number)[]>([]);
// Should clean up if Field removed
React.useEffect(() => {
@ -175,10 +180,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
mergedValidateStatus = 'success';
}
if (domErrorVisible && help) {
prevValidateStatusRef.current = mergedValidateStatus;
}
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${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-warning`]: mergedValidateStatus === 'warning',
[`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error',
[`${prefixCls}-item-has-error-leave`]:
!help && domErrorVisible && prevValidateStatusRef.current === 'error',
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
[`${prefixCls}-item-hidden`]: hidden,
};
@ -218,6 +217,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
'normalize',
'preserve',
'required',
'tooltip',
'validateFirst',
'validateStatus',
'valuePropName',
@ -238,6 +238,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
{...meta}
errors={mergedErrors}
prefixCls={prefixCls}
status={mergedValidateStatus}
onDomErrorVisibleChange={setDomErrorVisible}
validateStatus={mergedValidateStatus}
>
@ -252,17 +253,20 @@ function FormItem(props: FormItemProps): React.ReactElement {
const isRenderProps = typeof children === 'function';
// Record for real component render
const updateRef = React.useRef(0);
const updateRef = useRef(0);
updateRef.current += 1;
if (!hasName && !isRenderProps && !dependencies) {
return renderLayout(children) as JSX.Element;
}
const variables: Record<string, string> = {};
let variables: Record<string, string> = {};
if (typeof label === 'string') {
variables.label = label;
}
if (messageVariables) {
variables = { ...variables, ...messageVariables };
}
return (
<Field

View File

@ -4,14 +4,11 @@ import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
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 { ValidateStatus } from './FormItem';
import { FormContext } from './context';
import useCacheErrors from './hooks/useCacheErrors';
import useForceUpdate from '../_util/hooks/useForceUpdate';
import { FormContext, FormItemPrefixContext } from './context';
import ErrorList from './ErrorList';
interface FormItemInputMiscProps {
prefixCls: string;
@ -26,6 +23,7 @@ export interface FormItemInputProps {
wrapperCol?: ColProps;
help?: React.ReactNode;
extra?: React.ReactNode;
status?: ValidateStatus;
}
const iconMap: { [key: string]: any } = {
@ -37,6 +35,7 @@ const iconMap: { [key: string]: any } = {
const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
prefixCls,
status,
wrapperCol,
children,
help,
@ -46,8 +45,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
validateStatus,
extra,
}) => {
const forceUpdate = useForceUpdate();
const baseClassName = `${prefixCls}-item`;
const formContext = React.useContext(FormContext);
@ -56,24 +53,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
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(
() => () => {
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`
const IconNode = validateStatus && iconMap[validateStatus];
const icon =
@ -108,29 +81,13 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
<div className={`${baseClassName}-control-input-content`}>{children}</div>
{icon}
</div>
<CSSMotion
motionDeadline={500}
visible={visible}
motionName="show-help"
onLeaveEnd={() => {
onDomErrorVisibleChange(false);
}}
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>
<FormItemPrefixContext.Provider value={{ prefixCls, status }}>
<ErrorList
errors={errors}
help={help}
onDomErrorVisibleChange={onDomErrorVisibleChange}
/>
</FormItemPrefixContext.Provider>
{extra && <div className={`${baseClassName}-extra`}>{extra}</div>}
</Col>
</FormContext.Provider>

View File

@ -1,11 +1,33 @@
import * as React from 'react';
import classNames from 'classnames';
import QuestionCircleOutlined from '@ant-design/icons/QuestionCircleOutlined';
import Col, { ColProps } from '../grid/col';
import { FormLabelAlign } from './interface';
import { FormContext, FormContextProps } from './context';
import { RequiredMark } from './Form';
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
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 (typeof tooltip === 'object' && !React.isValidElement(tooltip)) {
return tooltip as WrapperTooltipProps;
}
return {
title: tooltip,
};
}
export interface FormItemLabelProps {
colon?: boolean;
@ -14,6 +36,7 @@ export interface FormItemLabelProps {
labelAlign?: FormLabelAlign;
labelCol?: ColProps;
requiredMark?: RequiredMark;
tooltip?: LabelTooltipType;
}
const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixCls: string }> = ({
@ -25,6 +48,7 @@ const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixC
colon,
required,
requiredMark,
tooltip,
}) => {
const [formLocale] = useLocaleReceiver('Form');
@ -58,6 +82,24 @@ const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixC
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
if (requiredMark === 'optional' && !required) {
labelChildren = (

View File

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

View File

@ -1078,7 +1078,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -1120,7 +1120,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -1193,7 +1193,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -1235,7 +1235,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -1334,7 +1334,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -1389,7 +1389,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -1480,7 +1480,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -1531,7 +1531,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -2788,10 +2788,14 @@ exports[`renders ./components/form/demo/nest-messages.md correctly 1`] = `
<div
class="ant-form-item-control-input-content"
>
<textarea
class="ant-input"
id="nest-messages_user_introduction"
/>
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
id="nest-messages_user_introduction"
/>
</div>
</div>
</div>
</div>
@ -3800,6 +3804,29 @@ exports[`renders ./components/form/demo/required-mark.md correctly 1`] = `
title="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>
</div>
<div
@ -3832,6 +3859,29 @@ exports[`renders ./components/form/demo/required-mark.md correctly 1`] = `
title="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
class="ant-form-item-optional"
>
@ -6395,7 +6445,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -6522,7 +6572,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-validating"
>
<div
role="alert"
@ -6702,7 +6752,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"
@ -7090,7 +7140,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-validating"
>
<div
role="alert"
@ -7179,7 +7229,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-error"
>
<div
role="alert"

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Form.List should render empty without errors 1`] = `null`;

View File

@ -560,6 +560,23 @@ describe('Form', () => {
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 () => {
// https://github.com/ant-design/ant-design/issues/25711
const wrapper = mount(
@ -754,4 +771,32 @@ describe('Form', () => {
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,81 @@ describe('Form.List', () => {
await sleep();
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();
});
});
it('should render empty without errors', () => {
const wrapper = mount(<Form.ErrorList />);
expect(wrapper.render()).toMatchSnapshot();
});
});

View File

@ -67,4 +67,20 @@ describe('Form.typescript', () => {
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 { FormLabelAlign } from './interface';
import { RequiredMark } from './Form';
import { ValidateStatus } from './FormItem';
/**
* Form Context
@ -49,3 +50,15 @@ export const FormProvider: React.FC<FormProviderProps> = props => {
const providerProps = omit(props, ['prefixCls']);
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 (
<Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}>
<Form.List name="names">
{(fields, { add, remove }) => {
<Form.List
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 (
<div>
{fields.map((field, index) => (
@ -96,6 +107,8 @@ const DynamicFieldSet = () => {
>
<PlusOutlined /> Add field at head
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</div>
);

View File

@ -16,6 +16,7 @@ Switch required or optional style with `requiredMark`.
```tsx
import React, { useState } from 'react';
import { Form, Input, Button, Radio } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
const FormLayoutDemo = () => {
const [form] = Form.useForm();
@ -40,10 +41,13 @@ const FormLayoutDemo = () => {
<Radio.Button value={false}>Hidden</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Field A" required>
<Form.Item label="Field A" required tooltip="This is a required field">
<Input placeholder="input placeholder" />
</Form.Item>
<Form.Item label="Field B">
<Form.Item
label="Field B"
tooltip={{ title: 'Tooltip with customize icon', icon: <InfoCircleOutlined /> }}
>
<Input placeholder="input placeholder" />
</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 | |
| label | Label text | ReactNode | - | |
| 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) | - | |
| 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 | |
| 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 | |
<<<<<<< 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) | - | |
> > > > > > > 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 | |
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.
### 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
Provides array management for fields.
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| name | Field name, support array | [NamePath](#NamePath) | - |
| children | Render function | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| name | Field name, support array | [NamePath](#NamePath) | - | |
| 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
<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 |
| 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
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`.
### 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?
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 InternalForm, { useForm, FormInstance, FormProps } from './Form';
import Item, { FormItemProps } from './FormItem';
import ErrorList, { ErrorListProps } from './ErrorList';
import List, { FormListProps } from './FormList';
import { FormProvider } from './context';
import devWarning from '../_util/devWarning';
@ -11,6 +12,7 @@ interface FormInterface extends InternalFormType {
useForm: typeof useForm;
Item: typeof Item;
List: typeof List;
ErrorList: typeof ErrorList;
Provider: typeof FormProvider;
/** @deprecated Only for warning usage. Do not use. */
@ -21,6 +23,7 @@ const Form = InternalForm as FormInterface;
Form.Item = Item;
Form.List = List;
Form.ErrorList = ErrorList;
Form.useForm = useForm;
Form.Provider = FormProvider;
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;

View File

@ -85,20 +85,10 @@ const validateMessages = {
| noStyle | 为 `true` 时不带样式,作为纯字段控件使用 | boolean | false | |
| label | `label` 标签的文本 | ReactNode | - | |
| 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) | - | |
| 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 | |
| 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 | |
<<<<<<< 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) | - | |
> > > > > > > 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 | |
被设置了 `name` 属性的 `Form.Item` 包装的控件,表单控件会自动添加 `value`(或 `valuePropName` 指定的其他属性) `onChange`(或 `trigger` 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:
@ -146,14 +136,30 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
你可以参考[示例](#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
为字段提供数组化管理。
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| name | 字段名,支持数组 | [NamePath](#NamePath) | - |
| children | 渲染函数 | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| name | 字段名,支持数组 | [NamePath](#NamePath) | - | |
| children | 渲染函数 | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - | |
| rules | 校验规则,仅支持自定义规则。需要配合 [ErrorList](#Form.ErrorList) 一同使用。 | { validator, message }[] | - | 4.7.0 |
```tsx
<Form.List>
@ -180,6 +186,14 @@ Form.List 渲染表单相关操作函数。
| remove | 删除表单项 | (index: number \| number[]) => void | number[]: 4.5.0 |
| move | 移动表单项 | (from: number, to: number) => void | - |
## Form.ErrorList
4.7.0 新增。错误展示组件,仅限配合 Form.List 的 rules 一同使用。
| 参数 | 说明 | 类型 | 默认值 |
| ------ | -------- | ----------- | ------ |
| errors | 错误列表 | ReactNode[] | - |
## Form.Provider
提供表单间联动功能,其下设置 `name` 的 Form 更新时,会自动触发对应事件。查看[示例](#components-form-demo-form-context)。
@ -365,6 +379,10 @@ validator(rule, value, callback) => {
在触发过程中,调用 `isFieldValidating` 会经历 `false` > `true` > `false` 的变化过程。
### 为什么 Form.List 不支持 `label` 还需要使用 ErrorList 展示错误?
Form.List 本身是 renderProps内部样式非常自由。因而默认配置 `label``error` 节点很难与之配合。如果你需要 antd 样式的 `label`,可以通过外部包裹 Form.Item 来实现。
### 为什么 Form.Item 的 `dependencies` 对 Form.List 下的字段没有效果?
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 {
& when (@form-item-trailing-colon=true) {
content: ':';

View File

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

View File

@ -1,7 +1,6 @@
@import '../../input/style/mixin';
.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 {
color: @text-color;
}

View File

@ -6,6 +6,18 @@
// ================================================================
/* 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 {
// ========================= 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 =======================
&-is-validating {
&.@{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 |
| height | Image height | string \| number | - | 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 |
| 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)

View File

@ -16,14 +16,23 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/D1dXz9PZqa/image.svg
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| ----------- | ---------------------------------- | ---------------- | ------ | ----- |
| alt | 图像描述 | string | - | 4.6.0 |
| fallback | 加载失败容错地址 | string | - | 4.6.0 |
| height | 图像高度 | string \| number | - | 4.6.0 |
| placeholder | 加载占位, 为 `true` 时使用默认占位 | ReactNode | - | 4.6.0 |
| preview | 是否开启预览 | boolean | true | 4.6.0 |
| src | 图片地址 | string | - | 4.6.0 |
| width | 图像宽度 | string \| number | - | 4.6.0 |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| alt | 图像描述 | string | - | 4.6.0 |
| fallback | 加载失败容错地址 | string | - | 4.6.0 |
| height | 图像高度 | string \| number | - | 4.6.0 |
| placeholder | 加载占位, 为 `true` 时使用默认占位 | ReactNode | - | 4.6.0 |
| preview | 预览参数,为 `false` 时禁用 | boolean \| \| [previewType](#previewType) | true | 4.6.0 [previewType](#previewType):4.7.0 |
| src | 图片地址 | string | - | 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)

View File

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

View File

@ -260,11 +260,15 @@ Array [
rows="1"
/>
</div>,
<textarea
class="ant-input"
rows="1"
style="width:100px"
/>,
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
rows="1"
style="width:100px"
/>
</div>,
<button
class="ant-btn ant-btn-primary"
type="button"
@ -1004,58 +1008,74 @@ Array [
</span>,
<br />,
<br />,
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
placeholder="textarea with clear icon"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
placeholder="textarea with clear icon"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>,
</div>,
]
`;
exports[`renders ./components/input/demo/autosize-textarea.md correctly 1`] = `
Array [
<textarea
class="ant-input"
placeholder="Autosize height based on content lines"
/>,
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
placeholder="Autosize height based on content lines"
/>
</div>,
<div
style="margin:24px 0"
/>,
<textarea
class="ant-input"
placeholder="Autosize height with minimum and maximum number of lines"
/>,
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
placeholder="Autosize height with minimum and maximum number of lines"
/>
</div>,
<div
style="margin:24px 0"
/>,
<textarea
class="ant-input"
placeholder="Controlled autosize"
/>,
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
placeholder="Controlled autosize"
/>
</div>,
]
`;
@ -1093,39 +1113,47 @@ exports[`renders ./components/input/demo/borderless-debug.md correctly 1`] = `
type="text"
value=""
/>
<textarea
class="ant-input ant-input-borderless"
placeholder="Unbordered"
/>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn ant-input-affix-wrapper-borderless"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input ant-input-borderless"
placeholder="Unbordered"
/>
</div>
<div
class="ant-input-textarea"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn ant-input-affix-wrapper-borderless"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input ant-input-borderless"
placeholder="Unbordered"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>
</div>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-borderless"
>
@ -2651,10 +2679,14 @@ Array [
`;
exports[`renders ./components/input/demo/textarea.md correctly 1`] = `
<textarea
class="ant-input"
rows="4"
/>
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
rows="4"
/>
</div>
`;
exports[`renders ./components/input/demo/textarea-resize.md correctly 1`] = `
@ -2668,13 +2700,29 @@ Array [
Auto Resize: false
</span>
</button>,
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
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.
</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"
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.
</textarea>,
]
maxlength="100"
/>
</div>
`;
exports[`renders ./components/input/demo/tooltip.md correctly 1`] = `

View File

@ -1,265 +1,305 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TextArea allowClear should change type when click 1`] = `
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
>
111
</textarea>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
>
<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>
111
</textarea>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
</span>
</div>
`;
exports[`TextArea allowClear should change type when click 2`] = `
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>
</div>
`;
exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 1`] = `
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>
</div>
`;
exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 2`] = `
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>
</div>
`;
exports[`TextArea allowClear should not show icon if defaultValue is undefined, null or empty string 3`] = `
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>
</div>
`;
exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 1`] = `
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>
</div>
`;
exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 2`] = `
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>
</div>
`;
exports[`TextArea allowClear should not show icon if value is undefined, null or empty string 3`] = `
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon-hidden ant-input-textarea-clear-icon"
role="button"
tabindex="-1"
>
<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>
<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>
</span>
</div>
`;
exports[`TextArea should support disabled 1`] = `
<textarea
class="ant-input ant-input-disabled"
disabled=""
/>
<div
class="ant-input-textarea"
>
<textarea
class="ant-input ant-input-disabled"
disabled=""
/>
</div>
`;
exports[`TextArea should support maxLength 1`] = `
<textarea
class="ant-input"
maxlength="10"
/>
<div
class="ant-input-textarea"
>
<textarea
class="ant-input"
maxlength="10"
/>
</div>
`;

View File

@ -130,7 +130,14 @@ describe('TextArea', () => {
const textarea = mount(<TextArea value="111" />);
input.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 | |
| onResize | The callback function that is triggered when resize | function({ width, height }) | - | |
| 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).

View File

@ -50,6 +50,8 @@ Input 的其他属性和 React 自带的 [input](https://facebook.github.io/reac
| allowClear | 可以点击清除图标删除内容 | boolean | false | |
| onResize | resize 回调 | function({ width, height }) | - | |
| 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) 一致。

View File

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

View File

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

View File

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

View File

@ -283,6 +283,17 @@ exports[`renders ./components/modal/demo/manual.md correctly 1`] = `
</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`] = `
Array [
<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 { destroyFns } from '../Modal';
import { sleep } from '../../../tests/utils';
const { confirm } = Modal;
jest.mock('rc-motion');
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(() => {});
/* 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(() => {
errorSpy.mockReset();
document.body.innerHTML = '';
@ -68,20 +101,39 @@ describe('Modal.confirm triggers callbacks correctly', () => {
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();
// Third Modal
$$('.ant-btn')[0].click();
expect(errorSpy).not.toHaveBeenCalled();
});
it('should allow Modal.comfirm without onOk been set', () => {
it('should allow Modal.confirm without onOk been set', () => {
open();
// Fourth Modal
$$('.ant-btn-primary')[0].click();
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', () => {
open({
onOk: () => Promise.resolve(''),
@ -90,16 +142,19 @@ describe('Modal.confirm triggers callbacks correctly', () => {
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');
open({
onOk: () => Promise.reject(error),
onOk: () => {
return Promise.reject(error);
},
});
$$('.ant-btn-primary')[0].click();
// wait promise
return Promise.resolve().then(() => {
expect(errorSpy).toHaveBeenCalledWith(error);
});
await sleep();
expect(errorSpy).toHaveBeenCalledWith(error);
});
it('shows animation when close', () => {
@ -107,6 +162,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
jest.useFakeTimers();
expect($$('.ant-modal-confirm')).toHaveLength(1);
$$('.ant-btn')[0].click();
jest.runAllTimers();
expect($$('.ant-modal-confirm')).toHaveLength(0);
jest.useRealTimers();
@ -158,25 +214,6 @@ describe('Modal.confirm triggers callbacks correctly', () => {
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', () => {
jest.useFakeTimers();
['info', 'success', 'warning', 'error'].forEach(type => {
@ -268,23 +305,35 @@ describe('Modal.confirm triggers callbacks correctly', () => {
it('destroyFns should reduce when instance.destroy', () => {
jest.useFakeTimers();
Modal.destroyAll(); // clear destroyFns
jest.runAllTimers();
const instances = [];
['info', 'success', 'warning', 'error'].forEach(type => {
const instance = Modal[type]({
title: 'title',
content: 'content',
});
// Render modal
act(() => {
jest.runAllTimers();
});
instances.push(instance);
});
const { length } = instances;
instances.forEach((instance, index) => {
expect(destroyFns.length).toBe(length - index);
instance.destroy();
jest.runAllTimers();
act(() => {
instance.destroy();
jest.runAllTimers();
});
expect(destroyFns.length).toBe(length - index - 1);
});
jest.useRealTimers();
});

View File

@ -1,11 +1,20 @@
import React from 'react';
import CSSMotion from 'rc-motion';
import { genCSSMotion } from 'rc-motion/lib/CSSMotion';
import { mount } from 'enzyme';
import Modal from '..';
import Button from '../../button';
jest.mock('rc-util/lib/Portal');
jest.mock('rc-motion');
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', () => {
jest.useFakeTimers();
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 |
| 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 | {} |
| modalRender | Custom modal content render | (node: ReactNode) => ReactNode | - | 4.7.0 |
| okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - |
| okText | Text of the OK button | ReactNode | `OK` |
| 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 |
| maskClosable | 点击蒙层是否允许关闭 | boolean | true |
| maskStyle | 遮罩样式 | object | {} |
| modalRender | 自定义渲染对话框 | (node: ReactNode) => ReactNode | - | 4.7.0 |
| okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - |
| okText | 确认按钮文字 | ReactNode | `确定` |
| okType | 确认按钮类型 | string | `primary` |

View File

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

View File

@ -87,3 +87,41 @@ Array [
</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');
});
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 |
| direction | The space direction | `vertical` \| `horizontal` | `horizontal` | 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 classNames from 'classnames';
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 Item from './Item';
@ -15,12 +15,11 @@ export interface SpaceProps {
direction?: 'horizontal' | 'vertical';
// No `stretch` since many components do not support that.
align?: 'start' | 'end' | 'center' | 'baseline';
split?: React.ReactNode;
}
const Space: React.FC<SpaceProps> = props => {
const { getPrefixCls, space, direction: directionConfig }: ConfigConsumerProps = React.useContext(
ConfigContext,
);
const { getPrefixCls, space, direction: directionConfig } = React.useContext(ConfigContext);
const {
size = space?.size || 'small',
@ -29,6 +28,7 @@ const Space: React.FC<SpaceProps> = props => {
children,
direction = 'horizontal',
prefixCls: customizePrefixCls,
split,
...otherProps
} = props;
@ -70,6 +70,7 @@ const Space: React.FC<SpaceProps> = props => {
size={size}
index={i}
marginDirection={marginDirection}
split={split}
>
{child}
</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 |
| direction | 间距方向 | `vertical` \| `horizontal` | `horizontal` | 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 | |
| sortDirections | Supported sort way, could be `ascend`, `descend` | Array | \[`ascend`, `descend`] | |
| 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

View File

@ -91,7 +91,7 @@ const columns = [
| getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | () => TableHtmlElement | |
| sortDirections | 支持的排序方式,取值为 `ascend` `descend` | Array | \[`ascend`, `descend`] | |
| 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 用法

View File

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

View File

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

View File

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

View File

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

View File

@ -690,6 +690,480 @@ exports[`renders ./components/upload/demo/drag.md correctly 1`] = `
</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`] = `
<span
class="ant-upload-picture-card-wrapper"

View File

@ -106,6 +106,31 @@ exports[`Upload List handle error 1`] = `
</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`] = `
<span
class=""
@ -1288,6 +1313,161 @@ exports[`Upload List should support removeIcon and downloadIcon 1`] = `
</span>
`;
exports[`Upload List should support removeIcon and downloadIcon 2`] = `
<span
class=""
>
<div
class="ant-upload ant-upload-select ant-upload-select-picture"
>
<span
class="ant-upload"
role="button"
tabindex="0"
>
<input
accept=""
style="display: none;"
type="file"
/>
<button
type="button"
>
upload
</button>
</span>
</div>
<div
class="ant-upload-list ant-upload-list-picture"
>
<div
class=""
>
<span>
<div
class="ant-upload-list-item ant-upload-list-item-uploading ant-upload-list-item-list-type-picture"
>
<div
class="ant-upload-list-item-info"
>
<span>
<div
class="ant-upload-list-item-thumbnail"
>
<span
aria-label="loading"
class="anticon anticon-loading"
role="img"
>
<svg
aria-hidden="true"
class="anticon-spin"
data-icon="loading"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
/>
</svg>
</span>
</div>
<a
class="ant-upload-list-item-name ant-upload-list-item-name-icon-count-1"
href="https://cdn.xxx.com/aaa"
rel="noopener noreferrer"
target="_blank"
title="image"
>
image
</a>
<span
class="ant-upload-list-item-card-actions picture"
>
<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"
>
<i>
RM
</i>
</button>
</span>
</span>
</div>
<div
class="ant-upload-list-item-progress"
/>
</div>
</span>
</div>
<div
class=""
>
<span>
<div
class="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture"
>
<div
class="ant-upload-list-item-info"
>
<span>
<a
class="ant-upload-list-item-thumbnail"
href="https://cdn.xxx.com/aaa"
rel="noopener noreferrer"
target="_blank"
>
<img
alt="image"
class="ant-upload-list-item-image"
src="https://cdn.xxx.com/aaa"
/>
</a>
<a
class="ant-upload-list-item-name ant-upload-list-item-name-icon-count-2"
href="https://cdn.xxx.com/aaa"
rel="noopener noreferrer"
target="_blank"
title="image"
>
image
</a>
<span
class="ant-upload-list-item-card-actions picture"
>
<button
class="ant-btn ant-btn-text ant-btn-sm ant-btn-icon-only ant-upload-list-item-card-actions-btn"
title="Download file"
type="button"
>
<i>
DL
</i>
</button>
<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"
>
<i>
RM
</i>
</button>
</span>
</span>
</div>
</div>
</span>
</div>
</div>
</span>
`;
exports[`Upload List should support showRemoveIcon and showPreviewIcon 1`] = `
<span
class=""

View File

@ -510,6 +510,7 @@ describe('Upload List', () => {
await sleep();
expect(handleChange.mock.calls.length).toBe(2);
});
it('should support removeIcon and downloadIcon', () => {
const list = [
{
@ -533,7 +534,7 @@ describe('Upload List', () => {
showUploadList={{
showRemoveIcon: true,
showDownloadIcon: true,
removeIcon: <i>RM</i>,
removeIcon: () => <i>RM</i>,
downloadIcon: <i>DL</i>,
}}
>
@ -541,6 +542,22 @@ describe('Upload List', () => {
</Upload>,
);
expect(wrapper.render()).toMatchSnapshot();
const wrapper2 = mount(
<Upload
listType="picture"
defaultFileList={list}
showUploadList={{
showRemoveIcon: true,
showDownloadIcon: true,
removeIcon: <i>RM</i>,
downloadIcon: () => <i>DL</i>,
}}
>
<button type="button">upload</button>
</Upload>,
);
expect(wrapper2.render()).toMatchSnapshot();
});
// https://github.com/ant-design/ant-design/issues/7762
@ -993,4 +1010,20 @@ describe('Upload List', () => {
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` | |
| 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) | |
| 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 | |
| openFileDialogOnClick | Click open file dialog | boolean | true | |
| 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 | - | |
| 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> | - | |
| 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 |
| itemRender | Custom item of uploadList | (originNode: ReactElement, file: UploadFile, fileList?: object\[]) => React.ReactNode | - | 4.7.0 |
### onChange

View File

@ -36,7 +36,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg
| name | 发到后台的文件参数名 | string | `file` | |
| 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) | |
| 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 | |
| openFileDialogOnClick | 点击打开文件对话框 | boolean | true | |
| 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 | -   | |
| onDownload | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页 | function(file): void | (跳转新标签页) | |
| 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 |
| itemRender | 自定义上传列表项 | (originNode: ReactElement, file: UploadFile, fileList?: object\[]) => React.ReactNode | - | 4.7.0 |
### onChange

View File

@ -56,8 +56,8 @@ export interface ShowUploadListInterface {
showRemoveIcon?: boolean;
showPreviewIcon?: boolean;
showDownloadIcon?: boolean;
removeIcon?: React.ReactNode;
downloadIcon?: React.ReactNode;
removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
}
export interface UploadLocale {
@ -111,6 +111,11 @@ export interface UploadProps<T = any> {
iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode;
isImageUrl?: (file: UploadFile) => boolean;
progress?: UploadListProgressProps;
itemRender?: (
originNode: React.ReactElement,
file: UploadFile,
fileList?: Array<UploadFile<T>>,
) => React.ReactNode;
}
export interface UploadState<T = any> {
@ -129,11 +134,16 @@ export interface UploadListProps<T = any> {
showRemoveIcon?: boolean;
showDownloadIcon?: boolean;
showPreviewIcon?: boolean;
removeIcon?: React.ReactNode;
downloadIcon?: React.ReactNode;
removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
locale: UploadLocale;
previewFile?: PreviewFileHandler;
iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode;
isImageUrl?: (file: UploadFile) => boolean;
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)
- 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
`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!
## 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
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-checkbox": "~2.3.0",
"rc-collapse": "~2.0.0",
"rc-dialog": "~8.2.1",
"rc-dialog": "~8.4.0",
"rc-drawer": "~4.1.0",
"rc-dropdown": "~3.2.0",
"rc-field-form": "~1.10.0",
"rc-image": "~3.0.6",
"rc-field-form": "~1.12.0",
"rc-image": "~3.2.1",
"rc-input-number": "~6.0.0",
"rc-mentions": "~1.5.0",
"rc-menu": "~8.7.1",
@ -140,7 +140,7 @@
"rc-slider": "~9.5.2",
"rc-steps": "~4.1.0",
"rc-switch": "~3.2.0",
"rc-table": "~7.9.2",
"rc-table": "~7.10.0",
"rc-tabs": "~11.6.0",
"rc-textarea": "~0.3.0",
"rc-tooltip": "~5.0.0",
@ -249,6 +249,7 @@
"react-dnd": "^11.1.1",
"react-dnd-html5-backend": "^11.1.1",
"react-dom": "^16.9.0",
"react-draggable": "^4.4.3",
"react-github-button": "^0.1.11",
"react-helmet-async": "^1.0.4",
"react-highlight-words": "^0.16.0",