Merge pull request #25817 from ant-design/resolve-conflict-1

chore: merge feature into master
This commit is contained in:
07akioni 2020-07-26 20:45:57 +08:00 committed by GitHub
commit d857800adc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2439 additions and 457 deletions

View File

@ -23,6 +23,7 @@ lib/**/*
node_modules
_site
dist
coverage
**/*.d.ts
# Scripts
scripts/previewEditor/**/*

View File

@ -7,8 +7,11 @@ export default function usePatchElement(): [
const [elements, setElements] = React.useState<React.ReactElement[]>([]);
function patchElement(element: React.ReactElement) {
// append a new element to elements (and create a new ref)
setElements(originElements => [...originElements, element]);
// return a function that removes the new element out of elements (and create a new ref)
// it works a little like useEffect
return () => {
setElements(originElements => originElements.filter(ele => ele !== element));
};

View File

@ -469,6 +469,120 @@ Array [
]
`;
exports[`renders ./components/avatar/demo/group.md correctly 1`] = `
Array [
<div
class="ant-avatar-group"
>
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
</span>
<span
class="ant-avatar ant-avatar-circle"
style="background-color:#f56a00"
>
<span
class="ant-avatar-string"
style="opacity:0"
>
K
</span>
</span>
<span
class="ant-avatar ant-avatar-circle ant-avatar-icon"
style="background-color:#87d068"
>
<span
aria-label="user"
class="anticon anticon-user"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="user"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"
/>
</svg>
</span>
</span>
<span
class="ant-avatar ant-avatar-circle ant-avatar-icon"
style="background-color:#1890ff"
>
<span
aria-label="ant-design"
class="anticon anticon-ant-design"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="ant-design"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M716.3 313.8c19-18.9 19-49.7 0-68.6l-69.9-69.9.1.1c-18.5-18.5-50.3-50.3-95.3-95.2-21.2-20.7-55.5-20.5-76.5.5L80.9 474.2a53.84 53.84 0 000 76.4L474.6 944a54.14 54.14 0 0076.5 0l165.1-165c19-18.9 19-49.7 0-68.6a48.7 48.7 0 00-68.7 0l-125 125.2c-5.2 5.2-13.3 5.2-18.5 0L189.5 521.4c-5.2-5.2-5.2-13.3 0-18.5l314.4-314.2c.4-.4.9-.7 1.3-1.1 5.2-4.1 12.4-3.7 17.2 1.1l125.2 125.1c19 19 49.8 19 68.7 0zM408.6 514.4a106.3 106.2 0 10212.6 0 106.3 106.2 0 10-212.6 0zm536.2-38.6L821.9 353.5c-19-18.9-49.8-18.9-68.7.1a48.4 48.4 0 000 68.6l83 82.9c5.2 5.2 5.2 13.3 0 18.5l-81.8 81.7a48.4 48.4 0 000 68.6 48.7 48.7 0 0068.7 0l121.8-121.7a53.93 53.93 0 00-.1-76.4z"
/>
</svg>
</span>
</span>
</div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-avatar-group"
>
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
</span>
<span
class="ant-avatar ant-avatar-circle"
style="background-color:#f56a00"
>
<span
class="ant-avatar-string"
style="opacity:0"
>
K
</span>
</span>
<span
class="ant-avatar ant-avatar-circle"
style="color:#f56a00;background-color:#fde3cf"
>
<span
class="ant-avatar-string"
style="opacity:0"
>
+2
</span>
</span>
</div>,
]
`;
exports[`renders ./components/avatar/demo/fallback.md correctly 1`] = `
Array [
<span

View File

@ -0,0 +1,213 @@
import * as React from 'react';
import classNames from 'classnames';
import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning';
import { composeRef } from '../_util/ref';
export interface AvatarProps {
/** Shape of avatar, options:`circle`, `square` */
shape?: 'circle' | 'square';
/*
* Size of avatar, options: `large`, `small`, `default`
* or a custom number size
* */
size?: 'large' | 'small' | 'default' | number;
gap?: number;
/** Src of image avatar */
src?: string;
/** Srcset of image avatar */
srcSet?: string;
draggable?: boolean;
/** icon to be used in avatar */
icon?: React.ReactNode;
style?: React.CSSProperties;
prefixCls?: string;
className?: string;
children?: React.ReactNode;
alt?: string;
/* callback when img load error */
/* return false to prevent Avatar show default fallback behavior, then you can do fallback by your self */
onError?: () => boolean;
}
const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (props, ref) => {
const [scale, setScale] = React.useState(1);
const [mounted, setMounted] = React.useState(false);
const [isImgExist, setIsImgExist] = React.useState(true);
const avatarNodeRef = React.useRef<HTMLElement>();
const avatarChildrenRef = React.useRef<HTMLElement>();
const avatarNodeMergeRef = composeRef(ref, avatarNodeRef);
let lastChildrenWidth: number;
let lastNodeWidth: number;
const { getPrefixCls } = React.useContext(ConfigContext);
const setScaleParam = () => {
if (!avatarChildrenRef.current || !avatarNodeRef.current) {
return;
}
const childrenWidth = avatarChildrenRef.current.offsetWidth; // offsetWidth avoid affecting be transform scale
const nodeWidth = avatarNodeRef.current.offsetWidth;
const { gap = 4 } = props;
// denominator is 0 is no meaning
if (
childrenWidth !== 0 &&
nodeWidth !== 0 &&
(lastChildrenWidth !== childrenWidth || lastNodeWidth !== nodeWidth)
) {
lastChildrenWidth = childrenWidth;
lastNodeWidth = nodeWidth;
}
if (gap * 2 < nodeWidth) {
setScale(nodeWidth - gap * 2 < childrenWidth ? (nodeWidth - gap * 2) / childrenWidth : 1);
}
};
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
setIsImgExist(true);
setScale(1);
}, [props.src]);
React.useEffect(() => {
setScaleParam();
}, [props.children, props.gap, props.size]);
React.useEffect(() => {
if (props.children) {
setScaleParam();
}
}, [isImgExist]);
const handleImgLoadError = () => {
const { onError } = props;
const errorFlag = onError ? onError() : undefined;
if (errorFlag !== false) {
setIsImgExist(false);
}
};
const {
prefixCls: customizePrefixCls,
shape,
size,
src,
srcSet,
icon,
className,
alt,
draggable,
children,
...others
} = props;
devWarning(
!(typeof icon === 'string' && icon.length > 2),
'Avatar',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
);
const prefixCls = getPrefixCls('avatar', customizePrefixCls);
const sizeCls = classNames({
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
});
const classString = classNames(prefixCls, className, sizeCls, {
[`${prefixCls}-${shape}`]: shape,
[`${prefixCls}-image`]: src && isImgExist,
[`${prefixCls}-icon`]: icon,
});
const sizeStyle: React.CSSProperties =
typeof size === 'number'
? {
width: size,
height: size,
lineHeight: `${size}px`,
fontSize: icon ? size / 2 : 18,
}
: {};
let childrenToRender;
if (src && isImgExist) {
childrenToRender = (
<img src={src} draggable={draggable} srcSet={srcSet} onError={handleImgLoadError} alt={alt} />
);
} else if (icon) {
childrenToRender = icon;
} else if (mounted || scale !== 1) {
const transformString = `scale(${scale}) translateX(-50%)`;
const childrenStyle: React.CSSProperties = {
msTransform: transformString,
WebkitTransform: transformString,
transform: transformString,
};
const sizeChildrenStyle: React.CSSProperties =
typeof size === 'number'
? {
lineHeight: `${size}px`,
}
: {};
childrenToRender = (
<span
className={`${prefixCls}-string`}
ref={(node: HTMLElement) => {
avatarChildrenRef.current = node;
}}
style={{ ...sizeChildrenStyle, ...childrenStyle }}
>
{children}
</span>
);
} else {
childrenToRender = (
<span
className={`${prefixCls}-string`}
style={{ opacity: 0 }}
ref={(node: HTMLElement) => {
avatarChildrenRef.current = node;
}}
>
{children}
</span>
);
}
// The event is triggered twice from bubbling up the DOM tree.
// see https://codesandbox.io/s/kind-snow-9lidz
delete others.onError;
delete others.gap;
return (
<span
{...others}
style={{ ...sizeStyle, ...others.style }}
className={classString}
ref={avatarNodeMergeRef as any}
>
{childrenToRender}
</span>
);
};
const Avatar = React.forwardRef<unknown, AvatarProps>(InternalAvatar);
Avatar.displayName = 'Avatar';
Avatar.defaultProps = {
shape: 'circle' as AvatarProps['shape'],
size: 'default' as AvatarProps['size'],
};
export default Avatar;

View File

@ -0,0 +1,47 @@
---
order: 4
title:
zh-CN: Avatar.Group
en-US: Avatar.Group
---
## zh-CN
头像组合展现。
## en-US
Avatar group display.
```tsx
import { Avatar, Divider, Tooltip } from 'antd';
import { UserOutlined, AntDesignOutlined } from '@ant-design/icons';
class Demo extends React.Component {
render() {
return (
<>
<Avatar.Group>
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>
<Tooltip title="Ant User" placement="top">
<Avatar style={{ backgroundColor: '#87d068' }} icon={<UserOutlined />} />
</Tooltip>
<Avatar style={{ backgroundColor: '#1890ff' }} icon={<AntDesignOutlined />} />
</Avatar.Group>
<Divider />
<Avatar.Group maxCount={2} maxStyle={{ color: '#f56a00', backgroundColor: '#fde3cf' }}>
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>
<Tooltip title="Ant User" placement="top">
<Avatar style={{ backgroundColor: '#87d068' }} icon={<UserOutlined />} />
</Tooltip>
<Avatar style={{ backgroundColor: '#1890ff' }} icon={<AntDesignOutlined />} />
</Avatar.Group>
</>
);
}
}
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -0,0 +1,59 @@
import * as React from 'react';
import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import { ConfigContext } from '../config-provider';
import Avatar from './avatar';
import Popover from '../popover';
export interface GroupProps {
className?: string;
children?: React.ReactNode;
style?: React.CSSProperties;
prefixCls?: string;
maxCount?: number;
maxStyle?: React.CSSProperties;
maxPopoverPlacement?: 'top' | 'bottom';
}
const Group: React.FC<GroupProps> = props => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const { prefixCls: customizePrefixCls, className = '', maxCount, maxStyle } = props;
const prefixCls = getPrefixCls('avatar-group', customizePrefixCls);
const cls = classNames(
prefixCls,
{
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
const { children, maxPopoverPlacement = 'top' } = props;
const childrenWithProps = toArray(children);
const numOfChildren = childrenWithProps.length;
if (maxCount && maxCount < numOfChildren) {
const childrenShow = childrenWithProps.slice(0, maxCount);
const childrenHidden = childrenWithProps.slice(maxCount, numOfChildren);
childrenShow.push(
<Popover
content={childrenHidden}
trigger="hover"
placement={maxPopoverPlacement}
overlayClassName={`${prefixCls}-popover`}
>
<Avatar style={maxStyle}>{`+${numOfChildren - maxCount}`}</Avatar>
</Popover>,
);
return (
<div className={cls} style={props.style}>
{childrenShow}
</div>
);
}
return (
<div className={cls} style={props.style}>
{children}
</div>
);
};
export default Group;

View File

@ -9,6 +9,8 @@ Avatars can be used to represent people or objects. It supports images, `Icon`s,
## API
### Avatar
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| icon | Custom icon type for an icon avatar | ReactNode | - | |
@ -21,3 +23,11 @@ Avatars can be used to represent people or objects. It supports images, `Icon`s,
| gap | Letter type unit distance between left and right sides | number | 4 | 4.3.0 |
> Tip: You can set `icon` or `children` as the fallback for image load error, with the priority of `icon` > `children`
### Avatar.Group (4.5.0+)
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| ------------------- | -------------------------------------- | ----------------- | ------ | ---- |
| maxCount | Max avatars to show | number | - | |
| maxStyle | The style of excess avatar style | CSSProperties | - | |
| maxPopoverPlacement | The placement of excess avatar Popover | `top` \| `bottom` | `top` | |

View File

@ -1,209 +1,16 @@
import * as React from 'react';
import classNames from 'classnames';
import InternalAvatar, { AvatarProps } from './avatar';
import Group from './group';
import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning';
export { AvatarProps } from './avatar';
export { GroupProps } from './group';
export interface AvatarProps {
/** Shape of avatar, options:`circle`, `square` */
shape?: 'circle' | 'square';
/*
* Size of avatar, options: `large`, `small`, `default`
* or a custom number size
* */
size?: 'large' | 'small' | 'default' | number;
gap?: number;
/** Src of image avatar */
src?: string;
/** Srcset of image avatar */
srcSet?: string;
draggable?: boolean;
/** icon to be used in avatar */
icon?: React.ReactNode;
style?: React.CSSProperties;
prefixCls?: string;
className?: string;
children?: React.ReactNode;
alt?: string;
/* callback when img load error */
/* return false to prevent Avatar show default fallback behavior, then you can do fallback by your self */
onError?: () => boolean;
interface CompoundedComponent
extends React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLElement>> {
Group: typeof Group;
}
const Avatar: React.FC<AvatarProps> = props => {
const [scale, setScale] = React.useState(1);
const [mounted, setMounted] = React.useState(false);
const [isImgExist, setIsImgExist] = React.useState(true);
const avatarNodeRef = React.useRef<HTMLElement>();
const avatarChildrenRef = React.useRef<HTMLElement>();
let lastChildrenWidth: number;
let lastNodeWidth: number;
const { getPrefixCls } = React.useContext(ConfigContext);
const setScaleParam = () => {
if (!avatarChildrenRef.current || !avatarNodeRef.current) {
return;
}
const childrenWidth = avatarChildrenRef.current.offsetWidth; // offsetWidth avoid affecting be transform scale
const nodeWidth = avatarNodeRef.current.offsetWidth;
const { gap = 4 } = props;
// denominator is 0 is no meaning
if (
childrenWidth !== 0 &&
nodeWidth !== 0 &&
(lastChildrenWidth !== childrenWidth || lastNodeWidth !== nodeWidth)
) {
lastChildrenWidth = childrenWidth;
lastNodeWidth = nodeWidth;
}
if (gap * 2 < nodeWidth) {
setScale(nodeWidth - gap * 2 < childrenWidth ? (nodeWidth - gap * 2) / childrenWidth : 1);
}
};
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
setIsImgExist(true);
setScale(1);
}, [props.src]);
React.useEffect(() => {
setScaleParam();
}, [props.children, props.gap, props.size]);
React.useEffect(() => {
if (props.children) {
setScaleParam();
}
}, [isImgExist]);
const handleImgLoadError = () => {
const { onError } = props;
const errorFlag = onError ? onError() : undefined;
if (errorFlag !== false) {
setIsImgExist(false);
}
};
const {
prefixCls: customizePrefixCls,
shape,
size,
src,
srcSet,
icon,
className,
alt,
draggable,
children,
...others
} = props;
devWarning(
!(typeof icon === 'string' && icon.length > 2),
'Avatar',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
);
const prefixCls = getPrefixCls('avatar', customizePrefixCls);
const sizeCls = classNames({
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
});
const classString = classNames(prefixCls, className, sizeCls, {
[`${prefixCls}-${shape}`]: shape,
[`${prefixCls}-image`]: src && isImgExist,
[`${prefixCls}-icon`]: icon,
});
const sizeStyle: React.CSSProperties =
typeof size === 'number'
? {
width: size,
height: size,
lineHeight: `${size}px`,
fontSize: icon ? size / 2 : 18,
}
: {};
let childrenToRender;
if (src && isImgExist) {
childrenToRender = (
<img src={src} draggable={draggable} srcSet={srcSet} onError={handleImgLoadError} alt={alt} />
);
} else if (icon) {
childrenToRender = icon;
} else if (mounted || scale !== 1) {
const transformString = `scale(${scale}) translateX(-50%)`;
const childrenStyle: React.CSSProperties = {
msTransform: transformString,
WebkitTransform: transformString,
transform: transformString,
};
const sizeChildrenStyle: React.CSSProperties =
typeof size === 'number'
? {
lineHeight: `${size}px`,
}
: {};
childrenToRender = (
<span
className={`${prefixCls}-string`}
ref={(node: HTMLElement) => {
avatarChildrenRef.current = node;
}}
style={{ ...sizeChildrenStyle, ...childrenStyle }}
>
{children}
</span>
);
} else {
childrenToRender = (
<span
className={`${prefixCls}-string`}
style={{ opacity: 0 }}
ref={(node: HTMLElement) => {
avatarChildrenRef.current = node;
}}
>
{children}
</span>
);
}
// The event is triggered twice from bubbling up the DOM tree.
// see https://codesandbox.io/s/kind-snow-9lidz
delete others.onError;
delete others.gap;
return (
<span
{...others}
style={{ ...sizeStyle, ...others.style }}
className={classString}
ref={(node: HTMLElement) => {
avatarNodeRef.current = node;
}}
>
{childrenToRender}
</span>
);
};
Avatar.defaultProps = {
shape: 'circle' as AvatarProps['shape'],
size: 'default' as AvatarProps['size'],
};
const Avatar = InternalAvatar as CompoundedComponent;
Avatar.Group = Group;
export { Group };
export default Avatar;

View File

@ -14,6 +14,8 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/aBcnbw68hP/Avatar.svg
## API
### Avatar
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| icon | 设置头像的自定义图标 | ReactNode | - | |
@ -26,3 +28,11 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/aBcnbw68hP/Avatar.svg
| gap | 字符类型距离左右两侧边界单位像素 | number | 4 | 4.3.0 |
> Tip你可以设置 `icon``children` 作为图片加载失败的默认 fallback 行为,优先级为 `icon` > `children`
### Avatar.Group (4.5.0+)
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| ------------------- | -------------------- | ----------------- | ------ | ---- |
| maxCount | 显示的最大头像个数 | number | - | |
| maxStyle | 多余头像样式 | CSSProperties | - | |
| maxPopoverPlacement | 多余头像气泡弹出位置 | `top` \| `bottom` | `top` | |

View File

@ -0,0 +1,22 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@avatar-prefix-cls: ~'@{ant-prefix}-avatar';
.@{avatar-prefix-cls}-group {
display: inline-flex;
.@{avatar-prefix-cls} {
border: 1px solid @avatar-group-border-color;
&:not(:first-child) {
margin-left: @avatar-group-overlapping;
}
}
&-popover {
.@{ant-prefix}-avatar + .@{ant-prefix}-avatar {
margin-left: @avatar-group-space;
}
}
}

View File

@ -61,3 +61,6 @@
}
}
}
@import './group';
@import './rtl';

View File

@ -1,2 +1,5 @@
import '../../style/index.less';
import './index.less';
// style dependencies
import '../../popover/style';

View File

@ -0,0 +1,20 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@avatar-prefix-cls: ~'@{ant-prefix}-avatar';
.@{avatar-prefix-cls}-group {
&-rtl {
.@{avatar-prefix-cls}:not(:first-child) {
margin-right: @avatar-group-overlapping;
margin-left: 0;
}
}
&-popover.@{ant-prefix}-popover-rtl {
.@{ant-prefix}-avatar + .@{ant-prefix}-avatar {
margin-right: @avatar-group-space;
margin-left: 0;
}
}
}

View File

@ -0,0 +1,53 @@
import * as React from 'react';
import classNames from 'classnames';
import { LiteralUnion } from '../_util/type';
import { PresetColorType } from '../_util/colors';
import { ConfigContext } from '../config-provider';
import { isPresetColor } from './utils';
type RibbonPlacement = 'start' | 'end';
export interface RibbonProps {
className?: string;
prefixCls?: string;
style?: React.CSSProperties; // style of ribbon element, not the wrapper
text?: React.ReactNode;
color?: LiteralUnion<PresetColorType, string>;
children?: React.ReactNode;
placement?: RibbonPlacement;
}
const Ribbon: React.FC<RibbonProps> = function Ribbon({
className,
prefixCls: customizePrefixCls,
style,
color,
children,
text,
placement = 'end',
}) {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('ribbon', customizePrefixCls);
const colorInPreset = isPresetColor(color);
const ribbonCls = classNames(prefixCls, className, `${prefixCls}-placement-${placement}`, {
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-color-${color}`]: colorInPreset,
});
const colorStyle: React.CSSProperties = {};
const cornerColorStyle: React.CSSProperties = {};
if (color && !colorInPreset) {
colorStyle.background = color;
cornerColorStyle.color = color;
}
return (
<div className={`${prefixCls}-wrapper`}>
{children}
<div className={ribbonCls} style={{ ...colorStyle, ...style }}>
{text}
<div className={`${prefixCls}-corner`} style={cornerColorStyle} />
</div>
</div>
);
};
export default Ribbon;

View File

@ -2044,6 +2044,173 @@ exports[`renders ./components/badge/demo/overflow.md correctly 1`] = `
</div>
`;
exports[`renders ./components/badge/demo/ribbbon.md correctly 1`] = `
<div
class="ant-ribbon-wrapper"
>
<div
class="ant-card ant-card-bordered"
>
<div
class="ant-card-body"
>
And raises the spyglass.
</div>
</div>
<div
class="ant-ribbon ant-ribbon-placement-end"
>
Pushes open the window
<div
class="ant-ribbon-corner"
/>
</div>
</div>
`;
exports[`renders ./components/badge/demo/ribbon-debug.md correctly 1`] = `
<div
class="ant-space ant-space-vertical"
style="width:100%"
>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<div
class="ant-ribbon-wrapper"
>
<div
class="ant-card ant-card-bordered"
>
<div
class="ant-card-body"
>
推开窗户举起望远镜
</div>
</div>
<div
class="ant-ribbon ant-ribbon-placement-end"
>
啦啦啦啦
<div
class="ant-ribbon-corner"
/>
</div>
</div>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<div
class="ant-ribbon-wrapper"
>
<div
class="ant-card ant-card-bordered"
>
<div
class="ant-card-body"
>
推开窗户举起望远镜
</div>
</div>
<div
class="ant-ribbon ant-ribbon-placement-end ant-ribbon-color-purple"
>
啦啦啦啦
<div
class="ant-ribbon-corner"
/>
</div>
</div>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<div
class="ant-ribbon-wrapper"
>
<div
class="ant-card ant-card-bordered"
>
<div
class="ant-card-body"
>
推开窗户举起望远镜
</div>
</div>
<div
class="ant-ribbon ant-ribbon-placement-end"
style="background:#2db7f5"
>
啦啦啦啦
<div
class="ant-ribbon-corner"
style="color:#2db7f5"
/>
</div>
</div>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<div
class="ant-ribbon-wrapper"
>
<div
class="ant-card ant-card-bordered"
>
<div
class="ant-card-body"
>
推开窗户举起望远镜
</div>
</div>
<div
class="ant-ribbon ant-ribbon-placement-start"
style="background:#2db7f5"
>
啦啦啦啦
<div
class="ant-ribbon-corner"
style="color:#2db7f5"
/>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-ribbon-wrapper"
>
<div
class="ant-card ant-card-bordered"
>
<div
class="ant-card-body"
>
推开窗户举起望远镜
</div>
</div>
<div
class="ant-ribbon ant-ribbon-placement-end"
style="background:#2db7f5"
>
啦啦啦啦
<div
class="ant-ribbon-corner"
style="color:#2db7f5"
/>
</div>
</div>
</div>
</div>
`;
exports[`renders ./components/badge/demo/status.md correctly 1`] = `
<div>
<span

View File

@ -3160,3 +3160,17 @@ exports[`Badge should support offset when count is a ReactNode 1`] = `
/>
</span>
`;
exports[`Ribbon rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-ribbon-wrapper"
>
<div
class="ant-ribbon ant-ribbon-placement-end ant-ribbon-rtl"
>
<div
class="ant-ribbon-corner"
/>
</div>
</div>
`;

View File

@ -141,3 +141,72 @@ describe('Badge', () => {
expect(wrapper).toMatchSnapshot();
});
});
describe('Ribbon', () => {
mountTest(Badge.Ribbon);
rtlTest(Badge.Ribbon);
describe('placement', () => {
it('works with `start` & `end` placement', () => {
const wrapperStart = mount(
<Badge.Ribbon placement="start">
<div />
</Badge.Ribbon>,
);
expect(wrapperStart.find('.ant-ribbon-placement-start').length).toEqual(1);
const wrapperEnd = mount(
<Badge.Ribbon placement="end">
<div />
</Badge.Ribbon>,
);
expect(wrapperEnd.find('.ant-ribbon-placement-end').length).toEqual(1);
});
});
describe('color', () => {
it('works with preset color', () => {
const wrapper = mount(
<Badge.Ribbon color="green">
<div />
</Badge.Ribbon>,
);
expect(wrapper.find('.ant-ribbon-color-green').length).toEqual(1);
});
it('works with custom color', () => {
const wrapperLeft = mount(
<Badge.Ribbon color="#888" placement="start">
<div />
</Badge.Ribbon>,
);
expect(wrapperLeft.find('.ant-ribbon').prop('style').background).toEqual('#888');
expect(wrapperLeft.find('.ant-ribbon-corner').prop('style').color).toEqual('#888');
const wrapperRight = mount(
<Badge.Ribbon color="#888" placement="end">
<div />
</Badge.Ribbon>,
);
expect(wrapperRight.find('.ant-ribbon').prop('style').background).toEqual('#888');
expect(wrapperRight.find('.ant-ribbon-corner').prop('style').color).toEqual('#888');
});
});
describe('text', () => {
it('works with string', () => {
const wrapper = mount(
<Badge.Ribbon text="cool">
<div />
</Badge.Ribbon>,
);
expect(wrapper.find('.ant-ribbon').text()).toEqual('cool');
});
it('works with element', () => {
const wrapper = mount(
<Badge.Ribbon text={<span className="cool" />}>
<div />
</Badge.Ribbon>,
);
expect(wrapper.find('.cool').length).toEqual(1);
});
});
});

View File

@ -0,0 +1,25 @@
---
order: 15
title:
zh-CN: 缎带
en-US: Ribbon
---
## zh-CN
使用缎带型的徽标。
## en-US
Use ribbon badge.
```jsx
import { Badge, Card } from 'antd';
ReactDOM.render(
<Badge.Ribbon text="Pushes open the window">
<Card>And raises the spyglass.</Card>
</Badge.Ribbon>,
mountNode,
);
```

View File

@ -0,0 +1,40 @@
---
order: 20
title:
zh-CN: Ribbon Debug
en-US: Ribbon Debug
debug: true
---
## zh-CN
Buggy!
## en-US
Buggy!
```jsx
import { Badge, Card, Space } from 'antd';
ReactDOM.render(
<Space direction="vertical" style={{ width: '100%' }}>
<Badge.Ribbon text="啦啦啦啦">
<Card>推开窗户举起望远镜</Card>
</Badge.Ribbon>
<Badge.Ribbon text="啦啦啦啦" color="purple">
<Card>推开窗户举起望远镜</Card>
</Badge.Ribbon>
<Badge.Ribbon text="啦啦啦啦" color="#2db7f5">
<Card>推开窗户举起望远镜</Card>
</Badge.Ribbon>
<Badge.Ribbon text="啦啦啦啦" color="#2db7f5" placement="start">
<Card>推开窗户举起望远镜</Card>
</Badge.Ribbon>
<Badge.Ribbon text="啦啦啦啦" color="#2db7f5" placement="end">
<Card>推开窗户举起望远镜</Card>
</Badge.Ribbon>
</Space>,
mountNode,
);
```

View File

@ -13,6 +13,8 @@ Badge normally appears in proximity to notifications or user avatars with eye-ca
## API
### Badge
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| color | Customize Badge dot color | string | - | |
@ -22,5 +24,13 @@ Badge normally appears in proximity to notifications or user avatars with eye-ca
| overflowCount | Max count to show | number | 99 | |
| showZero | Whether to show badge when `count` is zero | boolean | false | |
| status | Set Badge as a status dot | `success` \| `processing` \| `default` \| `error` \| `warning` | - | |
| text | If `status` is set, `text` sets the display text of the status `dot` | string | - | |
| text | If `status` is set, `text` sets the display text of the status `dot` | ReactNode | - | |
| title | Text to show when hovering over the badge | string | - | |
### Badge.Ribbon (4.5.0+)
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| color | Customize Ribbon color | string | - | |
| placement | The placement of the Ribbon, `start` and `end` follow text direction (RTL or LTR) | `start` \| `end` | `end` | |
| text | Content inside the Ribbon | ReactNode | - | |

View File

@ -2,13 +2,19 @@ import * as React from 'react';
import Animate from 'rc-animate';
import classNames from 'classnames';
import ScrollNumber from './ScrollNumber';
import { PresetColorTypes, PresetColorType, PresetStatusColorType } from '../_util/colors';
import Ribbon from './Ribbon';
import { PresetColorType, PresetStatusColorType } from '../_util/colors';
import { ConfigContext } from '../config-provider';
import { LiteralUnion } from '../_util/type';
import { cloneElement } from '../_util/reactNode';
import { isPresetColor } from './utils';
export { ScrollNumberProps } from './ScrollNumber';
interface CompoundedComponent extends React.FC<BadgeProps> {
Ribbon: typeof Ribbon;
}
export interface BadgeProps {
/** Number to show in badge */
count?: React.ReactNode;
@ -28,11 +34,7 @@ export interface BadgeProps {
title?: string;
}
function isPresetColor(color?: string): boolean {
return (PresetColorTypes as any[]).indexOf(color) !== -1;
}
const Badge: React.FC<BadgeProps> = ({
const Badge: CompoundedComponent = ({
prefixCls: customizePrefixCls,
scrollNumberPrefixCls: customizeScrollNumberPrefixCls,
children,
@ -210,4 +212,6 @@ const Badge: React.FC<BadgeProps> = ({
);
};
Badge.Ribbon = Ribbon;
export default Badge;

View File

@ -14,6 +14,8 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/6%26GF9WHwvY/Badge.svg
## API
### Badge
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| color | 自定义小圆点的颜色 | string | - | |
@ -23,5 +25,13 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/6%26GF9WHwvY/Badge.svg
| overflowCount | 展示封顶的数字值 | number | 99 | |
| showZero | 当数值为 0 时,是否展示 Badge | boolean | false | |
| status | 设置 Badge 为状态点 | `success` \| `processing` \| `default` \| `error` \| `warning` | - | |
| text | 在设置了 `status` 的前提下有效,设置状态点的文本 | string | - | |
| text | 在设置了 `status` 的前提下有效,设置状态点的文本 | ReactNode | - | |
| title | 设置鼠标放在状态点上时显示的文字 | string | - | |
### Badge.Ribbon (4.5.0+)
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| color | 自定义缎带的颜色 | string | - | |
| placement | 缎带的位置,`start` 和 `end` 随文字方向RTL 或 LTR变动 | `start` \| `end` | `end` | |
| text | 缎带中填入的内容 | ReactNode | - | |

View File

@ -1,2 +1,3 @@
import '../../style/index.less';
import './ribbon.less';
import './index.less';

View File

@ -0,0 +1,84 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@ribbon-prefix-cls: ~'@{ant-prefix}-ribbon';
@ribbon-wrapper-prefix-cls: ~'@{ant-prefix}-ribbon-wrapper';
.@{ribbon-wrapper-prefix-cls} {
position: relative;
}
.@{ribbon-prefix-cls} {
.reset-component;
position: absolute;
top: 8px;
height: 22px;
padding: 0 8px;
color: @badge-text-color;
line-height: 22px;
white-space: nowrap;
background-color: @primary-color;
border-radius: @border-radius-sm;
&-corner {
position: absolute;
top: 100%;
width: 8px;
height: 8px;
color: @primary-color;
border: 4px solid;
transform: scaleY(0.75);
transform-origin: top;
// If not support IE 11, use filter: brightness(75%) instead
&::after {
position: absolute;
top: -4px;
left: -4px;
width: inherit;
height: inherit;
color: rgba(0, 0, 0, 0.25);
border: inherit;
content: '';
}
}
// colors
// mixin to iterate over colors and create CSS class for each one
.make-color-classes(@i: length(@preset-colors)) when (@i > 0) {
.make-color-classes(@i - 1);
@color: extract(@preset-colors, @i);
@darkColor: '@{color}-6';
&-color-@{color} {
background-color: @@darkColor;
.@{ribbon-prefix-cls}-corner {
color: @@darkColor;
}
}
}
.make-color-classes();
// placement
&.@{ribbon-prefix-cls}-placement-end {
right: -8px;
border-bottom-right-radius: 0;
.@{ribbon-prefix-cls}-corner {
right: 0;
border-color: currentColor transparent transparent currentColor;
&::after {
border-color: currentColor transparent transparent currentColor;
}
}
}
&.@{ribbon-prefix-cls}-placement-start {
left: -8px;
border-bottom-left-radius: 0;
.@{ribbon-prefix-cls}-corner {
left: 0;
border-color: currentColor currentColor transparent transparent;
&::after {
border-color: currentColor currentColor transparent transparent;
}
}
}
}

View File

@ -3,6 +3,7 @@
@badge-prefix-cls: ~'@{ant-prefix}-badge';
@number-prefix-cls: ~'@{ant-prefix}-scroll-number';
@ribbon-prefix-cls: ~'@{ant-prefix}-ribbon';
.@{badge-prefix-cls} {
&-rtl {
@ -57,6 +58,38 @@
}
}
.@{ribbon-prefix-cls}-rtl {
direction: rtl;
&.@{ribbon-prefix-cls}-placement-end {
right: unset;
left: -8px;
border-bottom-right-radius: @border-radius-sm;
border-bottom-left-radius: 0;
.@{ribbon-prefix-cls}-corner {
right: unset;
left: 0;
border-color: currentColor currentColor transparent transparent;
&::after {
border-color: currentColor currentColor transparent transparent;
}
}
}
&.@{ribbon-prefix-cls}-placement-start {
right: -8px;
left: unset;
border-bottom-right-radius: 0;
border-bottom-left-radius: @border-radius-sm;
.@{ribbon-prefix-cls}-corner {
right: 0;
left: unset;
border-color: currentColor transparent transparent currentColor;
&::after {
border-color: currentColor transparent transparent currentColor;
}
}
}
}
@keyframes antZoomBadgeInRtl {
0% {
transform: scale(0) translate(-50%, -50%);

View File

@ -0,0 +1,6 @@
import { PresetColorTypes } from '../_util/colors';
// eslint-disable-next-line import/prefer-default-export
export function isPresetColor(color?: string): boolean {
return (PresetColorTypes as any[]).indexOf(color) !== -1;
}

View File

@ -14168,7 +14168,9 @@ exports[`ConfigProvider components Form configProvider 1`] = `
<div
class="config-form-item-explain"
>
<div>
<div
role="alert"
>
Bamboo is Light
</div>
</div>
@ -14203,7 +14205,9 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
<div
class="config-form-item-explain"
>
<div>
<div
role="alert"
>
Bamboo is Light
</div>
</div>
@ -14238,7 +14242,9 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
<div
class="config-form-item-explain"
>
<div>
<div
role="alert"
>
Bamboo is Light
</div>
</div>
@ -14273,7 +14279,9 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Bamboo is Light
</div>
</div>
@ -14308,7 +14316,9 @@ exports[`ConfigProvider components Form normal 1`] = `
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Bamboo is Light
</div>
</div>
@ -14343,7 +14353,9 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
<div
class="prefix-Form-item-explain"
>
<div>
<div
role="alert"
>
Bamboo is Light
</div>
</div>

View File

@ -70,7 +70,7 @@ describe('ConfigProvider', () => {
const App = () => {
const { renderEmpty } = React.useContext(ConfigContext);
return renderEmpty();
}
};
const wrapper = mount(
<ConfigProvider>
<App />

View File

@ -58,6 +58,7 @@ The following APIs are shared by DatePicker, RangePicker.
| locale | Localization configuration | object | [default](https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json) | |
| mode | The picker panel mode [Cannot select year or month anymore?](/docs/react/faq#When-set-mode-to-DatePicker/RangePicker,-cannot-select-year-or-month-anymore?) ) | `time` \| `date` \| `month` \| `year` \| `decade` | - | |
| open | The open state of picker | boolean | - | |
| panelRender | Customize panel render | (panelNode) => ReactNode | - | 4.5.0 |
| picker | Set picker type | `date` \| `week` \| `month` \| `quarter` \| `year` | `date` | `quarter`: 4.1.0 |
| placeholder | The placeholder of date input | string \| \[string,string] | - | |
| popupStyle | To customize the style of the popup calendar | CSSProperties | {} | |
@ -158,7 +159,7 @@ Added in `4.1.0`.
| showTime | To provide an additional time selection | object \| boolean | [TimePicker Options](/components/time-picker/#API) | |
| showTime.defaultValue | To set default time of selected date, [demo](#components-date-picker-demo-disabled-date) | [moment](http://momentjs.com/)\[] | \[moment(), moment()] | |
| value | To set date | \[[moment](http://momentjs.com/), [moment](http://momentjs.com/)] | - | |
| onCalendarChange | Callback function, can be executed when the start time or the end time of the range is changing | function(dates: \[moment, moment], dateStrings: \[string, string]) | - | |
| onCalendarChange | Callback function, can be executed when the start time or the end time of the range is changing. `info` argument is added in 4.4.0 | function(dates: \[moment, moment], dateStrings: \[string, string], info: { range:`start`\|`end` }) | - | |
| onChange | Callback function, can be executed when the selected time is changing | function(dates: \[moment, moment], dateStrings: \[string, string]) | - | |
<style>

View File

@ -60,6 +60,7 @@ import 'moment/locale/zh-cn';
| locale | 国际化配置 | object | [默认配置](https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json) | |
| mode | 日期面板的状态([设置后无法选择年份/月份?](/docs/react/faq#当我指定了-DatePicker/RangePicker-的-mode-属性后,点击后无法选择年份/月份?) | `time` \| `date` \| `month` \| `year` \| `decade` | - | |
| open | 控制弹层是否展开 | boolean | - | |
| panelRender | 自定义渲染面板 | (panelNode) => ReactNode | - | 4.5.0 |
| picker | 设置选择器类型 | `date` \| `week` \| `month` \| `quarter` \| `year` | `date` | `quarter`: 4.1.0 |
| placeholder | 输入框提示文字 | string \| \[string, string] | - | |
| popupStyle | 额外的弹出日历样式 | CSSProperties | {} | |
@ -160,7 +161,7 @@ import 'moment/locale/zh-cn';
| showTime | 增加时间选择功能 | Object\|boolean | [TimePicker Options](/components/time-picker/#API) | |
| showTime.defaultValue | 设置用户选择日期时默认的时分秒,[例子](#components-date-picker-demo-disabled-date) | [moment](http://momentjs.com/)\[] | \[moment(), moment()] | |
| value | 日期 | [moment](http://momentjs.com/)\[] | - | |
| onCalendarChange | 待选日期发生变化的回调 | function(dates: \[moment, moment\], dateStrings: \[string, string\]) | - | |
| onCalendarChange | 待选日期发生变化的回调。`info` 参数自 4.4.0 添加 | function(dates: \[moment, moment\], dateStrings: \[string, string\], info: { range:`start`\|`end` }) | - | |
| onChange | 日期范围发生变化的回调 | function(dates: \[moment, moment\], dateStrings: \[string, string\]) | - | |
<style>

View File

@ -3,12 +3,16 @@
exports[`renders ./components/descriptions/demo/basic.md correctly 1`] = `
<div
class="ant-descriptions"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
User Info
</div>
</div>
<div
class="ant-descriptions-view"
>
@ -106,12 +110,16 @@ exports[`renders ./components/descriptions/demo/basic.md correctly 1`] = `
exports[`renders ./components/descriptions/demo/border.md correctly 1`] = `
<div
class="ant-descriptions ant-descriptions-bordered"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
User Info
</div>
</div>
<div
class="ant-descriptions-view"
>
@ -289,12 +297,16 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = `
<div>
<div
class="ant-descriptions ant-descriptions-bordered"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
Responsive Descriptions
</div>
</div>
<div
class="ant-descriptions-view"
>
@ -481,12 +493,28 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = `
<br />
<div
class="ant-descriptions ant-descriptions-bordered"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
Custom Size
</div>
<div
class="ant-descriptions-extra"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Edit
</span>
</button>
</div>
</div>
<div
class="ant-descriptions-view"
>
@ -607,12 +635,28 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = `
<br />
<div
class="ant-descriptions"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
Custom Size
</div>
<div
class="ant-descriptions-extra"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Edit
</span>
</button>
</div>
</div>
<div
class="ant-descriptions-view"
>
@ -726,12 +770,16 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = `
exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = `
<div
class="ant-descriptions"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
User Info
</div>
</div>
<div
class="ant-descriptions-view"
>
@ -862,12 +910,16 @@ exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = `
exports[`renders ./components/descriptions/demo/vertical-border.md correctly 1`] = `
<div
class="ant-descriptions ant-descriptions-bordered"
>
<div
class="ant-descriptions-header"
>
<div
class="ant-descriptions-title"
>
User Info
</div>
</div>
<div
class="ant-descriptions-view"
>

View File

@ -227,4 +227,15 @@ describe('Descriptions', () => {
expect(wrapper.find('th').hasClass('ant-descriptions-item-label')).toBeTruthy();
expect(wrapper.find('td').hasClass('ant-descriptions-item-content')).toBeTruthy();
});
it('Descriptions support extra', () => {
const wrapper = mount(
<Descriptions extra="Edit">
<Descriptions.Item label="UserName">Zhou Maomao</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.find('.ant-descriptions-extra').exists()).toBe(true);
wrapper.setProps({ extra: undefined });
expect(wrapper.find('.ant-descriptions-extra').exists()).toBe(false);
});
});

View File

@ -14,7 +14,7 @@ title:
Custom sizes to fit in a variety of containers.
```jsx
import { Descriptions, Radio } from 'antd';
import { Descriptions, Radio, Button } from 'antd';
class Demo extends React.Component {
state = {
@ -38,7 +38,12 @@ class Demo extends React.Component {
</Radio.Group>
<br />
<br />
<Descriptions bordered title="Custom Size" size={this.state.size}>
<Descriptions
bordered
title="Custom Size"
size={this.state.size}
extra={<Button type="primary">Edit</Button>}
>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing">Prepaid</Descriptions.Item>
<Descriptions.Item label="time">18:00:00</Descriptions.Item>
@ -61,7 +66,11 @@ class Demo extends React.Component {
</Descriptions>
<br />
<br />
<Descriptions title="Custom Size" size={this.state.size}>
<Descriptions
title="Custom Size"
size={this.state.size}
extra={<Button type="primary">Edit</Button>}
>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing">Prepaid</Descriptions.Item>
<Descriptions.Item label="time">18:00:00</Descriptions.Item>

View File

@ -19,6 +19,7 @@ Commonly displayed on the details page.
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| title | The title of the description list, placed at the top | ReactNode | - | |
| extra | The action area of the description list, placed at the top-right | string \| ReactNode | - | 4.5.0 |
| bordered | Whether to display the border | boolean | false | |
| column | The number of `DescriptionItems` in a row,could be a number or a object like `{ xs: 8, sm: 16, md: 24}`,(Only set `bordered={true}` to take effect) | number | 3 | |
| size | Set the size of the list. Can be set to `middle`,`small`, or not filled | `default` \| `middle` \| `small` | - | |

View File

@ -100,6 +100,7 @@ export interface DescriptionsProps {
size?: 'middle' | 'small' | 'default';
children?: React.ReactNode;
title?: React.ReactNode;
extra?: React.ReactNode;
column?: number | Partial<Record<Breakpoint, number>>;
layout?: 'horizontal' | 'vertical';
colon?: boolean;
@ -108,6 +109,7 @@ export interface DescriptionsProps {
function Descriptions({
prefixCls: customizePrefixCls,
title,
extra,
column = DEFAULT_COLUMN_MAP,
colon = true,
bordered,
@ -148,7 +150,12 @@ function Descriptions({
})}
style={style}
>
{(title || extra) && (
<div className={`${prefixCls}-header`}>
{title && <div className={`${prefixCls}-title`}>{title}</div>}
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
</div>
)}
<div className={`${prefixCls}-view`}>
<table>

View File

@ -20,6 +20,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/MjtG9_FOI/Descriptions.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| title | 描述列表的标题,显示在最顶部 | ReactNode | - | |
| extra | 描述列表的操作区域,显示在右上方 | string \| ReactNode | - | 4.5.0 |
| bordered | 是否展示边框 | boolean | false | |
| column | 一行的 `DescriptionItems` 数量,可以写成像素值或支持响应式的对象写法 `{ xs: 8, sm: 16, md: 24}` | number | 3 | |
| size | 设置列表的大小。可以设置为 `middle` 、`small`, 或不填(只有设置 `bordered={true}` 生效) | `default` \| `middle` \| `small` | - | |

View File

@ -4,12 +4,27 @@
@descriptions-prefix-cls: ~'@{ant-prefix}-descriptions';
.@{descriptions-prefix-cls} {
&-title {
&-header {
display: flex;
align-items: center;
margin-bottom: @descriptions-title-margin-bottom;
}
&-title {
flex: auto;
overflow: hidden;
color: @heading-color;
font-weight: bold;
font-size: @font-size-lg;
line-height: @line-height-base;
white-space: nowrap;
text-overflow: ellipsis;
}
&-extra {
margin-left: auto;
color: @descriptions-extra-color;
font-size: @font-size-base;
}
&-view {

View File

@ -40,7 +40,7 @@ class MultiDrawer extends React.Component {
render() {
const { childrenDrawer, visible, hasChildren } = this.state;
const { placement } = this.props;
const { placement, push } = this.props;
return (
<div>
<Button type="primary" id="open_drawer" onClick={this.showDrawer}>
@ -57,6 +57,7 @@ class MultiDrawer extends React.Component {
getContainer={false}
placement={placement}
visible={visible}
push={push}
>
<Button type="primary" id="open_two_drawer" onClick={this.showChildrenDrawer}>
Two-level drawer
@ -147,4 +148,28 @@ describe('Drawer', () => {
expect(translateX).toEqual('translateY(180px)');
expect(wrapper.find('#two_drawer_text').exists()).toBe(true);
});
it('custom MultiDrawer push distance', () => {
const wrapper = mount(<MultiDrawer push={{ distance: 256 }} />);
wrapper.find('button#open_drawer').simulate('click');
wrapper.find('button#open_two_drawer').simulate('click');
const translateX = wrapper.find('.ant-drawer.test_drawer').get(0).props.style.transform;
expect(translateX).toEqual('translateX(-256px)');
});
it('custom MultiDrawer push with true', () => {
const wrapper = mount(<MultiDrawer push />);
wrapper.find('button#open_drawer').simulate('click');
wrapper.find('button#open_two_drawer').simulate('click');
const translateX = wrapper.find('.ant-drawer.test_drawer').get(0).props.style.transform;
expect(translateX).toEqual('translateX(-180px)');
});
it('custom MultiDrawer push with false', () => {
const wrapper = mount(<MultiDrawer push={false} />);
wrapper.find('button#open_drawer').simulate('click');
wrapper.find('button#open_two_drawer').simulate('click');
const translateX = wrapper.find('.ant-drawer.test_drawer').get(0).props.style.transform;
expect(translateX).toBeUndefined();
});
});

View File

@ -18,8 +18,8 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr
## API
| Props | Description | Type | Default |
| --- | --- | --- | --- |
| Props | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| closable | Whether a close (x) button is visible on top right of the Drawer dialog or not | boolean | true |
| closeIcon | Custom close icon | ReactNode | &lt;CloseOutlined /> |
| destroyOnClose | Whether to unmount child components on closing drawer or not | boolean | false |
@ -44,3 +44,4 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr
| keyboard | Whether support press esc to close | boolean | true |
| footer | The footer for Drawer | ReactNode | - |
| footerStyle | Style of the drawer footer part | CSSProperties | - |
| push | Nested drawers push behavior | boolean \| { distance: string \| number } | { distance: 180 } | 4.5.0+ |

View File

@ -19,6 +19,10 @@ type getContainerFunc = () => HTMLElement;
const PlacementTypes = tuple('top', 'right', 'bottom', 'left');
type placementType = typeof PlacementTypes[number];
export interface PushState {
distance: string | number;
}
export interface DrawerProps {
closable?: boolean;
closeIcon?: React.ReactNode;
@ -39,7 +43,7 @@ export interface DrawerProps {
height?: number | string;
zIndex?: number;
prefixCls?: string;
push?: boolean;
push?: boolean | PushState;
placement?: placementType;
onClose?: (e: EventType) => void;
afterVisibleChange?: (visible: boolean) => void;
@ -54,6 +58,7 @@ export interface IDrawerState {
push?: boolean;
}
const defaultPushState: PushState = { distance: 180 };
class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerState> {
static defaultProps = {
width: 256,
@ -64,6 +69,7 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
mask: true,
level: null,
keyboard: true,
push: defaultPushState,
};
readonly state = {
@ -103,15 +109,15 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
}
push = () => {
this.setState({
push: true,
});
if (this.props.push) {
this.setState({ push: true });
}
};
pull = () => {
this.setState({
push: false,
});
if (this.props.push) {
this.setState({ push: false });
}
};
onDestroyTransitionEnd = () => {
@ -127,13 +133,26 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
getDestroyOnClose = () => this.props.destroyOnClose && !this.props.visible;
getPushDistance = () => {
const { push } = this.props;
let distance: number | string;
if (typeof push === 'boolean') {
distance = push ? defaultPushState.distance : 0;
} else {
distance = push!.distance;
}
return parseFloat(String(distance || 0));
};
// get drawer push width or height
getPushTransform = (placement?: placementType) => {
const distance = this.getPushDistance();
if (placement === 'left' || placement === 'right') {
return `translateX(${placement === 'left' ? 180 : -180}px)`;
return `translateX(${placement === 'left' ? distance : -distance}px)`;
}
if (placement === 'top' || placement === 'bottom') {
return `translateY(${placement === 'top' ? 180 : -180}px)`;
return `translateY(${placement === 'top' ? distance : -distance}px)`;
}
};

View File

@ -17,8 +17,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
## API
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| closable | 是否显示右上角的关闭按钮 | boolean | true |
| closeIcon | 自定义关闭图标 | ReactNode | &lt;CloseOutlined /> |
| destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false |
@ -43,3 +43,4 @@ cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
| keyboard | 是否支持键盘 esc 关闭 | boolean | true |
| footer | 抽屉的页脚 | ReactNode | - |
| footerStyle | 抽屉页脚部件的样式 | CSSProperties | - |
| push | 用于设置多层 Drawer 的推动行为 | boolean \| { distance: string \| number } | { distance: 180 } | 4.5.0+ |

View File

@ -305,14 +305,20 @@ function FormItem(props: FormItemProps): React.ReactElement {
};
let childNode: React.ReactNode = null;
devWarning(
!(shouldUpdate && dependencies),
'Form.Item',
"`shouldUpdate` and `dependencies` shouldn't be used together. See https://ant.design/components/form/#dependencies.",
);
if (Array.isArray(children) && hasName) {
devWarning(false, 'Form.Item', '`children` is array of render props cannot have `name`.');
childNode = children;
} else if (isRenderProps && (!shouldUpdate || hasName)) {
} else if (isRenderProps && (!(shouldUpdate || dependencies) || hasName)) {
devWarning(
!!shouldUpdate,
!!(shouldUpdate || dependencies),
'Form.Item',
'`children` of render props only work with `shouldUpdate`.',
'`children` of render props only work with `shouldUpdate` or `dependencies`.',
);
devWarning(
!hasName,
@ -362,7 +368,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
{cloneElement(children, childProps)}
</MemoInput>
);
} else if (isRenderProps && shouldUpdate && !hasName) {
} else if (isRenderProps && (shouldUpdate || dependencies) && !hasName) {
childNode = (children as RenderChildren)(context);
} else {
devWarning(

View File

@ -122,7 +122,9 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
<div className={classNames(`${baseClassName}-explain`, motionClassName)} key="help">
{memoErrors.map((error, index) => (
// eslint-disable-next-line react/no-array-index-key
<div key={index}>{error}</div>
<div key={index} role="alert">
{error}
</div>
))}
</div>
);

View File

@ -11,7 +11,7 @@ interface FieldData {
interface Operation {
add: (defaultValue?: StoreValue) => void;
remove: (index: number) => void;
remove: (index: number | number[]) => void;
move: (from: number, to: number) => void;
}

View File

@ -933,6 +933,81 @@ exports[`renders ./components/form/demo/customized-form-controls.md correctly 1`
</form>
`;
exports[`renders ./components/form/demo/dep-debug.md correctly 1`] = `
<form
class="ant-form ant-form-horizontal"
id="debug"
>
0
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
for="debug_debug1"
title="debug1"
>
debug1
</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="debug_debug1"
type="text"
value="debug1"
/>
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
for="debug_debug2"
title="debug2"
>
debug2
</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="debug_debug2"
type="text"
value="debug2"
/>
</div>
</div>
</div>
</div>
</form>
`;
exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] = `
<form
class="ant-form ant-form-horizontal"
@ -1002,7 +1077,9 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Buggy!
</div>
</div>
@ -1042,7 +1119,9 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Buggy!
</div>
</div>
@ -1113,7 +1192,9 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Buggy!
</div>
</div>
@ -1153,7 +1234,9 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Buggy!
</div>
</div>
@ -1250,7 +1333,9 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Buggy!
</div>
</div>
@ -1303,7 +1388,9 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Buggy!
</div>
</div>
@ -1392,7 +1479,9 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Buggy!
</div>
</div>
@ -1441,7 +1530,9 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Buggy!
</div>
</div>
@ -5904,7 +5995,9 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Should be combination of numbers & alphabets
</div>
</div>
@ -6029,7 +6122,9 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
The information is being validated...
</div>
</div>
@ -6207,7 +6302,9 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Should be combination of numbers & alphabets
</div>
</div>
@ -6592,7 +6689,9 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
The information is being validated...
</div>
</div>
@ -6679,7 +6778,9 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
Please select the correct date
</div>
</div>
@ -7275,7 +7376,9 @@ exports[`renders ./components/form/demo/without-form-create.md correctly 1`] = `
<div
class="ant-form-item-explain"
>
<div>
<div
role="alert"
>
A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.
</div>
</div>

View File

@ -121,7 +121,19 @@ describe('Form', () => {
</Form>,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form.Item] `children` of render props only work with `shouldUpdate`.',
'Warning: [antd: Form.Item] `children` of render props only work with `shouldUpdate` or `dependencies`.',
);
});
it("`shouldUpdate` shouldn't work with `dependencies`", () => {
mount(
<Form>
<Form.Item shouldUpdate dependencies={[]}>
{() => null}
</Form.Item>
</Form>,
);
expect(errorSpy).toHaveBeenCalledWith(
"Warning: [antd: Form.Item] `shouldUpdate` and `dependencies` shouldn't be used together. See https://ant.design/components/form/#dependencies.",
);
});
it('`name` should not work with render props', () => {
@ -528,6 +540,26 @@ describe('Form', () => {
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('Bamboo is good!');
});
it('validation message should has alert role', async () => {
// https://github.com/ant-design/ant-design/issues/25711
const wrapper = mount(
// eslint-disable-next-line no-template-curly-in-string
<Form validateMessages={{ required: 'name is good!' }}>
<Form.Item name="test" rules={[{ required: true }]}>
<input />
</Form.Item>
</Form>,
);
wrapper.find('form').simulate('submit');
await sleep(100);
wrapper.update();
await sleep(100);
expect(wrapper.find('.ant-form-item-explain div').getDOMNode().getAttribute('role')).toBe(
'alert',
);
});
it('return same form instance', () => {
const instances = new Set();

View File

@ -0,0 +1,49 @@
---
order: 99
title:
zh-CN: Dep Debug
en-US: Dep Debug
---
## zh-CN
Buggy!
## en-US
Buggy!
```tsx
import { Form, Input } from 'antd';
let acc = 0;
const Demo = () => {
const [form] = Form.useForm();
return (
<Form
form={form}
name="debug"
initialValues={{
debug1: 'debug1',
debug2: 'debug2',
}}
>
<Form.Item noStyle dependencies={['debug1']}>
{() => {
return acc++;
// return <pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>;
}}
</Form.Item>
<Form.Item label="debug1" name="debug1">
<Input />
</Form.Item>
<Form.Item label="debug2" name="debug2">
<Input />
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -92,7 +92,7 @@ Form field component for data bidirectional binding, validation, layout, and so
| rules | Rules for field validation. Click [here](#components-form-demo-basic) to see an example | [Rule](#Rule)[] | - | |
| shouldUpdate | Custom field update logic. See [below](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | |
| trigger | When to collect the value of children node | string | `onChange` | |
| validateFirst | Whether stop validate on first rule of error for this field | boolean | false | |
| validateFirst | Whether stop validate on first rule of error for this field. Will parallel validate when `parallel` cofigured | boolean \| `parallel` | false | `parallel`: 4.5.0 |
| validateStatus | The validation status. If not provided, it will be generated by validation rule. options: `success` `warning` `error` `validating` | string | - | |
| validateTrigger | When to validate the value of children node | string \| string[] | `onChange` | |
| valuePropName | Props of children node, for example, the prop of Switch is 'checked'. This prop is an encapsulation of `getValueProps`, which will be invalid after customizing `getValueProps` | string | `value` | |
@ -109,6 +109,10 @@ After wrapped by `Form.Item` with `name` property, `value`(or other property def
Used when there are dependencies between fields. If a field has the `dependencies` prop, this field will automatically trigger updates and validations when upstream is updated. A common scenario is a user registration form with "password" and "confirm password" fields. The "Confirm Password" validation depends on the "Password" field. After setting `dependencies`, the "Password" field update will re-trigger the validation of "Check Password". You can refer [examples](#components-form-demo-register).
`dependencies` shouldn't be used together with `shouldUpdate`. Since it may cause chaos in updating logic.
`dependencies` supports `Form.Item` with render props children since `4.5.0`.
### shouldUpdate
Form updates only the modified field-related components for performance optimization purposes by incremental update. In most cases, you only need to write code or do validation with the [`dependencies`](#dependencies) property. In some specific cases, such as when a new field option appears with a filed value changed, or you just want to keep some area updating by form update, you can modify the update logic of Form.Item via the `shouldUpdate`.
@ -164,6 +168,16 @@ Provides array management for fields.
</Form.List>
```
## operation
Some operator functions in render form of Form.List.
| Property | Description | Type | Default |
| -------- | ---------------- | ----------------------------------- | --------------- |
| add | add form item | (defaultValue?: any) => void | - |
| remove | remove form item | (index: number \| number[]) => void | number[]: 4.5.0 |
| move | move form item | (from: number, to: number) => void | - |
## Form.Provider
Provide linkage between forms. If a sub form with `name` prop update, it will auto trigger Provider related events. See [example](#components-form-demo-form-context).

View File

@ -93,7 +93,7 @@ const validateMessages = {
| rules | 校验规则,设置字段的校验逻辑。点击[此处](#components-form-demo-basic)查看示例 | [Rule](#Rule)[] | - | |
| shouldUpdate | 自定义字段更新逻辑,说明[见下](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false | |
| trigger | 设置收集字段值变更的时机 | string | `onChange` | |
| validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验 | boolean | false | |
| validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验。设置 `parallel` 时会并行校验 | boolean \| `parallel` | false | `parallel`: 4.5.0 |
| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | - | |
| validateTrigger | 设置字段校验的时机 | string \| string[] | `onChange` | |
| valuePropName | 子节点的值的属性,如 Switch 的是 'checked'。该属性为 `getValueProps` 的封装,自定义 `getValueProps` 后会失效 | string | `value` | |
@ -110,6 +110,10 @@ const validateMessages = {
当字段间存在依赖关系时使用。如果一个字段设置了 `dependencies` 属性。那么它所依赖的字段更新时,该字段将自动触发更新与校验。一种常见的场景,就是注册用户表单的“密码”与“确认密码”字段。“确认密码”校验依赖于“密码”字段,设置 `dependencies` 后,“密码”字段更新会重新触发“校验密码”的校验逻辑。你可以参考[具体例子](#components-form-demo-register)。
`dependencies` 不应和 `shouldUpdate` 一起使用,因为这可能带来更新逻辑的混乱。
`4.5.0` 版本开始,`dependencies` 支持使用 render props 类型 children 的 `Form.Item`
### shouldUpdate
Form 通过增量更新方式,只更新被修改的字段相关组件以达到性能优化目的。大部分场景下,你只需要编写代码或者与 [`dependencies`](#dependencies) 属性配合校验即可。而在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 Form.Item 的更新逻辑。
@ -162,9 +166,20 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
))}
</div>
)}
1
</Form.List>
```
## operation
Form.List 渲染表单相关操作函数。
| 参数 | 说明 | 类型 | 默认值 |
| ------ | ---------- | ----------------------------------- | --------------- |
| add | 新增表单项 | (defaultValue?: any) => void | - |
| remove | 删除表单项 | (index: number \| number[]) => void | number[]: 4.5.0 |
| move | 移动表单项 | (from: number, to: number) => void | - |
## Form.Provider
提供表单间联动功能,其下设置 `name` 的 Form 更新时,会自动触发对应事件。查看[示例](#components-form-demo-form-context)。

View File

@ -28,6 +28,7 @@ interface BasicProps {
direction?: any;
focused?: boolean;
readOnly?: boolean;
bordered: boolean;
}
/**
@ -102,6 +103,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
direction,
style,
readOnly,
bordered,
} = this.props;
const suffixNode = this.renderSuffix(prefixCls);
if (!hasPrefixSuffix(this.props)) {
@ -120,6 +122,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
[`${prefixCls}-affix-wrapper-input-with-clear-btn`]: suffix && allowClear && value,
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-readonly`]: readOnly,
[`${prefixCls}-affix-wrapper-borderless`]: !bordered,
});
return (
<span
@ -132,7 +135,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
{cloneElement(element, {
style: null,
value,
className: getInputClassName(prefixCls, size, disabled),
className: getInputClassName(prefixCls, bordered, size, disabled),
})}
{suffixNode}
</span>
@ -178,7 +181,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
}
renderTextAreaWithClearIcon(prefixCls: string, element: React.ReactElement) {
const { value, allowClear, className, style, direction } = this.props;
const { value, allowClear, className, style, direction, bordered } = this.props;
if (!allowClear) {
return cloneElement(element, {
value,
@ -187,8 +190,11 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
const affixWrapperCls = classNames(
className,
`${prefixCls}-affix-wrapper`,
{ [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl' },
`${prefixCls}-affix-wrapper-textarea-with-clear-btn`,
{
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-borderless`]: !bordered,
},
);
return (
<span className={affixWrapperCls} style={style}>

View File

@ -47,6 +47,7 @@ export interface InputProps
prefix?: React.ReactNode;
suffix?: React.ReactNode;
allowClear?: boolean;
bordered?: boolean;
}
export function fixControlledValue<T>(value: T) {
@ -84,6 +85,7 @@ export function resolveOnChange(
export function getInputClassName(
prefixCls: string,
bordered: boolean,
size?: SizeType,
disabled?: boolean,
direction?: any,
@ -93,6 +95,7 @@ export function getInputClassName(
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-borderless`]: !bordered,
});
}
@ -220,6 +223,7 @@ class Input extends React.Component<InputProps, InputState> {
renderInput = (
prefixCls: string,
size: SizeType | undefined,
bordered: boolean,
input: ConfigConsumerProps['input'] = {},
) => {
const { className, addonBefore, addonAfter, size: customizeSize, disabled } = this.props;
@ -237,6 +241,7 @@ class Input extends React.Component<InputProps, InputState> {
'defaultValue',
'size',
'inputType',
'bordered',
]);
return (
<input
@ -247,7 +252,7 @@ class Input extends React.Component<InputProps, InputState> {
onBlur={this.onBlur}
onKeyDown={this.handleKeyDown}
className={classNames(
getInputClassName(prefixCls, customizeSize || size, disabled, this.direction),
getInputClassName(prefixCls, bordered, customizeSize || size, disabled, this.direction),
{
[className!]: className && !addonBefore && !addonAfter,
},
@ -287,7 +292,7 @@ class Input extends React.Component<InputProps, InputState> {
renderComponent = ({ getPrefixCls, direction, input }: ConfigConsumerProps) => {
const { value, focused } = this.state;
const { prefixCls: customizePrefixCls } = this.props;
const { prefixCls: customizePrefixCls, bordered = true } = this.props;
const prefixCls = getPrefixCls('input', customizePrefixCls);
this.direction = direction;
@ -300,12 +305,13 @@ class Input extends React.Component<InputProps, InputState> {
prefixCls={prefixCls}
inputType="input"
value={fixControlledValue(value)}
element={this.renderInput(prefixCls, size, input)}
element={this.renderInput(prefixCls, size, bordered, input)}
handleReset={this.handleReset}
ref={this.saveClearableInput}
direction={direction}
focused={focused}
triggerFocus={this.focus}
bordered={bordered}
/>
)}
</SizeContext.Consumer>

View File

@ -1,12 +1,14 @@
import * as React from 'react';
import RcTextArea, { TextAreaProps as RcTextAreaProps, ResizableTextArea } from 'rc-textarea';
import omit from 'omit.js';
import classNames from 'classnames';
import ClearableLabeledInput from './ClearableLabeledInput';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { fixControlledValue, resolveOnChange } from './Input';
export interface TextAreaProps extends RcTextAreaProps {
allowClear?: boolean;
bordered?: boolean;
}
export interface TextAreaState {
@ -69,10 +71,13 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
resolveOnChange(this.resizableTextArea.textArea, e, this.props.onChange);
};
renderTextArea = (prefixCls: string) => {
renderTextArea = (prefixCls: string, bordered: boolean) => {
return (
<RcTextArea
{...omit(this.props, ['allowClear'])}
{...omit(this.props, ['allowClear', 'bordered'])}
className={classNames(this.props.className, {
[`${prefixCls}-borderless`]: !bordered,
})}
prefixCls={prefixCls}
onChange={this.handleChange}
ref={this.saveTextArea}
@ -82,7 +87,7 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
renderComponent = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const { value } = this.state;
const { prefixCls: customizePrefixCls } = this.props;
const { prefixCls: customizePrefixCls, bordered = true } = this.props;
const prefixCls = getPrefixCls('input', customizePrefixCls);
return (
<ClearableLabeledInput
@ -91,10 +96,11 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
direction={direction}
inputType="text"
value={fixControlledValue(value)}
element={this.renderTextArea(prefixCls)}
element={this.renderTextArea(prefixCls, bordered)}
handleReset={this.handleReset}
ref={this.saveClearableInput}
triggerFocus={this.focus}
bordered={bordered}
/>
);
};

View File

@ -1063,6 +1063,141 @@ exports[`renders ./components/input/demo/basic.md correctly 1`] = `
/>
`;
exports[`renders ./components/input/demo/borderless.md correctly 1`] = `
<input
class="ant-input ant-input-borderless"
placeholder="Borderless"
type="text"
value=""
/>
`;
exports[`renders ./components/input/demo/borderless-debug.md correctly 1`] = `
<div
style="background-color:rgba(0, 0, 128, .2)"
>
<input
class="ant-input ant-input-borderless"
placeholder="Unbordered"
type="text"
value=""
/>
<input
class="ant-input ant-input-lg ant-input-borderless"
placeholder="Unbordered"
type="text"
value=""
/>
<textarea
class="ant-input ant-input-borderless"
placeholder="Unbordered"
/>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn ant-input-affix-wrapper-borderless"
>
<textarea
class="ant-input ant-input-borderless"
placeholder="Unbordered"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-textarea-clear-icon ant-input-textarea-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-borderless"
>
<input
class="ant-input ant-input-borderless"
placeholder="Unbordered"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-borderless"
>
<span
class="ant-input-prefix"
>
</span>
<input
class="ant-input ant-input-borderless"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
RMB
</span>
</span>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-disabled ant-input-affix-wrapper-borderless"
>
<span
class="ant-input-prefix"
>
</span>
<input
class="ant-input ant-input-disabled ant-input-borderless"
disabled=""
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
RMB
</span>
</span>
</div>
`;
exports[`renders ./components/input/demo/group.md correctly 1`] = `
<div
class="site-input-group-wrapper"

View File

@ -0,0 +1,34 @@
---
order: 98
title:
zh-CN: Borderless Debug
en-US: Borderless Debug
debug: true
---
## zh-CN
Buggy!
## en-US
Buggy!
```jsx
import { Input } from 'antd';
const { TextArea } = Input;
ReactDOM.render(
<div style={{ backgroundColor: 'rgba(0, 0, 128, .2)' }}>
<Input placeholder="Unbordered" bordered={false} />
<Input placeholder="Unbordered" bordered={false} size="large" />
<TextArea placeholder="Unbordered" bordered={false} />
<TextArea placeholder="Unbordered" bordered={false} allowClear />
<Input placeholder="Unbordered" bordered={false} allowClear />
<Input prefix="¥" suffix="RMB" bordered={false} />
<Input prefix="¥" suffix="RMB" disabled bordered={false} />
</div>,
mountNode,
);
```

View File

@ -0,0 +1,20 @@
---
order: 20
title:
zh-CN: 无边框
en-US: Borderless
---
## zh-CN
没有边框。
## en-US
No border.
```jsx
import { Input } from 'antd';
ReactDOM.render(<Input placeholder="Borderless" bordered={false} />, mountNode);
```

View File

@ -16,22 +16,23 @@ A basic widget for getting the user input is a text field. Keyboard and mouse ca
### Input
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| addonAfter | The label text displayed after (on the right side of) the input field | string \| ReactNode | - |
| addonBefore | The label text displayed before (on the left side of) the input field | string \| ReactNode | - |
| defaultValue | The initial input content | string | - |
| disabled | Whether the input is disabled | boolean | false |
| id | The ID for input | string | - |
| maxLength | The max length | number | - |
| prefix | The prefix icon for the Input | string \| ReactNode | - |
| size | The size of the input box. Note: in the context of a form, the `large` size is used | `large` \| `middle` \| `small` | - |
| suffix | The suffix icon for the Input | string \| ReactNode | - |
| type | The type of input, see: [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types)( use `Input.TextArea` instead of `type="textarea"`) | string | `text` |
| value | The input content value | string | - |
| onChange | Callback when user input | function(e) | - |
| onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - |
| allowClear | If allow to remove input content with clear icon | boolean | false |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| addonAfter | The label text displayed after (on the right side of) the input field | string \| ReactNode | - | |
| addonBefore | The label text displayed before (on the left side of) the input field | string \| ReactNode | - | |
| defaultValue | The initial input content | string | - | |
| disabled | Whether the input is disabled | boolean | false | |
| id | The ID for input | string | - | |
| maxLength | The max length | number | - | |
| prefix | The prefix icon for the Input | string \| ReactNode | - | |
| size | The size of the input box. Note: in the context of a form, the `large` size is used | `large` \| `middle` \| `small` | - | |
| suffix | The suffix icon for the Input | string \| ReactNode | - | |
| type | The type of input, see: [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types)( use `Input.TextArea` instead of `type="textarea"`) | string | `text` | |
| value | The input content value | string | - | |
| onChange | Callback when user input | function(e) | - | |
| onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - | |
| allowClear | If allow to remove input content with clear icon | boolean | false | |
| bordered | Whether has border style | boolean | true | 4.5.0 |
> When `Input` is used in a `Form.Item` context, if the `Form.Item` has the `id` and `options` props defined then `value`, `defaultValue`, and `id` props of `Input` are automatically set.
@ -39,14 +40,15 @@ The rest of the props of Input are exactly the same as the original [input](http
### Input.TextArea
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| autoSize | Height autosize feature, can be set to true \| false or an object { minRows: 2, maxRows: 6 } | boolean \| object | false |
| defaultValue | The initial input content | string | - |
| value | The input content value | string | - |
| onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - |
| allowClear | If allow to remove input content with clear icon | boolean | false |
| onResize | The callback function that is triggered when resize | function({ width, height }) | - |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| autoSize | Height autosize feature, can be set to true \| false or an object { minRows: 2, maxRows: 6 } | boolean \| object | false | |
| defaultValue | The initial input content | string | - | |
| value | The input content value | string | - | |
| onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - | |
| allowClear | If allow to remove input content with clear icon | boolean | false | |
| onResize | The callback function that is triggered when resize | function({ width, height }) | - | |
| bordered | Whether has border style | boolean | true | 4.5.0 |
The rest of the props of `Input.TextArea` are the same as the original [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).

View File

@ -17,22 +17,23 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xS9YEJhfe/Input.svg
### Input
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| addonAfter | 带标签的 input设置后置标签 | string \| ReactNode | - |
| addonBefore | 带标签的 input设置前置标签 | string \| ReactNode | - |
| defaultValue | 输入框默认内容 | string | - |
| disabled | 是否禁用状态,默认为 false | boolean | false |
| id | 输入框的 id | string | - |
| maxLength | 最大长度 | number | - |
| prefix | 带有前缀图标的 input | string \| ReactNode | - |
| size | 控件大小。注:标准表单内的输入框大小限制为 `large` | `large` \| `middle` \| `small` | - |
| suffix | 带有后缀图标的 input | string \| ReactNode | - |
| type | 声明 input 类型,同原生 input 标签的 type 属性,见:[MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#属性)(请直接使用 `Input.TextArea` 代替 `type="textarea"`) | string | `text` |
| value | 输入框内容 | string | - |
| onChange | 输入框内容变化时的回调 | function(e) | - |
| onPressEnter | 按下回车的回调 | function(e) | - |
| allowClear | 可以点击清除图标删除内容 | boolean | - |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| addonAfter | 带标签的 input设置后置标签 | string \| ReactNode | - | |
| addonBefore | 带标签的 input设置前置标签 | string \| ReactNode | - | |
| defaultValue | 输入框默认内容 | string | - | |
| disabled | 是否禁用状态,默认为 false | boolean | false | |
| id | 输入框的 id | string | - | |
| maxLength | 最大长度 | number | - | |
| prefix | 带有前缀图标的 input | string \| ReactNode | - | |
| size | 控件大小。注:标准表单内的输入框大小限制为 `large` | `large` \| `middle` \| `small` | - | |
| suffix | 带有后缀图标的 input | string \| ReactNode | - | |
| type | 声明 input 类型,同原生 input 标签的 type 属性,见:[MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#属性)(请直接使用 `Input.TextArea` 代替 `type="textarea"`) | string | `text` | |
| value | 输入框内容 | string | - | |
| onChange | 输入框内容变化时的回调 | function(e) | - | |
| onPressEnter | 按下回车的回调 | function(e) | - | |
| allowClear | 可以点击清除图标删除内容 | boolean | - | |
| bordered | 是否有边框 | boolean | true | 4.5.0 |
> 如果 `Input``Form.Item` 内,并且 `Form.Item` 设置了 `id``options` 属性,则 `value` `defaultValue``id` 属性会被自动设置。
@ -40,14 +41,15 @@ Input 的其他属性和 React 自带的 [input](https://facebook.github.io/reac
### Input.TextArea
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| autoSize | 自适应内容高度,可设置为 true \| false 或对象:{ minRows: 2, maxRows: 6 } | boolean \| object | false |
| defaultValue | 输入框默认内容 | string | - |
| value | 输入框内容 | string | - |
| onPressEnter | 按下回车的回调 | function(e) | - |
| allowClear | 可以点击清除图标删除内容 | boolean | false |
| onResize | resize 回调 | function({ width, height }) | - |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| autoSize | 自适应内容高度,可设置为 true \| false 或对象:{ minRows: 2, maxRows: 6 } | boolean \| object | false | |
| defaultValue | 输入框默认内容 | string | - | |
| value | 输入框内容 | string | - | |
| onPressEnter | 按下回车的回调 | function(e) | - | |
| allowClear | 可以点击清除图标删除内容 | boolean | false | |
| onResize | resize 回调 | function({ width, height }) | - | |
| bordered | 是否有边框 | boolean | true | 4.5.0 |
`Input.TextArea` 的其他属性和浏览器自带的 [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) 一致。

View File

@ -78,6 +78,19 @@
.disabled();
}
&-borderless {
&,
&:hover,
&:focus,
&-focused,
&-disabled,
&[disabled] {
background-color: transparent;
border: none;
box-shadow: none;
}
}
// Reset height for `textarea`s
textarea& {
max-width: 100%; // prevent textearea resize from coming out of its container

View File

@ -338,6 +338,12 @@
position: relative;
top: 1px;
display: inline-block;
margin: @menu-item-padding;
margin-top: 0;
margin-bottom: 0;
padding: @menu-item-padding;
padding-right: 0;
padding-left: 0;
vertical-align: bottom;
border-bottom: 2px solid transparent;
@ -350,6 +356,10 @@
}
}
> .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title {
padding: 0;
}
> .@{menu-prefix-cls}-item {
a {
color: @menu-item-color;

View File

@ -22,6 +22,17 @@ exports[`renders ./components/message/demo/duration.md correctly 1`] = `
</button>
`;
exports[`renders ./components/message/demo/hooks.md correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Display normal message
</span>
</button>
`;
exports[`renders ./components/message/demo/info.md correctly 1`] = `
<button
class="ant-btn ant-btn-primary"

View File

@ -0,0 +1,195 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import { mount } from 'enzyme';
import message from '..';
import ConfigProvider from '../../config-provider';
describe('message.hooks', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
afterEach(() => {
message.destroy();
});
it('should work', () => {
const Context = React.createContext('light');
const Demo = () => {
const [api, holder] = message.useMessage();
return (
<ConfigProvider prefixCls="my-test">
<Context.Provider value="bamboo">
<button
type="button"
onClick={() => {
api.open({
content: (
<Context.Consumer>
{name => <span className="hook-test-result">{name}</span>}
</Context.Consumer>
),
duration: 0,
});
}}
/>
{holder}
</Context.Provider>
</ConfigProvider>
);
};
const wrapper = mount(<Demo />);
wrapper.find('button').simulate('click');
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
});
it('should work with success', () => {
const Context = React.createContext('light');
const Demo = () => {
const [api, holder] = message.useMessage();
return (
<ConfigProvider prefixCls="my-test">
<Context.Provider value="bamboo">
<button
type="button"
onClick={() => {
api.success({
content: (
<Context.Consumer>
{name => <span className="hook-test-result">{name}</span>}
</Context.Consumer>
),
duration: 0,
});
}}
/>
{holder}
</Context.Provider>
</ConfigProvider>
);
};
const wrapper = mount(<Demo />);
wrapper.find('button').simulate('click');
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
expect(document.querySelectorAll('.anticon-check-circle').length).toBe(1);
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
});
it('should work with onClose', done => {
// if not use real timer, done won't be called
jest.useRealTimers();
const Demo = () => {
const [api, holder] = message.useMessage();
return (
<>
<button
type="button"
onClick={() => {
api.open({
content: 'amazing',
duration: 1,
onClose() {
done();
},
});
}}
/>
{holder}
</>
);
};
const wrapper = mount(<Demo />);
wrapper.find('button').simulate('click');
jest.useFakeTimers();
});
it('should work with close promise', done => {
// if not use real timer, done won't be called
jest.useRealTimers();
const Demo = () => {
const [api, holder] = message.useMessage();
return (
<>
<button
type="button"
onClick={() => {
api
.open({
content: 'good',
duration: 1,
})
.then(() => {
done();
});
}}
/>
{holder}
</>
);
};
const wrapper = mount(<Demo />);
wrapper.find('button').simulate('click');
jest.useFakeTimers();
});
it('should work with hide', () => {
let hide;
const Demo = () => {
const [api, holder] = message.useMessage();
return (
<ConfigProvider prefixCls="my-test">
<button
type="button"
onClick={() => {
hide = api.open({
content: 'nice',
duration: 0,
});
}}
/>
{holder}
</ConfigProvider>
);
};
const wrapper = mount(<Demo />);
wrapper.find('button').simulate('click');
jest.runAllTimers();
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
hide();
jest.runAllTimers();
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(0);
});
it('should be same hook', () => {
let count = 0;
const Demo = () => {
const [, forceUpdate] = React.useState({});
const [api] = message.useMessage();
React.useEffect(() => {
count += 1;
expect(count).toEqual(1);
forceUpdate();
}, [api]);
return null;
};
mount(<Demo />);
});
});

View File

@ -0,0 +1,42 @@
---
order: 10
title:
zh-CN: 通过 Hooks 获取上下文4.5.0+
en-US: Get context with hooks (4.5.0+)
---
## zh-CN
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`
## en-US
Use `message.useMessage` to get `contextHolder` with context accessible issue.
```jsx
import { message, Button } from 'antd';
const Context = React.createContext({ name: 'Default' });
function Demo() {
const [messsageApi, contextHolder] = message.useMessage();
const info = () => {
messsageApi.open({
type: 'info',
content: <Context.Consumer>{({ name }) => `Hello, ${name}!`}</Context.Consumer>,
duration: 1,
});
};
return (
<Context.Provider value={{ name: 'Ant Design' }}>
{contextHolder}
<Button type="primary" onClick={info}>
Display normal message
</Button>
</Context.Provider>
);
}
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -0,0 +1,92 @@
import * as React from 'react';
import useRCNotification from 'rc-notification/lib/useNotification';
import {
NotificationInstance as RCNotificationInstance,
NoticeContent as RCNoticeContent,
HolderReadyCallback as RCHolderReadyCallback,
} from 'rc-notification/lib/Notification';
import { ConfigConsumer, ConfigConsumerProps } from '../../config-provider';
import {
MessageInstance,
ArgsProps,
attachTypeApi,
ThenableArgument,
getKeyThenIncreaseKey,
} from '..';
export default function createUseMessage(
getRcNotificationInstance: (
args: ArgsProps,
callback: (info: { prefixCls: string; instance: RCNotificationInstance }) => void,
) => void,
getRCNoticeProps: (args: ArgsProps, prefixCls: string) => RCNoticeContent,
) {
const useMessage = (): [MessageInstance, React.ReactElement] => {
// We can only get content by render
let getPrefixCls: ConfigConsumerProps['getPrefixCls'];
// We create a proxy to handle delay created instance
let innerInstance: RCNotificationInstance | null = null;
const proxy = {
add: (noticeProps: RCNoticeContent, holderCallback?: RCHolderReadyCallback) => {
innerInstance?.component.add(noticeProps, holderCallback);
},
} as any;
const [hookNotify, holder] = useRCNotification(proxy);
function notify(args: ArgsProps) {
const { prefixCls: customizePrefixCls } = args;
const mergedPrefixCls = getPrefixCls('message', customizePrefixCls);
const target = args.key || getKeyThenIncreaseKey();
const closePromise = new Promise(resolve => {
const callback = () => {
if (typeof args.onClose === 'function') {
args.onClose();
}
return resolve(true);
};
getRcNotificationInstance(
{
...args,
prefixCls: mergedPrefixCls,
},
({ prefixCls, instance }) => {
innerInstance = instance;
hookNotify(getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls));
},
);
});
const result: any = () => {
if (innerInstance) {
innerInstance.removeNotice(target);
}
};
result.then = (filled: ThenableArgument, rejected: ThenableArgument) =>
closePromise.then(filled, rejected);
result.promise = closePromise;
return result;
}
// Fill functions
const hookApiRef = React.useRef<any>({});
hookApiRef.current.open = notify;
['success', 'info', 'warning', 'error', 'loading'].forEach(type =>
attachTypeApi(hookApiRef.current, type),
);
return [
hookApiRef.current,
<ConfigConsumer key="holder">
{(context: ConfigConsumerProps) => {
({ getPrefixCls } = context);
return holder;
}}
</ConfigConsumer>,
];
};
return useMessage;
}

View File

@ -78,13 +78,39 @@ message.config({
duration: 2,
maxCount: 3,
rtl: true,
prefixCls: 'my-message',
});
```
| Argument | Description | Type | Default |
| --- | --- | --- | --- |
| duration | Time before auto-dismiss, in seconds | number | 1.5 |
| getContainer | Return the mount node for Message | () => HTMLElement | () => document.body |
| maxCount | Max message show, drop oldest if exceed limit | number | - |
| top | Distance from top | number | 24 |
| rtl | Whether to enable RTL mode | boolean | false |
| Argument | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| duration | Time before auto-dismiss, in seconds | number | 1.5 | |
| getContainer | Return the mount node for Message | () => HTMLElement | () => document.body | |
| maxCount | Max message show, drop oldest if exceed limit | number | - | |
| top | Distance from top | number | 24 | |
| rtl | Whether to enable RTL mode | boolean | false | |
| prefixCls | The prefix className of message node | string | `ant-message` | 4.5.0 |
## FAQ
### Why I can not access context, redux in message?
antd will dynamic create React instance by `ReactDOM.render` when call message methods. Whose context is different with origin code located context.
When you need context info (like ConfigProvider context), you can use `message.useMessage` to get `api` instance and `contextHolder` node. And put it in your children:
```tsx
const [api, contextHolder] = message.useMessage();
return (
<Context1.Provider value="Ant">
{/* contextHolder is inside Context1 which means api will get value of Context1 */}
{contextHolder}
<Context2.Provider value="Design">
{/* contextHolder is outside Context2 which means api will **not** get value of Context2 */}
</Context2.Provider>
</Context1.Provider>
);
```
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.

View File

@ -1,28 +1,83 @@
import * as React from 'react';
import classNames from 'classnames';
import Notification from 'rc-notification';
import RCNotification from 'rc-notification';
import {
NotificationInstance as RCNotificationInstance,
NoticeContent,
} from 'rc-notification/lib/Notification';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
import createUseMessage from './hooks/useMessage';
type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
let messageInstance: RCNotificationInstance | null;
let defaultDuration = 3;
let defaultTop: number;
let messageInstance: any;
let key = 1;
let prefixCls = 'ant-message';
let localPrefixCls = 'ant-message';
let transitionName = 'move-up';
let getContainer: () => HTMLElement;
let maxCount: number;
let rtl = false;
function getMessageInstance(callback: (i: any) => void) {
export function getKeyThenIncreaseKey() {
return key++;
}
export interface ConfigOptions {
top?: number;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
transitionName?: string;
maxCount?: number;
rtl?: boolean;
}
function setMessageConfig(options: ConfigOptions) {
if (options.top !== undefined) {
defaultTop = options.top;
messageInstance = null; // delete messageInstance for new defaultTop
}
if (options.duration !== undefined) {
defaultDuration = options.duration;
}
if (options.prefixCls !== undefined) {
localPrefixCls = options.prefixCls;
}
if (options.getContainer !== undefined) {
getContainer = options.getContainer;
}
if (options.transitionName !== undefined) {
transitionName = options.transitionName;
messageInstance = null; // delete messageInstance for new transitionName
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
messageInstance = null;
}
if (options.rtl !== undefined) {
rtl = options.rtl;
}
}
function getRCNotificationInstance(
args: ArgsProps,
callback: (info: { prefixCls: string; instance: RCNotificationInstance }) => void,
) {
const prefixCls = args.prefixCls || localPrefixCls;
if (messageInstance) {
callback(messageInstance);
callback({
prefixCls,
instance: messageInstance,
});
return;
}
Notification.newInstance(
RCNotification.newInstance(
{
prefixCls,
transitionName,
@ -32,17 +87,21 @@ function getMessageInstance(callback: (i: any) => void) {
},
(instance: any) => {
if (messageInstance) {
callback(messageInstance);
callback({
prefixCls,
instance: messageInstance,
});
return;
}
messageInstance = instance;
callback(instance);
callback({
prefixCls,
instance,
});
},
);
}
type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
export interface ThenableArgument {
(val: any): void;
}
@ -53,10 +112,18 @@ export interface MessageType {
promise: Promise<void>;
}
const typeToIcon = {
info: InfoCircleFilled,
success: CheckCircleFilled,
error: CloseCircleFilled,
warning: ExclamationCircleFilled,
loading: LoadingOutlined,
};
export interface ArgsProps {
content: React.ReactNode;
duration: number | null;
type: NoticeType;
prefixCls?: string;
onClose?: () => void;
icon?: React.ReactNode;
key?: string | number;
@ -64,23 +131,29 @@ export interface ArgsProps {
className?: string;
}
const iconMap = {
info: InfoCircleFilled,
success: CheckCircleFilled,
error: CloseCircleFilled,
warning: ExclamationCircleFilled,
loading: LoadingOutlined,
};
function notice(args: ArgsProps): MessageType {
function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent {
const duration = args.duration !== undefined ? args.duration : defaultDuration;
const IconComponent = iconMap[args.type];
const IconComponent = typeToIcon[args.type];
const messageClass = classNames(`${prefixCls}-custom-content`, {
[`${prefixCls}-${args.type}`]: args.type,
[`${prefixCls}-rtl`]: rtl === true,
});
return {
key: args.key,
duration,
style: args.style || {},
className: args.className,
content: (
<div className={messageClass}>
{args.icon || (IconComponent && <IconComponent />)}
<span>{args.content}</span>
</div>
),
onClose: args.onClose,
};
}
function notice(args: ArgsProps): MessageType {
const target = args.key || key++;
const closePromise = new Promise(resolve => {
const callback = () => {
@ -89,20 +162,9 @@ function notice(args: ArgsProps): MessageType {
}
return resolve(true);
};
getMessageInstance(instance => {
instance.notice({
key: target,
duration,
style: args.style || {},
className: args.className,
content: (
<div className={messageClass} role="alert">
{args.icon || (IconComponent && <IconComponent />)}
<span>{args.content}</span>
</div>
),
onClose: callback,
});
getRCNotificationInstance(args, ({ prefixCls, instance }) => {
instance.notice(getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls));
});
});
const result: any = () => {
@ -128,44 +190,9 @@ function isArgsProps(content: JointContent): content is ArgsProps {
);
}
export interface ConfigOptions {
top?: number;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
transitionName?: string;
maxCount?: number;
rtl?: boolean;
}
const api: any = {
open: notice,
config(options: ConfigOptions) {
if (options.top !== undefined) {
defaultTop = options.top;
messageInstance = null; // delete messageInstance for new defaultTop
}
if (options.duration !== undefined) {
defaultDuration = options.duration;
}
if (options.prefixCls !== undefined) {
prefixCls = options.prefixCls;
}
if (options.getContainer !== undefined) {
getContainer = options.getContainer;
}
if (options.transitionName !== undefined) {
transitionName = options.transitionName;
messageInstance = null; // delete messageInstance for new transitionName
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
messageInstance = null;
}
if (options.rtl !== undefined) {
rtl = options.rtl;
}
},
config: setMessageConfig,
destroy() {
if (messageInstance) {
messageInstance.destroy();
@ -174,10 +201,14 @@ const api: any = {
},
};
['success', 'info', 'warning', 'error', 'loading'].forEach(type => {
api[type] = (content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose) => {
export function attachTypeApi(originalApi: any, type: string) {
originalApi[type] = (
content: JointContent,
duration?: ConfigDuration,
onClose?: ConfigOnClose,
) => {
if (isArgsProps(content)) {
return api.open({ ...content, type });
return originalApi.open({ ...content, type });
}
if (typeof duration === 'function') {
@ -185,22 +216,29 @@ const api: any = {
duration = undefined;
}
return api.open({ content, duration, type, onClose });
return originalApi.open({ content, duration, type, onClose });
};
});
}
['success', 'info', 'warning', 'error', 'loading'].forEach(type => attachTypeApi(api, type));
api.warn = api.warning;
api.useMessage = createUseMessage(getRCNotificationInstance, getRCNoticeProps);
export interface MessageApi {
export interface MessageInstance {
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
open(args: ArgsProps): MessageType;
}
export interface MessageApi extends MessageInstance {
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
config(options: ConfigOptions): void;
destroy(): void;
useMessage(): [MessageInstance, React.ReactElement];
}
export default api as MessageApi;

View File

@ -79,13 +79,39 @@ message.config({
duration: 2,
maxCount: 3,
rtl: true,
prefixCls: 'my-message',
});
```
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| duration | 默认自动关闭延时,单位秒 | number | 3 |
| getContainer | 配置渲染节点的输出位置 | () => HTMLElement | () => document.body |
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - |
| top | 消息距离顶部的位置 | number | 24 |
| rtl | 是否开启 RTL 模式 | boolean | false |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| duration | 默认自动关闭延时,单位秒 | number | 3 | |
| getContainer | 配置渲染节点的输出位置 | () => HTMLElement | () => document.body | |
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | |
| top | 消息距离顶部的位置 | number | 24 | |
| rtl | 是否开启 RTL 模式 | boolean | false | |
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 4.5.0 |
## FAQ
### 为什么 message 不能获取 context、redux 的内容?
直接调用 message 方法antd 会通过 `ReactDOM.render` 动态创建新的 React 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `message.useMessage` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
```tsx
const [api, contextHolder] = message.useMessage();
return (
<Context1.Provider value="Ant">
{/* contextHolder 在 Context1 内,它可以获得 Context1 的 context */}
{contextHolder}
<Context2.Provider value="Design">
{/* contextHolder 在 Context2 外,因而不会获得 Context2 的 context */}
</Context2.Provider>
</Context1.Provider>
);
```
**异同:**通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。

View File

@ -7,7 +7,7 @@
.reset-component;
position: fixed;
top: 16px;
top: 8px;
left: 0;
z-index: @zindex-message;
width: 100%;
@ -16,9 +16,6 @@
&-notice {
padding: 8px;
text-align: center;
&:first-child {
margin-top: -8px;
}
}
&-notice-content {

View File

@ -7,6 +7,7 @@ export interface ActionButtonProps {
actionFn?: (...args: any[]) => any | PromiseLike<any>;
closeModal: Function;
autoFocus?: boolean;
prefixCls: string;
buttonProps?: ButtonProps;
}
@ -76,12 +77,13 @@ const ActionButton: React.FC<ActionButtonProps> = props => {
handlePromiseOnOk(returnValueOfOnOk);
};
const { type, children, buttonProps } = props;
const { type, children, prefixCls, buttonProps } = props;
return (
<Button
{...convertLegacyProps(type)}
onClick={onClick}
loading={loading}
prefixCls={prefixCls}
{...buttonProps}
ref={ref}
>

View File

@ -8,6 +8,7 @@ interface ConfirmDialogProps extends ModalFuncProps {
afterClose?: () => void;
close: (...args: any[]) => void;
autoFocusButton?: null | 'ok' | 'cancel';
rootPrefixCls?: string;
}
const ConfirmDialog = (props: ConfirmDialogProps) => {
@ -28,6 +29,8 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
cancelText,
cancelButtonProps,
direction,
prefixCls,
rootPrefixCls,
} = props;
devWarning(
@ -38,7 +41,6 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
// 支持传入{ icon: null }来隐藏`Modal.confirm`默认的Icon
const okType = props.okType || 'primary';
const prefixCls = props.prefixCls || 'ant-modal';
const contentPrefixCls = `${prefixCls}-confirm`;
// 默认为 true保持向下兼容
const okCancel = 'okCancel' in props ? props.okCancel! : true;
@ -64,6 +66,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
closeModal={close}
autoFocus={autoFocusButton === 'cancel'}
buttonProps={cancelButtonProps}
prefixCls={`${rootPrefixCls}-btn`}
>
{cancelText}
</ActionButton>
@ -107,6 +110,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
closeModal={close}
autoFocus={autoFocusButton === 'ok'}
buttonProps={okButtonProps}
prefixCls={`${rootPrefixCls}-btn`}
>
{okText}
</ActionButton>

View File

@ -217,22 +217,16 @@ describe('Modal.confirm triggers callbacks correctly', () => {
});
it('prefixCls', () => {
jest.useFakeTimers();
open({ prefixCls: 'custom-modal' });
jest.runAllTimers();
expect($$('.custom-modal-mask')).toHaveLength(1);
expect($$('.custom-modal-wrap')).toHaveLength(1);
expect($$('.custom-modal-confirm')).toHaveLength(1);
expect($$('.custom-modal-confirm-body-wrapper')).toHaveLength(1);
jest.useRealTimers();
});
it('should be Modal.confirm without mask', () => {
jest.useFakeTimers();
open({ mask: false });
jest.runAllTimers();
expect($$('.ant-modal-mask')).toHaveLength(0);
jest.useRealTimers();
});
it('destroyFns should reduce when instance.destroy', () => {
@ -279,19 +273,15 @@ describe('Modal.confirm triggers callbacks correctly', () => {
});
it('ok button should trigger onOk once when click it many times quickly', () => {
jest.useFakeTimers();
const onOk = jest.fn();
open({ onOk });
jest.runAllTimers();
$$('.ant-btn-primary')[0].click();
$$('.ant-btn-primary')[0].click();
expect(onOk).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});
// https://github.com/ant-design/ant-design/issues/23358
it('ok button should trigger onOk multiple times when onOk has close argument', () => {
jest.useFakeTimers();
const onOk = jest.fn();
open({
onOk: close => {
@ -299,11 +289,39 @@ describe('Modal.confirm triggers callbacks correctly', () => {
(() => {})(close); // do nothing
},
});
jest.runAllTimers();
$$('.ant-btn-primary')[0].click();
$$('.ant-btn-primary')[0].click();
$$('.ant-btn-primary')[0].click();
expect(onOk).toHaveBeenCalledTimes(3);
});
it('should be able to config rootPrefixCls', () => {
jest.useFakeTimers();
Modal.config({
rootPrefixCls: 'my',
});
confirm({
title: 'title',
});
jest.runAllTimers();
expect(document.querySelectorAll('.ant-btn').length).toBe(0);
expect(document.querySelectorAll('.my-btn').length).toBe(2);
expect(document.querySelectorAll('.my-modal-confirm').length).toBe(1);
Modal.config({
rootPrefixCls: 'your',
});
confirm({
title: 'title',
});
jest.runAllTimers();
expect(document.querySelectorAll('.ant-btn').length).toBe(0);
expect(document.querySelectorAll('.my-btn').length).toBe(2);
expect(document.querySelectorAll('.my-modal-confirm').length).toBe(1);
expect(document.querySelectorAll('.your-btn').length).toBe(2);
expect(document.querySelectorAll('.your-modal-confirm').length).toBe(1);
Modal.config({
rootPrefixCls: 'ant',
});
jest.useRealTimers();
});
});

View File

@ -8,6 +8,12 @@ import { getConfirmLocale } from './locale';
import { ModalFuncProps, destroyFns } from './Modal';
import ConfirmDialog from './ConfirmDialog';
let defaultRootPrefixCls = 'ant';
function getRootPrefixCls() {
return defaultRootPrefixCls;
}
export type ModalFunc = (
props: ModalFuncProps,
) => {
@ -49,7 +55,7 @@ export default function confirm(config: ModalFuncProps) {
}
}
function render({ okText, cancelText, ...props }: any) {
function render({ okText, cancelText, prefixCls, ...props }: any) {
/**
* https://github.com/ant-design/ant-design/issues/23623
* Sync render blocks React event. Let's make this async.
@ -59,6 +65,8 @@ export default function confirm(config: ModalFuncProps) {
ReactDOM.render(
<ConfirmDialog
{...props}
prefixCls={prefixCls || `${getRootPrefixCls()}-modal`}
rootPrefixCls={getRootPrefixCls()}
okText={okText || (props.okCancel ? runtimeLocale.okText : runtimeLocale.justOkText)}
cancelText={cancelText || runtimeLocale.cancelText}
/>,
@ -138,3 +146,9 @@ export function withConfirm(props: ModalFuncProps): ModalFuncProps {
...props,
};
}
export function globalConfig({ rootPrefixCls }: { rootPrefixCls?: string }) {
if (rootPrefixCls) {
defaultRootPrefixCls = rootPrefixCls;
}
}

View File

@ -123,6 +123,16 @@ React.useEffect(() => {
return <div>{contextHolder}</div>;
```
### Modal.config() `4.5.0+`
Like `message.config()`, `Modal.config()` could set `Modal.confirm` props globally (such as `prefixCls`), and it will affect `Modal.confirm|success|info|error|warning` **static methods only**.
```jsx
Modal.config({
rootPrefixCls: 'ant',
});
```
## FAQ
### Why I can not access context, redux in Modal.xxx?

View File

@ -6,6 +6,7 @@ import confirm, {
withError,
withConfirm,
ModalStaticFunctions,
globalConfig,
} from './confirm';
export { ActionButtonProps } from './ActionButton';
@ -15,7 +16,8 @@ function modalWarn(props: ModalFuncProps) {
return confirm(withWarn(props));
}
type Modal = typeof OriginModal & ModalStaticFunctions & { destroyAll: () => void };
type Modal = typeof OriginModal &
ModalStaticFunctions & { destroyAll: () => void; config: typeof globalConfig };
const Modal = OriginModal as Modal;
Modal.info = function infoFn(props: ModalFuncProps) {
@ -47,4 +49,6 @@ Modal.destroyAll = function destroyAllFn() {
}
};
Modal.config = globalConfig;
export default Modal;

View File

@ -98,7 +98,7 @@ modal.destroy();
- `Modal.destroyAll`
使用 `Modal.destroyAll()` 可以销毁弹出的确认窗(即上述的 Modal.info、Modal.success、Modal.error、Modal.warning、Modal.confirm。通常用于路由监听当中处理路由前进、后退不能销毁确认对话框的问题而不用各处去使用实例的返回值进行关闭modal.destroy() 适用于主动关闭,而不是路由这样被动关闭)
使用 `Modal.destroyAll()` 可以销毁弹出的确认窗(即上述的 `Modal.info``Modal.success``Modal.error``Modal.warning``Modal.confirm`)。通常用于路由监听当中,处理路由前进、后退不能销毁确认对话框的问题,而不用各处去使用实例的返回值进行关闭(`modal.destroy()` 适用于主动关闭,而不是路由这样被动关闭)
```jsx
import { browserHistory } from 'react-router';
@ -125,6 +125,18 @@ React.useEffect(() => {
return <div>{contextHolder}</div>;
```
### Modal.config() `4.5.0+`
类似 `message.config()`,全局设置 `Modal.confirm` 等方法的属性(如 `prefixCls`)。
> 此方法只对 `Modal.confirm|success|info|error|warning` 等**静态方法**生效,`<Modal />` 的调用方式是读取 ConfigProvider 的设置。
```jsx
Modal.config({
rootPrefixCls: 'ant',
});
```
## FAQ
### 为什么 Modal 方法不能获取 context、redux 的内容?

View File

@ -23,6 +23,7 @@ let defaultPrefixCls = 'ant-notification';
let defaultPlacement: NotificationPlacement = 'topRight';
let defaultGetContainer: () => HTMLElement;
let defaultCloseIcon: React.ReactNode;
let rtl = false;
export interface ConfigProps {
top?: number;
@ -35,7 +36,6 @@ export interface ConfigProps {
rtl?: boolean;
}
let rtl = false;
function setNotificationConfig(options: ConfigProps) {
const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options;
if (prefixCls !== undefined) {
@ -225,12 +225,14 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
};
}
const api: any = {
open: (args: ArgsProps) => {
function notice(args: ArgsProps) {
getNotificationInstance(args, ({ prefixCls, instance }) => {
instance.notice(getRCNoticeProps(args, prefixCls));
});
},
}
const api: any = {
open: notice,
close(key: string) {
Object.keys(notificationInstance).forEach(cacheKey =>
Promise.resolve(notificationInstance[cacheKey]).then(instance => {

View File

@ -357,7 +357,7 @@
// Card
// ---
@card-actions-background: @background-color-light;
@card-actions-background: @component-background;
@card-skeleton-bg: #303030;
@card-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.64), 0 3px 6px 0 rgba(0, 0, 0, 0.48),
0 5px 12px 4px rgba(0, 0, 0, 0.36);

View File

@ -239,6 +239,7 @@
@descriptions-item-trailing-colon: true;
@descriptions-item-label-colon-margin-right: 8px;
@descriptions-item-label-colon-margin-left: 2px;
@descriptions-extra-color: @text-color;
// Divider
@divider-text-padding: 1em;
@ -670,7 +671,7 @@
@card-inner-head-padding: 12px;
@card-padding-base: 24px;
@card-padding-base-sm: @card-padding-base / 2;
@card-actions-background: @background-color-light;
@card-actions-background: @component-background;
@card-actions-li-margin: 12px 0;
@card-skeleton-bg: #cfd8dc;
@card-background: @component-background;
@ -740,6 +741,9 @@
@avatar-bg: #ccc;
@avatar-color: #fff;
@avatar-border-radius: @border-radius-base;
@avatar-group-overlapping: -8px;
@avatar-group-space: 3px;
@avatar-group-border-color: #fff;
// Switch
// ---

View File

@ -13,6 +13,7 @@ exports[`Transfer.Search should show cross icon when input value exists 1`] = `
value=""
>
<ClearableLabeledInput
bordered={true}
element={
<input
className="ant-input"
@ -141,6 +142,7 @@ exports[`Transfer.Search should show cross icon when input value exists 2`] = `
value="a"
>
<ClearableLabeledInput
bordered={true}
element={
<input
className="ant-input"

View File

@ -39,6 +39,7 @@ Almost anything can be represented in a tree structure. Examples include directo
| showIcon | Shows the icon before a TreeNode's title. There is no default style; you must set a custom style for it if set to true | boolean | false | |
| switcherIcon | Customize collapse/expand icon of tree node | ReactNode | - | |
| showLine | Shows a connecting line | boolean \| {showLeafIcon: boolean} | false | |
| titleRender | Customize tree node title render | (nodeData) => ReactNode | - | 4.5.0 |
| treeData | The treeNodes data Array, if set it then you need not to construct children TreeNode. (key should be unique across the whole array) | array&lt;{ key, title, children, \[disabled, selectable] }> | - | |
| virtual | Disable virtual scroll when set to false | boolean | true | 4.1.0 |
| onCheck | Callback function for when the onCheck event occurs | function(checkedKeys, e:{checked: bool, checkedNodes, node, event, halfCheckedKeys}) | - | |

View File

@ -40,6 +40,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg
| showIcon | 是否展示 TreeNode title 前的图标,没有默认样式,如设置为 true需要自行定义图标相关样式 | boolean | false | |
| switcherIcon | 自定义树节点的展开/折叠图标 | ReactNode | - |
| showLine | 是否展示连接线 | boolean \| {showLeafIcon: boolean} | false | |
| titleRender | 自定义渲染节点 | (nodeData) => ReactNode | - | 4.5.0 |
| treeData | treeNodes 数据,如果设置则不需要手动构造 TreeNode 节点key 在整个树范围内唯一) | array&lt;{key, title, children, \[disabled, selectable]}> | - | |
| virtual | 设置 false 时关闭虚拟滚动 | boolean | true | 4.1.0 |
| onCheck | 点击复选框触发 | function(checkedKeys, e:{checked: bool, checkedNodes, node, event, halfCheckedKeys}) | - | |

View File

@ -112,6 +112,26 @@ Like [the explaination](https://github.com/ant-design/ant-design/issues/11586#is
You can refer to [this article](https://juejin.im/post/5cf65c366fb9a07eca6968f9) or [this article](https://www.cnblogs.com/zyl-Tara/p/10197177.html), using `mode` and `onPanelChange` to encapsulate a `YearPicker` or `MonthRangePicker` for your needs. Or you can wait for our [antd@4.0](https://github.com/ant-design/ant-design/issues/16911), in which we are already planning to [add more XxxPickers](https://github.com/ant-design/ant-design/issues/4524#issuecomment-480576884) to meet those requirements.
### message/notification/Modal.confirm lost styles when set `prefixCls` on ConfigProvider?
Static methods like message/notification/Modal.confirm are not using the same render tree as `<Button />`, but rendered to indepent DOM node created by `ReactDOM.render`, which cannot access React context from ConfigProvider. Consider two solutions here:
1. Replace original usages with [message.useMessage](https://ant.design/components/message/#components-message-demo-hooks), [notification.useNotification](https://ant.design/components/notification/#Why-I-can-not-access-context,-redux-in-notification) and [Modal.useModal](https://ant.design/components/modal/#Why-I-can-not-access-context,-redux-in-Modal.xxx).
2. Use `message.config`, `notification.config` and `Modal.config` to config `prefixCls` globally.
```js
message.config({
prefixCls: 'ant-message',
});
notification.config({
prefixCls: 'ant-notification',
});
Modal.config({
rootPrefixCls: 'ant',
});
```
### How to spell Ant Design correctly?
- ✅ **Ant Design**: Capitalized with space, for the design language.

View File

@ -126,6 +126,26 @@ antd 内部会对 props 进行浅比较实现性能优化。当状态变更,
你可以参照 [这篇文章](https://juejin.im/post/5cf65c366fb9a07eca6968f9) 或者 [这篇文章](https://www.cnblogs.com/zyl-Tara/p/10197177.html) 里的做法,利用 `mode``onPanelChange` 等方法去封装一个 `YearPicker` 等组件。我们计划(已经支持)在 [antd@4.0](https://github.com/ant-design/ant-design/issues/16911) 中直接[添加更多相关日期组件](https://github.com/ant-design/ant-design/issues/4524#issuecomment-480576884)来支持这些需求。届时不再需要使用 `mode="year|month"`,而是直接可以用 `YearPicker` `MonthPicker`,并且 `disabledDate` 也可以正确作用于这些 Picker。
### ConfigProvider 设置 `prefixCls`message/notification/Modal.confirm 生成的节点样式丢失了?
message/notification/Modal.confirm 等静态方法不同于 `<Button />` 的渲染方式,是单独渲染在 `ReactDOM.render` 生成的 DOM 树节点上,无法共享 ConfigProvider 提供的 context 信息。你有两种解决方式:
1. 使用官方提供的 [message.useMessage](<[message.useMessage](https://ant.design/components/message-cn/#components-message-demo-hooks)>)、[notification.useNotification](https://ant.design/components/notification-cn/#%E4%B8%BA%E4%BB%80%E4%B9%88-notification-%E4%B8%8D%E8%83%BD%E8%8E%B7%E5%8F%96-context%E3%80%81redux-%E7%9A%84%E5%86%85%E5%AE%B9%EF%BC%9F) 和 [Modal.useModal](https://ant.design/components/modal-cn/#%E4%B8%BA%E4%BB%80%E4%B9%88-Modal-%E6%96%B9%E6%B3%95%E4%B8%8D%E8%83%BD%E8%8E%B7%E5%8F%96-context%E3%80%81redux-%E7%9A%84%E5%86%85%E5%AE%B9%EF%BC%9F) 来调用这些方法。
2. 使用 `message.config`、`notification.config` 和 `Modal.config` 方法全局设置 `prefixCls`
```js
message.config({
prefixCls: 'ant-message',
});
notification.config({
prefixCls: 'ant-notification',
});
Modal.config({
rootPrefixCls: 'ant', // 因为 Modal.confirm 里有 button所以 `prefixCls: 'ant-modal'` 不够用。
});
```
### 如何正确的拼写 Ant Design
- ✅ **Ant Design**:用空格分隔的首字母大写单词,指代设计语言。

View File

@ -122,13 +122,13 @@
"rc-dialog": "~8.1.0",
"rc-drawer": "~4.1.0",
"rc-dropdown": "~3.1.2",
"rc-field-form": "~1.5.0",
"rc-field-form": "~1.8.0",
"rc-input-number": "~5.1.1",
"rc-mentions": "~1.4.0",
"rc-menu": "~8.5.0",
"rc-notification": "~4.4.0",
"rc-pagination": "~2.4.1",
"rc-picker": "~1.10.6",
"rc-picker": "~1.13.0",
"rc-progress": "~3.0.0",
"rc-rate": "~2.8.2",
"rc-resize-observer": "^0.2.3",
@ -140,7 +140,7 @@
"rc-tabs": "~11.5.0",
"rc-textarea": "~0.3.0",
"rc-tooltip": "~4.2.0",
"rc-tree": "~3.6.0",
"rc-tree": "~3.8.0",
"rc-tree-select": "~4.1.0",
"rc-trigger": "~4.3.0",
"rc-upload": "~3.2.0",

View File

@ -15,6 +15,8 @@
& > .ant-menu-submenu {
min-width: 72px;
height: @header-height;
margin-right: 0;
margin-left: 0;
line-height: @header-height - @menu-item-border - 2px;
border-top: @menu-item-border solid transparent;