chore: sync feature into master

This commit is contained in:
afc163 2021-07-12 19:28:42 +08:00
commit c3eda10999
95 changed files with 22416 additions and 16226 deletions

View File

@ -5,10 +5,16 @@ import { LegacyButtonType, ButtonProps, convertLegacyProps } from '../button/but
export interface ActionButtonProps { export interface ActionButtonProps {
type?: LegacyButtonType; type?: LegacyButtonType;
actionFn?: (...args: any[]) => any | PromiseLike<any>; actionFn?: (...args: any[]) => any | PromiseLike<any>;
closeModal: Function; close: Function;
autoFocus?: boolean; autoFocus?: boolean;
prefixCls: string; prefixCls: string;
buttonProps?: ButtonProps; buttonProps?: ButtonProps;
emitEvent?: boolean;
quitOnNullishReturnValue?: boolean;
}
function isThenable(thing?: PromiseLike<any>): boolean {
return !!(thing && !!thing.then);
} }
const ActionButton: React.FC<ActionButtonProps> = props => { const ActionButton: React.FC<ActionButtonProps> = props => {
@ -30,16 +36,16 @@ const ActionButton: React.FC<ActionButtonProps> = props => {
}, []); }, []);
const handlePromiseOnOk = (returnValueOfOnOk?: PromiseLike<any>) => { const handlePromiseOnOk = (returnValueOfOnOk?: PromiseLike<any>) => {
const { closeModal } = props; const { close } = props;
if (!returnValueOfOnOk || !returnValueOfOnOk.then) { if (!isThenable(returnValueOfOnOk)) {
return; return;
} }
setLoading(true); setLoading(true);
returnValueOfOnOk.then( returnValueOfOnOk!.then(
(...args: any[]) => { (...args: any[]) => {
// It's unnecessary to set loading=false, for the Modal will be unmounted after close. setLoading(false);
// setState({ loading: false }); close(...args);
closeModal(...args); clickedRef.current = false;
}, },
(e: Error) => { (e: Error) => {
// Emit error when catch promise reject // Emit error when catch promise reject
@ -52,25 +58,32 @@ const ActionButton: React.FC<ActionButtonProps> = props => {
); );
}; };
const onClick = () => { const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const { actionFn, closeModal } = props; const { actionFn, close } = props;
if (clickedRef.current) { if (clickedRef.current) {
return; return;
} }
clickedRef.current = true; clickedRef.current = true;
if (!actionFn) { if (!actionFn) {
closeModal(); close();
return; return;
} }
let returnValueOfOnOk; let returnValueOfOnOk;
if (actionFn.length) { if (props.emitEvent) {
returnValueOfOnOk = actionFn(closeModal); returnValueOfOnOk = actionFn(e);
if (props.quitOnNullishReturnValue && !isThenable(returnValueOfOnOk)) {
clickedRef.current = false;
close(e);
return;
}
} else if (actionFn.length) {
returnValueOfOnOk = actionFn(close);
// https://github.com/ant-design/ant-design/issues/23358 // https://github.com/ant-design/ant-design/issues/23358
clickedRef.current = false; clickedRef.current = false;
} else { } else {
returnValueOfOnOk = actionFn(); returnValueOfOnOk = actionFn();
if (!returnValueOfOnOk) { if (!returnValueOfOnOk) {
closeModal(); close();
return; return;
} }
} }

View File

@ -3,7 +3,10 @@ import { MotionEvent } from 'rc-motion/lib/interface';
// ================== Collapse Motion ================== // ================== Collapse Motion ==================
const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 }); const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 });
const getRealHeight: MotionEventHandler = node => ({ height: node.scrollHeight, opacity: 1 }); const getRealHeight: MotionEventHandler = node => {
const { scrollHeight } = node;
return { height: scrollHeight, opacity: 1 };
};
const getCurrentHeight: MotionEventHandler = node => ({ height: node.offsetHeight }); const getCurrentHeight: MotionEventHandler = node => ({ height: node.offsetHeight });
const skipOpacityTransition: MotionEndEventHandler = (_, event: MotionEvent) => const skipOpacityTransition: MotionEndEventHandler = (_, event: MotionEvent) =>
event?.deadline === true || (event as TransitionEvent).propertyName === 'height'; event?.deadline === true || (event as TransitionEvent).propertyName === 'height';

View File

@ -193,4 +193,22 @@ describe('Avatar Render', () => {
wrapper.detach(); wrapper.detach();
global.document.body.removeChild(div); global.document.body.removeChild(div);
}); });
it('should exist crossorigin attribute', () => {
const LOAD_SUCCESS_SRC = 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png';
const wrapper = mount(
<Avatar src={LOAD_SUCCESS_SRC} crossOrigin="anonymous">
crossorigin
</Avatar>,
);
expect(wrapper.html().includes('crossorigin')).toEqual(true);
expect(wrapper.find('img').prop('crossOrigin')).toEqual('anonymous');
});
it('should not exist crossorigin attribute', () => {
const LOAD_SUCCESS_SRC = 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png';
const wrapper = mount(<Avatar src={LOAD_SUCCESS_SRC}>crossorigin</Avatar>);
expect(wrapper.html().includes('crossorigin')).toEqual(false);
expect(wrapper.find('img').prop('crossOrigin')).toEqual(undefined);
});
}); });

View File

@ -29,6 +29,7 @@ export interface AvatarProps {
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
alt?: string; alt?: string;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
/* callback when img load error */ /* callback when img load error */
/* return false to prevent Avatar show default fallback behavior, then you can do fallback by your self */ /* return false to prevent Avatar show default fallback behavior, then you can do fallback by your self */
onError?: () => boolean; onError?: () => boolean;
@ -95,6 +96,7 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
alt, alt,
draggable, draggable,
children, children,
crossOrigin,
...others ...others
} = props; } = props;
@ -158,7 +160,14 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
let childrenToRender; let childrenToRender;
if (typeof src === 'string' && isImgExist) { if (typeof src === 'string' && isImgExist) {
childrenToRender = ( childrenToRender = (
<img src={src} draggable={draggable} srcSet={srcSet} onError={handleImgLoadError} alt={alt} /> <img
src={src}
draggable={draggable}
srcSet={srcSet}
onError={handleImgLoadError}
alt={alt}
crossOrigin={crossOrigin}
/>
); );
} else if (hasImageElement) { } else if (hasImageElement) {
childrenToRender = src; childrenToRender = src;

View File

@ -21,6 +21,7 @@ Avatars can be used to represent people or objects. It supports images, `Icon`s,
| src | The address of the image for an image avatar or image element | string \| ReactNode | - | ReactNode: 4.8.0 | | src | The address of the image for an image avatar or image element | string \| ReactNode | - | ReactNode: 4.8.0 |
| srcSet | A list of sources to use for different screen resolutions | string | - | | | srcSet | A list of sources to use for different screen resolutions | string | - | |
| draggable | Whether the picture is allowed to be dragged | boolean \| `'true'` \| `'false'` | - | | | draggable | Whether the picture is allowed to be dragged | boolean \| `'true'` \| `'false'` | - | |
| crossOrigin | CORS settings attributes | `'anonymous'` \| `'use-credentials'` \| `''` | - | 4.17.0 |
| onError | Handler when img load error, return false to prevent default fallback behavior | () => boolean | - | | | onError | Handler when img load error, return false to prevent default fallback behavior | () => boolean | - | |
> Tip: You can set `icon` or `children` as the fallback for image load error, with the priority of `icon` > `children` > Tip: You can set `icon` or `children` as the fallback for image load error, with the priority of `icon` > `children`

View File

@ -26,6 +26,7 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/aBcnbw68hP/Avatar.svg
| src | 图片类头像的资源地址或者图片元素 | string \| ReactNode | - | ReactNode: 4.8.0 | | src | 图片类头像的资源地址或者图片元素 | string \| ReactNode | - | ReactNode: 4.8.0 |
| srcSet | 设置图片类头像响应式资源地址 | string | - | | | srcSet | 设置图片类头像响应式资源地址 | string | - | |
| draggable | 图片是否允许拖动 | boolean \| `'true'` \| `'false'` | - | | | draggable | 图片是否允许拖动 | boolean \| `'true'` \| `'false'` | - | |
| crossOrigin | CORS 属性设置 | `'anonymous'` \| `'use-credentials'` \| `''` | - | 4.17.0 |
| onError | 图片加载失败的事件,返回 false 会关闭组件默认的 fallback 行为 | () => boolean | - | | | onError | 图片加载失败的事件,返回 false 会关闭组件默认的 fallback 行为 | () => boolean | - | |
> Tip你可以设置 `icon``children` 作为图片加载失败的默认 fallback 行为,优先级为 `icon` > `children` > Tip你可以设置 `icon``children` 作为图片加载失败的默认 fallback 行为,优先级为 `icon` > `children`

View File

@ -0,0 +1,3 @@
import bnBD from '../../date-picker/locale/bn_BD';
export default bnBD;

View File

@ -209,6 +209,7 @@ exports[`renders ./components/cascader/demo/default-value.md correctly 1`] = `
> >
<span <span
class="ant-cascader-picker-label" class="ant-cascader-picker-label"
title="Zhejiang / Hangzhou / West Lake"
> >
Zhejiang / Hangzhou / West Lake Zhejiang / Hangzhou / West Lake
</span> </span>

View File

@ -1117,6 +1117,7 @@ exports[`Cascader support controlled mode 1`] = `
> >
<span <span
class="ant-cascader-picker-label" class="ant-cascader-picker-label"
title="Zhejiang / Hangzhou / West Lake"
> >
Zhejiang / Hangzhou / West Lake Zhejiang / Hangzhou / West Lake
</span> </span>

View File

@ -316,7 +316,7 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
}; };
getLabel() { getLabel() {
const { options, displayRender = defaultDisplayRender as Function } = this.props; const { options, displayRender = defaultDisplayRender } = this.props;
const names = getFilledFieldNames(this.props); const names = getFilledFieldNames(this.props);
const { value } = this.state; const { value } = this.state;
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value; const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
@ -618,9 +618,15 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
inputIcon = <DownOutlined className={arrowCls} />; inputIcon = <DownOutlined className={arrowCls} />;
} }
const label = this.getLabel();
const input: React.ReactElement = children || ( const input: React.ReactElement = children || (
<span style={style} className={pickerCls}> <span style={style} className={pickerCls}>
<span className={`${prefixCls}-picker-label`}>{this.getLabel()}</span> <span
className={`${prefixCls}-picker-label`}
title={typeof label === 'string' && label ? label : undefined}
>
{label}
</span>
<Input <Input
{...inputProps} {...inputProps}
tabIndex={-1} tabIndex={-1}

View File

@ -12199,7 +12199,7 @@ exports[`ConfigProvider components Drawer configProvider 1`] = `
/> />
<div <div
class="config-drawer-content-wrapper" class="config-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="config-drawer-content" class="config-drawer-content"
@ -12208,34 +12208,37 @@ exports[`ConfigProvider components Drawer configProvider 1`] = `
class="config-drawer-wrapper-body" class="config-drawer-wrapper-body"
> >
<div <div
class="config-drawer-header-no-title" class="config-drawer-header config-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="config-drawer-header-title"
class="config-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="config-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="config-drawer-body" class="config-drawer-body"
@ -12260,7 +12263,7 @@ exports[`ConfigProvider components Drawer configProvider componentSize large 1`]
/> />
<div <div
class="config-drawer-content-wrapper" class="config-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="config-drawer-content" class="config-drawer-content"
@ -12269,34 +12272,37 @@ exports[`ConfigProvider components Drawer configProvider componentSize large 1`]
class="config-drawer-wrapper-body" class="config-drawer-wrapper-body"
> >
<div <div
class="config-drawer-header-no-title" class="config-drawer-header config-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="config-drawer-header-title"
class="config-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="config-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="config-drawer-body" class="config-drawer-body"
@ -12321,7 +12327,7 @@ exports[`ConfigProvider components Drawer configProvider componentSize middle 1`
/> />
<div <div
class="config-drawer-content-wrapper" class="config-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="config-drawer-content" class="config-drawer-content"
@ -12330,34 +12336,37 @@ exports[`ConfigProvider components Drawer configProvider componentSize middle 1`
class="config-drawer-wrapper-body" class="config-drawer-wrapper-body"
> >
<div <div
class="config-drawer-header-no-title" class="config-drawer-header config-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="config-drawer-header-title"
class="config-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="config-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="config-drawer-body" class="config-drawer-body"
@ -12382,7 +12391,7 @@ exports[`ConfigProvider components Drawer configProvider virtual and dropdownMat
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -12391,34 +12400,37 @@ exports[`ConfigProvider components Drawer configProvider virtual and dropdownMat
class="ant-drawer-wrapper-body" class="ant-drawer-wrapper-body"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -12443,7 +12455,7 @@ exports[`ConfigProvider components Drawer normal 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -12452,34 +12464,37 @@ exports[`ConfigProvider components Drawer normal 1`] = `
class="ant-drawer-wrapper-body" class="ant-drawer-wrapper-body"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -12504,7 +12519,7 @@ exports[`ConfigProvider components Drawer prefixCls 1`] = `
/> />
<div <div
class="prefix-Drawer-content-wrapper" class="prefix-Drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="prefix-Drawer-content" class="prefix-Drawer-content"
@ -12513,34 +12528,37 @@ exports[`ConfigProvider components Drawer prefixCls 1`] = `
class="prefix-Drawer-wrapper-body" class="prefix-Drawer-wrapper-body"
> >
<div <div
class="prefix-Drawer-header-no-title" class="prefix-Drawer-header prefix-Drawer-header-close-only"
> >
<button <div
aria-label="Close" class="prefix-Drawer-header-title"
class="prefix-Drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="prefix-Drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="prefix-Drawer-body" class="prefix-Drawer-body"
@ -13260,9 +13278,10 @@ exports[`ConfigProvider components Form configProvider 1`] = `
</div> </div>
</div> </div>
<div <div
class="config-form-item-explain config-form-item-explain-error" class="config-form-item-explain config-form-item-explain-connected"
> >
<div <div
class="config-form-item-explain-error"
role="alert" role="alert"
> >
Bamboo is Light Bamboo is Light
@ -13297,9 +13316,10 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
</div> </div>
</div> </div>
<div <div
class="config-form-item-explain config-form-item-explain-error" class="config-form-item-explain config-form-item-explain-connected"
> >
<div <div
class="config-form-item-explain-error"
role="alert" role="alert"
> >
Bamboo is Light Bamboo is Light
@ -13334,9 +13354,10 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
</div> </div>
</div> </div>
<div <div
class="config-form-item-explain config-form-item-explain-error" class="config-form-item-explain config-form-item-explain-connected"
> >
<div <div
class="config-form-item-explain-error"
role="alert" role="alert"
> >
Bamboo is Light Bamboo is Light
@ -13371,9 +13392,10 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Bamboo is Light Bamboo is Light
@ -13408,9 +13430,10 @@ exports[`ConfigProvider components Form normal 1`] = `
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Bamboo is Light Bamboo is Light
@ -13445,9 +13468,10 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
</div> </div>
</div> </div>
<div <div
class="prefix-Form-item-explain prefix-Form-item-explain-error" class="prefix-Form-item-explain prefix-Form-item-explain-connected"
> >
<div <div
class="prefix-Form-item-explain-error"
role="alert" role="alert"
> >
Bamboo is Light Bamboo is Light

View File

@ -61,7 +61,8 @@ Setting `Modal`、`Message`、`Notification` rootPrefixCls.
```jsx ```jsx
ConfigProvider.config({ ConfigProvider.config({
prefixCls: 'ant', prefixCls: 'ant', // 4.13.0+
iconPrefixCls: 'anticon', // 4.17.0+
}); });
``` ```

View File

@ -84,11 +84,19 @@ interface ProviderChildrenProps extends ConfigProviderProps {
} }
export const defaultPrefixCls = 'ant'; export const defaultPrefixCls = 'ant';
export const defaultIconPrefixCls = 'anticon';
let globalPrefixCls: string; let globalPrefixCls: string;
let globalIconPrefixCls: string;
const setGlobalConfig = (params: Pick<ConfigProviderProps, 'prefixCls'>) => { const setGlobalConfig = ({
if (params.prefixCls !== undefined) { prefixCls,
globalPrefixCls = params.prefixCls; iconPrefixCls,
}: Pick<ConfigProviderProps, 'prefixCls' | 'iconPrefixCls'>) => {
if (prefixCls !== undefined) {
globalPrefixCls = prefixCls;
}
if (iconPrefixCls !== undefined) {
globalIconPrefixCls = iconPrefixCls;
} }
}; };
@ -96,11 +104,16 @@ function getGlobalPrefixCls() {
return globalPrefixCls || defaultPrefixCls; return globalPrefixCls || defaultPrefixCls;
} }
function getGlobalIconPrefixCls() {
return globalIconPrefixCls || defaultIconPrefixCls;
}
export const globalConfig = () => ({ export const globalConfig = () => ({
getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => { getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => {
if (customizePrefixCls) return customizePrefixCls; if (customizePrefixCls) return customizePrefixCls;
return suffixCls ? `${getGlobalPrefixCls()}-${suffixCls}` : getGlobalPrefixCls(); return suffixCls ? `${getGlobalPrefixCls()}-${suffixCls}` : getGlobalPrefixCls();
}, },
getIconPrefixCls: getGlobalIconPrefixCls,
getRootPrefixCls: (rootPrefixCls?: string, customizePrefixCls?: string) => { getRootPrefixCls: (rootPrefixCls?: string, customizePrefixCls?: string) => {
// Customize rootPrefixCls is first priority // Customize rootPrefixCls is first priority
if (rootPrefixCls) { if (rootPrefixCls) {
@ -187,9 +200,10 @@ const ProviderChildren: React.FC<ProviderChildrenProps> = props => {
}, },
); );
const memoIconContextValue = React.useMemo(() => ({ prefixCls: iconPrefixCls, csp }), [ const memoIconContextValue = React.useMemo(
iconPrefixCls, () => ({ prefixCls: iconPrefixCls, csp }),
]); [iconPrefixCls],
);
let childNode = children; let childNode = children;
// Additional Form provider // Additional Form provider

View File

@ -62,7 +62,8 @@ export default () => (
```jsx ```jsx
ConfigProvider.config({ ConfigProvider.config({
prefixCls: 'ant', prefixCls: 'ant', // 4.13.0+
iconPrefixCls: 'anticon', // 4.17.0+
}); });
``` ```

View File

@ -0,0 +1,27 @@
import CalendarLocale from 'rc-picker/lib/locale/bn_BD';
import TimePickerLocale from '../../time-picker/locale/bn_BD';
import { PickerLocale } from '../generatePicker';
// Merge into a locale object
const locale: PickerLocale = {
lang: {
placeholder: 'তারিখ নির্বাচন',
yearPlaceholder: 'বছর নির্বাচন',
quarterPlaceholder: 'কোয়ার্টার নির্বাচন',
monthPlaceholder: 'মাস নির্বাচন',
weekPlaceholder: 'সপ্তাহ নির্বাচন',
rangePlaceholder: ['শুরুর তারিখ', 'শেষ তারিখ'],
rangeYearPlaceholder: ['শুরুর বছর', 'শেষ বছর'],
rangeMonthPlaceholder: ['শুরুর মাস', 'শেষ মাস'],
rangeWeekPlaceholder: ['শুরুর সপ্তাহ', 'শেষ সপ্তাহ'],
...CalendarLocale,
},
timePickerLocale: {
...TimePickerLocale,
},
};
// All settings at:
// https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json
export default locale;

View File

@ -13,7 +13,7 @@ exports[`Drawer className is test_drawer 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -23,34 +23,37 @@ exports[`Drawer className is test_drawer 1`] = `
style="opacity:0;transition:opacity .3s" style="opacity:0;transition:opacity .3s"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -77,7 +80,7 @@ exports[`Drawer closable is false 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -110,7 +113,7 @@ exports[`Drawer destroyOnClose is true 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -120,34 +123,37 @@ exports[`Drawer destroyOnClose is true 1`] = `
style="opacity:0;transition:opacity .3s" style="opacity:0;transition:opacity .3s"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -186,34 +192,37 @@ exports[`Drawer getContainer return undefined 2`] = `
class="ant-drawer-wrapper-body" class="ant-drawer-wrapper-body"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar: 0px;"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -241,7 +250,7 @@ exports[`Drawer have a footer 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -250,34 +259,37 @@ exports[`Drawer have a footer 1`] = `
class="ant-drawer-wrapper-body" class="ant-drawer-wrapper-body"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -309,7 +321,7 @@ exports[`Drawer have a title 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -321,36 +333,39 @@ exports[`Drawer have a title 1`] = `
class="ant-drawer-header" class="ant-drawer-header"
> >
<div <div
class="ant-drawer-title" class="ant-drawer-header-title"
> >
Test Title <button
</div> aria-label="Close"
<button class="ant-drawer-close"
aria-label="Close" type="button"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
<div
class="ant-drawer-title"
>
Test Title
</div>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -386,34 +401,37 @@ exports[`Drawer render correctly 1`] = `
class="ant-drawer-wrapper-body" class="ant-drawer-wrapper-body"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -449,34 +467,37 @@ exports[`Drawer render top drawer 1`] = `
class="ant-drawer-wrapper-body" class="ant-drawer-wrapper-body"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -506,7 +527,7 @@ exports[`Drawer style/drawerStyle/headerStyle/bodyStyle should work 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -516,35 +537,38 @@ exports[`Drawer style/drawerStyle/headerStyle/bodyStyle should work 1`] = `
style="background-color:#08c" style="background-color:#08c"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
style="background-color:#08c" style="background-color:#08c"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"
@ -581,18 +605,21 @@ exports[`Drawer support closeIcon 1`] = `
class="ant-drawer-wrapper-body" class="ant-drawer-wrapper-body"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
> >
<span> <button
close aria-label="Close"
</span> class="ant-drawer-close"
</button> type="button"
>
<span>
close
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"

View File

@ -22,7 +22,7 @@ exports[`Drawer render correctly 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="width: 256px;" style="width: 378px;"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -31,34 +31,37 @@ exports[`Drawer render correctly 1`] = `
class="ant-drawer-wrapper-body" class="ant-drawer-wrapper-body"
> >
<div <div
class="ant-drawer-header-no-title" class="ant-drawer-header ant-drawer-header-close-only"
> >
<button <div
aria-label="Close" class="ant-drawer-header-title"
class="ant-drawer-close"
style="--scroll-bar: 0px;"
type="button"
> >
<span <button
aria-label="close" aria-label="Close"
class="anticon anticon-close" class="ant-drawer-close"
role="img" type="button"
> >
<svg <span
aria-hidden="true" aria-label="close"
data-icon="close" class="anticon anticon-close"
fill="currentColor" role="img"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<path <svg
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" aria-hidden="true"
/> data-icon="close"
</svg> fill="currentColor"
</span> focusable="false"
</button> height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div> </div>
<div <div
class="ant-drawer-body" class="ant-drawer-body"

View File

@ -26,6 +26,111 @@ exports[`renders ./components/drawer/demo/config-provider.md correctly 1`] = `
</div> </div>
`; `;
exports[`renders ./components/drawer/demo/extra.md correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<div
class="ant-radio-group ant-radio-group-outline"
>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio"
>
<input
class="ant-radio-input"
type="radio"
value="top"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
top
</span>
</label>
<label
class="ant-radio-wrapper ant-radio-wrapper-checked"
>
<span
class="ant-radio ant-radio-checked"
>
<input
checked=""
class="ant-radio-input"
type="radio"
value="right"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
right
</span>
</label>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio"
>
<input
class="ant-radio-input"
type="radio"
value="bottom"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
bottom
</span>
</label>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio"
>
<input
class="ant-radio-input"
type="radio"
value="left"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
left
</span>
</label>
</div>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open
</span>
</button>
</div>
</div>
`;
exports[`renders ./components/drawer/demo/form-in-drawer.md correctly 1`] = ` exports[`renders ./components/drawer/demo/form-in-drawer.md correctly 1`] = `
<button <button
class="ant-btn ant-btn-primary" class="ant-btn ant-btn-primary"
@ -55,7 +160,7 @@ exports[`renders ./components/drawer/demo/form-in-drawer.md correctly 1`] = `
</svg> </svg>
</span> </span>
<span> <span>
New account New account
</span> </span>
</button> </button>
`; `;
@ -217,7 +322,7 @@ exports[`renders ./components/drawer/demo/render-in-current.md correctly 1`] = `
/> />
<div <div
class="ant-drawer-content-wrapper" class="ant-drawer-content-wrapper"
style="transform:translateX(100%);-ms-transform:translateX(100%);width:256px" style="transform:translateX(100%);-ms-transform:translateX(100%);width:378px"
> >
<div <div
class="ant-drawer-content" class="ant-drawer-content"
@ -229,9 +334,13 @@ exports[`renders ./components/drawer/demo/render-in-current.md correctly 1`] = `
class="ant-drawer-header" class="ant-drawer-header"
> >
<div <div
class="ant-drawer-title" class="ant-drawer-header-title"
> >
Basic Drawer <div
class="ant-drawer-title"
>
Basic Drawer
</div>
</div> </div>
</div> </div>
<div <div
@ -249,6 +358,38 @@ exports[`renders ./components/drawer/demo/render-in-current.md correctly 1`] = `
</div> </div>
`; `;
exports[`renders ./components/drawer/demo/size.md correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Default Size (378px)
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Large Size (736px)
</span>
</button>
</div>
</div>
`;
exports[`renders ./components/drawer/demo/user-profile.md correctly 1`] = ` exports[`renders ./components/drawer/demo/user-profile.md correctly 1`] = `
<div <div
class="ant-list ant-list-split ant-list-bordered" class="ant-list ant-list-split ant-list-bordered"

View File

@ -7,7 +7,7 @@ title:
## zh-CN ## zh-CN
基础抽屉,点击触发按钮抽屉从右滑出,点击遮罩区关闭 基础抽屉,点击触发按钮抽屉从右滑出,点击遮罩区关闭
## en-US ## en-US
@ -30,13 +30,7 @@ const App: React.FC = () => {
<Button type="primary" onClick={showDrawer}> <Button type="primary" onClick={showDrawer}>
Open Open
</Button> </Button>
<Drawer <Drawer title="Basic Drawer" placement="right" onClose={onClose} visible={visible}>
title="Basic Drawer"
placement="right"
closable={false}
onClose={onClose}
visible={visible}
>
<p>Some contents...</p> <p>Some contents...</p>
<p>Some contents...</p> <p>Some contents...</p>
<p>Some contents...</p> <p>Some contents...</p>

View File

@ -0,0 +1,71 @@
---
order: 1.1
title:
zh-CN: 额外操作
en-US: Extra Actions
---
## zh-CN
在 Ant Design 规范中,操作按钮建议放在抽屉的右上角,可以使用 `extra` 属性来实现。
## en-US
Extra actions should be placed at corner of drawer in Ant Design, you can using `extra` prop for that.
```tsx
import React, { useState } from 'react';
import { Drawer, Button, Space, Radio } from 'antd';
import { DrawerProps } from 'antd/es/drawer';
import { RadioChangeEvent } from 'antd/es/radio';
const App: React.FC = () => {
const [visible, setVisible] = useState(false);
const [placement, setPlacement] = useState<DrawerProps['placement']>('right');
const showDrawer = () => {
setVisible(true);
};
const onChange = (e: RadioChangeEvent) => {
setPlacement(e.target.value);
};
const onClose = () => {
setVisible(false);
};
return (
<>
<Space>
<Radio.Group value={placement} onChange={onChange}>
<Radio value="top">top</Radio>
<Radio value="right">right</Radio>
<Radio value="bottom">bottom</Radio>
<Radio value="left">left</Radio>
</Radio.Group>
<Button type="primary" onClick={showDrawer}>
Open
</Button>
</Space>
<Drawer
title="Drawer with extra actions"
placement={placement}
width={500}
onClose={onClose}
visible={visible}
extra={
<Space>
<Button onClick={onClose}>Cancel</Button>
<Button type="primary" onClick={onClose}>
OK
</Button>
</Space>
}
>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Drawer>
</>
);
};
ReactDOM.render(<App />, mountNode);
```

View File

@ -14,7 +14,7 @@ title:
Use a form in Drawer with a submit button. Use a form in Drawer with a submit button.
```jsx ```jsx
import { Drawer, Form, Button, Col, Row, Input, Select, DatePicker } from 'antd'; import { Drawer, Form, Button, Col, Row, Input, Select, DatePicker, Space } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
const { Option } = Select; const { Option } = Select;
@ -37,8 +37,8 @@ class DrawerForm extends React.Component {
render() { render() {
return ( return (
<> <>
<Button type="primary" onClick={this.showDrawer}> <Button type="primary" onClick={this.showDrawer} icon={<PlusOutlined />}>
<PlusOutlined /> New account New account
</Button> </Button>
<Drawer <Drawer
title="Create a new account" title="Create a new account"
@ -46,19 +46,13 @@ class DrawerForm extends React.Component {
onClose={this.onClose} onClose={this.onClose}
visible={this.state.visible} visible={this.state.visible}
bodyStyle={{ paddingBottom: 80 }} bodyStyle={{ paddingBottom: 80 }}
footer={ extra={
<div <Space>
style={{ <Button onClick={this.onClose}>Cancel</Button>
textAlign: 'right',
}}
>
<Button onClick={this.onClose} style={{ marginRight: 8 }}>
Cancel
</Button>
<Button onClick={this.onClose} type="primary"> <Button onClick={this.onClose} type="primary">
Submit Submit
</Button> </Button>
</div> </Space>
} }
> >
<Form layout="vertical" hideRequiredMark> <Form layout="vertical" hideRequiredMark>

View File

@ -7,7 +7,7 @@ title:
## zh-CN ## zh-CN
自定义位置,点击触发按钮抽屉从相应的位置滑出,点击遮罩区关闭 自定义位置,点击触发按钮抽屉从相应的位置滑出,点击遮罩区关闭
## en-US ## en-US
@ -42,7 +42,7 @@ class App extends React.Component {
return ( return (
<> <>
<Space> <Space>
<Radio.Group defaultValue={placement} onChange={this.onChange}> <Radio.Group value={placement} onChange={this.onChange}>
<Radio value="top">top</Radio> <Radio value="top">top</Radio>
<Radio value="right">right</Radio> <Radio value="right">right</Radio>
<Radio value="bottom">bottom</Radio> <Radio value="bottom">bottom</Radio>

View File

@ -7,11 +7,11 @@ title:
## zh-CN ## zh-CN
渲染在当前 dom 里。自定义容器,查看 getContainer。 渲染在当前 dom 里。自定义容器,查看 `getContainer`
## en-US ## en-US
Render in current dom. custom container, check getContainer. Render in current dom. custom container, check `getContainer`.
```jsx ```jsx
import { Drawer, Button } from 'antd'; import { Drawer, Button } from 'antd';

View File

@ -0,0 +1,69 @@
---
order: 10
title:
zh-CN: 预设宽度
en-US: Presetted size
---
## zh-CN
抽屉的默认宽度为 `378px`,另外还提供一个大号抽屉 `736px`,可以用 `size` 属性来设置。
## en-US
The default width (or height) of Drawer is `378px`, and there is a presetted large size `736px`.
```tsx
import React, { useState } from 'react';
import { Drawer, Button, Space } from 'antd';
import { DrawerProps } from 'antd/es/drawer';
const App: React.FC = () => {
const [visible, setVisible] = useState(false);
const [size, setSize] = useState<DrawerProps['size']>();
const showDefaultDrawer = () => {
setSize('default');
setVisible(true);
};
const showLargeDrawer = () => {
setSize('large');
setVisible(true);
};
const onClose = () => {
setVisible(false);
};
return (
<>
<Space>
<Button type="primary" onClick={showDefaultDrawer}>
Open Default Size (378px)
</Button>
<Button type="primary" onClick={showLargeDrawer}>
Open Large Size (736px)
</Button>
</Space>
<Drawer
title={`${size} Drawer`}
placement="right"
size={size}
onClose={onClose}
visible={visible}
extra={
<Space>
<Button onClick={onClose}>Cancel</Button>
<Button type="primary" onClick={onClose}>
OK
</Button>
</Space>
}
>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Drawer>
</>
);
};
ReactDOM.render(<App />, mountNode);
```

View File

@ -28,6 +28,7 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr
| contentWrapperStyle | Style of the drawer wrapper of content part | CSSProperties | - | | | contentWrapperStyle | Style of the drawer wrapper of content part | CSSProperties | - | |
| destroyOnClose | Whether to unmount child components on closing drawer or not | boolean | false | | | destroyOnClose | Whether to unmount child components on closing drawer or not | boolean | false | |
| drawerStyle | Style of the popup layer element | object | - | | | drawerStyle | Style of the popup layer element | object | - | |
| extra | Extra actions area at corner | ReactNode | - | 4.17.0 |
| footer | The footer for Drawer | ReactNode | - | | | footer | The footer for Drawer | ReactNode | - | |
| footerStyle | Style of the drawer footer part | CSSProperties | - | | | footerStyle | Style of the drawer footer part | CSSProperties | - | |
| forceRender | Prerender Drawer component forcely | boolean | false | | | forceRender | Prerender Drawer component forcely | boolean | false | |
@ -41,8 +42,9 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr
| placement | The placement of the Drawer | `top` \| `right` \| `bottom` \| `left` | `right` | | | placement | The placement of the Drawer | `top` \| `right` \| `bottom` \| `left` | `right` | |
| push | Nested drawers push behavior | boolean \| { distance: string \| number } | { distance: 180 } | 4.5.0+ | | push | Nested drawers push behavior | boolean \| { distance: string \| number } | { distance: 180 } | 4.5.0+ |
| style | Style of wrapper element which **contains mask** compare to `drawerStyle` | CSSProperties | - | | | style | Style of wrapper element which **contains mask** compare to `drawerStyle` | CSSProperties | - | |
| size | presetted size of drawer, default `378px` and large `736px` | 'default' \| 'large' | 'default' | 4.17.0 |
| title | The title for Drawer | ReactNode | - | | | title | The title for Drawer | ReactNode | - | |
| visible | Whether the Drawer dialog is visible or not | boolean | false | | | visible | Whether the Drawer dialog is visible or not | boolean | false | |
| width | Width of the Drawer dialog | string \| number | 256 | | | width | Width of the Drawer dialog | string \| number | 378 | |
| zIndex | The `z-index` of the Drawer | number | 1000 | | | zIndex | The `z-index` of the Drawer | number | 1000 | |
| onClose | Specify a callback that will be called when a user clicks mask, close button or Cancel button | function(e) | - | | | onClose | Specify a callback that will be called when a user clicks mask, close button or Cancel button | function(e) | - | |

View File

@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import RcDrawer from 'rc-drawer'; import RcDrawer from 'rc-drawer';
import getScrollBarSize from 'rc-util/lib/getScrollBarSize';
import CloseOutlined from '@ant-design/icons/CloseOutlined'; import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import { ConfigContext, DirectionType } from '../config-provider'; import { ConfigContext, DirectionType } from '../config-provider';
@ -23,6 +22,9 @@ type getContainerFunc = () => HTMLElement;
const PlacementTypes = tuple('top', 'right', 'bottom', 'left'); const PlacementTypes = tuple('top', 'right', 'bottom', 'left');
type placementType = typeof PlacementTypes[number]; type placementType = typeof PlacementTypes[number];
const SizeTypes = tuple('default', 'large');
type sizeType = typeof SizeTypes[number];
export interface PushState { export interface PushState {
distance: string | number; distance: string | number;
} }
@ -36,6 +38,7 @@ export interface DrawerProps {
mask?: boolean; mask?: boolean;
maskStyle?: React.CSSProperties; maskStyle?: React.CSSProperties;
style?: React.CSSProperties; style?: React.CSSProperties;
size?: sizeType;
/** Wrapper dom node style of header and body */ /** Wrapper dom node style of header and body */
drawerStyle?: React.CSSProperties; drawerStyle?: React.CSSProperties;
headerStyle?: React.CSSProperties; headerStyle?: React.CSSProperties;
@ -54,6 +57,7 @@ export interface DrawerProps {
className?: string; className?: string;
handler?: React.ReactNode; handler?: React.ReactNode;
keyboard?: boolean; keyboard?: boolean;
extra?: React.ReactNode;
footer?: React.ReactNode; footer?: React.ReactNode;
footerStyle?: React.CSSProperties; footerStyle?: React.CSSProperties;
level?: string | string[] | null | undefined; level?: string | string[] | null | undefined;
@ -72,8 +76,9 @@ const defaultPushState: PushState = { distance: 180 };
const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>( const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
( (
{ {
width = 256, width,
height = 256, height,
size = 'default',
closable = true, closable = true,
placement = 'right' as placementType, placement = 'right' as placementType,
maskClosable = true, maskClosable = true,
@ -97,6 +102,7 @@ const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
onClose, onClose,
footer, footer,
footerStyle, footerStyle,
extra,
...rest ...rest
}, },
ref, ref,
@ -168,9 +174,11 @@ const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
} }
const offsetStyle: any = {}; const offsetStyle: any = {};
if (placement === 'left' || placement === 'right') { if (placement === 'left' || placement === 'right') {
offsetStyle.width = width; const defaultWidth = size === 'large' ? 736 : 378;
offsetStyle.width = typeof width === 'undefined' ? defaultWidth : width;
} else { } else {
offsetStyle.height = height; const defaultHeight = size === 'large' ? 736 : 378;
offsetStyle.height = typeof height === 'undefined' ? defaultHeight : height;
} }
return offsetStyle; return offsetStyle;
}; };
@ -205,36 +213,29 @@ const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
}; };
}; };
function renderCloseIcon() { const closeIconNode = closable && (
return ( <button type="button" onClick={onClose} aria-label="Close" className={`${prefixCls}-close`}>
closable && ( {closeIcon}
<button </button>
type="button" );
onClick={onClose}
aria-label="Close"
className={`${prefixCls}-close`}
style={
{
'--scroll-bar': `${getScrollBarSize()}px`,
} as any
}
>
{closeIcon}
</button>
)
);
}
function renderHeader() { function renderHeader() {
if (!title && !closable) { if (!title && !closable) {
return null; return null;
} }
const headerClassName = title ? `${prefixCls}-header` : `${prefixCls}-header-no-title`;
return ( return (
<div className={headerClassName} style={headerStyle}> <div
{title && <div className={`${prefixCls}-title`}>{title}</div>} className={classNames(`${prefixCls}-header`, {
{closable && renderCloseIcon()} [`${prefixCls}-header-close-only`]: closable && !title && !extra,
})}
style={headerStyle}
>
<div className={`${prefixCls}-header-title`}>
{closeIconNode}
{title && <div className={`${prefixCls}-title`}>{title}</div>}
</div>
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
</div> </div>
); );
} }

View File

@ -27,6 +27,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
| contentWrapperStyle | 可用于设置 Drawer 包裹内容部分的样式 | CSSProperties | - | | | contentWrapperStyle | 可用于设置 Drawer 包裹内容部分的样式 | CSSProperties | - | |
| destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false | | | destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false | |
| drawerStyle | 用于设置 Drawer 弹出层的样式 | CSSProperties | - | | | drawerStyle | 用于设置 Drawer 弹出层的样式 | CSSProperties | - | |
| extra | 抽屉右上角的操作区域 | ReactNode | - | 4.17.0 |
| footer | 抽屉的页脚 | ReactNode | - | | | footer | 抽屉的页脚 | ReactNode | - | |
| footerStyle | 抽屉页脚部件的样式 | CSSProperties | - | | | footerStyle | 抽屉页脚部件的样式 | CSSProperties | - | |
| forceRender | 预渲染 Drawer 内元素 | boolean | false | | | forceRender | 预渲染 Drawer 内元素 | boolean | false | |
@ -39,9 +40,10 @@ cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
| maskStyle | 遮罩样式 | CSSProperties | {} | | | maskStyle | 遮罩样式 | CSSProperties | {} | |
| placement | 抽屉的方向 | `top` \| `right` \| `bottom` \| `left` | `right` | | | placement | 抽屉的方向 | `top` \| `right` \| `bottom` \| `left` | `right` | |
| push | 用于设置多层 Drawer 的推动行为 | boolean \| { distance: string \| number } | { distance: 180 } | 4.5.0+ | | push | 用于设置多层 Drawer 的推动行为 | boolean \| { distance: string \| number } | { distance: 180 } | 4.5.0+ |
| size | 预设抽屉宽度或高度default `378px` 和 large `736px` | 'default' \| 'large' | 'default' | 4.17.0 |
| style | 可用于设置 Drawer 最外层容器的样式,和 `drawerStyle` 的区别是作用节点包括 `mask` | CSSProperties | - | | | style | 可用于设置 Drawer 最外层容器的样式,和 `drawerStyle` 的区别是作用节点包括 `mask` | CSSProperties | - | |
| title | 标题 | ReactNode | - | | | title | 标题 | ReactNode | - | |
| visible | Drawer 是否可见 | boolean | - | | | visible | Drawer 是否可见 | boolean | - | |
| width | 宽度 | string \| number | 256 | | | width | 宽度 | string \| number | 378 | |
| zIndex | 设置 Drawer 的 `z-index` | number | 1000 | | | zIndex | 设置 Drawer 的 `z-index` | number | 1000 | |
| onClose | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | - | | | onClose | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | - | |

View File

@ -148,12 +148,8 @@
} }
&-close { &-close {
position: absolute; display: inline-block;
top: 0; margin-right: 12px;
right: 0;
z-index: @zindex-popup-close;
display: block;
padding: @drawer-header-close-padding;
color: @modal-close-color; color: @modal-close-color;
font-weight: 700; font-weight: 700;
font-size: @font-size-lg; font-size: @font-size-lg;
@ -174,26 +170,29 @@
color: @icon-color-hover; color: @icon-color-hover;
text-decoration: none; text-decoration: none;
} }
.@{drawer-prefix-cls}-header-no-title & {
margin-right: var(--scroll-bar);
/* stylelint-disable-next-line function-calc-no-invalid */
padding-right: ~'calc(@{drawer-header-close-padding} - var(--scroll-bar))';
}
} }
&-header { &-header {
position: relative; position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: @drawer-header-padding; padding: @drawer-header-padding;
color: @text-color; color: @text-color;
background: @drawer-bg; background: @drawer-bg;
border-bottom: @border-width-base @border-style-base @border-color-split; border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0; border-radius: @border-radius-base @border-radius-base 0 0;
}
&-header-no-title { &-title {
color: @text-color; display: flex;
background: @drawer-bg; align-items: center;
justify-content: space-between;
}
&-close-only {
padding-bottom: 0;
border: none;
}
} }
&-wrapper-body { &-wrapper-body {

View File

@ -9,8 +9,8 @@
&-close { &-close {
.@{drawer-prefix-cls}-rtl & { .@{drawer-prefix-cls}-rtl & {
right: auto; margin-right: 0;
left: 0; margin-left: 12px;
} }
} }
} }

View File

@ -1,95 +1,117 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import CSSMotion from 'rc-motion'; import CSSMotion, { CSSMotionList } 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'; import { FormItemPrefixContext } from './context';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import { ValidateStatus } from './FormItem';
import collapseMotion from '../_util/motion';
const EMPTY_LIST: React.ReactNode[] = []; const EMPTY_LIST: React.ReactNode[] = [];
interface ErrorEntity {
error: React.ReactNode;
errorStatus?: ValidateStatus;
key: string;
}
function toErrorEntity(
error: React.ReactNode,
errorStatus: ValidateStatus | undefined,
prefix: string,
index: number = 0,
): ErrorEntity {
return {
key: typeof error === 'string' ? error : `${prefix}-${index}`,
error,
errorStatus,
};
}
export interface ErrorListProps { export interface ErrorListProps {
errors?: React.ReactNode[];
/** @private Internal Usage. Do not use in your production */
help?: React.ReactNode; help?: React.ReactNode;
/** @private Internal Usage. Do not use in your production */ helpStatus?: ValidateStatus;
onDomErrorVisibleChange?: (visible: boolean) => void; errors?: React.ReactNode[];
warnings?: React.ReactNode[];
className?: string;
} }
export default function ErrorList({ export default function ErrorList({
errors = EMPTY_LIST,
help, help,
onDomErrorVisibleChange, helpStatus,
errors = EMPTY_LIST,
warnings = EMPTY_LIST,
className: rootClassName,
}: ErrorListProps) { }: ErrorListProps) {
const forceUpdate = useForceUpdate(); const { prefixCls } = React.useContext(FormItemPrefixContext);
const { prefixCls, status } = React.useContext(FormItemPrefixContext);
const { getPrefixCls } = React.useContext(ConfigContext); const { getPrefixCls } = React.useContext(ConfigContext);
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`; const baseClassName = `${prefixCls}-item-explain`;
const rootPrefixCls = getPrefixCls(); const rootPrefixCls = getPrefixCls();
const fullKeyList = React.useMemo(() => {
if (help !== undefined && help !== null) {
return [toErrorEntity(help, helpStatus, 'help')];
}
return [
...errors.map((error, index) => toErrorEntity(error, 'error', 'error', index)),
...warnings.map((warning, index) => toErrorEntity(warning, 'warning', 'warning', index)),
];
}, [help, helpStatus, errors, warnings]);
return ( return (
<CSSMotion <CSSMotion
motionDeadline={500} {...collapseMotion}
visible={visible}
motionName={`${rootPrefixCls}-show-help`} motionName={`${rootPrefixCls}-show-help`}
onLeaveEnd={() => { motionAppear={false}
onDomErrorVisibleChange?.(false); motionEnter={false}
visible={!!fullKeyList.length}
onLeaveStart={node => {
// Force disable css override style in index.less configured
node.style.height = 'auto';
return { height: node.offsetHeight };
}} }}
> >
{({ className: motionClassName }: { className?: string }) => ( {holderProps => {
<div const { className: holderClassName, style: holderStyle } = holderProps;
className={classNames(
baseClassName, return (
{ <div
[`${baseClassName}-${innerStatus}`]: innerStatus, className={classNames(baseClassName, holderClassName, rootClassName)}
}, style={holderStyle}
motionClassName, >
)} <CSSMotionList
key="help" keys={fullKeyList}
> {...collapseMotion}
{memoErrors.map((error, index) => ( motionName={`${rootPrefixCls}-show-help-item`}
// eslint-disable-next-line react/no-array-index-key component={false}
<div key={index} role="alert"> >
{error} {itemProps => {
</div> const {
))} key,
</div> error,
)} errorStatus,
className: itemClassName,
style: itemStyle,
} = itemProps;
return (
<div
key={key}
role="alert"
className={classNames(itemClassName, {
[`${baseClassName}-${errorStatus}`]: errorStatus,
})}
style={itemStyle}
>
{error}
</div>
);
}}
</CSSMotionList>
</div>
);
}}
</CSSMotion> </CSSMotion>
); );
} }

View File

@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useContext, useRef } from 'react'; import { useContext } from 'react';
import isEqual from 'lodash/isEqual';
import classNames from 'classnames'; import classNames from 'classnames';
import { Field, FormInstance } from 'rc-field-form'; import { Field, FormInstance } from 'rc-field-form';
import { FieldProps } from 'rc-field-form/lib/Field'; import { FieldProps } from 'rc-field-form/lib/Field';
@ -14,14 +13,20 @@ import { tuple } from '../_util/type';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import FormItemLabel, { FormItemLabelProps, LabelTooltipType } from './FormItemLabel'; import FormItemLabel, { FormItemLabelProps, LabelTooltipType } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput'; import FormItemInput, { FormItemInputProps } from './FormItemInput';
import { FormContext, FormItemContext } from './context'; import { FormContext, NoStyleItemContext } from './context';
import { toArray, getFieldId } from './util'; import { toArray, getFieldId } from './util';
import { cloneElement, isValidElement } from '../_util/reactNode'; import { cloneElement, isValidElement } from '../_util/reactNode';
import useFrameState from './hooks/useFrameState'; import useFrameState from './hooks/useFrameState';
import useDebounce from './hooks/useDebounce';
import useItemRef from './hooks/useItemRef'; import useItemRef from './hooks/useItemRef';
const NAME_SPLIT = '__SPLIT__'; const NAME_SPLIT = '__SPLIT__';
interface FieldError {
errors: string[];
warnings: string[];
}
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type ValidateStatus = typeof ValidateStatuses[number]; export type ValidateStatus = typeof ValidateStatuses[number];
@ -31,7 +36,7 @@ type ChildrenType<Values = any> = RenderChildren<Values> | React.ReactNode;
interface MemoInputProps { interface MemoInputProps {
value: any; value: any;
update: number; update: any;
children: React.ReactNode; children: React.ReactNode;
} }
@ -68,6 +73,16 @@ function hasValidName(name?: NamePath): Boolean {
return !(name === undefined || name === null); return !(name === undefined || name === null);
} }
function genEmptyMeta(): Meta {
return {
errors: [],
warnings: [],
touched: false,
validating: false,
name: [],
};
}
function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElement { function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElement {
const { const {
name, name,
@ -91,104 +106,109 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
hidden, hidden,
...restProps ...restProps
} = props; } = props;
const destroyRef = useRef(false);
const { getPrefixCls } = useContext(ConfigContext); const { getPrefixCls } = useContext(ConfigContext);
const { name: formName, requiredMark } = useContext(FormContext); const { name: formName, requiredMark } = useContext(FormContext);
const { updateItemErrors } = useContext(FormItemContext); const isRenderProps = typeof children === 'function';
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help); const notifyParentMetaChange = useContext(NoStyleItemContext);
const [inlineErrors, setInlineErrors] = useFrameState<Record<string, string[]>>({});
const { validateTrigger: contextValidateTrigger } = useContext(FieldContext); const { validateTrigger: contextValidateTrigger } = useContext(FieldContext);
const mergedValidateTrigger = const mergedValidateTrigger =
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger; validateTrigger !== undefined ? validateTrigger : contextValidateTrigger;
function setDomErrorVisible(visible: boolean) {
if (!destroyRef.current) {
innerSetDomErrorVisible(visible);
}
}
const hasName = hasValidName(name); const hasName = hasValidName(name);
// Cache Field NamePath
const nameRef = useRef<(string | number)[]>([]);
// Should clean up if Field removed
React.useEffect(
() => () => {
destroyRef.current = true;
updateItemErrors(nameRef.current.join(NAME_SPLIT), []);
},
[],
);
const prefixCls = getPrefixCls('form', customizePrefixCls); const prefixCls = getPrefixCls('form', customizePrefixCls);
// ======================== Errors ======================== // ======================== Errors ========================
// Collect noStyle Field error to the top FormItem // >>>>> Collect sub field errors
const updateChildItemErrors = noStyle const [subFieldErrors, setSubFieldErrors] = useFrameState<Record<string, FieldError>>({});
? updateItemErrors
: (subName: string, subErrors: string[], originSubName?: string) => {
setInlineErrors((prevInlineErrors = {}) => {
// Clean up origin error when name changed
if (originSubName && originSubName !== subName) {
delete prevInlineErrors[originSubName];
}
if (!isEqual(prevInlineErrors[subName], subErrors)) { // >>>>> Current field errors
return { const [meta, setMeta] = React.useState<Meta>(() => genEmptyMeta());
...prevInlineErrors,
[subName]: subErrors, const onMetaChange = (nextMeta: Meta & { destroy?: boolean }) => {
}; // Destroy will reset all the meta
} setMeta(nextMeta.destroy ? genEmptyMeta() : nextMeta);
return prevInlineErrors;
}); // Bump to parent since noStyle
if (noStyle && notifyParentMetaChange) {
let namePath = nextMeta.name;
if (fieldKey !== undefined) {
namePath = Array.isArray(fieldKey) ? fieldKey : [fieldKey!];
}
notifyParentMetaChange(nextMeta, namePath);
}
};
// >>>>> Collect noStyle Field error to the top FormItem
const onSubItemMetaChange = (subMeta: Meta & { destroy: boolean }, uniqueKeys: React.Key[]) => {
// Only `noStyle` sub item will trigger
setSubFieldErrors(prevSubFieldErrors => {
const clone = {
...prevSubFieldErrors,
}; };
// name: ['user', 1] + key: [4] = ['user', 4]
const mergedNamePath = [...subMeta.name.slice(0, -1), ...uniqueKeys];
const mergedNameKey = mergedNamePath.join(NAME_SPLIT);
if (subMeta.destroy) {
// Remove
delete clone[mergedNameKey];
} else {
// Update
clone[mergedNameKey] = subMeta;
}
return clone;
});
};
// >>>>> Get merged errors
const [mergedErrors, mergedWarnings] = React.useMemo(() => {
const errorList: string[] = [...meta.errors];
const warningList: string[] = [...meta.warnings];
Object.values(subFieldErrors).forEach(subFieldError => {
errorList.push(...(subFieldError.errors || []));
warningList.push(...(subFieldError.warnings || []));
});
return [errorList, warningList];
}, [subFieldErrors, meta.errors, meta.warnings]);
const debounceErrors = useDebounce(mergedErrors);
const debounceWarnings = useDebounce(mergedWarnings);
// ===================== Children Ref ===================== // ===================== Children Ref =====================
const getItemRef = useItemRef(); const getItemRef = useItemRef();
// ======================== Render ========================
function renderLayout( function renderLayout(
baseChildren: React.ReactNode, baseChildren: React.ReactNode,
fieldId?: string, fieldId?: string,
meta?: Meta,
isRequired?: boolean, isRequired?: boolean,
): React.ReactNode { ): React.ReactNode {
if (noStyle && !hidden) { if (noStyle && !hidden) {
return baseChildren; return baseChildren;
} }
// ======================== Errors ========================
// >>> collect sub errors
let subErrorList: string[] = [];
Object.keys(inlineErrors).forEach(subName => {
subErrorList = [...subErrorList, ...(inlineErrors[subName] || [])];
});
// >>> merged errors
let mergedErrors: React.ReactNode[];
if (help !== undefined && help !== null) {
mergedErrors = toArray(help);
} else {
mergedErrors = meta ? meta.errors : [];
mergedErrors = [...mergedErrors, ...subErrorList];
}
// ======================== Status ======================== // ======================== Status ========================
let mergedValidateStatus: ValidateStatus = ''; let mergedValidateStatus: ValidateStatus = '';
if (validateStatus !== undefined) { if (validateStatus !== undefined) {
mergedValidateStatus = validateStatus; mergedValidateStatus = validateStatus;
} else if (meta?.validating) { } else if (meta?.validating) {
mergedValidateStatus = 'validating'; mergedValidateStatus = 'validating';
} else if (meta?.errors?.length || subErrorList.length) { } else if (debounceErrors.length) {
mergedValidateStatus = 'error'; mergedValidateStatus = 'error';
} else if (debounceWarnings.length) {
mergedValidateStatus = 'warning';
} else if (meta?.touched) { } else if (meta?.touched) {
mergedValidateStatus = 'success'; mergedValidateStatus = 'success';
} }
const itemClassName = { const itemClassName = {
[`${prefixCls}-item`]: true, [`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: domErrorVisible || !!help, [`${prefixCls}-item-with-help`]: help || debounceErrors.length || debounceWarnings.length,
[`${className}`]: !!className, [`${className}`]: !!className,
// Status // Status
@ -238,26 +258,21 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
<FormItemInput <FormItemInput
{...props} {...props}
{...meta} {...meta}
errors={mergedErrors} errors={debounceErrors}
warnings={debounceWarnings}
prefixCls={prefixCls} prefixCls={prefixCls}
status={mergedValidateStatus} status={mergedValidateStatus}
onDomErrorVisibleChange={setDomErrorVisible}
validateStatus={mergedValidateStatus} validateStatus={mergedValidateStatus}
help={help}
> >
<FormItemContext.Provider value={{ updateItemErrors: updateChildItemErrors }}> <NoStyleItemContext.Provider value={onSubItemMetaChange}>
{baseChildren} {baseChildren}
</FormItemContext.Provider> </NoStyleItemContext.Provider>
</FormItemInput> </FormItemInput>
</Row> </Row>
); );
} }
const isRenderProps = typeof children === 'function';
// Record for real component render
const updateRef = useRef(0);
updateRef.current += 1;
if (!hasName && !isRenderProps && !dependencies) { if (!hasName && !isRenderProps && !dependencies) {
return renderLayout(children) as JSX.Element; return renderLayout(children) as JSX.Element;
} }
@ -272,46 +287,31 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
variables = { ...variables, ...messageVariables }; variables = { ...variables, ...messageVariables };
} }
// >>>>> With Field
return ( return (
<Field <Field
{...props} {...props}
messageVariables={variables} messageVariables={variables}
trigger={trigger} trigger={trigger}
validateTrigger={mergedValidateTrigger} validateTrigger={mergedValidateTrigger}
onReset={() => { onMetaChange={onMetaChange}
setDomErrorVisible(false);
}}
> >
{(control, meta, context) => { {(control, renderMeta, context) => {
const { errors } = meta; const mergedName = toArray(name).length && renderMeta ? renderMeta.name : [];
const mergedName = toArray(name).length && meta ? meta.name : [];
const fieldId = getFieldId(mergedName, formName); const fieldId = getFieldId(mergedName, formName);
if (noStyle) {
// Clean up origin one
const originErrorName = nameRef.current.join(NAME_SPLIT);
nameRef.current = [...mergedName];
if (fieldKey) {
const fieldKeys = Array.isArray(fieldKey) ? fieldKey : [fieldKey];
nameRef.current = [...mergedName.slice(0, -1), ...fieldKeys];
}
updateItemErrors(nameRef.current.join(NAME_SPLIT), errors, originErrorName);
}
const isRequired = const isRequired =
required !== undefined required !== undefined
? required ? required
: !!( : !!(
rules && rules &&
rules.some(rule => { rules.some(rule => {
if (rule && typeof rule === 'object' && rule.required) { if (rule && typeof rule === 'object' && rule.required && !rule.warningOnly) {
return true; return true;
} }
if (typeof rule === 'function') { if (typeof rule === 'function') {
const ruleEntity = rule(context); const ruleEntity = rule(context);
return ruleEntity && ruleEntity.required; return ruleEntity && ruleEntity.required && !ruleEntity.warningOnly;
} }
return false; return false;
}) })
@ -379,10 +379,7 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
}); });
childNode = ( childNode = (
<MemoInput <MemoInput value={mergedControl[props.valuePropName || 'value']} update={children}>
value={mergedControl[props.valuePropName || 'value']}
update={updateRef.current}
>
{cloneElement(children, childProps)} {cloneElement(children, childProps)}
</MemoInput> </MemoInput>
); );
@ -397,7 +394,7 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
childNode = children; childNode = children;
} }
return renderLayout(childNode, fieldId, meta, isRequired); return renderLayout(childNode, fieldId, isRequired);
}} }}
</Field> </Field>
); );

View File

@ -14,9 +14,9 @@ interface FormItemInputMiscProps {
prefixCls: string; prefixCls: string;
children: React.ReactNode; children: React.ReactNode;
errors: React.ReactNode[]; errors: React.ReactNode[];
warnings: React.ReactNode[];
hasFeedback?: boolean; hasFeedback?: boolean;
validateStatus?: ValidateStatus; validateStatus?: ValidateStatus;
onDomErrorVisibleChange: (visible: boolean) => void;
/** @private Internal Usage, do not use in any of your production. */ /** @private Internal Usage, do not use in any of your production. */
_internalItemRender?: { _internalItemRender?: {
mark: string; mark: string;
@ -33,9 +33,9 @@ interface FormItemInputMiscProps {
export interface FormItemInputProps { export interface FormItemInputProps {
wrapperCol?: ColProps; wrapperCol?: ColProps;
help?: React.ReactNode;
extra?: React.ReactNode; extra?: React.ReactNode;
status?: ValidateStatus; status?: ValidateStatus;
help?: React.ReactNode;
} }
const iconMap: { [key: string]: any } = { const iconMap: { [key: string]: any } = {
@ -51,13 +51,13 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
status, status,
wrapperCol, wrapperCol,
children, children,
help,
errors, errors,
onDomErrorVisibleChange, warnings,
hasFeedback, hasFeedback,
_internalItemRender: formItemRender, _internalItemRender: formItemRender,
validateStatus, validateStatus,
extra, extra,
help,
} = props; } = props;
const baseClassName = `${prefixCls}-item`; const baseClassName = `${prefixCls}-item`;
@ -67,13 +67,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className); const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
React.useEffect(
() => () => {
onDomErrorVisibleChange(false);
},
[],
);
// Should provides additional icon if `hasFeedback` // Should provides additional icon if `hasFeedback`
const IconNode = validateStatus && iconMap[validateStatus]; const IconNode = validateStatus && iconMap[validateStatus];
const icon = const icon =
@ -96,7 +89,13 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
); );
const errorListDom = ( const errorListDom = (
<FormItemPrefixContext.Provider value={{ prefixCls, status }}> <FormItemPrefixContext.Provider value={{ prefixCls, status }}>
<ErrorList errors={errors} help={help} onDomErrorVisibleChange={onDomErrorVisibleChange} /> <ErrorList
errors={errors}
warnings={warnings}
help={help}
helpStatus={status}
className={`${baseClassName}-explain-connected`}
/>
</FormItemPrefixContext.Provider> </FormItemPrefixContext.Provider>
); );

View File

@ -25,7 +25,7 @@ export interface FormListProps {
children: ( children: (
fields: FormListFieldData[], fields: FormListFieldData[],
operation: FormListOperation, operation: FormListOperation,
meta: { errors: React.ReactNode[] }, meta: { errors: React.ReactNode[]; warnings: React.ReactNode[] },
) => React.ReactNode; ) => React.ReactNode;
} }
@ -48,6 +48,7 @@ const FormList: React.FC<FormListProps> = ({
operation, operation,
{ {
errors: meta.errors, errors: meta.errors,
warnings: meta.warnings,
}, },
)} )}
</FormItemPrefixContext.Provider> </FormItemPrefixContext.Provider>

View File

@ -306,6 +306,7 @@ exports[`renders ./components/form/demo/advanced-search.md correctly 1`] = `
exports[`renders ./components/form/demo/basic.md correctly 1`] = ` exports[`renders ./components/form/demo/basic.md correctly 1`] = `
<form <form
autocomplete="off"
class="ant-form ant-form-horizontal" class="ant-form ant-form-horizontal"
id="basic" id="basic"
> >
@ -1073,9 +1074,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Buggy! Buggy!
@ -1115,9 +1117,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Buggy! Buggy!
@ -1188,9 +1191,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Buggy! Buggy!
@ -1230,9 +1234,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Buggy! Buggy!
@ -1329,9 +1334,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Buggy! Buggy!
@ -1384,9 +1390,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Buggy! Buggy!
@ -1475,9 +1482,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Buggy! Buggy!
@ -1526,9 +1534,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Buggy! Buggy!
@ -3312,6 +3321,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
> >
<span <span
class="ant-cascader-picker-label" class="ant-cascader-picker-label"
title="Zhejiang / Hangzhou / West Lake"
> >
Zhejiang / Hangzhou / West Lake Zhejiang / Hangzhou / West Lake
</span> </span>
@ -6472,9 +6482,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Should be combination of numbers & alphabets Should be combination of numbers & alphabets
@ -6597,9 +6608,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span> </span>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-validating" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-validating"
role="alert" role="alert"
> >
The information is being validated... The information is being validated...
@ -6774,9 +6786,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span> </span>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Should be combination of numbers & alphabets Should be combination of numbers & alphabets
@ -7154,9 +7167,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span> </span>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-validating" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-validating"
role="alert" role="alert"
> >
The information is being validated... The information is being validated...
@ -7242,9 +7256,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain ant-form-item-explain-error" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class="ant-form-item-explain-error"
role="alert" role="alert"
> >
Please select the correct date Please select the correct date
@ -7720,6 +7735,97 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</form> </form>
`; `;
exports[`renders ./components/form/demo/warning-only.md correctly 1`] = `
<form
autocomplete="off"
class="ant-form ant-form-vertical"
>
<div
style="overflow:hidden"
>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class="ant-form-item-required"
for="url"
title="URL"
>
URL
</label>
</div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="url"
placeholder="input placeholder"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="submit"
>
<span>
Submit
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn"
type="button"
>
<span>
Fill
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
`;
exports[`renders ./components/form/demo/without-form-create.md correctly 1`] = ` exports[`renders ./components/form/demo/without-form-create.md correctly 1`] = `
<form <form
class="ant-form ant-form-horizontal" class="ant-form ant-form-horizontal"
@ -7825,9 +7931,10 @@ exports[`renders ./components/form/demo/without-form-create.md correctly 1`] = `
</div> </div>
</div> </div>
<div <div
class="ant-form-item-explain" class="ant-form-item-explain ant-form-item-explain-connected"
> >
<div <div
class=""
role="alert" role="alert"
> >
A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself. A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import scrollIntoView from 'scroll-into-view-if-needed'; import scrollIntoView from 'scroll-into-view-if-needed';
import Form from '..'; import Form from '..';
import Input from '../../input'; import Input from '../../input';
@ -20,10 +21,17 @@ describe('Form', () => {
scrollIntoView.mockImplementation(() => {}); scrollIntoView.mockImplementation(() => {});
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
async function change(wrapper, index, value) { async function change(wrapper, index, value, executeMockTimer) {
wrapper.find(Input).at(index).simulate('change', { target: { value } }); wrapper.find(Input).at(index).simulate('change', { target: { value } });
await sleep(200); await sleep(200);
wrapper.update();
if (executeMockTimer) {
act(() => {
jest.runAllTimers();
wrapper.update();
});
await sleep(1);
}
} }
beforeEach(() => { beforeEach(() => {
@ -42,6 +50,8 @@ describe('Form', () => {
describe('noStyle Form.Item', () => { describe('noStyle Form.Item', () => {
it('work', async () => { it('work', async () => {
jest.useFakeTimers();
const onChange = jest.fn(); const onChange = jest.fn();
const wrapper = mount( const wrapper = mount(
@ -54,14 +64,18 @@ describe('Form', () => {
</Form>, </Form>,
); );
await change(wrapper, 0, ''); await change(wrapper, 0, '', true);
expect(wrapper.find('.ant-form-item-with-help').length).toBeTruthy(); expect(wrapper.find('.ant-form-item-with-help').length).toBeTruthy();
expect(wrapper.find('.ant-form-item-has-error').length).toBeTruthy(); expect(wrapper.find('.ant-form-item-has-error').length).toBeTruthy();
expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalled();
jest.useRealTimers();
}); });
it('should clean up', async () => { it('should clean up', async () => {
jest.useFakeTimers();
const Demo = () => { const Demo = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
@ -105,12 +119,14 @@ describe('Form', () => {
}; };
const wrapper = mount(<Demo />); const wrapper = mount(<Demo />);
await change(wrapper, 0, '1'); await change(wrapper, 0, '1', true);
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('aaa'); expect(wrapper.find('.ant-form-item-explain').text()).toEqual('aaa');
await change(wrapper, 0, '2'); await change(wrapper, 0, '2', true);
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('ccc'); expect(wrapper.find('.ant-form-item-explain').text()).toEqual('ccc');
await change(wrapper, 0, '1'); await change(wrapper, 0, '1', true);
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('aaa'); expect(wrapper.find('.ant-form-item-explain').text()).toEqual('aaa');
jest.useRealTimers();
}); });
}); });
@ -315,6 +331,8 @@ describe('Form', () => {
// https://github.com/ant-design/ant-design/issues/20706 // https://github.com/ant-design/ant-design/issues/20706
it('Error change should work', async () => { it('Error change should work', async () => {
jest.useFakeTimers();
const wrapper = mount( const wrapper = mount(
<Form> <Form>
<Form.Item <Form.Item
@ -338,15 +356,17 @@ describe('Form', () => {
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
for (let i = 0; i < 3; i += 1) { for (let i = 0; i < 3; i += 1) {
await change(wrapper, 0, ''); await change(wrapper, 0, '', true);
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual("'name' is required"); expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual("'name' is required");
await change(wrapper, 0, 'p'); await change(wrapper, 0, 'p', true);
await sleep(100); await sleep(100);
wrapper.update(); wrapper.update();
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('not a p'); expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('not a p');
} }
/* eslint-enable */ /* eslint-enable */
jest.useRealTimers();
}); });
// https://github.com/ant-design/ant-design/issues/20813 // https://github.com/ant-design/ant-design/issues/20813
@ -428,6 +448,8 @@ describe('Form', () => {
}); });
it('Form.Item with `help` should display error style when validate failed', async () => { it('Form.Item with `help` should display error style when validate failed', async () => {
jest.useFakeTimers();
const wrapper = mount( const wrapper = mount(
<Form> <Form>
<Form.Item name="test" help="help" rules={[{ required: true, message: 'message' }]}> <Form.Item name="test" help="help" rules={[{ required: true, message: 'message' }]}>
@ -436,12 +458,16 @@ describe('Form', () => {
</Form>, </Form>,
); );
await change(wrapper, 0, ''); await change(wrapper, 0, '', true);
expect(wrapper.find('.ant-form-item').first().hasClass('ant-form-item-has-error')).toBeTruthy(); expect(wrapper.find('.ant-form-item').first().hasClass('ant-form-item-has-error')).toBeTruthy();
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('help'); expect(wrapper.find('.ant-form-item-explain').text()).toEqual('help');
jest.useRealTimers();
}); });
it('clear validation message when ', async () => { it('clear validation message when ', async () => {
jest.useFakeTimers();
const wrapper = mount( const wrapper = mount(
<Form> <Form>
<Form.Item name="username" rules={[{ required: true, message: 'message' }]}> <Form.Item name="username" rules={[{ required: true, message: 'message' }]}>
@ -449,14 +475,18 @@ describe('Form', () => {
</Form.Item> </Form.Item>
</Form>, </Form>,
); );
await change(wrapper, 0, '1'); await change(wrapper, 0, '1', true);
expect(wrapper.find('.ant-form-item-explain').length).toBeFalsy(); expect(wrapper.find('.ant-form-item-explain').length).toBeFalsy();
await change(wrapper, 0, '');
await change(wrapper, 0, '', true);
expect(wrapper.find('.ant-form-item-explain').length).toBeTruthy(); expect(wrapper.find('.ant-form-item-explain').length).toBeTruthy();
await change(wrapper, 0, '123');
await change(wrapper, 0, '123', true);
await sleep(800); await sleep(800);
wrapper.update(); wrapper.update();
expect(wrapper.find('.ant-form-item-explain').length).toBeFalsy(); expect(wrapper.find('.ant-form-item-explain').length).toBeFalsy();
jest.useRealTimers();
}); });
// https://github.com/ant-design/ant-design/issues/21167 // https://github.com/ant-design/ant-design/issues/21167

View File

@ -200,34 +200,6 @@ describe('Form.List', () => {
jest.useRealTimers(); 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', () => { it('should render empty without errors', () => {
const wrapper = mount(<Form.ErrorList />); const wrapper = mount(<Form.ErrorList />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import { Meta } from 'rc-field-form/lib/interface';
import { FormProvider as RcFormProvider } from 'rc-field-form'; import { FormProvider as RcFormProvider } from 'rc-field-form';
import { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/FormContext'; import { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/FormContext';
import { ColProps } from '../grid/col'; import { ColProps } from '../grid/col';
@ -25,14 +26,9 @@ export const FormContext = React.createContext<FormContextProps>({
itemRef: (() => {}) as any, itemRef: (() => {}) as any,
}); });
/** Form Item Context. Used for Form noStyle Item error collection */ /** `noStyle` Form Item Context. Used for error collection */
export interface FormItemContextProps { export type ReportMetaChange = (meta: Meta, uniqueKeys: React.Key[]) => void;
updateItemErrors: (name: string, errors: string[], originName?: string) => void; export const NoStyleItemContext = React.createContext<ReportMetaChange | null>(null);
}
export const FormItemContext = React.createContext<FormItemContextProps>({
updateItemErrors: () => {},
});
/** Form Provider */ /** Form Provider */
export interface FormProviderProps extends Omit<RcFormProviderProps, 'validateMessages'> { export interface FormProviderProps extends Omit<RcFormProviderProps, 'validateMessages'> {

View File

@ -33,6 +33,7 @@ const Demo = () => {
initialValues={{ remember: true }} initialValues={{ remember: true }}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={onFinishFailed} onFinishFailed={onFinishFailed}
autoComplete="off"
> >
<Form.Item <Form.Item
label="Username" label="Username"

View File

@ -15,7 +15,7 @@ We recommend use `Form.useForm` to create data control. If you are using class c
```tsx ```tsx
import { Form, Input, Button, Select } from 'antd'; import { Form, Input, Button, Select } from 'antd';
import { FormInstance } from 'antd/lib/form'; import { FormInstance } from 'antd/es/form';
const { Option } = Select; const { Option } = Select;

View File

@ -0,0 +1,73 @@
---
order: 3.2
title:
zh-CN: 非阻塞校验
en-US: No block rule
---
## zh-CN
`rule` 添加 `warningOnly` 后校验不再阻塞表单提交。
## en-US
`rule` with `warningOnly` will not block form submit.
```tsx
import React from 'react';
import { Form, Input, message, Button, Space } from 'antd';
const Demo = () => {
const [form] = Form.useForm();
const onFinish = () => {
message.success('Submit success!');
};
const onFinishFailed = () => {
message.error('Submit failed!');
};
const onFill = () => {
form.setFieldsValue({
url: 'https://taobao.com/',
});
};
return (
<Form
form={form}
layout="vertical"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<div style={{ overflow: 'hidden' }}>
<Form.Item
name="url"
label="URL"
rules={[
{ required: true },
{ type: 'url', warningOnly: true },
{ type: 'string', min: 6 },
]}
>
<Input placeholder="input placeholder" />
</Form.Item>
</div>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button htmlType="button" onClick={onFill}>
Fill
</Button>
</Space>
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -1,47 +0,0 @@
import * as React from 'react';
import useForceUpdate from '../../_util/hooks/useForceUpdate';
/** Always debounce error to avoid [error -> null -> error] blink */
export default function useCacheErrors(
errors: React.ReactNode[],
changeTrigger: (visible: boolean) => void,
directly: boolean,
): [boolean, React.ReactNode[]] {
const cacheRef = React.useRef({
errors,
visible: !!errors.length,
});
const forceUpdate = useForceUpdate();
const update = () => {
const prevVisible = cacheRef.current.visible;
const newVisible = !!errors.length;
const prevErrors = cacheRef.current.errors;
cacheRef.current.errors = errors;
cacheRef.current.visible = newVisible;
if (prevVisible !== newVisible) {
changeTrigger(newVisible);
} else if (
prevErrors.length !== errors.length ||
prevErrors.some((prevErr, index) => prevErr !== errors[index])
) {
forceUpdate();
}
};
React.useEffect(() => {
if (!directly) {
const timeout = setTimeout(update, 10);
return () => clearTimeout(timeout);
}
}, [errors]);
if (directly) {
update();
}
return [cacheRef.current.visible, cacheRef.current.errors];
}

View File

@ -0,0 +1,19 @@
import * as React from 'react';
export default function useDebounce<T>(value: T[]): T[] {
const [cacheValue, setCacheValue] = React.useState(value);
React.useEffect(() => {
const timeout = setTimeout(
() => {
setCacheValue(value);
},
value.length ? 0 : 10,
);
return () => {
clearTimeout(timeout);
};
}, [value]);
return cacheValue;
}

View File

@ -302,22 +302,23 @@ Rule supports a config object, or a function returning config object:
type Rule = RuleConfig | ((form: FormInstance) => RuleConfig); type Rule = RuleConfig | ((form: FormInstance) => RuleConfig);
``` ```
| Name | Description | Type | | Name | Description | Type | Version |
| --- | --- | --- | | --- | --- | --- | --- |
| defaultField | Validate rule for all array elements, valid when `type` is `array` | [rule](#Rule) | | defaultField | Validate rule for all array elements, valid when `type` is `array` | [rule](#Rule) | |
| enum | Match enum value. You need to set `type` to `enum` to enable this | any\[] | | enum | Match enum value. You need to set `type` to `enum` to enable this | any\[] | |
| fields | Validate rule for child elements, valid when `type` is `array` or `object` | Record&lt;string, [rule](#Rule)> | | fields | Validate rule for child elements, valid when `type` is `array` or `object` | Record&lt;string, [rule](#Rule)> | |
| len | Length of string, number, array | number | | len | Length of string, number, array | number | |
| max | `type` required: max length of `string`, `number`, `array` | number | | max | `type` required: max length of `string`, `number`, `array` | number | |
| message | Error message. Will auto generate by [template](#validateMessages) if not provided | string | | message | Error message. Will auto generate by [template](#validateMessages) if not provided | string | |
| min | `type` required: min length of `string`, `number`, `array` | number | | min | `type` required: min length of `string`, `number`, `array` | number | |
| pattern | Regex pattern | RegExp | | pattern | Regex pattern | RegExp | |
| required | Required field | boolean | | required | Required field | boolean | |
| transform | Transform value to the rule before validation | (value) => any | | transform | Transform value to the rule before validation | (value) => any | |
| type | Normally `string` \|`number` \|`boolean` \|`url` \| `email`. More type to ref [here](https://github.com/yiminghe/async-validator#type) | string | | type | Normally `string` \|`number` \|`boolean` \|`url` \| `email`. More type to ref [here](https://github.com/yiminghe/async-validator#type) | string | |
| validateTrigger | Set validate trigger event. Must be the sub set of `validateTrigger` in Form.Item | string \| string\[] | | validateTrigger | Set validate trigger event. Must be the sub set of `validateTrigger` in Form.Item | string \| string\[] | |
| validator | Customize validation rule. Accept Promise as return. See [example](#components-form-demo-register) | ([rule](#Rule), value) => Promise | | validator | Customize validation rule. Accept Promise as return. See [example](#components-form-demo-register) | ([rule](#Rule), value) => Promise | |
| whitespace | Failed if only has whitespace, only work with `type: 'string'` rule | boolean | | warningOnly | Warning only. Not block form submit | boolean | 4.17.0 |
| whitespace | Failed if only has whitespace, only work with `type: 'string'` rule | boolean | |
## Migrate to v4 ## Migrate to v4

View File

@ -301,22 +301,23 @@ Rule 支持接收 object 进行配置,也支持 function 来动态获取 form
type Rule = RuleConfig | ((form: FormInstance) => RuleConfig); type Rule = RuleConfig | ((form: FormInstance) => RuleConfig);
``` ```
| 名称 | 说明 | 类型 | | 名称 | 说明 | 类型 | 版本 |
| --- | --- | --- | | --- | --- | --- | --- |
| defaultField | 仅在 `type``array` 类型时有效,用于指定数组元素的校验规则 | [rule](#Rule) | | defaultField | 仅在 `type``array` 类型时有效,用于指定数组元素的校验规则 | [rule](#Rule) | |
| enum | 是否匹配枚举中的值(需要将 `type` 设置为 `enum` | any\[] | | enum | 是否匹配枚举中的值(需要将 `type` 设置为 `enum` | any\[] | |
| fields | 仅在 `type``array``object` 类型时有效,用于指定子元素的校验规则 | Record&lt;string, [rule](#Rule)> | | fields | 仅在 `type``array``object` 类型时有效,用于指定子元素的校验规则 | Record&lt;string, [rule](#Rule)> | |
| len | string 类型时为字符串长度number 类型时为确定数字; array 类型时为数组长度 | number | | len | string 类型时为字符串长度number 类型时为确定数字; array 类型时为数组长度 | number | |
| max | 必须设置 `type`string 类型为字符串最大长度number 类型时为最大值array 类型时为数组最大长度 | number | | max | 必须设置 `type`string 类型为字符串最大长度number 类型时为最大值array 类型时为数组最大长度 | number | |
| message | 错误信息,不设置时会通过[模板](#validateMessages)自动生成 | string | | message | 错误信息,不设置时会通过[模板](#validateMessages)自动生成 | string | |
| min | 必须设置 `type`string 类型为字符串最小长度number 类型时为最小值array 类型时为数组最小长度 | number | | min | 必须设置 `type`string 类型为字符串最小长度number 类型时为最小值array 类型时为数组最小长度 | number | |
| pattern | 正则表达式匹配 | RegExp | | pattern | 正则表达式匹配 | RegExp | |
| required | 是否为必选字段 | boolean | | required | 是否为必选字段 | boolean | |
| transform | 将字段值转换成目标值后进行校验 | (value) => any | | transform | 将字段值转换成目标值后进行校验 | (value) => any | |
| type | 类型,常见有 `string` \|`number` \|`boolean` \|`url` \| `email`。更多请参考[此处](https://github.com/yiminghe/async-validator#type) | string | | type | 类型,常见有 `string` \|`number` \|`boolean` \|`url` \| `email`。更多请参考[此处](https://github.com/yiminghe/async-validator#type) | string | |
| validateTrigger | 设置触发验证时机,必须是 Form.Item 的 `validateTrigger` 的子集 | string \| string\[] | | validateTrigger | 设置触发验证时机,必须是 Form.Item 的 `validateTrigger` 的子集 | string \| string\[] | |
| validator | 自定义校验,接收 Promise 作为返回值。[示例](#components-form-demo-register)参考 | ([rule](#Rule), value) => Promise | | validator | 自定义校验,接收 Promise 作为返回值。[示例](#components-form-demo-register)参考 | ([rule](#Rule), value) => Promise | |
| whitespace | 如果字段仅包含空格则校验不通过,只在 `type: 'string'` 时生效 | boolean | | warningOnly | 仅警告,不阻塞表单提交 | boolean | 4.17.0 |
| whitespace | 如果字段仅包含空格则校验不通过,只在 `type: 'string'` 时生效 | boolean | |
## 从 v3 升级到 v4 ## 从 v3 升级到 v4

View File

@ -61,9 +61,12 @@
margin-bottom: @form-item-margin-bottom; margin-bottom: @form-item-margin-bottom;
vertical-align: top; vertical-align: top;
// We delay one frame (0.017s) here to let CSSMotion goes
transition: margin-bottom @animation-duration-slow 0.017s linear;
&-with-help { &-with-help {
margin-bottom: 0; margin-bottom: 0;
transition: none;
} }
&-hidden, &-hidden,
@ -89,7 +92,6 @@
> label { > label {
position: relative; position: relative;
// display: inline;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
height: @form-item-label-height; height: @form-item-label-height;
@ -179,10 +181,12 @@
} }
} }
// ==============================================================
// = Explain =
// ==============================================================
&-explain, &-explain,
&-extra { &-extra {
clear: both; clear: both;
min-height: @form-item-margin-bottom;
color: @text-color-secondary; color: @text-color-secondary;
font-size: @font-size-base; font-size: @font-size-base;
line-height: @line-height-base; line-height: @line-height-base;
@ -190,43 +194,64 @@
.explainAndExtraDistance((@form-item-margin-bottom - @form-font-height) / 2); .explainAndExtraDistance((@form-item-margin-bottom - @form-font-height) / 2);
} }
&-explain-connected {
height: 0;
min-height: 0;
opacity: 0;
}
&-extra {
min-height: @form-item-margin-bottom;
}
.@{ant-prefix}-input-textarea-show-count { .@{ant-prefix}-input-textarea-show-count {
&::after { &::after {
margin-bottom: -22px; margin-bottom: -22px;
} }
} }
}
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) { &-with-help &-explain {
@name: ~'@{ant-prefix}-@{className}'; height: auto;
.make-motion(@name, @keyframeName, @duration); min-height: @form-item-margin-bottom;
.@{name}-enter,
.@{name}-appear {
opacity: 0;
animation-timing-function: @ease-in-out;
}
.@{name}-leave {
animation-timing-function: @ease-in-out;
}
}
.show-help-motion(show-help, antShowHelp, 0.3s);
@keyframes antShowHelpIn {
0% {
transform: translateY(-5px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1; opacity: 1;
} }
} }
@keyframes antShowHelpOut { // >>>>>>>>>> Motion <<<<<<<<<<
to { // Explain holder
.@{ant-prefix}-show-help {
transition: height @animation-duration-slow linear, min-height @animation-duration-slow linear,
margin-bottom @animation-duration-slow @ease-in-out,
opacity @animation-duration-slow @ease-in-out;
&-leave {
min-height: @form-item-margin-bottom;
&-active {
min-height: 0;
}
}
}
// Explain
.@{ant-prefix}-show-help-item {
overflow: hidden;
transition: height @animation-duration-slow @ease-in-out,
opacity @animation-duration-slow @ease-in-out, transform @animation-duration-slow @ease-in-out !important;
&-appear,
&-enter {
transform: translateY(-5px); transform: translateY(-5px);
opacity: 0; opacity: 0;
&-active {
transform: translateY(0);
opacity: 1;
}
}
&-leave-active {
transform: translateY(-5px);
} }
} }

View File

@ -9,11 +9,11 @@
// ========================= Explain ========================= // ========================= Explain =========================
/* To support leave along ErrorList. We add additional className to handle explain style */ /* To support leave along ErrorList. We add additional className to handle explain style */
&-explain { &-explain {
&&-error { &-error {
color: @error-color; color: @error-color;
} }
&&-warning { &-warning {
color: @warning-color; color: @warning-color;
} }
} }

View File

@ -20,7 +20,7 @@ When a numeric value needs to be provided.
| decimalSeparator | Decimal separator | string | - | - | | decimalSeparator | Decimal separator | string | - | - |
| defaultValue | The initial value | number | - | - | | defaultValue | The initial value | number | - | - |
| disabled | If disable the input | boolean | false | - | | disabled | If disable the input | boolean | false | - |
| formatter | Specifies the format of the value presented | function(value: number \| string): string | - | - | | formatter | Specifies the format of the value presented | function(value: number \| string, info: { userTyping: boolean, input: string }): string | - | info: 4.17.0 |
| keyboard | If enable keyboard behavior | boolean | true | 4.12.0 | | keyboard | If enable keyboard behavior | boolean | true | 4.12.0 |
| max | The max value | number | [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) | - | | max | The max value | number | [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) | - |
| min | The min value | number | [Number.MIN_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) | - | | min | The min value | number | [Number.MIN_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) | - |

View File

@ -23,7 +23,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg
| decimalSeparator | 小数点 | string | - | - | | decimalSeparator | 小数点 | string | - | - |
| defaultValue | 初始值 | number | - | - | | defaultValue | 初始值 | number | - | - |
| disabled | 禁用 | boolean | false | - | | disabled | 禁用 | boolean | false | - |
| formatter | 指定输入框展示值的格式 | function(value: number \| string): string | - | - | | formatter | 指定输入框展示值的格式 | function(value: number \| string, info: { userTyping: boolean, input: string }): string | - | info: 4.17.0 |
| keyboard | 是否启用键盘快捷行为 | boolean | true | 4.12.0 | | keyboard | 是否启用键盘快捷行为 | boolean | true | 4.12.0 |
| max | 最大值 | number | [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) | - | | max | 最大值 | number | [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) | - |
| min | 最小值 | number | [Number.MIN_SAFE_INTEGER](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) | - | | min | 最小值 | number | [Number.MIN_SAFE_INTEGER](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) | - |

View File

@ -696,6 +696,7 @@ Array [
> >
<span <span
class="ant-cascader-picker-label" class="ant-cascader-picker-label"
title="Zhejiang / Hangzhou / West Lake"
> >
Zhejiang / Hangzhou / West Lake Zhejiang / Hangzhou / West Lake
</span> </span>

View File

@ -19,6 +19,7 @@ import LocaleProvider from '..';
import arEG from '../ar_EG'; import arEG from '../ar_EG';
import azAZ from '../az_AZ'; import azAZ from '../az_AZ';
import bgBG from '../bg_BG'; import bgBG from '../bg_BG';
import bnBD from '../bn_BD';
import byBY from '../by_BY'; import byBY from '../by_BY';
import caES from '../ca_ES'; import caES from '../ca_ES';
import csCZ from '../cs_CZ'; import csCZ from '../cs_CZ';
@ -81,6 +82,7 @@ const locales = [
azAZ, azAZ,
arEG, arEG,
bgBG, bgBG,
bnBD,
byBY, byBY,
caES, caES,
csCZ, csCZ,
@ -229,9 +231,10 @@ describe('Locale Provider', () => {
<ModalDemo /> <ModalDemo />
</LocaleProvider>, </LocaleProvider>,
); );
const currentConfirmNode = document.querySelectorAll('.ant-modal-confirm')[ const currentConfirmNode =
document.querySelectorAll('.ant-modal-confirm').length - 1 document.querySelectorAll('.ant-modal-confirm')[
]; document.querySelectorAll('.ant-modal-confirm').length - 1
];
let cancelButtonText = currentConfirmNode.querySelectorAll( let cancelButtonText = currentConfirmNode.querySelectorAll(
'.ant-btn:not(.ant-btn-primary) span', '.ant-btn:not(.ant-btn-primary) span',
)[0].innerHTML; )[0].innerHTML;

View File

@ -0,0 +1,3 @@
import locale from '../locale/bn_BD';
export default locale;

134
components/locale/bn_BD.tsx Normal file
View File

@ -0,0 +1,134 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/bn_BD';
import DatePicker from '../date-picker/locale/bn_BD';
import TimePicker from '../time-picker/locale/bn_BD';
import Calendar from '../calendar/locale/bn_BD';
import { Locale } from '../locale-provider';
const typeTemplate = '${label} টি সঠিক ${type} নয়।';
const localeValues: Locale = {
locale: 'bn-bd',
Pagination,
DatePicker,
TimePicker,
Calendar,
global: {
placeholder: 'অনুগ্রহ করে নির্বাচন করুন',
},
Table: {
filterTitle: 'ফিল্টার মেনু',
filterConfirm: 'ঠিক',
filterReset: 'রিসেট',
filterEmptyText: 'ফিল্টার নেই',
emptyText: 'কোনও ডেটা নেই',
selectAll: 'বর্তমান পৃষ্ঠা নির্বাচন করুন',
selectInvert: 'বর্তমান পৃষ্ঠাটি উল্টে দিন',
selectNone: 'সমস্ত ডেটা সাফ করুন',
selectionAll: 'সমস্ত ডেটা নির্বাচন করুন',
sortTitle: 'সাজান',
expand: 'সারি প্রসারিত করুন',
collapse: 'সারি সঙ্কুচিত করুন',
triggerDesc: 'অবতরণকে সাজানোর জন্য ক্লিক করুন',
triggerAsc: 'আরোহী বাছাই করতে ক্লিক করুন',
cancelSort: 'বাছাই বাতিল করতে ক্লিক করুন',
},
Modal: {
okText: 'ঠিক',
cancelText: 'বাতিল',
justOkText: 'ঠিক',
},
Popconfirm: {
okText: 'ঠিক',
cancelText: 'বাতিল',
},
Transfer: {
titles: ['', ''],
searchPlaceholder: 'এখানে অনুসন্ধান',
itemUnit: 'আইটেম',
itemsUnit: 'আইটেমসমূহ',
remove: 'অপসারণ',
selectCurrent: 'বর্তমান পৃষ্ঠা নির্বাচন করুন',
removeCurrent: 'বর্তমান পৃষ্ঠাটি সরান',
selectAll: 'সমস্ত ডেটা নির্বাচন করুন',
removeAll: 'সমস্ত ডেটা সরান',
selectInvert: 'বর্তমান পৃষ্ঠাটি উল্টে দিন',
},
Upload: {
uploading: 'আপলোড হচ্ছে ...',
removeFile: 'ফাইল সরান',
uploadError: 'আপলোডে সমস্যা',
previewFile: 'ফাইলের পূর্বরূপ',
downloadFile: 'ফাইল ডাউনলোড',
},
Empty: {
description: 'কোনও ডেটা নেই',
},
Icon: {
icon: 'আইকন',
},
Text: {
edit: 'সম্পাদনা',
copy: 'অনুলিপি',
copied: 'অনুলিপি হয়েছে',
expand: 'বিস্তৃত করা',
},
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} প্যাটার্নের সাথে মেলে না',
},
},
},
Image: {
preview: 'পূর্বরূপ',
},
};
export default localeValues;

View File

@ -96,19 +96,20 @@ describe('message.config', () => {
}); });
it('should be able to global config rootPrefixCls', () => { it('should be able to global config rootPrefixCls', () => {
ConfigProvider.config({ prefixCls: 'prefix-test' }); ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
message.info('last'); message.info('last');
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0); expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-message-notice').length).toBe(1); expect(document.querySelectorAll('.prefix-test-message-notice')).toHaveLength(1);
ConfigProvider.config({ prefixCls: 'ant' }); expect(document.querySelectorAll('.bamboo-info-circle')).toHaveLength(1);
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null });
}); });
it('should be able to config prefixCls', () => { it('should be able to config prefixCls', () => {
message.config({ message.config({
prefixCls: 'prefix-test', prefixCls: 'prefix-test',
}); });
message.info('last'); message.info('last');
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0); expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-notice').length).toBe(1); expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
message.config({ message.config({
prefixCls: '', // can be set to empty, ant default value is set in ConfigProvider prefixCls: '', // can be set to empty, ant default value is set in ConfigProvider
}); });
@ -119,7 +120,7 @@ describe('message.config', () => {
transitionName: '', transitionName: '',
}); });
message.info('last'); message.info('last');
expect(document.querySelectorAll('.ant-move-up-enter').length).toBe(0); expect(document.querySelectorAll('.ant-move-up-enter')).toHaveLength(0);
message.config({ message.config({
transitionName: 'ant-move-up', transitionName: 'ant-move-up',
}); });

View File

@ -11,7 +11,7 @@ import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled'; import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled'; import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
import createUseMessage from './hooks/useMessage'; import createUseMessage from './hooks/useMessage';
import { globalConfig } from '../config-provider'; import ConfigProvider, { globalConfig } from '../config-provider';
type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading'; type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
@ -74,16 +74,18 @@ function getRCNotificationInstance(
callback: (info: { callback: (info: {
prefixCls: string; prefixCls: string;
rootPrefixCls: string; rootPrefixCls: string;
iconPrefixCls: string;
instance: RCNotificationInstance; instance: RCNotificationInstance;
}) => void, }) => void,
) { ) {
const { prefixCls: customizePrefixCls } = args; const { prefixCls: customizePrefixCls } = args;
const { getPrefixCls, getRootPrefixCls } = globalConfig(); const { getPrefixCls, getRootPrefixCls, getIconPrefixCls } = globalConfig();
const prefixCls = getPrefixCls('message', customizePrefixCls || localPrefixCls); const prefixCls = getPrefixCls('message', customizePrefixCls || localPrefixCls);
const rootPrefixCls = getRootPrefixCls(args.rootPrefixCls, prefixCls); const rootPrefixCls = getRootPrefixCls(args.rootPrefixCls, prefixCls);
const iconPrefixCls = getIconPrefixCls();
if (messageInstance) { if (messageInstance) {
callback({ prefixCls, rootPrefixCls, instance: messageInstance }); callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance: messageInstance });
return; return;
} }
@ -97,7 +99,7 @@ function getRCNotificationInstance(
RCNotification.newInstance(instanceConfig, (instance: any) => { RCNotification.newInstance(instanceConfig, (instance: any) => {
if (messageInstance) { if (messageInstance) {
callback({ prefixCls, rootPrefixCls, instance: messageInstance }); callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance: messageInstance });
return; return;
} }
messageInstance = instance; messageInstance = instance;
@ -106,7 +108,7 @@ function getRCNotificationInstance(
(messageInstance as any).config = instanceConfig; (messageInstance as any).config = instanceConfig;
} }
callback({ prefixCls, rootPrefixCls, instance }); callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance });
}); });
} }
@ -139,7 +141,11 @@ export interface ArgsProps {
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void; onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
} }
function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent { function getRCNoticeProps(
args: ArgsProps,
prefixCls: string,
iconPrefixCls?: string,
): NoticeContent {
const duration = args.duration !== undefined ? args.duration : defaultDuration; const duration = args.duration !== undefined ? args.duration : defaultDuration;
const IconComponent = typeToIcon[args.type]; const IconComponent = typeToIcon[args.type];
const messageClass = classNames(`${prefixCls}-custom-content`, { const messageClass = classNames(`${prefixCls}-custom-content`, {
@ -152,10 +158,12 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent {
style: args.style || {}, style: args.style || {},
className: args.className, className: args.className,
content: ( content: (
<div className={messageClass}> <ConfigProvider iconPrefixCls={iconPrefixCls}>
{args.icon || (IconComponent && <IconComponent />)} <div className={messageClass}>
<span>{args.content}</span> {args.icon || (IconComponent && <IconComponent />)}
</div> <span>{args.content}</span>
</div>
</ConfigProvider>
), ),
onClose: args.onClose, onClose: args.onClose,
onClick: args.onClick, onClick: args.onClick,
@ -172,8 +180,10 @@ function notice(args: ArgsProps): MessageType {
return resolve(true); return resolve(true);
}; };
getRCNotificationInstance(args, ({ prefixCls, instance }) => { getRCNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
instance.notice(getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls)); instance.notice(
getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls, iconPrefixCls),
);
}); });
}); });
const result: any = () => { const result: any = () => {

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Dialog, { ModalFuncProps } from './Modal'; import Dialog, { ModalFuncProps } from './Modal';
import ActionButton from './ActionButton'; import ActionButton from '../_util/ActionButton';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import ConfigProvider from '../config-provider'; import ConfigProvider from '../config-provider';
import { getTransitionName } from '../_util/motion'; import { getTransitionName } from '../_util/motion';
@ -11,6 +11,7 @@ interface ConfirmDialogProps extends ModalFuncProps {
close: (...args: any[]) => void; close: (...args: any[]) => void;
autoFocusButton?: null | 'ok' | 'cancel'; autoFocusButton?: null | 'ok' | 'cancel';
rootPrefixCls: string; rootPrefixCls: string;
iconPrefixCls?: string;
} }
const ConfirmDialog = (props: ConfirmDialogProps) => { const ConfirmDialog = (props: ConfirmDialogProps) => {
@ -33,6 +34,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
direction, direction,
prefixCls, prefixCls,
rootPrefixCls, rootPrefixCls,
iconPrefixCls,
bodyStyle, bodyStyle,
closable = false, closable = false,
closeIcon, closeIcon,
@ -68,7 +70,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
const cancelButton = okCancel && ( const cancelButton = okCancel && (
<ActionButton <ActionButton
actionFn={onCancel} actionFn={onCancel}
closeModal={close} close={close}
autoFocus={autoFocusButton === 'cancel'} autoFocus={autoFocusButton === 'cancel'}
buttonProps={cancelButtonProps} buttonProps={cancelButtonProps}
prefixCls={`${rootPrefixCls}-btn`} prefixCls={`${rootPrefixCls}-btn`}
@ -78,33 +80,33 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
); );
return ( return (
<Dialog <ConfigProvider prefixCls={rootPrefixCls} iconPrefixCls={iconPrefixCls}>
prefixCls={prefixCls} <Dialog
className={classString} prefixCls={prefixCls}
wrapClassName={classNames({ [`${contentPrefixCls}-centered`]: !!props.centered })} className={classString}
onCancel={() => close({ triggerCancel: true })} wrapClassName={classNames({ [`${contentPrefixCls}-centered`]: !!props.centered })}
visible={visible} onCancel={() => close({ triggerCancel: true })}
title="" visible={visible}
footer="" title=""
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)} footer=""
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)} transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
mask={mask} maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
maskClosable={maskClosable} mask={mask}
maskStyle={maskStyle} maskClosable={maskClosable}
style={style} maskStyle={maskStyle}
width={width} style={style}
zIndex={zIndex} width={width}
afterClose={afterClose} zIndex={zIndex}
keyboard={keyboard} afterClose={afterClose}
centered={centered} keyboard={keyboard}
getContainer={getContainer} centered={centered}
closable={closable} getContainer={getContainer}
closeIcon={closeIcon} closable={closable}
modalRender={modalRender} closeIcon={closeIcon}
focusTriggerAfterClose={focusTriggerAfterClose} modalRender={modalRender}
> focusTriggerAfterClose={focusTriggerAfterClose}
<div className={`${contentPrefixCls}-body-wrapper`}> >
<ConfigProvider prefixCls={rootPrefixCls}> <div className={`${contentPrefixCls}-body-wrapper`}>
<div className={`${contentPrefixCls}-body`} style={bodyStyle}> <div className={`${contentPrefixCls}-body`} style={bodyStyle}>
{icon} {icon}
{props.title === undefined ? null : ( {props.title === undefined ? null : (
@ -112,22 +114,22 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
)} )}
<div className={`${contentPrefixCls}-content`}>{props.content}</div> <div className={`${contentPrefixCls}-content`}>{props.content}</div>
</div> </div>
</ConfigProvider> <div className={`${contentPrefixCls}-btns`}>
<div className={`${contentPrefixCls}-btns`}> {cancelButton}
{cancelButton} <ActionButton
<ActionButton type={okType}
type={okType} actionFn={onOk}
actionFn={onOk} close={close}
closeModal={close} autoFocus={autoFocusButton === 'ok'}
autoFocus={autoFocusButton === 'ok'} buttonProps={okButtonProps}
buttonProps={okButtonProps} prefixCls={`${rootPrefixCls}-btn`}
prefixCls={`${rootPrefixCls}-btn`} >
> {okText}
{okText} </ActionButton>
</ActionButton> </div>
</div> </div>
</div> </Dialog>
</Dialog> </ConfigProvider>
); );
}; };

View File

@ -1,5 +1,7 @@
import * as React from 'react';
import TestUtils, { act } from 'react-dom/test-utils'; import TestUtils, { act } from 'react-dom/test-utils';
import CSSMotion from 'rc-motion'; import CSSMotion from 'rc-motion';
import { SmileOutlined } from '@ant-design/icons';
import { genCSSMotion } from 'rc-motion/lib/CSSMotion'; import { genCSSMotion } from 'rc-motion/lib/CSSMotion';
import KeyCode from 'rc-util/lib/KeyCode'; import KeyCode from 'rc-util/lib/KeyCode';
import { resetWarned } from 'rc-util/lib/warning'; import { resetWarned } from 'rc-util/lib/warning';
@ -472,13 +474,14 @@ describe('Modal.confirm triggers callbacks correctly', () => {
it('should be able to global config rootPrefixCls', () => { it('should be able to global config rootPrefixCls', () => {
jest.useFakeTimers(); jest.useFakeTimers();
ConfigProvider.config({ prefixCls: 'my' }); ConfigProvider.config({ prefixCls: 'my', iconPrefixCls: 'bamboo' });
confirm({ title: 'title' }); confirm({ title: 'title', icon: <SmileOutlined /> });
jest.runAllTimers(); jest.runAllTimers();
expect(document.querySelectorAll('.ant-btn').length).toBe(0); expect(document.querySelectorAll('.ant-btn').length).toBe(0);
expect(document.querySelectorAll('.my-btn').length).toBe(2); expect(document.querySelectorAll('.my-btn').length).toBe(2);
expect(document.querySelectorAll('.bamboo-smile').length).toBe(1);
expect(document.querySelectorAll('.my-modal-confirm').length).toBe(1); expect(document.querySelectorAll('.my-modal-confirm').length).toBe(1);
ConfigProvider.config({ prefixCls: 'ant' }); ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null });
jest.useRealTimers(); jest.useRealTimers();
}); });

View File

@ -18,9 +18,7 @@ function getRootPrefixCls() {
type ConfigUpdate = ModalFuncProps | ((prevConfig: ModalFuncProps) => ModalFuncProps); type ConfigUpdate = ModalFuncProps | ((prevConfig: ModalFuncProps) => ModalFuncProps);
export type ModalFunc = ( export type ModalFunc = (props: ModalFuncProps) => {
props: ModalFuncProps,
) => {
destroy: () => void; destroy: () => void;
update: (configUpdate: ConfigUpdate) => void; update: (configUpdate: ConfigUpdate) => void;
}; };
@ -60,16 +58,18 @@ export default function confirm(config: ModalFuncProps) {
*/ */
setTimeout(() => { setTimeout(() => {
const runtimeLocale = getConfirmLocale(); const runtimeLocale = getConfirmLocale();
const { getPrefixCls } = globalConfig(); const { getPrefixCls, getIconPrefixCls } = globalConfig();
// because Modal.config  set rootPrefixCls, which is different from other components // because Modal.config  set rootPrefixCls, which is different from other components
const rootPrefixCls = getPrefixCls(undefined, getRootPrefixCls()); const rootPrefixCls = getPrefixCls(undefined, getRootPrefixCls());
const prefixCls = customizePrefixCls || `${rootPrefixCls}-modal`; const prefixCls = customizePrefixCls || `${rootPrefixCls}-modal`;
const iconPrefixCls = getIconPrefixCls();
ReactDOM.render( ReactDOM.render(
<ConfirmDialog <ConfirmDialog
{...props} {...props}
prefixCls={prefixCls} prefixCls={prefixCls}
rootPrefixCls={rootPrefixCls} rootPrefixCls={rootPrefixCls}
iconPrefixCls={iconPrefixCls}
okText={okText || (props.okCancel ? runtimeLocale.okText : runtimeLocale.justOkText)} okText={okText || (props.okCancel ? runtimeLocale.okText : runtimeLocale.justOkText)}
cancelText={cancelText || runtimeLocale.cancelText} cancelText={cancelText || runtimeLocale.cancelText}
/>, />,

View File

@ -11,7 +11,7 @@ title:
## en-US ## en-US
Asynchronously close a modal dialog when a the OK button is pressed. For example, you can use this pattern when you submit a form. Asynchronously close a modal dialog when the OK button is pressed. For example, you can use this pattern when you submit a form.
```jsx ```jsx
import { Modal, Button } from 'antd'; import { Modal, Button } from 'antd';

View File

@ -9,7 +9,6 @@ import confirm, {
modalGlobalConfig, modalGlobalConfig,
} from './confirm'; } from './confirm';
export { ActionButtonProps } from './ActionButton';
export { ModalProps, ModalFuncProps } from './Modal'; export { ModalProps, ModalFuncProps } from './Modal';
function modalWarn(props: ModalFuncProps) { function modalWarn(props: ModalFuncProps) {

View File

@ -102,11 +102,12 @@ describe('notification', () => {
}); });
it('should be able to global config rootPrefixCls', () => { it('should be able to global config rootPrefixCls', () => {
ConfigProvider.config({ prefixCls: 'prefix-test' }); ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
notification.open({ message: 'Notification Title', duration: 0 }); notification.success({ message: 'Notification Title', duration: 0 });
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(0); expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-notification-notice').length).toBe(1); expect(document.querySelectorAll('.prefix-test-notification-notice')).toHaveLength(1);
ConfigProvider.config({ prefixCls: 'ant' }); expect(document.querySelectorAll('.bamboo-check-circle')).toHaveLength(1);
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null });
}); });
it('should be able to config prefixCls', () => { it('should be able to config prefixCls', () => {
@ -117,8 +118,8 @@ describe('notification', () => {
message: 'Notification Title', message: 'Notification Title',
duration: 0, duration: 0,
}); });
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(0); expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-notice').length).toBe(1); expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
notification.config({ notification.config({
prefixCls: '', prefixCls: '',
}); });

View File

@ -8,7 +8,7 @@ import CloseCircleOutlined from '@ant-design/icons/CloseCircleOutlined';
import ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined'; import ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined';
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined'; import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import createUseNotification from './hooks/useNotification'; import createUseNotification from './hooks/useNotification';
import { globalConfig } from '../config-provider'; import ConfigProvider, { globalConfig } from '../config-provider';
export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
@ -108,7 +108,11 @@ function getPlacementStyle(
function getNotificationInstance( function getNotificationInstance(
args: ArgsProps, args: ArgsProps,
callback: (info: { prefixCls: string; instance: RCNotificationInstance }) => void, callback: (info: {
prefixCls: string;
iconPrefixCls: string;
instance: RCNotificationInstance;
}) => void,
) { ) {
const { const {
placement = defaultPlacement, placement = defaultPlacement,
@ -118,14 +122,15 @@ function getNotificationInstance(
closeIcon = defaultCloseIcon, closeIcon = defaultCloseIcon,
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
} = args; } = args;
const { getPrefixCls } = globalConfig(); const { getPrefixCls, getIconPrefixCls } = globalConfig();
const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls); const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
const iconPrefixCls = getIconPrefixCls();
const cacheKey = `${prefixCls}-${placement}`; const cacheKey = `${prefixCls}-${placement}`;
const cacheInstance = notificationInstance[cacheKey]; const cacheInstance = notificationInstance[cacheKey];
if (cacheInstance) { if (cacheInstance) {
Promise.resolve(cacheInstance).then(instance => { Promise.resolve(cacheInstance).then(instance => {
callback({ prefixCls: `${prefixCls}-notice`, instance }); callback({ prefixCls: `${prefixCls}-notice`, iconPrefixCls, instance });
}); });
return; return;
@ -154,6 +159,7 @@ function getNotificationInstance(
resolve(notification); resolve(notification);
callback({ callback({
prefixCls: `${prefixCls}-notice`, prefixCls: `${prefixCls}-notice`,
iconPrefixCls,
instance: notification, instance: notification,
}); });
}, },
@ -188,7 +194,7 @@ export interface ArgsProps {
closeIcon?: React.ReactNode; closeIcon?: React.ReactNode;
} }
function getRCNoticeProps(args: ArgsProps, prefixCls: string) { function getRCNoticeProps(args: ArgsProps, prefixCls: string, iconPrefixCls?: string) {
const { const {
duration: durationArg, duration: durationArg,
icon, icon,
@ -221,15 +227,17 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
return { return {
content: ( content: (
<div className={iconNode ? `${prefixCls}-with-icon` : ''} role="alert"> <ConfigProvider iconPrefixCls={iconPrefixCls}>
{iconNode} <div className={iconNode ? `${prefixCls}-with-icon` : ''} role="alert">
<div className={`${prefixCls}-message`}> {iconNode}
{autoMarginTag} <div className={`${prefixCls}-message`}>
{message} {autoMarginTag}
{message}
</div>
<div className={`${prefixCls}-description`}>{description}</div>
{btn ? <span className={`${prefixCls}-btn`}>{btn}</span> : null}
</div> </div>
<div className={`${prefixCls}-description`}>{description}</div> </ConfigProvider>
{btn ? <span className={`${prefixCls}-btn`}>{btn}</span> : null}
</div>
), ),
duration, duration,
closable: true, closable: true,
@ -244,8 +252,8 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
} }
function notice(args: ArgsProps) { function notice(args: ArgsProps) {
getNotificationInstance(args, ({ prefixCls, instance }) => { getNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
instance.notice(getRCNoticeProps(args, prefixCls)); instance.notice(getRCNoticeProps(args, prefixCls, iconPrefixCls));
}); });
} }

View File

@ -179,3 +179,14 @@ exports[`renders ./components/popconfirm/demo/placement.md correctly 1`] = `
</div> </div>
</div> </div>
`; `;
exports[`renders ./components/popconfirm/demo/promise.md correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Popconfirm with Promise
</span>
</button>
`;

View File

@ -131,6 +131,24 @@ describe('Popconfirm', () => {
expect(onVisibleChange).toHaveBeenLastCalledWith(false, eventObject); expect(onVisibleChange).toHaveBeenLastCalledWith(false, eventObject);
}); });
it('should support onConfirm to return Promise', async () => {
const confirm = () => new Promise(res => setTimeout(res, 300));
const onVisibleChange = jest.fn();
const popconfirm = mount(
<Popconfirm title="code" onConfirm={confirm} onVisibleChange={onVisibleChange}>
<span>show me your code</span>
</Popconfirm>,
);
const triggerNode = popconfirm.find('span').at(0);
triggerNode.simulate('click');
expect(onVisibleChange).toHaveBeenCalledTimes(1);
popconfirm.find('.ant-btn').at(0).simulate('click');
await sleep(400);
expect(onVisibleChange).toHaveBeenCalledWith(false, eventObject);
});
it('should support customize icon', () => { it('should support customize icon', () => {
const wrapper = mount( const wrapper = mount(
<Popconfirm title="code" icon={<span className="customize-icon">custom-icon</span>}> <Popconfirm title="code" icon={<span className="customize-icon">custom-icon</span>}>

View File

@ -0,0 +1,37 @@
---
order: 7
title:
zh-CN: 基于 Promise 的异步关闭
en-US: Asynchronously close on Promise
---
## zh-CN
点击确定后异步关闭 Popconfirm例如提交表单。
## en-US
Asynchronously close a popconfirm when the OK button is pressed. For example, you can use this pattern when you submit a form.
```jsx
import { Button, Popconfirm } from 'antd';
const App = () => {
const confirm = () =>
new Promise(resolve => {
setTimeout(() => resolve(), 3000);
});
return (
<Popconfirm
title="Title"
onConfirm={confirm}
onVisibleChange={() => console.log('visible change')}
>
<Button type="primary">Open Popconfirm with Promise</Button>
</Popconfirm>
);
};
ReactDOM.render(<App />, mountNode);
```

View File

@ -12,6 +12,7 @@ import { ConfigContext } from '../config-provider';
import { getRenderPropValue, RenderFunction } from '../_util/getRenderPropValue'; import { getRenderPropValue, RenderFunction } from '../_util/getRenderPropValue';
import { cloneElement } from '../_util/reactNode'; import { cloneElement } from '../_util/reactNode';
import { getTransitionName } from '../_util/motion'; import { getTransitionName } from '../_util/motion';
import ActionButton from '../_util/ActionButton';
export interface PopconfirmProps extends AbstractTooltipProps { export interface PopconfirmProps extends AbstractTooltipProps {
title: React.ReactNode | RenderFunction; title: React.ReactNode | RenderFunction;
@ -40,6 +41,7 @@ export interface PopconfirmLocale {
} }
const Popconfirm = React.forwardRef<unknown, PopconfirmProps>((props, ref) => { const Popconfirm = React.forwardRef<unknown, PopconfirmProps>((props, ref) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const [visible, setVisible] = useMergedState(false, { const [visible, setVisible] = useMergedState(false, {
value: props.visible, value: props.visible,
defaultValue: props.defaultVisible, defaultValue: props.defaultVisible,
@ -54,11 +56,12 @@ const Popconfirm = React.forwardRef<unknown, PopconfirmProps>((props, ref) => {
props.onVisibleChange?.(value, e); props.onVisibleChange?.(value, e);
}; };
const onConfirm = (e: React.MouseEvent<HTMLButtonElement>) => { const close = (e: React.MouseEvent<HTMLButtonElement>) => {
settingVisible(false, e); settingVisible(false, e);
props.onConfirm?.call(this, e);
}; };
const onConfirm = (e: React.MouseEvent<HTMLButtonElement>) => props.onConfirm?.call(this, e);
const onCancel = (e: React.MouseEvent<HTMLButtonElement>) => { const onCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
settingVisible(false, e); settingVisible(false, e);
props.onCancel?.call(this, e); props.onCancel?.call(this, e);
@ -90,21 +93,21 @@ const Popconfirm = React.forwardRef<unknown, PopconfirmProps>((props, ref) => {
<Button onClick={onCancel} size="small" {...cancelButtonProps}> <Button onClick={onCancel} size="small" {...cancelButtonProps}>
{cancelText || popconfirmLocale.cancelText} {cancelText || popconfirmLocale.cancelText}
</Button> </Button>
<Button <ActionButton
onClick={onConfirm} buttonProps={{ size: 'small', ...convertLegacyProps(okType), ...okButtonProps }}
{...convertLegacyProps(okType)} actionFn={onConfirm}
size="small" close={close}
{...okButtonProps} prefixCls={getPrefixCls('btn')}
quitOnNullishReturnValue
emitEvent
> >
{okText || popconfirmLocale.okText} {okText || popconfirmLocale.okText}
</Button> </ActionButton>
</div> </div>
</div> </div>
); );
}; };
const { getPrefixCls } = React.useContext(ConfigContext);
const { const {
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
placement, placement,

View File

@ -6,11 +6,12 @@ import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
export interface SkeletonButtonProps extends Omit<SkeletonElementProps, 'size'> { export interface SkeletonButtonProps extends Omit<SkeletonElementProps, 'size'> {
size?: 'large' | 'small' | 'default'; size?: 'large' | 'small' | 'default';
block?: boolean;
} }
const SkeletonButton = (props: SkeletonButtonProps) => { const SkeletonButton = (props: SkeletonButtonProps) => {
const renderSkeletonButton = ({ getPrefixCls }: ConfigConsumerProps) => { const renderSkeletonButton = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className, active } = props; const { prefixCls: customizePrefixCls, className, active, block = false } = props;
const prefixCls = getPrefixCls('skeleton', customizePrefixCls); const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
const otherProps = omit(props, ['prefixCls']); const otherProps = omit(props, ['prefixCls']);
const cls = classNames( const cls = classNames(
@ -18,6 +19,7 @@ const SkeletonButton = (props: SkeletonButtonProps) => {
`${prefixCls}-element`, `${prefixCls}-element`,
{ {
[`${prefixCls}-active`]: active, [`${prefixCls}-active`]: active,
[`${prefixCls}-block`]: block,
}, },
className, className,
); );

View File

@ -118,18 +118,6 @@ Array [
/> />
</div> </div>
</div> </div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<div
class="ant-skeleton ant-skeleton-element"
>
<span
class="ant-skeleton-button"
/>
</div>
</div>
<div <div
class="ant-space-item" class="ant-space-item"
style="margin-right:8px" style="margin-right:8px"
@ -157,6 +145,15 @@ Array [
</div>, </div>,
<br />, <br />,
<br />, <br />,
<div
class="ant-skeleton ant-skeleton-element"
>
<span
class="ant-skeleton-button"
/>
</div>,
<br />,
<br />,
<div <div
class="ant-skeleton ant-skeleton-element" class="ant-skeleton ant-skeleton-element"
> >
@ -222,6 +219,45 @@ Array [
</div> </div>
</div> </div>
</div> </div>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
title="Button Block"
>
Button Block
</label>
</div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
</div>
<div <div
class="ant-row ant-form-item" class="ant-row ant-form-item"
> >

View File

@ -250,6 +250,16 @@ exports[`Skeleton button element active 1`] = `
</div> </div>
`; `;
exports[`Skeleton button element block 1`] = `
<div
class="ant-skeleton ant-skeleton-element ant-skeleton-block"
>
<span
class="ant-skeleton-button"
/>
</div>
`;
exports[`Skeleton button element shape 1`] = ` exports[`Skeleton button element shape 1`] = `
<div <div
class="ant-skeleton ant-skeleton-element" class="ant-skeleton ant-skeleton-element"

View File

@ -80,6 +80,10 @@ describe('Skeleton', () => {
const wrapper = genSkeletonButton({ active: true }); const wrapper = genSkeletonButton({ active: true });
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
}); });
it('block', () => {
const wrapper = genSkeletonButton({ block: true });
expect(wrapper.render()).toMatchSnapshot();
});
it('size', () => { it('size', () => {
const wrapperDefault = genSkeletonButton({ size: 'default' }); const wrapperDefault = genSkeletonButton({ size: 'default' });
expect(wrapperDefault.render()).toMatchSnapshot(); expect(wrapperDefault.render()).toMatchSnapshot();

View File

@ -19,6 +19,7 @@ import { Skeleton, Space, Divider, Switch, Form, Radio } from 'antd';
class Demo extends React.Component { class Demo extends React.Component {
state = { state = {
active: false, active: false,
block: false,
size: 'default', size: 'default',
buttonShape: 'default', buttonShape: 'default',
avatarShape: 'circle', avatarShape: 'circle',
@ -28,6 +29,10 @@ class Demo extends React.Component {
this.setState({ active: checked }); this.setState({ active: checked });
}; };
handleBlockChange = checked => {
this.setState({ block: checked });
};
handleSizeChange = e => { handleSizeChange = e => {
this.setState({ size: e.target.value }); this.setState({ size: e.target.value });
}; };
@ -37,23 +42,28 @@ class Demo extends React.Component {
}; };
render() { render() {
const { active, size, buttonShape, avatarShape } = this.state; const { active, size, buttonShape, avatarShape, block } = this.state;
return ( return (
<> <>
<Space> <Space>
<Skeleton.Button active={active} size={size} shape={buttonShape} /> <Skeleton.Button active={active} size={size} shape={buttonShape} block={block} />
<Skeleton.Button active={active} size={size} shape={buttonShape} />
<Skeleton.Avatar active={active} size={size} shape={avatarShape} /> <Skeleton.Avatar active={active} size={size} shape={avatarShape} />
<Skeleton.Input style={{ width: 200 }} active={active} size={size} /> <Skeleton.Input style={{ width: 200 }} active={active} size={size} />
</Space> </Space>
<br /> <br />
<br /> <br />
<Skeleton.Button active={active} size={size} shape={buttonShape} block={block} />
<br />
<br />
<Skeleton.Image /> <Skeleton.Image />
<Divider /> <Divider />
<Form layout="inline" style={{ margin: '16px 0' }}> <Form layout="inline" style={{ margin: '16px 0' }}>
<Form.Item label="Active"> <Form.Item label="Active">
<Switch checked={active} onChange={this.handleActiveChange} /> <Switch checked={active} onChange={this.handleActiveChange} />
</Form.Item> </Form.Item>
<Form.Item label="Button Block">
<Switch checked={block} onChange={this.handleBlockChange} />
</Form.Item>
<Form.Item label="Size"> <Form.Item label="Size">
<Radio.Group value={size} onChange={this.handleSizeChange}> <Radio.Group value={size} onChange={this.handleSizeChange}>
<Radio.Button value="default">Default</Radio.Button> <Radio.Button value="default">Default</Radio.Button>

View File

@ -38,9 +38,9 @@ Provide a placeholder while you wait for content to load, or to visualise conten
### SkeletonTitleProps ### SkeletonTitleProps
| Property | Description | Type | Default | | Property | Description | Type | Default |
| --- | --- | --- | --- | | -------- | ---------------------- | ---------------- | ------- |
| width | Set the width of title | number \| string | - | | width | Set the width of title | number \| string | - |
### SkeletonParagraphProps ### SkeletonParagraphProps
@ -54,12 +54,13 @@ Provide a placeholder while you wait for content to load, or to visualise conten
| Property | Description | Type | Default | | Property | Description | Type | Default |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| active | Show animation effect | boolean | false | | active | Show animation effect | boolean | false |
| block | Option to fit button width to its parent width | boolean | false |
| shape | Set the shape of button | `circle` \| `round` \| `default` | - | | shape | Set the shape of button | `circle` \| `round` \| `default` | - |
| size | Set the size of button | `large` \| `small` \| `default` | - | | size | Set the size of button | `large` \| `small` \| `default` | - |
### SkeletonInputProps ### SkeletonInputProps
| Property | Description | Type | Default | | Property | Description | Type | Default |
| --- | --- | --- | --- | | -------- | --------------------- | ------------------------------- | ------- |
| active | Show animation effect | boolean | false | | active | Show animation effect | boolean | false |
| size | Set the size of input | `large` \| `small` \| `default` | - | | size | Set the size of input | `large` \| `small` \| `default` | - |

View File

@ -39,9 +39,9 @@ cover: https://gw.alipayobjects.com/zos/alicdn/KpcciCJgv/Skeleton.svg
### SkeletonTitleProps ### SkeletonTitleProps
| 属性 | 说明 | 类型 | 默认值 | | 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- | | ----- | -------------------- | ---------------- | ------ |
| width | 设置标题占位图的宽度 | number \| string | - | | width | 设置标题占位图的宽度 | number \| string | - |
### SkeletonParagraphProps ### SkeletonParagraphProps
@ -52,15 +52,16 @@ cover: https://gw.alipayobjects.com/zos/alicdn/KpcciCJgv/Skeleton.svg
### SkeletonButtonProps ### SkeletonButtonProps
| 属性 | 说明 | 类型 | 默认值 | | 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- | | ------ | ------------------------------ | -------------------------------- | ------ |
| active | 是否展示动画效果 | boolean | false | | active | 是否展示动画效果 | boolean | false |
| shape | 指定按钮的形状 | `circle` \| `round` \| `default` | - | | block | 将按钮宽度调整为其父宽度的选项 | boolean | false |
| size | 设置按钮的大小 | `large` \| `small` \| `default` | - | | shape | 指定按钮的形状 | `circle` \| `round` \| `default` | - |
| size | 设置按钮的大小 | `large` \| `small` \| `default` | - |
### SkeletonInputProps ### SkeletonInputProps
| 属性 | 说明 | 类型 | 默认值 | | 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- | | ------ | ---------------- | ------------------------------- | ------ |
| active | 是否展示动画效果 | boolean | false | | active | 是否展示动画效果 | boolean | false |
| size | 设置输入框的大小 | `large` \| `small` \| `default` | - | | size | 设置输入框的大小 | `large` \| `small` \| `default` | - |

View File

@ -109,6 +109,15 @@
} }
} }
// Skeleton Block Button
&.@{skeleton-prefix-cls}-block {
width: 100%;
.@{skeleton-button-prefix-cls} {
width: 100%;
}
}
// Skeleton element // Skeleton element
&-element { &-element {
display: inline-block; display: inline-block;
@ -214,10 +223,12 @@
.skeleton-element-button-size(@size) { .skeleton-element-button-size(@size) {
width: @size * 2; width: @size * 2;
min-width: @size * 2;
.skeleton-element-common-size(@size); .skeleton-element-common-size(@size);
&.@{skeleton-button-prefix-cls}-circle { &.@{skeleton-button-prefix-cls}-circle {
width: @size; width: @size;
min-width: @size;
border-radius: 50%; border-radius: 50%;
} }

View File

@ -16705,7 +16705,21 @@ exports[`renders ./components/table/demo/sticky.md correctly 1`] = `
colspan="2" colspan="2"
style="position:sticky;left:0" style="position:sticky;left:0"
> >
Fix Left <button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
Fixed Top
</span>
</button>
</td> </td>
<td <td
class="ant-table-cell" class="ant-table-cell"

View File

@ -14,7 +14,7 @@ title:
For long tableneed to scroll to view the header and scroll barthen you can now set the fixed header and scroll bar to follow the page. For long tableneed to scroll to view the header and scroll barthen you can now set the fixed header and scroll bar to follow the page.
```jsx ```jsx
import { Table } from 'antd'; import { Table, Switch } from 'antd';
const columns = [ const columns = [
{ {
@ -93,26 +93,38 @@ for (let i = 0; i < 100; i++) {
}); });
} }
ReactDOM.render( const Demo = () => {
<Table const [fixedTop, setFixedTop] = React.useState(false);
columns={columns}
dataSource={data} return (
scroll={{ x: 1500 }} <Table
summary={pageData => ( columns={columns}
<Table.Summary fixed> dataSource={data}
<Table.Summary.Row> scroll={{ x: 1500 }}
<Table.Summary.Cell index={0} colSpan={2}> summary={pageData => (
Fix Left <Table.Summary fixed={fixedTop ? 'top' : 'bottom'}>
</Table.Summary.Cell> <Table.Summary.Row>
<Table.Summary.Cell index={2} colSpan={8}> <Table.Summary.Cell index={0} colSpan={2}>
Scroll Context <Switch
</Table.Summary.Cell> checkedChildren="Fixed Top"
<Table.Summary.Cell index={10}>Fix Right</Table.Summary.Cell> unCheckedChildren="Fixed Top"
</Table.Summary.Row> checked={fixedTop}
</Table.Summary> onChange={() => {
)} setFixedTop(!fixedTop);
sticky }}
/>, />
mountNode, </Table.Summary.Cell>
); <Table.Summary.Cell index={2} colSpan={8}>
Scroll Context
</Table.Summary.Cell>
<Table.Summary.Cell index={10}>Fix Right</Table.Summary.Cell>
</Table.Summary.Row>
</Table.Summary>
)}
sticky
/>
);
};
ReactDOM.render(<Demo />, mountNode);
``` ```

View File

@ -0,0 +1,8 @@
import { TimePickerLocale } from '../index';
const locale: TimePickerLocale = {
placeholder: 'সময় নির্বাচন',
rangePlaceholder: ['সময় শুরু', 'শেষ সময়'],
};
export default locale;

View File

@ -146,11 +146,11 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
> >
<button <button
class="ant-btn ant-btn-sm" class="ant-btn ant-btn-sm"
style="float:right;margin:5px" style="float:left;margin:5px"
type="button" type="button"
> >
<span> <span>
reload Left button reload
</span> </span>
</button> </button>
</div> </div>
@ -361,7 +361,7 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
type="button" type="button"
> >
<span> <span>
reload Right button reload
</span> </span>
</button> </button>
</div> </div>

View File

@ -283,6 +283,19 @@ describe('Transfer', () => {
expect(headerText(wrapper)).toEqual('1/2 People'); expect(headerText(wrapper)).toEqual('1/2 People');
}); });
it('should display the correct notFoundContent', () => {
const wrapper = mount(
<Transfer dataSource={[]} locale={{ notFoundContent: ['No Source', 'No Target'] }} />,
);
expect(
wrapper.find(TransferList).at(0).find('.ant-transfer-list-body-not-found').at(0).text(),
).toEqual('No Source');
expect(
wrapper.find(TransferList).at(1).find('.ant-transfer-list-body-not-found').at(0).text(),
).toEqual('No Target');
});
it('should just check the filtered item when click on check all after search by input', () => { it('should just check the filtered item when click on check all after search by input', () => {
const filterOption = (inputValue, option) => option.description.indexOf(inputValue) > -1; const filterOption = (inputValue, option) => option.description.indexOf(inputValue) > -1;
const renderFunc = item => item.title; const renderFunc = item => item.title;

View File

@ -50,11 +50,20 @@ class App extends React.Component {
this.setState({ targetKeys }); this.setState({ targetKeys });
}; };
renderFooter = () => ( renderFooter = (props, { direction }) => {
<Button size="small" style={{ float: 'right', margin: 5 }} onClick={this.getMock}> if (direction === 'left') {
reload return (
</Button> <Button size="small" style={{ float: 'left', margin: 5 }} onClick={this.getMock}>
); Left button reload
</Button>
);
}
return (
<Button size="small" style={{ float: 'right', margin: 5 }} onClick={this.getMock}>
Right button reload
</Button>
);
};
render() { render() {
return ( return (

View File

@ -24,9 +24,9 @@ One or more elements can be selected from either column, one click on the proper
| dataSource | Used for setting the source data. The elements that are part of this array will be present the left column. Except the elements whose keys are included in `targetKeys` prop | [RecordType extends TransferItem = TransferItem](https://git.io/vMM64)\[] | \[] | | | dataSource | Used for setting the source data. The elements that are part of this array will be present the left column. Except the elements whose keys are included in `targetKeys` prop | [RecordType extends TransferItem = TransferItem](https://git.io/vMM64)\[] | \[] | |
| disabled | Whether disabled transfer | boolean | false | | | disabled | Whether disabled transfer | boolean | false | |
| filterOption | A function to determine whether an item should show in search result list | (inputValue, option): boolean | - | | | filterOption | A function to determine whether an item should show in search result list | (inputValue, option): boolean | - | |
| footer | A function used for rendering the footer | (props) => ReactNode | - | | | footer | A function used for rendering the footer | (props, { direction }) => ReactNode | - | direction: 4.17.0 |
| listStyle | A custom CSS style used for rendering the transfer columns | object \| ({direction: `left` \| `right`}) => object | - | | | listStyle | A custom CSS style used for rendering the transfer columns | object \| ({direction: `left` \| `right`}) => object | - | |
| locale | The i18n text including filter, empty text, item unit, etc | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode; } | { itemUnit: `item`, itemsUnit: `items`, notFoundContent: `The list is empty`, searchPlaceholder: `Search here` } | | | locale | The i18n text including filter, empty text, item unit, etc | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode \| ReactNode[]; } | { itemUnit: `item`, itemsUnit: `items`, notFoundContent: `The list is empty`, searchPlaceholder: `Search here` } | |
| oneWay | Display as single direction style | boolean | false | 4.3.0 | | oneWay | Display as single direction style | boolean | false | 4.3.0 |
| operations | A set of operations that are sorted from top to bottom | string\[] | \[`>`, `<`] | | | operations | A set of operations that are sorted from top to bottom | string\[] | \[`>`, `<`] | |
| operationStyle | A custom CSS style used for rendering the operations column | object | - | | | operationStyle | A custom CSS style used for rendering the operations column | object | - | |

View File

@ -47,7 +47,7 @@ export type SelectAllLabel =
export interface TransferLocale { export interface TransferLocale {
titles: React.ReactNode[]; titles: React.ReactNode[];
notFoundContent?: React.ReactNode; notFoundContent?: React.ReactNode | React.ReactNode[];
searchPlaceholder: string; searchPlaceholder: string;
itemUnit: string; itemUnit: string;
itemsUnit: string; itemsUnit: string;

View File

@ -27,9 +27,9 @@ cover: https://gw.alipayobjects.com/zos/alicdn/QAXskNI4G/Transfer.svg
| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外 | [RecordType extends TransferItem = TransferItem](https://git.io/vMM64)\[] | \[] | | | dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外 | [RecordType extends TransferItem = TransferItem](https://git.io/vMM64)\[] | \[] | |
| disabled | 是否禁用 | boolean | false | | | disabled | 是否禁用 | boolean | false | |
| filterOption | 接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 true反之则返回 false | (inputValue, option): boolean | - | | | filterOption | 接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 true反之则返回 false | (inputValue, option): boolean | - | |
| footer | 底部渲染函数 | (props) => ReactNode | - | | | footer | 底部渲染函数 | (props, { direction }) => ReactNode | - | direction: 4.17.0 |
| listStyle | 两个穿梭框的自定义样式 | object\|({direction: `left` \| `right`}) => object | - | | | listStyle | 两个穿梭框的自定义样式 | object\|({direction: `left` \| `right`}) => object | - | |
| locale | 各种语言 | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode; } | { itemUnit: `项`, itemsUnit: `项`, searchPlaceholder: `请输入搜索内容` } | | | locale | 各种语言 | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode \| ReactNode[]; } | { itemUnit: `项`, itemsUnit: `项`, searchPlaceholder: `请输入搜索内容` } | |
| oneWay | 展示为单向样式 | boolean | false | 4.3.0 | | oneWay | 展示为单向样式 | boolean | false | 4.3.0 |
| operations | 操作文案集合,顺序从上至下 | string\[] | \[`>`, `<`] | | | operations | 操作文案集合,顺序从上至下 | string\[] | \[`>`, `<`] | |
| pagination | 使用分页样式,自定义渲染列表下无效 | boolean \| { pageSize: number } | false | 4.3.0 | | pagination | 使用分页样式,自定义渲染列表下无效 | boolean \| { pageSize: number } | false | 4.3.0 |

View File

@ -59,7 +59,10 @@ export interface TransferListProps<RecordType> extends TransferLocale {
itemUnit: string; itemUnit: string;
itemsUnit: string; itemsUnit: string;
renderList?: RenderListFunction<RecordType>; renderList?: RenderListFunction<RecordType>;
footer?: (props: TransferListProps<RecordType>) => React.ReactNode; footer?: (
props: TransferListProps<RecordType>,
info?: { direction: TransferDirection },
) => React.ReactNode;
onScroll: (e: React.UIEvent<HTMLUListElement>) => void; onScroll: (e: React.UIEvent<HTMLUListElement>) => void;
disabled?: boolean; disabled?: boolean;
direction: TransferDirection; direction: TransferDirection;
@ -75,7 +78,7 @@ interface TransferListState {
} }
export default class TransferList< export default class TransferList<
RecordType extends KeyWiseTransferItem RecordType extends KeyWiseTransferItem,
> extends React.PureComponent<TransferListProps<RecordType>, TransferListState> { > extends React.PureComponent<TransferListProps<RecordType>, TransferListState> {
static defaultProps = { static defaultProps = {
dataSource: [], dataSource: [],
@ -183,7 +186,7 @@ export default class TransferList<
searchPlaceholder: string, searchPlaceholder: string,
filterValue: string, filterValue: string,
filteredItems: RecordType[], filteredItems: RecordType[],
notFoundContent: React.ReactNode, notFoundContent: React.ReactNode | React.ReactNode,
filteredRenderItems: RenderedItem<RecordType>[], filteredRenderItems: RenderedItem<RecordType>[],
checkedKeys: string[], checkedKeys: string[],
renderList?: RenderListFunction<RecordType>, renderList?: RenderListFunction<RecordType>,
@ -210,6 +213,11 @@ export default class TransferList<
selectedKeys: checkedKeys, selectedKeys: checkedKeys,
}); });
const getNotFoundContent = () => {
const contentIndex = this.props.direction === 'left' ? 0 : 1;
return Array.isArray(notFoundContent) ? notFoundContent[contentIndex] : notFoundContent;
};
let bodyNode: React.ReactNode; let bodyNode: React.ReactNode;
// We should wrap customize list body in a classNamed div to use flex layout. // We should wrap customize list body in a classNamed div to use flex layout.
if (customize) { if (customize) {
@ -218,7 +226,7 @@ export default class TransferList<
bodyNode = filteredItems.length ? ( bodyNode = filteredItems.length ? (
bodyContent bodyContent
) : ( ) : (
<div className={`${prefixCls}-body-not-found`}>{notFoundContent}</div> <div className={`${prefixCls}-body-not-found`}>{getNotFoundContent()}</div>
); );
} }
@ -314,10 +322,12 @@ export default class TransferList<
showSelectAll, showSelectAll,
showRemove, showRemove,
pagination, pagination,
direction,
} = this.props; } = this.props;
// Custom Layout // Custom Layout
const footerDom = footer && footer(this.props); const footerDom =
footer && (footer.length < 2 ? footer(this.props) : footer(this.props, { direction }));
const listCls = classNames(prefixCls, { const listCls = classNames(prefixCls, {
[`${prefixCls}-with-pagination`]: !!pagination, [`${prefixCls}-with-pagination`]: !!pagination,

View File

@ -381,3 +381,111 @@ exports[`renders ./components/tree-select/demo/treeData.md correctly 1`] = `
</span> </span>
</div> </div>
`; `;
exports[`renders ./components/tree-select/demo/treeLine.md correctly 1`] = `
<div
class="ant-space ant-space-vertical"
>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<button
aria-checked="true"
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
treeLine
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
showLeafIcon
</span>
</button>
</div>
<div
class="ant-space-item"
>
<div
class="ant-select ant-tree-select ant-select-single ant-select-show-arrow"
style="width:300px"
>
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
>
<input
aria-activedescendant="undefined_list_0"
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-select-selection-search-input"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
</span>
<span
class="ant-select-selection-placeholder"
/>
</div>
<span
aria-hidden="true"
class="ant-select-arrow"
style="user-select:none;-webkit-user-select:none"
unselectable="on"
>
<span
aria-label="down"
class="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
`;

View File

@ -45,6 +45,7 @@ class Demo extends React.Component {
treeData: this.state.treeData.concat([ treeData: this.state.treeData.concat([
this.genTreeNode(id, false), this.genTreeNode(id, false),
this.genTreeNode(id, true), this.genTreeNode(id, true),
this.genTreeNode(id, true),
]), ]),
}); });
resolve(); resolve();

View File

@ -0,0 +1,56 @@
---
order: 6
title:
zh-CN: 线性样式
en-US: Show Tree Line
---
## zh-CN
通过 `treeLine` 配置线性样式。
## en-US
Use `treeLine` to show the line style.
```tsx
import { TreeSelect, Switch, Space } from 'antd';
const { TreeNode } = TreeSelect;
const Demo = () => {
const [treeLine, setTreeLine] = React.useState(true);
const [showLeafIcon, setShowLeafIcon] = React.useState(false);
return (
<Space direction="vertical">
<Switch
checkedChildren="treeLine"
unCheckedChildren="treeLine"
checked={treeLine}
onChange={() => setTreeLine(!treeLine)}
/>
<Switch
disabled={!treeLine}
checkedChildren="showLeafIcon"
unCheckedChildren="showLeafIcon"
checked={showLeafIcon}
onChange={() => setShowLeafIcon(!showLeafIcon)}
/>
<TreeSelect treeLine={treeLine && { showLeafIcon }} style={{ width: 300 }}>
<TreeNode value="parent 1" title="parent 1">
<TreeNode value="parent 1-0" title="parent 1-0">
<TreeNode value="leaf1" title="my leaf" />
<TreeNode value="leaf2" title="your leaf" />
</TreeNode>
<TreeNode value="parent 1-1" title="parent 1-1">
<TreeNode value="sss" title="sss" />
</TreeNode>
</TreeNode>
</TreeSelect>
</Space>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -50,6 +50,7 @@ Tree selection control.
| treeDefaultExpandedKeys | Default expanded treeNodes | string\[] | - | | | treeDefaultExpandedKeys | Default expanded treeNodes | string\[] | - | |
| treeExpandedKeys | Set expanded keys | string\[] | - | | | treeExpandedKeys | Set expanded keys | string\[] | - | |
| treeIcon | Shows the icon before a TreeNode's title. There is no default style; you must set a custom style for it if set to `true` | boolean | false | | | treeIcon | Shows the icon before a TreeNode's title. There is no default style; you must set a custom style for it if set to `true` | boolean | false | |
| treeLine | Show the line. Ref [Tree - showLine](/components/tree/#components-tree-demo-line) | boolean \| object | false | 4.17.0 |
| treeNodeFilterProp | Will be used for filtering if `filterTreeNode` returns true | string | `value` | | | treeNodeFilterProp | Will be used for filtering if `filterTreeNode` returns true | string | `value` | |
| treeNodeLabelProp | Will render as content of select | string | `title` | | | treeNodeLabelProp | Will render as content of select | string | `title` | |
| value | To set the current selected treeNode(s) | string \| string\[] | - | | | value | To set the current selected treeNode(s) | string \| string\[] | - | |
@ -62,10 +63,10 @@ Tree selection control.
### Tree Methods ### Tree Methods
| Name | Description | Version | | Name | Description | Version |
| --- | --- | --- | | ------- | ------------ | ------- |
| blur() | Remove focus | | | blur() | Remove focus | |
| focus() | Get focus | | | focus() | Get focus | |
### TreeNode props ### TreeNode props

View File

@ -11,7 +11,7 @@ import omit from 'rc-util/lib/omit';
import { DefaultValueType } from 'rc-tree-select/lib/interface'; import { DefaultValueType } from 'rc-tree-select/lib/interface';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import { AntTreeNodeProps } from '../tree'; import { AntTreeNodeProps, TreeProps } from '../tree';
import getIcons from '../select/utils/iconUtil'; import getIcons from '../select/utils/iconUtil';
import renderSwitcherIcon from '../tree/utils/iconUtil'; import renderSwitcherIcon from '../tree/utils/iconUtil';
import SizeContext, { SizeType } from '../config-provider/SizeContext'; import SizeContext, { SizeType } from '../config-provider/SizeContext';
@ -30,11 +30,18 @@ export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[];
export interface TreeSelectProps<T> export interface TreeSelectProps<T>
extends Omit< extends Omit<
RcTreeSelectProps<T>, RcTreeSelectProps<T>,
'showTreeIcon' | 'treeMotion' | 'inputIcon' | 'mode' | 'getInputElement' | 'backfill' | 'showTreeIcon'
| 'treeMotion'
| 'inputIcon'
| 'mode'
| 'getInputElement'
| 'backfill'
| 'treeLine'
> { > {
suffixIcon?: React.ReactNode; suffixIcon?: React.ReactNode;
size?: SizeType; size?: SizeType;
bordered?: boolean; bordered?: boolean;
treeLine?: TreeProps['showLine'];
} }
export interface RefTreeSelectProps { export interface RefTreeSelectProps {
@ -140,6 +147,7 @@ const InternalTreeSelect = <T extends DefaultValueType>(
treeCheckable={ treeCheckable={
treeCheckable ? <span className={`${prefixCls}-tree-checkbox-inner`} /> : treeCheckable treeCheckable ? <span className={`${prefixCls}-tree-checkbox-inner`} /> : treeCheckable
} }
treeLine={!!treeLine}
inputIcon={suffixIcon} inputIcon={suffixIcon}
multiple={multiple} multiple={multiple}
removeIcon={removeIcon} removeIcon={removeIcon}

View File

@ -51,6 +51,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
| treeDefaultExpandedKeys | 默认展开的树节点 | string\[] | - | | | treeDefaultExpandedKeys | 默认展开的树节点 | string\[] | - | |
| treeExpandedKeys | 设置展开的树节点 | string\[] | - | | | treeExpandedKeys | 设置展开的树节点 | string\[] | - | |
| treeIcon | 是否展示 TreeNode title 前的图标,没有默认样式,如设置为 true需要自行定义图标相关样式 | boolean | false | | | treeIcon | 是否展示 TreeNode title 前的图标,没有默认样式,如设置为 true需要自行定义图标相关样式 | boolean | false | |
| treeLine | 是否展示线条样式,请参考 [Tree - showLine](/components/tree/#components-tree-demo-line) | boolean \| object | false | 4.17.0 |
| treeNodeFilterProp | 输入项过滤对应的 treeNode 属性 | string | `value` | | | treeNodeFilterProp | 输入项过滤对应的 treeNode 属性 | string | `value` | |
| treeNodeLabelProp | 作为显示的 prop 设置 | string | `title` | | | treeNodeLabelProp | 作为显示的 prop 设置 | string | `title` | |
| value | 指定当前选中的条目 | string \| string\[] | - | | | value | 指定当前选中的条目 | string \| string\[] | - | |
@ -63,25 +64,25 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
### Tree 方法 ### Tree 方法
| 名称 | 描述 | 版本 | | 名称 | 描述 | 版本 |
| --- | --- | --- | | ------- | -------- | ---- |
| blur() | 移除焦点 | | | blur() | 移除焦点 | |
| focus() | 获取焦点 | | | focus() | 获取焦点 | |
### TreeNode props ### TreeNode props
> 建议使用 treeData 来代替 TreeNode免去手工构造麻烦 > 建议使用 treeData 来代替 TreeNode免去手工构造麻烦
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --------------- | -------------------------------------------------- | --------- | ------ | ---- |
| checkable | 当树为 Checkbox 时,设置独立节点是否展示 Checkbox | boolean | - | | | checkable | 当树为 Checkbox 时,设置独立节点是否展示 Checkbox | boolean | - | |
| disableCheckbox | 禁掉 Checkbox | boolean | false | | | disableCheckbox | 禁掉 Checkbox | boolean | false | |
| disabled | 是否禁用 | boolean | false | | | disabled | 是否禁用 | boolean | false | |
| isLeaf | 是否是叶子节点 | boolean | false | | | isLeaf | 是否是叶子节点 | boolean | false | |
| key | 此项必须设置(其值在整个树范围内唯一) | string | - | | | key | 此项必须设置(其值在整个树范围内唯一) | string | - | |
| selectable | 是否可选 | boolean | true | | | selectable | 是否可选 | boolean | true | |
| title | 树节点显示的内容 | ReactNode | `---` | | | title | 树节点显示的内容 | ReactNode | `---` | |
| value | 默认根据此属性值进行筛选(其值在整个树范围内唯一) | string | - | | | value | 默认根据此属性值进行筛选(其值在整个树范围内唯一) | string | - | |
## FAQ ## FAQ

View File

@ -11,7 +11,7 @@
.@{tree-select-prefix-cls} { .@{tree-select-prefix-cls} {
// ======================= Dropdown ======================= // ======================= Dropdown =======================
&-dropdown { &-dropdown {
padding: @padding-xs (@padding-xs / 2) 0; padding: @padding-xs (@padding-xs / 2);
&-rtl { &-rtl {
direction: rtl; direction: rtl;
@ -24,8 +24,6 @@
align-items: stretch; align-items: stretch;
.@{select-tree-prefix-cls}-treenode { .@{select-tree-prefix-cls}-treenode {
padding-bottom: @padding-xs;
.@{select-tree-prefix-cls}-node-content-wrapper { .@{select-tree-prefix-cls}-node-content-wrapper {
flex: auto; flex: auto;
} }

View File

@ -1,7 +1,6 @@
@import '../../style/mixins/index'; @import '../../style/mixins/index';
@tree-prefix-cls: ~'@{ant-prefix}-tree'; @tree-prefix-cls: ~'@{ant-prefix}-tree';
@tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode';
@select-tree-prefix-cls: ~'@{ant-prefix}-select-tree'; @select-tree-prefix-cls: ~'@{ant-prefix}-select-tree';
@tree-motion: ~'@{ant-prefix}-motion-collapse'; @tree-motion: ~'@{ant-prefix}-motion-collapse';
@tree-node-padding: (@padding-xs / 2); @tree-node-padding: (@padding-xs / 2);
@ -259,15 +258,15 @@
} }
} }
} }
}
.@{tree-node-prefix-cls}-leaf-last { .@{custom-tree-node-prefix-cls}-leaf-last {
.@{tree-prefix-cls}-switcher { .@{custom-tree-prefix-cls}-switcher {
&-leaf-line { &-leaf-line {
&::before { &::before {
top: auto !important; top: auto !important;
bottom: auto !important; bottom: auto !important;
height: @tree-title-height - 10px !important; height: @tree-title-height - 10px !important;
}
} }
} }
} }

View File

@ -33,6 +33,7 @@ The following languages are currently supported:
| Arabic | ar_EG | | Arabic | ar_EG |
| Azerbaijani | az_AZ | | Azerbaijani | az_AZ |
| Bulgarian | bg_BG | | Bulgarian | bg_BG |
| Bangla (Bangladesh) | bn_BD |
| Belarusian | by_BY | | Belarusian | by_BY |
| Catalan | ca_ES | | Catalan | ca_ES |
| Czech | cs_CZ | | Czech | cs_CZ |

View File

@ -30,6 +30,7 @@ return (
| 阿拉伯语 | ar_EG | | 阿拉伯语 | ar_EG |
| 阿塞拜疆语 | az_AZ | | 阿塞拜疆语 | az_AZ |
| 保加利亚语 | bg_BG | | 保加利亚语 | bg_BG |
| 孟加拉语(孟加拉国) | bn_BD |
| 白俄罗斯语 | by_BY | | 白俄罗斯语 | by_BY |
| 加泰罗尼亚语 | ca_ES | | 加泰罗尼亚语 | ca_ES |
| 捷克语 | cs_CZ | | 捷克语 | cs_CZ |

View File

@ -123,15 +123,15 @@
"rc-dialog": "~8.5.1", "rc-dialog": "~8.5.1",
"rc-drawer": "~4.3.0", "rc-drawer": "~4.3.0",
"rc-dropdown": "~3.2.0", "rc-dropdown": "~3.2.0",
"rc-field-form": "~1.20.0", "rc-field-form": "~1.21.0-2",
"rc-image": "~5.2.4", "rc-image": "~5.2.4",
"rc-input-number": "~7.1.0", "rc-input-number": "~7.2.0",
"rc-mentions": "~1.6.1", "rc-mentions": "~1.6.1",
"rc-menu": "~9.0.12", "rc-menu": "~9.0.12",
"rc-motion": "^2.4.0", "rc-motion": "^2.4.4",
"rc-notification": "~4.5.7", "rc-notification": "~4.5.7",
"rc-pagination": "~3.1.6", "rc-pagination": "~3.1.7",
"rc-picker": "~2.5.10", "rc-picker": "~2.5.14",
"rc-progress": "~3.1.0", "rc-progress": "~3.1.0",
"rc-rate": "~2.9.0", "rc-rate": "~2.9.0",
"rc-resize-observer": "^1.0.0", "rc-resize-observer": "^1.0.0",
@ -139,7 +139,7 @@
"rc-slider": "~9.7.1", "rc-slider": "~9.7.1",
"rc-steps": "~4.1.0", "rc-steps": "~4.1.0",
"rc-switch": "~3.2.0", "rc-switch": "~3.2.0",
"rc-table": "~7.15.1", "rc-table": "~7.16.0",
"rc-tabs": "~11.9.1", "rc-tabs": "~11.9.1",
"rc-textarea": "~0.3.0", "rc-textarea": "~0.3.0",
"rc-tooltip": "~5.1.1", "rc-tooltip": "~5.1.1",