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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <span
class="ant-cascader-picker-label" class="ant-cascader-picker-label"
title="Zhejiang / Hangzhou / West Lake"
> >
Zhejiang / Hangzhou / West Lake Zhejiang / Hangzhou / West Lake
</span> </span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,95 +1,117 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import CSSMotion from 'rc-motion'; import CSSMotion, { CSSMotionList } from 'rc-motion';
import useMemo from 'rc-util/lib/hooks/useMemo';
import useCacheErrors from './hooks/useCacheErrors';
import useForceUpdate from '../_util/hooks/useForceUpdate';
import { FormItemPrefixContext } from './context'; import { FormItemPrefixContext } from './context';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import { ValidateStatus } from './FormItem';
import collapseMotion from '../_util/motion';
const EMPTY_LIST: React.ReactNode[] = []; const EMPTY_LIST: React.ReactNode[] = [];
interface ErrorEntity {
error: React.ReactNode;
errorStatus?: ValidateStatus;
key: string;
}
function toErrorEntity(
error: React.ReactNode,
errorStatus: ValidateStatus | undefined,
prefix: string,
index: number = 0,
): ErrorEntity {
return {
key: typeof error === 'string' ? error : `${prefix}-${index}`,
error,
errorStatus,
};
}
export interface ErrorListProps { export interface ErrorListProps {
errors?: React.ReactNode[];
/** @private Internal Usage. Do not use in your production */
help?: React.ReactNode; help?: React.ReactNode;
/** @private Internal Usage. Do not use in your production */ helpStatus?: ValidateStatus;
onDomErrorVisibleChange?: (visible: boolean) => void; errors?: React.ReactNode[];
warnings?: React.ReactNode[];
className?: string;
} }
export default function ErrorList({ export default function ErrorList({
errors = EMPTY_LIST,
help, help,
onDomErrorVisibleChange, helpStatus,
errors = EMPTY_LIST,
warnings = EMPTY_LIST,
className: rootClassName,
}: ErrorListProps) { }: ErrorListProps) {
const forceUpdate = useForceUpdate(); const { prefixCls } = React.useContext(FormItemPrefixContext);
const { prefixCls, status } = React.useContext(FormItemPrefixContext);
const { getPrefixCls } = React.useContext(ConfigContext); const { getPrefixCls } = React.useContext(ConfigContext);
const [visible, cacheErrors] = useCacheErrors(
errors,
changedVisible => {
if (changedVisible) {
/**
* We trigger in sync to avoid dom shaking but this get warning in react 16.13.
*
* So use Promise to keep in micro async to handle this.
* https://github.com/ant-design/ant-design/issues/21698#issuecomment-593743485
*/
Promise.resolve().then(() => {
onDomErrorVisibleChange?.(true);
});
}
forceUpdate();
},
!!help,
);
const memoErrors = useMemo(
() => cacheErrors,
visible,
(_, nextVisible) => nextVisible,
);
// Memo status in same visible
const [innerStatus, setInnerStatus] = React.useState(status);
React.useEffect(() => {
if (visible && status) {
setInnerStatus(status);
}
}, [visible, status]);
const baseClassName = `${prefixCls}-item-explain`; const baseClassName = `${prefixCls}-item-explain`;
const rootPrefixCls = getPrefixCls(); const rootPrefixCls = getPrefixCls();
const fullKeyList = React.useMemo(() => {
if (help !== undefined && help !== null) {
return [toErrorEntity(help, helpStatus, 'help')];
}
return [
...errors.map((error, index) => toErrorEntity(error, 'error', 'error', index)),
...warnings.map((warning, index) => toErrorEntity(warning, 'warning', 'warning', index)),
];
}, [help, helpStatus, errors, warnings]);
return ( return (
<CSSMotion <CSSMotion
motionDeadline={500} {...collapseMotion}
visible={visible}
motionName={`${rootPrefixCls}-show-help`} motionName={`${rootPrefixCls}-show-help`}
onLeaveEnd={() => { motionAppear={false}
onDomErrorVisibleChange?.(false); motionEnter={false}
visible={!!fullKeyList.length}
onLeaveStart={node => {
// Force disable css override style in index.less configured
node.style.height = 'auto';
return { height: node.offsetHeight };
}} }}
> >
{({ className: motionClassName }: { className?: string }) => ( {holderProps => {
<div const { className: holderClassName, style: holderStyle } = holderProps;
className={classNames(
baseClassName, return (
{ <div
[`${baseClassName}-${innerStatus}`]: innerStatus, className={classNames(baseClassName, holderClassName, rootClassName)}
}, style={holderStyle}
motionClassName, >
)} <CSSMotionList
key="help" keys={fullKeyList}
> {...collapseMotion}
{memoErrors.map((error, index) => ( motionName={`${rootPrefixCls}-show-help-item`}
// eslint-disable-next-line react/no-array-index-key component={false}
<div key={index} role="alert"> >
{error} {itemProps => {
</div> const {
))} key,
</div> error,
)} errorStatus,
className: itemClassName,
style: itemStyle,
} = itemProps;
return (
<div
key={key}
role="alert"
className={classNames(itemClassName, {
[`${baseClassName}-${errorStatus}`]: errorStatus,
})}
style={itemStyle}
>
{error}
</div>
);
}}
</CSSMotionList>
</div>
);
}}
</CSSMotion> </CSSMotion>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,10 +17,11 @@ When a numeric value needs to be provided.
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| autoFocus | If get focus when component mounted | boolean | false | - | | autoFocus | If get focus when component mounted | boolean | false | - |
| bordered | Whether has border style | boolean | true | 4.12.0 | | bordered | Whether has border style | boolean | true | 4.12.0 |
| controls | Whether to show `+-` controls | boolean | true | 4.17.0 |
| decimalSeparator | Decimal separator | string | - | - | | decimalSeparator | Decimal separator | string | - | - |
| defaultValue | The initial value | number | - | - | | defaultValue | The initial value | number | - | - |
| disabled | If disable the input | boolean | false | - | | disabled | If disable the input | boolean | false | - |
| formatter | Specifies the format of the value presented | function(value: number \| string): string | - | - | | formatter | Specifies the format of the value presented | function(value: number \| string, info: { userTyping: boolean, input: string }): string | - | info: 4.17.0 |
| keyboard | If enable keyboard behavior | boolean | true | 4.12.0 | | keyboard | If enable keyboard behavior | boolean | true | 4.12.0 |
| max | The max value | number | [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) | - | | max | The max value | number | [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) | - |
| min | The min value | number | [Number.MIN_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) | - | | min | The min value | number | [Number.MIN_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) | - |

View File

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

View File

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

View File

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

View File

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

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').first().text()).toEqual('L');
expect(wrapper.find('.ant-menu-inline-collapsed-noicon').last().text()).toEqual('B'); 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="1">Option 1</Menu.Item>
<Menu.Item key="2">Option 2</Menu.Item> <Menu.Item key="2">Option 2</Menu.Item>
</Menu.ItemGroup> </Menu.ItemGroup>
<Menu.ItemGroup title="Iteom 2"> <Menu.ItemGroup title="Item 2">
<Menu.Item key="3">Option 3</Menu.Item> <Menu.Item key="3">Option 3</Menu.Item>
<Menu.Item key="4">Option 4</Menu.Item> <Menu.Item key="4">Option 4</Menu.Item>
</Menu.ItemGroup> </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. 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 ## FAQ
### Why will Menu's children be rendered twice? ### Why will Menu's children be rendered twice?

View File

@ -1,5 +1,5 @@
import * as React from 'react'; 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 classNames from 'classnames';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import EllipsisOutlined from '@ant-design/icons/EllipsisOutlined'; import EllipsisOutlined from '@ant-design/icons/EllipsisOutlined';
@ -11,6 +11,9 @@ import { SiderContext, SiderContextProps } from '../layout/Sider';
import collapseMotion from '../_util/motion'; import collapseMotion from '../_util/motion';
import { cloneElement } from '../_util/reactNode'; import { cloneElement } from '../_util/reactNode';
import MenuContext, { MenuTheme } from './MenuContext'; import MenuContext, { MenuTheme } from './MenuContext';
import MenuDivider from './MenuDivider';
export { MenuDividerProps } from './MenuDivider';
export { MenuItemGroupProps } from 'rc-menu'; export { MenuItemGroupProps } from 'rc-menu';
@ -113,7 +116,7 @@ class InternalMenu extends React.Component<InternalMenuProps> {
// We should keep this as ref-able // We should keep this as ref-able
class Menu extends React.Component<MenuProps, {}> { class Menu extends React.Component<MenuProps, {}> {
static Divider = Divider; static Divider = MenuDivider;
static Item = Item; 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 ## FAQ
### 为何 Menu 的子元素会渲染两次? ### 为何 Menu 的子元素会渲染两次?

View File

@ -118,10 +118,15 @@
} }
&-item-divider { &-item-divider {
height: 1px;
overflow: hidden; overflow: hidden;
line-height: 0; 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, &-horizontal &-item,
@ -238,12 +243,8 @@
} }
& > &-item-divider { & > &-item-divider {
height: 1px;
margin: 1px 0; margin: 1px 0;
padding: 0; padding: 0;
overflow: hidden;
line-height: 0;
background-color: @border-color-split;
} }
&-submenu { &-submenu {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import confirm, {
import useModal from './useModal'; import useModal from './useModal';
import destroyFns from './destroyFns'; import destroyFns from './destroyFns';
export { ActionButtonProps } from './ActionButton';
export { ModalProps, ModalFuncProps } from './Modal'; export { ModalProps, ModalFuncProps } from './Modal';
function modalWarn(props: ModalFuncProps) { 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', () => { it('should be able to global config rootPrefixCls', () => {
ConfigProvider.config({ prefixCls: 'prefix-test' }); ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
notification.open({ message: 'Notification Title', duration: 0 }); notification.success({ message: 'Notification Title', duration: 0 });
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(0); expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-notification-notice').length).toBe(1); expect(document.querySelectorAll('.prefix-test-notification-notice')).toHaveLength(1);
ConfigProvider.config({ prefixCls: 'ant' }); expect(document.querySelectorAll('.bamboo-check-circle')).toHaveLength(1);
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null });
}); });
it('should be able to config prefixCls', () => { it('should be able to config prefixCls', () => {
@ -117,8 +118,8 @@ describe('notification', () => {
message: 'Notification Title', message: 'Notification Title',
duration: 0, duration: 0,
}); });
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(0); expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-notice').length).toBe(1); expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
notification.config({ notification.config({
prefixCls: '', prefixCls: '',
}); });

View File

@ -65,7 +65,7 @@ notification.config({
``` ```
| Property | Description | Type | Default | | Property | Description | Type | Default |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| bottom | Distance from the bottom of the viewport, when `placement` is `bottomRight` or `bottomLeft` (unit: pixels) | number | 24 | | bottom | Distance from the bottom of the viewport, when `placement` is `bottomRight` or `bottomLeft` (unit: pixels) | number | 24 |
| closeIcon | Custom close icon | ReactNode | - | | 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 | | 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` | | placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` |
| rtl | Whether to enable RTL mode | boolean | false | | 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 | | 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 ## FAQ
@ -100,4 +101,4 @@ return (
### How to set static methods prefixCls ### 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 ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined';
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined'; import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import createUseNotification from './hooks/useNotification'; import createUseNotification from './hooks/useNotification';
import { globalConfig } from '../config-provider'; import ConfigProvider, { globalConfig } from '../config-provider';
export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
@ -25,6 +25,7 @@ let defaultPlacement: NotificationPlacement = 'topRight';
let defaultGetContainer: () => HTMLElement; let defaultGetContainer: () => HTMLElement;
let defaultCloseIcon: React.ReactNode; let defaultCloseIcon: React.ReactNode;
let rtl = false; let rtl = false;
let maxCount: number;
export interface ConfigProps { export interface ConfigProps {
top?: number; top?: number;
@ -35,6 +36,7 @@ export interface ConfigProps {
getContainer?: () => HTMLElement; getContainer?: () => HTMLElement;
closeIcon?: React.ReactNode; closeIcon?: React.ReactNode;
rtl?: boolean; rtl?: boolean;
maxCount?: number;
} }
function setNotificationConfig(options: ConfigProps) { function setNotificationConfig(options: ConfigProps) {
@ -65,6 +67,9 @@ function setNotificationConfig(options: ConfigProps) {
if (options.rtl !== undefined) { if (options.rtl !== undefined) {
rtl = options.rtl; rtl = options.rtl;
} }
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
}
} }
function getPlacementStyle( function getPlacementStyle(
@ -108,7 +113,11 @@ function getPlacementStyle(
function getNotificationInstance( function getNotificationInstance(
args: ArgsProps, args: ArgsProps,
callback: (info: { prefixCls: string; instance: RCNotificationInstance }) => void, callback: (info: {
prefixCls: string;
iconPrefixCls: string;
instance: RCNotificationInstance;
}) => void,
) { ) {
const { const {
placement = defaultPlacement, placement = defaultPlacement,
@ -118,14 +127,15 @@ function getNotificationInstance(
closeIcon = defaultCloseIcon, closeIcon = defaultCloseIcon,
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
} = args; } = args;
const { getPrefixCls } = globalConfig(); const { getPrefixCls, getIconPrefixCls } = globalConfig();
const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls); const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
const iconPrefixCls = getIconPrefixCls();
const cacheKey = `${prefixCls}-${placement}`; const cacheKey = `${prefixCls}-${placement}`;
const cacheInstance = notificationInstance[cacheKey]; const cacheInstance = notificationInstance[cacheKey];
if (cacheInstance) { if (cacheInstance) {
Promise.resolve(cacheInstance).then(instance => { Promise.resolve(cacheInstance).then(instance => {
callback({ prefixCls: `${prefixCls}-notice`, instance }); callback({ prefixCls: `${prefixCls}-notice`, iconPrefixCls, instance });
}); });
return; return;
@ -149,11 +159,13 @@ function getNotificationInstance(
style: getPlacementStyle(placement, top, bottom), style: getPlacementStyle(placement, top, bottom),
getContainer, getContainer,
closeIcon: closeIconToRender, closeIcon: closeIconToRender,
maxCount,
}, },
notification => { notification => {
resolve(notification); resolve(notification);
callback({ callback({
prefixCls: `${prefixCls}-notice`, prefixCls: `${prefixCls}-notice`,
iconPrefixCls,
instance: notification, instance: notification,
}); });
}, },
@ -188,7 +200,7 @@ export interface ArgsProps {
closeIcon?: React.ReactNode; closeIcon?: React.ReactNode;
} }
function getRCNoticeProps(args: ArgsProps, prefixCls: string) { function getRCNoticeProps(args: ArgsProps, prefixCls: string, iconPrefixCls?: string) {
const { const {
duration: durationArg, duration: durationArg,
icon, icon,
@ -221,15 +233,17 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
return { return {
content: ( content: (
<div className={iconNode ? `${prefixCls}-with-icon` : ''} role="alert"> <ConfigProvider iconPrefixCls={iconPrefixCls}>
{iconNode} <div className={iconNode ? `${prefixCls}-with-icon` : ''} role="alert">
<div className={`${prefixCls}-message`}> {iconNode}
{autoMarginTag} <div className={`${prefixCls}-message`}>
{message} {autoMarginTag}
{message}
</div>
<div className={`${prefixCls}-description`}>{description}</div>
{btn ? <span className={`${prefixCls}-btn`}>{btn}</span> : null}
</div> </div>
<div className={`${prefixCls}-description`}>{description}</div> </ConfigProvider>
{btn ? <span className={`${prefixCls}-btn`}>{btn}</span> : null}
</div>
), ),
duration, duration,
closable: true, closable: true,
@ -244,8 +258,8 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
} }
function notice(args: ArgsProps) { function notice(args: ArgsProps) {
getNotificationInstance(args, ({ prefixCls, instance }) => { getNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
instance.notice(getRCNoticeProps(args, prefixCls)); instance.notice(getRCNoticeProps(args, prefixCls, iconPrefixCls));
}); });
} }

View File

@ -66,7 +66,7 @@ notification.config({
``` ```
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| bottom | 消息从底部弹出时,距离底部的位置,单位像素 | number | 24 | | bottom | 消息从底部弹出时,距离底部的位置,单位像素 | number | 24 |
| closeIcon | 自定义关闭图标 | ReactNode | - | | closeIcon | 自定义关闭图标 | ReactNode | - |
| duration | 默认自动关闭延时,单位秒 | number | 4.5 | | duration | 默认自动关闭延时,单位秒 | number | 4.5 |
@ -74,6 +74,7 @@ notification.config({
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | | placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` |
| rtl | 是否开启 RTL 模式 | boolean | false | | rtl | 是否开启 RTL 模式 | boolean | false |
| top | 消息从顶部弹出时,距离顶部的位置,单位像素 | number | 24 | | top | 消息从顶部弹出时,距离顶部的位置,单位像素 | number | 24 |
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | |
## FAQ ## FAQ
@ -101,4 +102,4 @@ return (
### 静态方法如何设置 prefixCls ### 静态方法如何设置 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>
</div> </div>
`; `;
exports[`renders ./components/popconfirm/demo/promise.md correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Popconfirm with Promise
</span>
</button>
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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