mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-25 11:40:04 +08:00
Merge pull request #25817 from ant-design/resolve-conflict-1
chore: merge feature into master
This commit is contained in:
commit
d857800adc
@ -23,6 +23,7 @@ lib/**/*
|
||||
node_modules
|
||||
_site
|
||||
dist
|
||||
coverage
|
||||
**/*.d.ts
|
||||
# Scripts
|
||||
scripts/previewEditor/**/*
|
@ -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));
|
||||
};
|
||||
|
@ -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
|
||||
|
213
components/avatar/avatar.tsx
Normal file
213
components/avatar/avatar.tsx
Normal 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;
|
47
components/avatar/demo/group.md
Normal file
47
components/avatar/demo/group.md
Normal 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);
|
||||
```
|
59
components/avatar/group.tsx
Normal file
59
components/avatar/group.tsx
Normal 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;
|
@ -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` | |
|
||||
|
@ -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;
|
||||
|
@ -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` | |
|
||||
|
22
components/avatar/style/group.less
Normal file
22
components/avatar/style/group.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -61,3 +61,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import './group';
|
||||
@import './rtl';
|
||||
|
@ -1,2 +1,5 @@
|
||||
import '../../style/index.less';
|
||||
import './index.less';
|
||||
|
||||
// style dependencies
|
||||
import '../../popover/style';
|
||||
|
20
components/avatar/style/rtl.less
Normal file
20
components/avatar/style/rtl.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
53
components/badge/Ribbon.tsx
Normal file
53
components/badge/Ribbon.tsx
Normal 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;
|
@ -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
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
25
components/badge/demo/ribbbon.md
Normal file
25
components/badge/demo/ribbbon.md
Normal 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,
|
||||
);
|
||||
```
|
40
components/badge/demo/ribbon-debug.md
Normal file
40
components/badge/demo/ribbon-debug.md
Normal 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,
|
||||
);
|
||||
```
|
@ -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 | - | |
|
||||
|
@ -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;
|
||||
|
@ -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 | - | |
|
||||
|
@ -1,2 +1,3 @@
|
||||
import '../../style/index.less';
|
||||
import './ribbon.less';
|
||||
import './index.less';
|
||||
|
84
components/badge/style/ribbon.less
Normal file
84
components/badge/style/ribbon.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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%);
|
||||
|
6
components/badge/utils.tsx
Normal file
6
components/badge/utils.tsx
Normal 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;
|
||||
}
|
@ -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>
|
||||
|
@ -70,7 +70,7 @@ describe('ConfigProvider', () => {
|
||||
const App = () => {
|
||||
const { renderEmpty } = React.useContext(ConfigContext);
|
||||
return renderEmpty();
|
||||
}
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ConfigProvider>
|
||||
<App />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -5,9 +5,13 @@ exports[`renders ./components/descriptions/demo/basic.md correctly 1`] = `
|
||||
class="ant-descriptions"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
User Info
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
>
|
||||
User Info
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-descriptions-view"
|
||||
@ -108,9 +112,13 @@ exports[`renders ./components/descriptions/demo/border.md correctly 1`] = `
|
||||
class="ant-descriptions ant-descriptions-bordered"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
User Info
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
>
|
||||
User Info
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-descriptions-view"
|
||||
@ -291,9 +299,13 @@ exports[`renders ./components/descriptions/demo/responsive.md correctly 1`] = `
|
||||
class="ant-descriptions ant-descriptions-bordered"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
Responsive Descriptions
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
>
|
||||
Responsive Descriptions
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-descriptions-view"
|
||||
@ -483,9 +495,25 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = `
|
||||
class="ant-descriptions ant-descriptions-bordered"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
Custom Size
|
||||
<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"
|
||||
@ -609,9 +637,25 @@ exports[`renders ./components/descriptions/demo/size.md correctly 1`] = `
|
||||
class="ant-descriptions"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
Custom Size
|
||||
<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"
|
||||
@ -728,9 +772,13 @@ exports[`renders ./components/descriptions/demo/vertical.md correctly 1`] = `
|
||||
class="ant-descriptions"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
User Info
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
>
|
||||
User Info
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-descriptions-view"
|
||||
@ -864,9 +912,13 @@ exports[`renders ./components/descriptions/demo/vertical-border.md correctly 1`]
|
||||
class="ant-descriptions ant-descriptions-bordered"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
User Info
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
>
|
||||
User Info
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-descriptions-view"
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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` | - | |
|
||||
|
@ -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 && <div className={`${prefixCls}-title`}>{title}</div>}
|
||||
{(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>
|
||||
|
@ -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` | - | |
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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 | <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+ |
|
||||
|
@ -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)`;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -17,8 +17,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| closable | 是否显示右上角的关闭按钮 | boolean | true |
|
||||
| closeIcon | 自定义关闭图标 | ReactNode | <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+ |
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
|
49
components/form/demo/dep-debug.md
Normal file
49
components/form/demo/dep-debug.md
Normal 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);
|
||||
```
|
@ -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).
|
||||
|
@ -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)。
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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"
|
||||
|
34
components/input/demo/borderless-debug.md
Normal file
34
components/input/demo/borderless-debug.md
Normal 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,
|
||||
);
|
||||
```
|
20
components/input/demo/borderless.md
Normal file
20
components/input/demo/borderless.md
Normal 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);
|
||||
```
|
@ -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).
|
||||
|
||||
|
@ -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) 一致。
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
195
components/message/__tests__/hooks.test.js
Normal file
195
components/message/__tests__/hooks.test.js
Normal 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 />);
|
||||
});
|
||||
});
|
42
components/message/demo/hooks.md
Normal file
42
components/message/demo/hooks.md
Normal 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);
|
||||
```
|
92
components/message/hooks/useMessage.tsx
Normal file
92
components/message/hooks/useMessage.tsx
Normal 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;
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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?
|
||||
|
@ -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;
|
||||
|
@ -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 的内容?
|
||||
|
@ -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) {
|
||||
};
|
||||
}
|
||||
|
||||
function notice(args: ArgsProps) {
|
||||
getNotificationInstance(args, ({ prefixCls, instance }) => {
|
||||
instance.notice(getRCNoticeProps(args, prefixCls));
|
||||
});
|
||||
}
|
||||
|
||||
const api: any = {
|
||||
open: (args: ArgsProps) => {
|
||||
getNotificationInstance(args, ({ prefixCls, instance }) => {
|
||||
instance.notice(getRCNoticeProps(args, prefixCls));
|
||||
});
|
||||
},
|
||||
open: notice,
|
||||
close(key: string) {
|
||||
Object.keys(notificationInstance).forEach(cacheKey =>
|
||||
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
// ---
|
||||
|
@ -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"
|
||||
|
@ -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<{ 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}) | - | |
|
||||
|
@ -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<{key, title, children, \[disabled, selectable]}> | - | |
|
||||
| virtual | 设置 false 时关闭虚拟滚动 | boolean | true | 4.1.0 |
|
||||
| onCheck | 点击复选框触发 | function(checkedKeys, e:{checked: bool, checkedNodes, node, event, halfCheckedKeys}) | - | |
|
||||
|
@ -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.
|
||||
|
@ -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**:用空格分隔的首字母大写单词,指代设计语言。
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user