merge feature into master

This commit is contained in:
afc163 2021-08-31 13:35:13 +08:00
commit 7085e22a96
122 changed files with 23038 additions and 6330 deletions

View File

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

View File

@ -3,7 +3,10 @@ import { MotionEvent } from 'rc-motion/lib/interface';
// ================== Collapse Motion ==================
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 ? node.offsetHeight : 0 });
const skipOpacityTransition: MotionEndEventHandler = (_, event: MotionEvent) =>
event?.deadline === true || (event as TransitionEvent).propertyName === 'height';

View File

@ -193,4 +193,22 @@ describe('Avatar Render', () => {
wrapper.detach();
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;
children?: React.ReactNode;
alt?: string;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
/* callback when img load error */
/* return false to prevent Avatar show default fallback behavior, then you can do fallback by your self */
onError?: () => boolean;
@ -95,6 +96,7 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
alt,
draggable,
children,
crossOrigin,
...others
} = props;
@ -158,7 +160,14 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
let childrenToRender;
if (typeof src === 'string' && isImgExist) {
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) {
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 |
| srcSet | A list of sources to use for different screen resolutions | string | - | |
| 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 | - | |
> 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 |
| srcSet | 设置图片类头像响应式资源地址 | string | - | |
| draggable | 图片是否允许拖动 | boolean \| `'true'` \| `'false'` | - | |
| crossOrigin | CORS 属性设置 | `'anonymous'` \| `'use-credentials'` \| `''` | - | 4.17.0 |
| onError | 图片加载失败的事件,返回 false 会关闭组件默认的 fallback 行为 | () => boolean | - | |
> 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

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

View File

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

View File

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

View File

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

View File

@ -316,7 +316,7 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
};
getLabel() {
const { options, displayRender = defaultDisplayRender as Function } = this.props;
const { options, displayRender = defaultDisplayRender } = this.props;
const names = getFilledFieldNames(this.props);
const { value } = this.state;
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
@ -618,9 +618,15 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
inputIcon = <DownOutlined className={arrowCls} />;
}
const label = this.getLabel();
const input: React.ReactElement = children || (
<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
{...inputProps}
tabIndex={-1}

View File

@ -48,7 +48,7 @@
direction: ltr;
background-color: @checkbox-check-bg;
border: @checkbox-border-width @border-style-base @border-color-base;
border-radius: @border-radius-base;
border-radius: @checkbox-border-radius;
// Fix IE checked style
// https://github.com/ant-design/ant-design/issues/12597
border-collapse: separate;

View File

@ -12199,7 +12199,7 @@ exports[`ConfigProvider components Drawer configProvider 1`] = `
/>
<div
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
class="config-drawer-content"
@ -12208,34 +12208,37 @@ exports[`ConfigProvider components Drawer configProvider 1`] = `
class="config-drawer-wrapper-body"
>
<div
class="config-drawer-header-no-title"
class="config-drawer-header config-drawer-header-close-only"
>
<button
aria-label="Close"
class="config-drawer-close"
style="--scroll-bar:0px"
type="button"
<div
class="config-drawer-header-title"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
<button
aria-label="Close"
class="config-drawer-close"
type="button"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<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>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="config-drawer-body"
@ -12260,7 +12263,7 @@ exports[`ConfigProvider components Drawer configProvider componentSize large 1`]
/>
<div
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
class="config-drawer-content"
@ -12269,34 +12272,37 @@ exports[`ConfigProvider components Drawer configProvider componentSize large 1`]
class="config-drawer-wrapper-body"
>
<div
class="config-drawer-header-no-title"
class="config-drawer-header config-drawer-header-close-only"
>
<button
aria-label="Close"
class="config-drawer-close"
style="--scroll-bar:0px"
type="button"
<div
class="config-drawer-header-title"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
<button
aria-label="Close"
class="config-drawer-close"
type="button"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<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>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="config-drawer-body"
@ -12321,7 +12327,7 @@ exports[`ConfigProvider components Drawer configProvider componentSize middle 1`
/>
<div
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
class="config-drawer-content"
@ -12330,34 +12336,37 @@ exports[`ConfigProvider components Drawer configProvider componentSize middle 1`
class="config-drawer-wrapper-body"
>
<div
class="config-drawer-header-no-title"
class="config-drawer-header config-drawer-header-close-only"
>
<button
aria-label="Close"
class="config-drawer-close"
style="--scroll-bar:0px"
type="button"
<div
class="config-drawer-header-title"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
<button
aria-label="Close"
class="config-drawer-close"
type="button"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<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>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="config-drawer-body"
@ -12382,7 +12391,7 @@ exports[`ConfigProvider components Drawer configProvider virtual and dropdownMat
/>
<div
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
class="ant-drawer-content"
@ -12391,34 +12400,37 @@ exports[`ConfigProvider components Drawer configProvider virtual and dropdownMat
class="ant-drawer-wrapper-body"
>
<div
class="ant-drawer-header-no-title"
class="ant-drawer-header ant-drawer-header-close-only"
>
<button
aria-label="Close"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
<div
class="ant-drawer-header-title"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
<button
aria-label="Close"
class="ant-drawer-close"
type="button"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<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>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="ant-drawer-body"
@ -12443,7 +12455,7 @@ exports[`ConfigProvider components Drawer normal 1`] = `
/>
<div
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
class="ant-drawer-content"
@ -12452,34 +12464,37 @@ exports[`ConfigProvider components Drawer normal 1`] = `
class="ant-drawer-wrapper-body"
>
<div
class="ant-drawer-header-no-title"
class="ant-drawer-header ant-drawer-header-close-only"
>
<button
aria-label="Close"
class="ant-drawer-close"
style="--scroll-bar:0px"
type="button"
<div
class="ant-drawer-header-title"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
<button
aria-label="Close"
class="ant-drawer-close"
type="button"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<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>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="ant-drawer-body"
@ -12504,7 +12519,7 @@ exports[`ConfigProvider components Drawer prefixCls 1`] = `
/>
<div
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
class="prefix-Drawer-content"
@ -12513,34 +12528,37 @@ exports[`ConfigProvider components Drawer prefixCls 1`] = `
class="prefix-Drawer-wrapper-body"
>
<div
class="prefix-Drawer-header-no-title"
class="prefix-Drawer-header prefix-Drawer-header-close-only"
>
<button
aria-label="Close"
class="prefix-Drawer-close"
style="--scroll-bar:0px"
type="button"
<div
class="prefix-Drawer-header-title"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
<button
aria-label="Close"
class="prefix-Drawer-close"
type="button"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<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>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="prefix-Drawer-body"
@ -13260,9 +13278,10 @@ exports[`ConfigProvider components Form configProvider 1`] = `
</div>
</div>
<div
class="config-form-item-explain config-form-item-explain-error"
class="config-form-item-explain config-form-item-explain-connected"
>
<div
class="config-form-item-explain-error"
role="alert"
>
Bamboo is Light
@ -13297,9 +13316,10 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
</div>
</div>
<div
class="config-form-item-explain config-form-item-explain-error"
class="config-form-item-explain config-form-item-explain-connected"
>
<div
class="config-form-item-explain-error"
role="alert"
>
Bamboo is Light
@ -13334,9 +13354,10 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
</div>
</div>
<div
class="config-form-item-explain config-form-item-explain-error"
class="config-form-item-explain config-form-item-explain-connected"
>
<div
class="config-form-item-explain-error"
role="alert"
>
Bamboo is Light
@ -13371,9 +13392,10 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Bamboo is Light
@ -13408,9 +13430,10 @@ exports[`ConfigProvider components Form normal 1`] = `
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Bamboo is Light
@ -13445,9 +13468,10 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
</div>
</div>
<div
class="prefix-Form-item-explain prefix-Form-item-explain-error"
class="prefix-Form-item-explain prefix-Form-item-explain-connected"
>
<div
class="prefix-Form-item-explain-error"
role="alert"
>
Bamboo is Light
@ -35502,11 +35526,11 @@ exports[`ConfigProvider components Tree configProvider 1`] = `
<div>
<div
class="config-tree config-tree-icon-hide"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -35565,11 +35589,11 @@ exports[`ConfigProvider components Tree configProvider 1`] = `
</div>
<div
class="config-tree config-tree-block-node config-tree-directory"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -35656,11 +35680,11 @@ exports[`ConfigProvider components Tree configProvider componentSize large 1`] =
<div>
<div
class="config-tree config-tree-icon-hide"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -35719,11 +35743,11 @@ exports[`ConfigProvider components Tree configProvider componentSize large 1`] =
</div>
<div
class="config-tree config-tree-block-node config-tree-directory"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -35810,11 +35834,11 @@ exports[`ConfigProvider components Tree configProvider componentSize middle 1`]
<div>
<div
class="config-tree config-tree-icon-hide"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -35873,11 +35897,11 @@ exports[`ConfigProvider components Tree configProvider componentSize middle 1`]
</div>
<div
class="config-tree config-tree-block-node config-tree-directory"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -35964,11 +35988,11 @@ exports[`ConfigProvider components Tree configProvider virtual and dropdownMatch
<div>
<div
class="ant-tree ant-tree-icon-hide"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -36027,11 +36051,11 @@ exports[`ConfigProvider components Tree configProvider virtual and dropdownMatch
</div>
<div
class="ant-tree ant-tree-block-node ant-tree-directory"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -36118,11 +36142,11 @@ exports[`ConfigProvider components Tree normal 1`] = `
<div>
<div
class="ant-tree ant-tree-icon-hide"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -36181,11 +36205,11 @@ exports[`ConfigProvider components Tree normal 1`] = `
</div>
<div
class="ant-tree ant-tree-block-node ant-tree-directory"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -36272,11 +36296,11 @@ exports[`ConfigProvider components Tree prefixCls 1`] = `
<div>
<div
class="prefix-Tree prefix-Tree-icon-hide"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""
@ -36335,11 +36359,11 @@ exports[`ConfigProvider components Tree prefixCls 1`] = `
</div>
<div
class="prefix-Tree prefix-Tree-block-node prefix-Tree-directory"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""

View File

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

View File

@ -85,11 +85,19 @@ interface ProviderChildrenProps extends ConfigProviderProps {
}
export const defaultPrefixCls = 'ant';
export const defaultIconPrefixCls = 'anticon';
let globalPrefixCls: string;
let globalIconPrefixCls: string;
const setGlobalConfig = (params: Pick<ConfigProviderProps, 'prefixCls'>) => {
if (params.prefixCls !== undefined) {
globalPrefixCls = params.prefixCls;
const setGlobalConfig = ({
prefixCls,
iconPrefixCls,
}: Pick<ConfigProviderProps, 'prefixCls' | 'iconPrefixCls'>) => {
if (prefixCls !== undefined) {
globalPrefixCls = prefixCls;
}
if (iconPrefixCls !== undefined) {
globalIconPrefixCls = iconPrefixCls;
}
};
@ -97,11 +105,16 @@ function getGlobalPrefixCls() {
return globalPrefixCls || defaultPrefixCls;
}
function getGlobalIconPrefixCls() {
return globalIconPrefixCls || defaultIconPrefixCls;
}
export const globalConfig = () => ({
getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => {
if (customizePrefixCls) return customizePrefixCls;
return suffixCls ? `${getGlobalPrefixCls()}-${suffixCls}` : getGlobalPrefixCls();
},
getIconPrefixCls: getGlobalIconPrefixCls,
getRootPrefixCls: (rootPrefixCls?: string, customizePrefixCls?: string) => {
// Customize rootPrefixCls is first priority
if (rootPrefixCls) {

View File

@ -62,7 +62,8 @@ export default () => (
```jsx
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

@ -0,0 +1,27 @@
import CalendarLocale from 'rc-picker/lib/locale/ml_IN';
import TimePickerLocale from '../../time-picker/locale/ml_IN';
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

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

View File

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

View File

@ -26,6 +26,111 @@ exports[`renders ./components/drawer/demo/config-provider.md correctly 1`] = `
</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`] = `
<button
class="ant-btn ant-btn-primary"
@ -55,7 +160,7 @@ exports[`renders ./components/drawer/demo/form-in-drawer.md correctly 1`] = `
</svg>
</span>
<span>
New account
New account
</span>
</button>
`;
@ -217,7 +322,7 @@ exports[`renders ./components/drawer/demo/render-in-current.md correctly 1`] = `
/>
<div
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
class="ant-drawer-content"
@ -229,9 +334,13 @@ exports[`renders ./components/drawer/demo/render-in-current.md correctly 1`] = `
class="ant-drawer-header"
>
<div
class="ant-drawer-title"
class="ant-drawer-header-title"
>
Basic Drawer
<div
class="ant-drawer-title"
>
Basic Drawer
</div>
</div>
</div>
<div
@ -249,6 +358,38 @@ exports[`renders ./components/drawer/demo/render-in-current.md correctly 1`] = `
</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`] = `
<div
class="ant-list ant-list-split ant-list-bordered"

View File

@ -7,7 +7,7 @@ title:
## zh-CN
基础抽屉,点击触发按钮抽屉从右滑出,点击遮罩区关闭
基础抽屉,点击触发按钮抽屉从右滑出,点击遮罩区关闭
## en-US
@ -30,13 +30,7 @@ const App: React.FC = () => {
<Button type="primary" onClick={showDrawer}>
Open
</Button>
<Drawer
title="Basic Drawer"
placement="right"
closable={false}
onClose={onClose}
visible={visible}
>
<Drawer title="Basic Drawer" placement="right" onClose={onClose} visible={visible}>
<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.
```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';
const { Option } = Select;
@ -37,8 +37,8 @@ class DrawerForm extends React.Component {
render() {
return (
<>
<Button type="primary" onClick={this.showDrawer}>
<PlusOutlined /> New account
<Button type="primary" onClick={this.showDrawer} icon={<PlusOutlined />}>
New account
</Button>
<Drawer
title="Create a new account"
@ -46,19 +46,13 @@ class DrawerForm extends React.Component {
onClose={this.onClose}
visible={this.state.visible}
bodyStyle={{ paddingBottom: 80 }}
footer={
<div
style={{
textAlign: 'right',
}}
>
<Button onClick={this.onClose} style={{ marginRight: 8 }}>
Cancel
</Button>
extra={
<Space>
<Button onClick={this.onClose}>Cancel</Button>
<Button onClick={this.onClose} type="primary">
Submit
</Button>
</div>
</Space>
}
>
<Form layout="vertical" hideRequiredMark>

View File

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

View File

@ -7,11 +7,11 @@ title:
## zh-CN
渲染在当前 dom 里。自定义容器,查看 getContainer。
渲染在当前 dom 里。自定义容器,查看 `getContainer`
## en-US
Render in current dom. custom container, check getContainer.
Render in current dom. custom container, check `getContainer`.
```jsx
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 | - | |
| destroyOnClose | Whether to unmount child components on closing drawer or not | boolean | false | |
| drawerStyle | Style of the popup layer element | object | - | |
| extra | Extra actions area at corner | ReactNode | - | 4.17.0 |
| footer | The footer for Drawer | ReactNode | - | |
| footerStyle | Style of the drawer footer part | CSSProperties | - | |
| 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` | |
| 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 | - | |
| size | presetted size of drawer, default `378px` and large `736px` | 'default' \| 'large' | 'default' | 4.17.0 |
| title | The title for Drawer | ReactNode | - | |
| 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 | |
| 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 RcDrawer from 'rc-drawer';
import getScrollBarSize from 'rc-util/lib/getScrollBarSize';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames';
import { ConfigContext, DirectionType } from '../config-provider';
@ -23,6 +22,9 @@ type getContainerFunc = () => HTMLElement;
const PlacementTypes = tuple('top', 'right', 'bottom', 'left');
type placementType = typeof PlacementTypes[number];
const SizeTypes = tuple('default', 'large');
type sizeType = typeof SizeTypes[number];
export interface PushState {
distance: string | number;
}
@ -36,6 +38,7 @@ export interface DrawerProps {
mask?: boolean;
maskStyle?: React.CSSProperties;
style?: React.CSSProperties;
size?: sizeType;
/** Wrapper dom node style of header and body */
drawerStyle?: React.CSSProperties;
headerStyle?: React.CSSProperties;
@ -54,6 +57,7 @@ export interface DrawerProps {
className?: string;
handler?: React.ReactNode;
keyboard?: boolean;
extra?: React.ReactNode;
footer?: React.ReactNode;
footerStyle?: React.CSSProperties;
level?: string | string[] | null | undefined;
@ -72,8 +76,9 @@ const defaultPushState: PushState = { distance: 180 };
const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
(
{
width = 256,
height = 256,
width,
height,
size = 'default',
closable = true,
placement = 'right' as placementType,
maskClosable = true,
@ -97,6 +102,7 @@ const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
onClose,
footer,
footerStyle,
extra,
...rest
},
ref,
@ -168,9 +174,11 @@ const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
}
const offsetStyle: any = {};
if (placement === 'left' || placement === 'right') {
offsetStyle.width = width;
const defaultWidth = size === 'large' ? 736 : 378;
offsetStyle.width = typeof width === 'undefined' ? defaultWidth : width;
} else {
offsetStyle.height = height;
const defaultHeight = size === 'large' ? 736 : 378;
offsetStyle.height = typeof height === 'undefined' ? defaultHeight : height;
}
return offsetStyle;
};
@ -205,36 +213,29 @@ const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
};
};
function renderCloseIcon() {
return (
closable && (
<button
type="button"
onClick={onClose}
aria-label="Close"
className={`${prefixCls}-close`}
style={
{
'--scroll-bar': `${getScrollBarSize()}px`,
} as any
}
>
{closeIcon}
</button>
)
);
}
const closeIconNode = closable && (
<button type="button" onClick={onClose} aria-label="Close" className={`${prefixCls}-close`}>
{closeIcon}
</button>
);
function renderHeader() {
if (!title && !closable) {
return null;
}
const headerClassName = title ? `${prefixCls}-header` : `${prefixCls}-header-no-title`;
return (
<div className={headerClassName} style={headerStyle}>
{title && <div className={`${prefixCls}-title`}>{title}</div>}
{closable && renderCloseIcon()}
<div
className={classNames(`${prefixCls}-header`, {
[`${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>
);
}

View File

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

View File

@ -148,12 +148,8 @@
}
&-close {
position: absolute;
top: 0;
right: 0;
z-index: @zindex-popup-close;
display: block;
padding: @drawer-header-close-padding;
display: inline-block;
margin-right: 12px;
color: @modal-close-color;
font-weight: 700;
font-size: @font-size-lg;
@ -174,26 +170,29 @@
color: @icon-color-hover;
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 {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: @drawer-header-padding;
color: @text-color;
background: @drawer-bg;
border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0;
}
&-header-no-title {
color: @text-color;
background: @drawer-bg;
&-title {
display: flex;
align-items: center;
justify-content: space-between;
}
&-close-only {
padding-bottom: 0;
border: none;
}
}
&-wrapper-body {

View File

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

View File

@ -1,95 +1,117 @@
import * as React from 'react';
import classNames from 'classnames';
import CSSMotion from 'rc-motion';
import useMemo from 'rc-util/lib/hooks/useMemo';
import useCacheErrors from './hooks/useCacheErrors';
import useForceUpdate from '../_util/hooks/useForceUpdate';
import CSSMotion, { CSSMotionList } from 'rc-motion';
import { FormItemPrefixContext } from './context';
import { ConfigContext } from '../config-provider';
import { ValidateStatus } from './FormItem';
import collapseMotion from '../_util/motion';
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 {
errors?: React.ReactNode[];
/** @private Internal Usage. Do not use in your production */
help?: React.ReactNode;
/** @private Internal Usage. Do not use in your production */
onDomErrorVisibleChange?: (visible: boolean) => void;
helpStatus?: ValidateStatus;
errors?: React.ReactNode[];
warnings?: React.ReactNode[];
className?: string;
}
export default function ErrorList({
errors = EMPTY_LIST,
help,
onDomErrorVisibleChange,
helpStatus,
errors = EMPTY_LIST,
warnings = EMPTY_LIST,
className: rootClassName,
}: ErrorListProps) {
const forceUpdate = useForceUpdate();
const { prefixCls, status } = React.useContext(FormItemPrefixContext);
const { prefixCls } = React.useContext(FormItemPrefixContext);
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 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 (
<CSSMotion
motionDeadline={500}
visible={visible}
{...collapseMotion}
motionName={`${rootPrefixCls}-show-help`}
onLeaveEnd={() => {
onDomErrorVisibleChange?.(false);
motionAppear={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 }) => (
<div
className={classNames(
baseClassName,
{
[`${baseClassName}-${innerStatus}`]: innerStatus,
},
motionClassName,
)}
key="help"
>
{memoErrors.map((error, index) => (
// eslint-disable-next-line react/no-array-index-key
<div key={index} role="alert">
{error}
</div>
))}
</div>
)}
{holderProps => {
const { className: holderClassName, style: holderStyle } = holderProps;
return (
<div
className={classNames(baseClassName, holderClassName, rootClassName)}
style={holderStyle}
>
<CSSMotionList
keys={fullKeyList}
{...collapseMotion}
motionName={`${rootPrefixCls}-show-help-item`}
component={false}
>
{itemProps => {
const {
key,
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>
);
}

View File

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

View File

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

View File

@ -25,7 +25,7 @@ export interface FormListProps {
children: (
fields: FormListFieldData[],
operation: FormListOperation,
meta: { errors: React.ReactNode[] },
meta: { errors: React.ReactNode[]; warnings: React.ReactNode[] },
) => React.ReactNode;
}
@ -48,6 +48,7 @@ const FormList: React.FC<FormListProps> = ({
operation,
{
errors: meta.errors,
warnings: meta.warnings,
},
)}
</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`] = `
<form
autocomplete="off"
class="ant-form ant-form-horizontal"
id="basic"
>
@ -1073,9 +1074,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Buggy!
@ -1115,9 +1117,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Buggy!
@ -1188,9 +1191,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Buggy!
@ -1230,9 +1234,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Buggy!
@ -1329,9 +1334,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Buggy!
@ -1384,9 +1390,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Buggy!
@ -1475,9 +1482,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Buggy!
@ -1526,9 +1534,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Buggy!
@ -3427,6 +3436,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
>
<span
class="ant-cascader-picker-label"
title="Zhejiang / Hangzhou / West Lake"
>
Zhejiang / Hangzhou / West Lake
</span>
@ -6591,9 +6601,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Should be combination of numbers & alphabets
@ -6716,9 +6727,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-validating"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-validating"
role="alert"
>
The information is being validated...
@ -6893,9 +6905,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Should be combination of numbers & alphabets
@ -7273,9 +7286,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-validating"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-validating"
role="alert"
>
The information is being validated...
@ -7361,9 +7375,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-error"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Please select the correct date
@ -7839,6 +7854,97 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</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`] = `
<form
class="ant-form ant-form-horizontal"
@ -7944,9 +8050,10 @@ exports[`renders ./components/form/demo/without-form-create.md correctly 1`] = `
</div>
</div>
<div
class="ant-form-item-explain"
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class=""
role="alert"
>
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 { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import scrollIntoView from 'scroll-into-view-if-needed';
import Form from '..';
import Input from '../../input';
@ -20,10 +21,17 @@ describe('Form', () => {
scrollIntoView.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 } });
await sleep(200);
wrapper.update();
if (executeMockTimer) {
act(() => {
jest.runAllTimers();
wrapper.update();
});
await sleep(1);
}
}
beforeEach(() => {
@ -42,6 +50,8 @@ describe('Form', () => {
describe('noStyle Form.Item', () => {
it('work', async () => {
jest.useFakeTimers();
const onChange = jest.fn();
const wrapper = mount(
@ -54,14 +64,18 @@ describe('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-has-error').length).toBeTruthy();
expect(onChange).toHaveBeenCalled();
jest.useRealTimers();
});
it('should clean up', async () => {
jest.useFakeTimers();
const Demo = () => {
const [form] = Form.useForm();
@ -105,12 +119,14 @@ describe('Form', () => {
};
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');
await change(wrapper, 0, '2');
await change(wrapper, 0, '2', true);
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');
jest.useRealTimers();
});
});
@ -315,6 +331,8 @@ describe('Form', () => {
// https://github.com/ant-design/ant-design/issues/20706
it('Error change should work', async () => {
jest.useFakeTimers();
const wrapper = mount(
<Form>
<Form.Item
@ -338,15 +356,17 @@ describe('Form', () => {
/* eslint-disable no-await-in-loop */
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");
await change(wrapper, 0, 'p');
await change(wrapper, 0, 'p', true);
await sleep(100);
wrapper.update();
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('not a p');
}
/* eslint-enable */
jest.useRealTimers();
});
// 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 () => {
jest.useFakeTimers();
const wrapper = mount(
<Form>
<Form.Item name="test" help="help" rules={[{ required: true, message: 'message' }]}>
@ -436,12 +458,16 @@ describe('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-explain').text()).toEqual('help');
jest.useRealTimers();
});
it('clear validation message when ', async () => {
jest.useFakeTimers();
const wrapper = mount(
<Form>
<Form.Item name="username" rules={[{ required: true, message: 'message' }]}>
@ -449,14 +475,18 @@ describe('Form', () => {
</Form.Item>
</Form>,
);
await change(wrapper, 0, '1');
await change(wrapper, 0, '1', true);
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();
await change(wrapper, 0, '123');
await change(wrapper, 0, '123', true);
await sleep(800);
wrapper.update();
expect(wrapper.find('.ant-form-item-explain').length).toBeFalsy();
jest.useRealTimers();
});
// https://github.com/ant-design/ant-design/issues/21167

View File

@ -200,34 +200,6 @@ describe('Form.List', () => {
jest.useRealTimers();
});
describe('ErrorList component', () => {
it('should trigger onDomErrorVisibleChange by motion end', async () => {
jest.useFakeTimers();
const onDomErrorVisibleChange = jest.fn();
const wrapper = mount(
<Form.ErrorList
errors={['bamboo is light']}
onDomErrorVisibleChange={onDomErrorVisibleChange}
/>,
);
await act(async () => {
await sleep();
jest.runAllTimers();
wrapper.update();
});
act(() => {
wrapper.find('CSSMotion').props().onLeaveEnd();
});
expect(onDomErrorVisibleChange).toHaveBeenCalledWith(false);
jest.useRealTimers();
});
});
it('should render empty without errors', () => {
const wrapper = mount(<Form.ErrorList />);
expect(wrapper.render()).toMatchSnapshot();

View File

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

View File

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

View File

@ -15,7 +15,7 @@ We recommend use `Form.useForm` to create data control. If you are using class c
```tsx
import { Form, Input, Button, Select } from 'antd';
import { FormInstance } from 'antd/lib/form';
import { FormInstance } from 'antd/es/form';
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);
```
| Name | Description | Type |
| --- | --- | --- |
| 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\[] |
| 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 |
| max | `type` required: max length of `string`, `number`, `array` | number |
| message | Error message. Will auto generate by [template](#validateMessages) if not provided | string |
| min | `type` required: min length of `string`, `number`, `array` | number |
| pattern | Regex pattern | RegExp |
| required | Required field | boolean |
| 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 |
| 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 |
| whitespace | Failed if only has whitespace, only work with `type: 'string'` rule | boolean |
| Name | Description | Type | Version |
| --- | --- | --- | --- |
| 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\[] | |
| 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 | |
| max | `type` required: max length of `string`, `number`, `array` | number | |
| message | Error message. Will auto generate by [template](#validateMessages) if not provided | string | |
| min | `type` required: min length of `string`, `number`, `array` | number | |
| pattern | Regex pattern | RegExp | |
| required | Required field | boolean | |
| 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 | |
| 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 | |
| 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

View File

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

View File

@ -61,9 +61,12 @@
margin-bottom: @form-item-margin-bottom;
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 {
margin-bottom: 0;
transition: none;
}
&-hidden,
@ -179,10 +182,12 @@
}
}
// ==============================================================
// = Explain =
// ==============================================================
&-explain,
&-extra {
clear: both;
min-height: @form-item-margin-bottom;
color: @text-color-secondary;
font-size: @font-size-base;
line-height: @line-height-base;
@ -190,43 +195,64 @@
.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 {
&::after {
margin-bottom: -22px;
}
}
}
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) {
@name: ~'@{ant-prefix}-@{className}';
.make-motion(@name, @keyframeName, @duration);
.@{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);
&-with-help &-explain {
height: auto;
min-height: @form-item-margin-bottom;
opacity: 1;
}
}
@keyframes antShowHelpOut {
to {
// >>>>>>>>>> Motion <<<<<<<<<<
// 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);
opacity: 0;
&-active {
transform: translateY(0);
opacity: 1;
}
}
&-leave-active {
transform: translateY(-5px);
}
}

View File

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

View File

@ -17,10 +17,11 @@ When a numeric value needs to be provided.
| --- | --- | --- | --- | --- |
| autoFocus | If get focus when component mounted | boolean | false | - |
| bordered | Whether has border style | boolean | true | 4.12.0 |
| controls | Whether to show `+-` controls | boolean | true | 4.17.0 |
| decimalSeparator | Decimal separator | string | - | - |
| defaultValue | The initial value | number | - | - |
| 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 |
| 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) | - |

View File

@ -20,10 +20,11 @@ cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg
| --- | --- | --- | --- | --- |
| autoFocus | 自动获取焦点 | boolean | false | - |
| bordered | 是否有边框 | boolean | true | 4.12.0 |
| controls | 是否显示增减按钮 | boolean | true | 4.17.0 |
| decimalSeparator | 小数点 | string | - | - |
| defaultValue | 初始值 | number | - | - |
| 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 |
| 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) | - |

View File

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

View File

@ -19,6 +19,7 @@ import LocaleProvider from '..';
import arEG from '../ar_EG';
import azAZ from '../az_AZ';
import bgBG from '../bg_BG';
import bnBD from '../bn_BD';
import byBY from '../by_BY';
import caES from '../ca_ES';
import csCZ from '../cs_CZ';
@ -53,6 +54,7 @@ import kuIQ from '../ku_IQ';
import lvLV from '../lv_LV';
import ltLT from '../lt_LT';
import mkMK from '../mk_MK';
import mlIN from '../ml_IN';
import mnMN from '../mn_MN';
import msMY from '../ms_MY';
import nbNO from '../nb_NO';
@ -76,11 +78,13 @@ import viVN from '../vi_VN';
import zhCN from '../zh_CN';
import zhHK from '../zh_HK';
import zhTW from '../zh_TW';
import urPK from '../ur_PK';
const locales = [
azAZ,
arEG,
bgBG,
bnBD,
byBY,
caES,
csCZ,
@ -113,6 +117,7 @@ const locales = [
kuIQ,
ltLT,
mkMK,
mlIN,
msMY,
mnMN,
nbNO,
@ -138,6 +143,7 @@ const locales = [
zhCN,
zhHK,
zhTW,
urPK,
];
const { Option } = Select;
@ -229,9 +235,10 @@ describe('Locale Provider', () => {
<ModalDemo />
</LocaleProvider>,
);
const currentConfirmNode = document.querySelectorAll('.ant-modal-confirm')[
document.querySelectorAll('.ant-modal-confirm').length - 1
];
const currentConfirmNode =
document.querySelectorAll('.ant-modal-confirm')[
document.querySelectorAll('.ant-modal-confirm').length - 1
];
let cancelButtonText = currentConfirmNode.querySelectorAll(
'.ant-btn:not(.ant-btn-primary) span',
)[0].innerHTML;

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import locale from '../locale/ur_PK';
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;

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

@ -0,0 +1,134 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/ml_IN';
import DatePicker from '../date-picker/locale/ml_IN';
import TimePicker from '../time-picker/locale/ml_IN';
import Calendar from '../calendar/locale/ml_IN';
import { Locale } from '../locale-provider';
const typeTemplate = '${label} അസാധുവായ ${type} ആണ്';
const localeValues: Locale = {
locale: 'ml',
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: '(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;

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

@ -0,0 +1,134 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/ur_PK';
import DatePicker from '../date-picker/locale/ur_PK';
import TimePicker from '../time-picker/locale/ur_PK';
import Calendar from '../calendar/locale/ur_PK';
import { Locale } from '../locale-provider';
const typeTemplate = '${label} درست نہیں ہے ${type}';
const localeValues: Locale = {
locale: 'ur',
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

@ -0,0 +1,32 @@
import * as React from 'react';
import classNames from 'classnames';
import { Divider } from 'rc-menu';
import { ConfigContext } from '../config-provider';
export interface MenuDividerProps extends React.HTMLAttributes<HTMLLIElement> {
className?: string;
prefixCls?: string;
style?: React.CSSProperties;
dashed?: boolean;
}
const MenuDivider: React.FC<MenuDividerProps> = ({
prefixCls: customizePrefixCls,
className,
dashed,
...restProps
}) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('menu', customizePrefixCls);
const classString = classNames(
{
[`${prefixCls}-item-divider-dashed`]: !!dashed,
},
className,
);
return <Divider className={classString} {...restProps} />;
};
export default MenuDivider;

View File

@ -821,4 +821,25 @@ describe('Menu', () => {
expect(wrapper.find('.ant-menu-inline-collapsed-noicon').first().text()).toEqual('L');
expect(wrapper.find('.ant-menu-inline-collapsed-noicon').last().text()).toEqual('B');
});
it('divider should show', () => {
const wrapper = mount(
<Menu mode="vertical">
<SubMenu key="sub1" title="Navigation One">
<Menu.Item key="1">Option 1</Menu.Item>
</SubMenu>
<Menu.Divider dashed />
<SubMenu key="sub2" title="Navigation Two">
<Menu.Item key="2">Option 2</Menu.Item>
</SubMenu>
<Menu.Divider />
<SubMenu key="sub4" title="Navigation Three">
<Menu.Item key="3">Option 3</Menu.Item>
</SubMenu>
</Menu>,
);
expect(wrapper.find('li.ant-menu-item-divider').length).toBe(2);
expect(wrapper.find('li.ant-menu-item-divider-dashed').length).toBe(1);
});
});

View File

@ -30,7 +30,7 @@ ReactDOM.render(
<Menu.Item key="1">Option 1</Menu.Item>
<Menu.Item key="2">Option 2</Menu.Item>
</Menu.ItemGroup>
<Menu.ItemGroup title="Iteom 2">
<Menu.ItemGroup title="Item 2">
<Menu.Item key="3">Option 3</Menu.Item>
<Menu.Item key="4">Option 4</Menu.Item>
</Menu.ItemGroup>

View File

@ -111,6 +111,10 @@ More layouts with navigation: [Layout](/components/layout).
Divider line in between menu items, only used in vertical popup Menu or Dropdown Menu.
| Param | Description | Type | Default value | Version |
| ------ | ---------------------- | ------- | ------------- | ------- |
| dashed | Whether line is dashed | boolean | false | 4.17.0 |
## FAQ
### Why will Menu's children be rendered twice?

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import RcMenu, { Divider, ItemGroup, MenuProps as RcMenuProps } from 'rc-menu';
import RcMenu, { ItemGroup, MenuProps as RcMenuProps } from 'rc-menu';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import EllipsisOutlined from '@ant-design/icons/EllipsisOutlined';
@ -11,6 +11,9 @@ import { SiderContext, SiderContextProps } from '../layout/Sider';
import collapseMotion from '../_util/motion';
import { cloneElement } from '../_util/reactNode';
import MenuContext, { MenuTheme } from './MenuContext';
import MenuDivider from './MenuDivider';
export { MenuDividerProps } from './MenuDivider';
export { MenuItemGroupProps } from 'rc-menu';
@ -113,7 +116,7 @@ class InternalMenu extends React.Component<InternalMenuProps> {
// We should keep this as ref-able
class Menu extends React.Component<MenuProps, {}> {
static Divider = Divider;
static Divider = MenuDivider;
static Item = Item;

View File

@ -112,6 +112,10 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3XZcjGpvK/Menu.svg
菜单项分割线,只用在弹出菜单内。
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| ------ | -------- | ------- | ------ | ------ |
| dashed | 是否虚线 | boolean | false | 4.17.0 |
## FAQ
### 为何 Menu 的子元素会渲染两次?

View File

@ -118,10 +118,15 @@
}
&-item-divider {
height: 1px;
overflow: hidden;
line-height: 0;
background-color: @border-color-split;
border-color: @border-color-split;
border-style: solid;
border-width: 1px 0 0;
}
&-item-divider-dashed {
border-style: dashed;
}
&-horizontal &-item,
@ -238,12 +243,8 @@
}
& > &-item-divider {
height: 1px;
margin: 1px 0;
padding: 0;
overflow: hidden;
line-height: 0;
background-color: @border-color-split;
}
&-submenu {

View File

@ -96,19 +96,20 @@ describe('message.config', () => {
});
it('should be able to global config rootPrefixCls', () => {
ConfigProvider.config({ prefixCls: 'prefix-test' });
ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
message.info('last');
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
expect(document.querySelectorAll('.prefix-test-message-notice').length).toBe(1);
ConfigProvider.config({ prefixCls: 'ant' });
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-message-notice')).toHaveLength(1);
expect(document.querySelectorAll('.bamboo-info-circle')).toHaveLength(1);
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null });
});
it('should be able to config prefixCls', () => {
message.config({
prefixCls: 'prefix-test',
});
message.info('last');
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
expect(document.querySelectorAll('.prefix-test-notice').length).toBe(1);
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
message.config({
prefixCls: '', // can be set to empty, ant default value is set in ConfigProvider
});
@ -119,7 +120,7 @@ describe('message.config', () => {
transitionName: '',
});
message.info('last');
expect(document.querySelectorAll('.ant-move-up-enter').length).toBe(0);
expect(document.querySelectorAll('.ant-move-up-enter')).toHaveLength(0);
message.config({
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 InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
import createUseMessage from './hooks/useMessage';
import { globalConfig } from '../config-provider';
import ConfigProvider, { globalConfig } from '../config-provider';
type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
@ -74,16 +74,18 @@ function getRCNotificationInstance(
callback: (info: {
prefixCls: string;
rootPrefixCls: string;
iconPrefixCls: string;
instance: RCNotificationInstance;
}) => void,
) {
const { prefixCls: customizePrefixCls } = args;
const { getPrefixCls, getRootPrefixCls } = globalConfig();
const { getPrefixCls, getRootPrefixCls, getIconPrefixCls } = globalConfig();
const prefixCls = getPrefixCls('message', customizePrefixCls || localPrefixCls);
const rootPrefixCls = getRootPrefixCls(args.rootPrefixCls, prefixCls);
const iconPrefixCls = getIconPrefixCls();
if (messageInstance) {
callback({ prefixCls, rootPrefixCls, instance: messageInstance });
callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance: messageInstance });
return;
}
@ -97,7 +99,7 @@ function getRCNotificationInstance(
RCNotification.newInstance(instanceConfig, (instance: any) => {
if (messageInstance) {
callback({ prefixCls, rootPrefixCls, instance: messageInstance });
callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance: messageInstance });
return;
}
messageInstance = instance;
@ -106,7 +108,7 @@ function getRCNotificationInstance(
(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;
}
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 IconComponent = typeToIcon[args.type];
const messageClass = classNames(`${prefixCls}-custom-content`, {
@ -152,10 +158,12 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent {
style: args.style || {},
className: args.className,
content: (
<div className={messageClass}>
{args.icon || (IconComponent && <IconComponent />)}
<span>{args.content}</span>
</div>
<ConfigProvider iconPrefixCls={iconPrefixCls}>
<div className={messageClass}>
{args.icon || (IconComponent && <IconComponent />)}
<span>{args.content}</span>
</div>
</ConfigProvider>
),
onClose: args.onClose,
onClick: args.onClick,
@ -172,8 +180,10 @@ function notice(args: ArgsProps): MessageType {
return resolve(true);
};
getRCNotificationInstance(args, ({ prefixCls, instance }) => {
instance.notice(getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls));
getRCNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
instance.notice(
getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls, iconPrefixCls),
);
});
});
const result: any = () => {

View File

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

View File

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

View File

@ -11,7 +11,7 @@ title:
## 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
import { Modal, Button } from 'antd';

View File

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

View File

@ -0,0 +1,44 @@
import notification, { getInstance } from '..';
import { sleep } from '../../../tests/utils';
describe('notification.config', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterAll(() => {
notification.destroy();
});
it('should be able to config maxCount', async () => {
notification.config({
maxCount: 5,
duration: 0.5,
});
for (let i = 0; i < 10; i += 1) {
notification.open({
message: 'Notification message',
key: i,
});
}
notification.open({
message: 'Notification last',
key: '11',
});
await Promise.resolve();
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(5);
expect(document.querySelectorAll('.ant-notification-notice')[4].textContent).toBe(
'Notification last',
);
jest.runAllTimers();
await sleep(500);
expect((await getInstance('ant-notification-topRight')).component.state.notices).toHaveLength(
0,
);
});
});

View File

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

View File

@ -65,7 +65,7 @@ notification.config({
```
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| --- | --- | --- | --- | --- |
| bottom | Distance from the bottom of the viewport, when `placement` is `bottomRight` or `bottomLeft` (unit: pixels) | number | 24 |
| closeIcon | Custom close icon | ReactNode | - |
| duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 |
@ -73,6 +73,7 @@ notification.config({
| placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` |
| rtl | Whether to enable RTL mode | boolean | false |
| top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels) | number | 24 |
| maxCount | Max Notification show, drop oldest if exceed limit | number | - | |
## FAQ
@ -100,4 +101,4 @@ return (
### How to set static methods prefixCls
You can config with [`ConfigProvider.config`](/components/config-provider/#ConfigProvider.config()-4.13.0+)
You can config with [`ConfigProvider.config`](</components/config-provider/#ConfigProvider.config()-4.13.0+>)

View File

@ -8,7 +8,7 @@ import CloseCircleOutlined from '@ant-design/icons/CloseCircleOutlined';
import ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined';
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import createUseNotification from './hooks/useNotification';
import { globalConfig } from '../config-provider';
import ConfigProvider, { globalConfig } from '../config-provider';
export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
@ -25,6 +25,7 @@ let defaultPlacement: NotificationPlacement = 'topRight';
let defaultGetContainer: () => HTMLElement;
let defaultCloseIcon: React.ReactNode;
let rtl = false;
let maxCount: number;
export interface ConfigProps {
top?: number;
@ -35,6 +36,7 @@ export interface ConfigProps {
getContainer?: () => HTMLElement;
closeIcon?: React.ReactNode;
rtl?: boolean;
maxCount?: number;
}
function setNotificationConfig(options: ConfigProps) {
@ -65,6 +67,9 @@ function setNotificationConfig(options: ConfigProps) {
if (options.rtl !== undefined) {
rtl = options.rtl;
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
}
}
function getPlacementStyle(
@ -108,7 +113,11 @@ function getPlacementStyle(
function getNotificationInstance(
args: ArgsProps,
callback: (info: { prefixCls: string; instance: RCNotificationInstance }) => void,
callback: (info: {
prefixCls: string;
iconPrefixCls: string;
instance: RCNotificationInstance;
}) => void,
) {
const {
placement = defaultPlacement,
@ -118,14 +127,15 @@ function getNotificationInstance(
closeIcon = defaultCloseIcon,
prefixCls: customizePrefixCls,
} = args;
const { getPrefixCls } = globalConfig();
const { getPrefixCls, getIconPrefixCls } = globalConfig();
const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
const iconPrefixCls = getIconPrefixCls();
const cacheKey = `${prefixCls}-${placement}`;
const cacheInstance = notificationInstance[cacheKey];
if (cacheInstance) {
Promise.resolve(cacheInstance).then(instance => {
callback({ prefixCls: `${prefixCls}-notice`, instance });
callback({ prefixCls: `${prefixCls}-notice`, iconPrefixCls, instance });
});
return;
@ -149,11 +159,13 @@ function getNotificationInstance(
style: getPlacementStyle(placement, top, bottom),
getContainer,
closeIcon: closeIconToRender,
maxCount,
},
notification => {
resolve(notification);
callback({
prefixCls: `${prefixCls}-notice`,
iconPrefixCls,
instance: notification,
});
},
@ -188,7 +200,7 @@ export interface ArgsProps {
closeIcon?: React.ReactNode;
}
function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
function getRCNoticeProps(args: ArgsProps, prefixCls: string, iconPrefixCls?: string) {
const {
duration: durationArg,
icon,
@ -221,15 +233,17 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
return {
content: (
<div className={iconNode ? `${prefixCls}-with-icon` : ''} role="alert">
{iconNode}
<div className={`${prefixCls}-message`}>
{autoMarginTag}
{message}
<ConfigProvider iconPrefixCls={iconPrefixCls}>
<div className={iconNode ? `${prefixCls}-with-icon` : ''} role="alert">
{iconNode}
<div className={`${prefixCls}-message`}>
{autoMarginTag}
{message}
</div>
<div className={`${prefixCls}-description`}>{description}</div>
{btn ? <span className={`${prefixCls}-btn`}>{btn}</span> : null}
</div>
<div className={`${prefixCls}-description`}>{description}</div>
{btn ? <span className={`${prefixCls}-btn`}>{btn}</span> : null}
</div>
</ConfigProvider>
),
duration,
closable: true,
@ -244,8 +258,8 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
}
function notice(args: ArgsProps) {
getNotificationInstance(args, ({ prefixCls, instance }) => {
instance.notice(getRCNoticeProps(args, prefixCls));
getNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
instance.notice(getRCNoticeProps(args, prefixCls, iconPrefixCls));
});
}

View File

@ -66,7 +66,7 @@ notification.config({
```
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| --- | --- | --- | --- | --- |
| bottom | 消息从底部弹出时,距离底部的位置,单位像素 | number | 24 |
| closeIcon | 自定义关闭图标 | ReactNode | - |
| duration | 默认自动关闭延时,单位秒 | number | 4.5 |
@ -74,6 +74,7 @@ notification.config({
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` |
| rtl | 是否开启 RTL 模式 | boolean | false |
| top | 消息从顶部弹出时,距离顶部的位置,单位像素 | number | 24 |
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | |
## FAQ
@ -101,4 +102,4 @@ return (
### 静态方法如何设置 prefixCls
你可以通过 [`ConfigProvider.config`](/components/config-provider/#ConfigProvider.config()-4.13.0+) 进行设置。
你可以通过 [`ConfigProvider.config`](</components/config-provider/#ConfigProvider.config()-4.13.0+>) 进行设置。

View File

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

View File

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

View File

@ -118,18 +118,6 @@ Array [
/>
</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
class="ant-space-item"
style="margin-right:8px"
@ -157,6 +145,15 @@ Array [
</div>,
<br />,
<br />,
<div
class="ant-skeleton ant-skeleton-element"
>
<span
class="ant-skeleton-button"
/>
</div>,
<br />,
<br />,
<div
class="ant-skeleton ant-skeleton-element"
>
@ -222,6 +219,45 @@ Array [
</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
class="ant-row ant-form-item"
>

View File

@ -250,6 +250,16 @@ exports[`Skeleton button element active 1`] = `
</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`] = `
<div
class="ant-skeleton ant-skeleton-element"

View File

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

View File

@ -19,6 +19,7 @@ import { Skeleton, Space, Divider, Switch, Form, Radio } from 'antd';
class Demo extends React.Component {
state = {
active: false,
block: false,
size: 'default',
buttonShape: 'default',
avatarShape: 'circle',
@ -28,6 +29,10 @@ class Demo extends React.Component {
this.setState({ active: checked });
};
handleBlockChange = checked => {
this.setState({ block: checked });
};
handleSizeChange = e => {
this.setState({ size: e.target.value });
};
@ -37,23 +42,28 @@ class Demo extends React.Component {
};
render() {
const { active, size, buttonShape, avatarShape } = this.state;
const { active, size, buttonShape, avatarShape, block } = this.state;
return (
<>
<Space>
<Skeleton.Button active={active} size={size} shape={buttonShape} />
<Skeleton.Button active={active} size={size} shape={buttonShape} />
<Skeleton.Button active={active} size={size} shape={buttonShape} block={block} />
<Skeleton.Avatar active={active} size={size} shape={avatarShape} />
<Skeleton.Input style={{ width: 200 }} active={active} size={size} />
</Space>
<br />
<br />
<Skeleton.Button active={active} size={size} shape={buttonShape} block={block} />
<br />
<br />
<Skeleton.Image />
<Divider />
<Form layout="inline" style={{ margin: '16px 0' }}>
<Form.Item label="Active">
<Switch checked={active} onChange={this.handleActiveChange} />
</Form.Item>
<Form.Item label="Button Block">
<Switch checked={block} onChange={this.handleBlockChange} />
</Form.Item>
<Form.Item label="Size">
<Radio.Group value={size} onChange={this.handleSizeChange}>
<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
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| width | Set the width of title | number \| string | - |
| Property | Description | Type | Default |
| -------- | ---------------------- | ---------------- | ------- |
| width | Set the width of title | number \| string | - |
### SkeletonParagraphProps
@ -54,12 +54,13 @@ Provide a placeholder while you wait for content to load, or to visualise conten
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| 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` | - |
| size | Set the size of button | `large` \| `small` \| `default` | - |
### SkeletonInputProps
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| active | Show animation effect | boolean | false |
| size | Set the size of input | `large` \| `small` \| `default` | - |
| Property | Description | Type | Default |
| -------- | --------------------- | ------------------------------- | ------- |
| active | Show animation effect | boolean | false |
| 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
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| width | 设置标题占位图的宽度 | number \| string | - |
| 属性 | 说明 | 类型 | 默认值 |
| ----- | -------------------- | ---------------- | ------ |
| width | 设置标题占位图的宽度 | number \| string | - |
### SkeletonParagraphProps
@ -52,15 +52,16 @@ cover: https://gw.alipayobjects.com/zos/alicdn/KpcciCJgv/Skeleton.svg
### SkeletonButtonProps
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| active | 是否展示动画效果 | boolean | false |
| shape | 指定按钮的形状 | `circle` \| `round` \| `default` | - |
| size | 设置按钮的大小 | `large` \| `small` \| `default` | - |
| 属性 | 说明 | 类型 | 默认值 |
| ------ | ------------------------------ | -------------------------------- | ------ |
| active | 是否展示动画效果 | boolean | false |
| block | 将按钮宽度调整为其父宽度的选项 | boolean | false |
| shape | 指定按钮的形状 | `circle` \| `round` \| `default` | - |
| size | 设置按钮的大小 | `large` \| `small` \| `default` | - |
### SkeletonInputProps
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| active | 是否展示动画效果 | boolean | false |
| size | 设置输入框的大小 | `large` \| `small` \| `default` | - |
| 属性 | 说明 | 类型 | 默认值 |
| ------ | ---------------- | ------------------------------- | ------ |
| active | 是否展示动画效果 | boolean | false |
| 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
&-element {
display: inline-block;
@ -214,10 +223,12 @@
.skeleton-element-button-size(@size) {
width: @size * 2;
min-width: @size * 2;
.skeleton-element-common-size(@size);
&.@{skeleton-button-prefix-cls}-circle {
width: @size;
min-width: @size;
border-radius: 50%;
}

View File

@ -230,6 +230,7 @@
@checkbox-check-color: #fff;
@checkbox-check-bg: @checkbox-check-color;
@checkbox-border-width: @border-width-base;
@checkbox-border-radius: @border-radius-base;
@checkbox-group-item-margin-right: 8px;
// Descriptions

View File

@ -16705,7 +16705,21 @@ exports[`renders ./components/table/demo/sticky.md correctly 1`] = `
colspan="2"
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
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.
```jsx
import { Table } from 'antd';
import { Table, Switch } from 'antd';
const columns = [
{
@ -93,26 +93,38 @@ for (let i = 0; i < 100; i++) {
});
}
ReactDOM.render(
<Table
columns={columns}
dataSource={data}
scroll={{ x: 1500 }}
summary={pageData => (
<Table.Summary fixed>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={2}>
Fix Left
</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
/>,
mountNode,
);
const Demo = () => {
const [fixedTop, setFixedTop] = React.useState(false);
return (
<Table
columns={columns}
dataSource={data}
scroll={{ x: 1500 }}
summary={pageData => (
<Table.Summary fixed={fixedTop ? 'top' : 'bottom'}>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={2}>
<Switch
checkedChildren="Fixed Top"
unCheckedChildren="Fixed Top"
checked={fixedTop}
onChange={() => {
setFixedTop(!fixedTop);
}}
/>
</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

@ -0,0 +1,8 @@
import { TimePickerLocale } from '../index';
const locale: TimePickerLocale = {
placeholder: 'സമയം തിരഞ്ഞെടുക്കുക',
rangePlaceholder: ['ആരംഭ സമയം', 'അവസാന സമയം'],
};
export default locale;

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
class="ant-btn ant-btn-sm"
style="float:right;margin:5px"
style="float:left;margin:5px"
type="button"
>
<span>
reload
Left button reload
</span>
</button>
</div>
@ -361,7 +361,7 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
type="button"
>
<span>
reload
Right button reload
</span>
</button>
</div>
@ -4247,11 +4247,11 @@ exports[`renders ./components/transfer/demo/tree-transfer.md correctly 1`] = `
>
<div
class="ant-tree ant-tree-icon-hide ant-tree-block-node"
role="tree"
>
<div
role="tree"
>
<div>
<input
aria-label="for screen reader"
style="width:0;height:0;display:flex;overflow:hidden;opacity:0;border:0;padding:0;margin:0"
tabindex="0"
value=""

Some files were not shown because too many files have changed in this diff Show More