mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-28 21:19:37 +08:00
Merge pull request #27064 from ant-design/feature
chore: merge feature into master
This commit is contained in:
commit
0fec3aa602
@ -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'];
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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>);
|
||||
|
@ -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"
|
||||
|
@ -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`] = `
|
||||
|
@ -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}
|
||||
>
|
||||
|
27
components/avatar/demo/responsive.md
Normal file
27
components/avatar/demo/responsive.md
Normal 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,
|
||||
);
|
||||
```
|
@ -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 | - | |
|
||||
|
@ -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 | - | |
|
||||
|
@ -2,4 +2,5 @@ import '../../style/index.less';
|
||||
import './index.less';
|
||||
|
||||
// style dependencies
|
||||
// deps-lint-skip: grid
|
||||
import '../../popover/style';
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
`;
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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() | |
|
||||
|
@ -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() | |
|
||||
|
@ -6,7 +6,14 @@ import { PickerLocale } from '../generatePicker';
|
||||
const locale: PickerLocale = {
|
||||
lang: {
|
||||
placeholder: 'เลือกวันที่',
|
||||
yearPlaceholder: 'เลือกปี',
|
||||
quarterPlaceholder: 'เลือกไตรมาส',
|
||||
monthPlaceholder: 'เลือกเดือน',
|
||||
weekPlaceholder: 'เลือกสัปดาห์',
|
||||
rangePlaceholder: ['วันเริ่มต้น', 'วันสิ้นสุด'],
|
||||
rangeYearPlaceholder: ['ปีเริ่มต้น', 'ปีสิ้นสุด'],
|
||||
rangeMonthPlaceholder: ['เดือนเริ่มต้น', 'เดือนสิ้นสุด'],
|
||||
rangeWeekPlaceholder: ['สัปดาห์เริ่มต้น', 'สัปดาห์สิ้นสุด'],
|
||||
...CalendarLocale,
|
||||
},
|
||||
timePickerLocale: {
|
||||
|
95
components/form/ErrorList.tsx
Normal file
95
components/form/ErrorList.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 = (
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Form.List should render empty without errors 1`] = `null`;
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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: '',
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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`:
|
||||
|
@ -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;
|
||||
|
@ -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`,比如:
|
||||
|
@ -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: ':';
|
||||
|
@ -3,3 +3,4 @@ import './index.less';
|
||||
|
||||
// style dependencies
|
||||
import '../../grid/style';
|
||||
import '../../tooltip/style';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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`] = `
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
22
components/input/demo/textarea-show-count.md
Normal file
22
components/input/demo/textarea-show-count.md
Normal 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);
|
||||
```
|
@ -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).
|
||||
|
||||
|
@ -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) 一致。
|
||||
|
||||
|
@ -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';
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
|
@ -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;"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
|
98
components/modal/demo/modal-render.md
Normal file
98
components/modal/demo/modal-render.md
Normal 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'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);
|
||||
```
|
@ -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` |
|
||||
|
@ -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` |
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
30
components/space/demo/split.md
Normal file
30
components/space/demo/split.md
Normal 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);
|
||||
```
|
@ -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 |
|
||||
|
@ -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>
|
||||
|
@ -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 |
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 用法
|
||||
|
||||
|
@ -607,7 +607,7 @@
|
||||
z-index: @table-sticky-zindex;
|
||||
}
|
||||
&-scroll {
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: @table-sticky-zindex;
|
||||
display: flex;
|
||||
|
@ -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 /> });
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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"
|
||||
|
@ -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=""
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
164
components/upload/demo/drag-sorting.md
Normal file
164
components/upload/demo/drag-sorting.md
Normal 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;
|
||||
}
|
||||
```
|
@ -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<dataURL: string> | - | |
|
||||
| isImageUrl | Customize if render <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<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
|
||||
|
||||
|
@ -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 | 自定义缩略图是否使用 <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<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
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user