chore: merge feature into master

This commit is contained in:
afc163 2021-09-29 08:43:06 +08:00
commit d35576d675
24 changed files with 485 additions and 42 deletions

View File

@ -479,6 +479,43 @@ Array [
</span> </span>
</span> </span>
</div>, </div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-avatar-group"
>
<span
class="ant-avatar ant-avatar-lg ant-avatar-circle ant-avatar-image"
>
<img
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
</span>
<span
class="ant-avatar ant-avatar-lg ant-avatar-circle"
style="background-color:#f56a00"
>
<span
class="ant-avatar-string"
style="opacity:0"
>
K
</span>
</span>
<span
class="ant-avatar ant-avatar-lg ant-avatar-circle"
style="color:#f56a00;background-color:#fde3cf;cursor:pointer"
>
<span
class="ant-avatar-string"
style="opacity:0"
>
+2
</span>
</span>
</div>,
] ]
`; `;

View File

@ -49,6 +49,20 @@ const Demo = () => (
</Tooltip> </Tooltip>
<Avatar style={{ backgroundColor: '#1890ff' }} icon={<AntDesignOutlined />} /> <Avatar style={{ backgroundColor: '#1890ff' }} icon={<AntDesignOutlined />} />
</Avatar.Group> </Avatar.Group>
<Divider />
<Avatar.Group
maxCount={2}
maxPopoverTrigger="click"
size="large"
maxStyle={{ color: '#f56a00', backgroundColor: '#fde3cf', cursor: 'pointer' }}
>
<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>
</> </>
); );

View File

@ -15,6 +15,7 @@ export interface GroupProps {
maxCount?: number; maxCount?: number;
maxStyle?: React.CSSProperties; maxStyle?: React.CSSProperties;
maxPopoverPlacement?: 'top' | 'bottom'; maxPopoverPlacement?: 'top' | 'bottom';
maxPopoverTrigger?: 'hover' | 'focus' | 'click';
/* /*
* Size of avatar, options: `large`, `small`, `default` * Size of avatar, options: `large`, `small`, `default`
* or a custom number size * or a custom number size
@ -36,7 +37,7 @@ const Group: React.FC<GroupProps> = props => {
className, className,
); );
const { children, maxPopoverPlacement = 'top' } = props; const { children, maxPopoverPlacement = 'top', maxPopoverTrigger = 'hover' } = props;
const childrenWithProps = toArray(children).map((child, index) => const childrenWithProps = toArray(children).map((child, index) =>
cloneElement(child, { cloneElement(child, {
key: `avatar-key-${index}`, key: `avatar-key-${index}`,
@ -51,7 +52,7 @@ const Group: React.FC<GroupProps> = props => {
<Popover <Popover
key="avatar-popover-key" key="avatar-popover-key"
content={childrenHidden} content={childrenHidden}
trigger="hover" trigger={maxPopoverTrigger}
placement={maxPopoverPlacement} placement={maxPopoverPlacement}
overlayClassName={`${prefixCls}-popover`} overlayClassName={`${prefixCls}-popover`}
> >

View File

@ -32,5 +32,6 @@ Avatars can be used to represent people or objects. It supports images, `Icon`s,
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| maxCount | Max avatars to show | number | - | | | maxCount | Max avatars to show | number | - | |
| maxPopoverPlacement | The placement of excess avatar Popover | `top` \| `bottom` | `top` | | | maxPopoverPlacement | The placement of excess avatar Popover | `top` \| `bottom` | `top` | |
| maxPopoverTrigger | Set the trigger of excess avatar Popover | `hover` \| `focus` \| `click` | `hover` | 4.17.0 |
| maxStyle | The style of excess avatar style | CSSProperties | - | | | maxStyle | The style of excess avatar style | CSSProperties | - | |
| size | The size of the avatar | number \| `large` \| `small` \| `default` \| { xs: number, sm: number, ...} | `default` | 4.8.0 | | size | The size of the avatar | number \| `large` \| `small` \| `default` \| { xs: number, sm: number, ...} | `default` | 4.8.0 |

View File

@ -37,5 +37,6 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/aBcnbw68hP/Avatar.svg
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| maxCount | 显示的最大头像个数 | number | - | | | maxCount | 显示的最大头像个数 | number | - | |
| maxPopoverPlacement | 多余头像气泡弹出位置 | `top` \| `bottom` | `top` | | | maxPopoverPlacement | 多余头像气泡弹出位置 | `top` \| `bottom` | `top` | |
| maxPopoverTrigger | 设置多余头像 Popover 的触发方式 | `hover` \| `focus` \| `click` | `hover` | 4.17.0 |
| maxStyle | 多余头像样式 | CSSProperties | - | | | maxStyle | 多余头像样式 | CSSProperties | - | |
| size | 设置头像的大小 | number \| `large` \| `small` \| `default` \| { xs: number, sm: number, ...} | `default` | 4.8.0 | | size | 设置头像的大小 | number \| `large` \| `small` \| `default` \| { xs: number, sm: number, ...} | `default` | 4.8.0 |

View File

@ -173,5 +173,33 @@ Array [
<p> <p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>, </p>,
<div
class="ant-divider ant-divider-horizontal ant-divider-with-text ant-divider-with-text-left ant-divider-no-default-orientation-margin-left"
role="separator"
>
<span
class="ant-divider-inner-text"
style="margin-left:0"
>
Left Text with 0 orientationMargin
</span>
</div>,
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
<div
class="ant-divider ant-divider-horizontal ant-divider-with-text ant-divider-with-text-right ant-divider-no-default-orientation-margin-right"
role="separator"
>
<span
class="ant-divider-inner-text"
style="margin-right:50px"
>
Right Text with 50px orientationMargin
</span>
</div>,
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
] ]
`; `;

View File

@ -37,6 +37,20 @@ ReactDOM.render(
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista
probare, quae sunt a te dicta? Refert tamen, quo modo. probare, quae sunt a te dicta? Refert tamen, quo modo.
</p> </p>
<Divider orientation="left" orientationMargin="0">
Left Text with 0 orientationMargin
</Divider>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista
probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>
<Divider orientation="right" orientationMargin={50}>
Right Text with 50px orientationMargin
</Divider>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista
probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>
</>, </>,
mountNode, mountNode,
); );

View File

@ -16,9 +16,11 @@ A divider line separates different content.
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| children | The wrapped title | ReactNode | - | |
| className | The className of container | string | - | | | className | The className of container | string | - | |
| dashed | Whether line is dashed | boolean | false | | | dashed | Whether line is dashed | boolean | false | |
| orientation | The position of title inside divider | `left` \| `right` \| `center` | `center` | | | orientation | The position of title inside divider | `left` \| `right` \| `center` | `center` | |
| orientationMargin | The margin-left/right between the title and its closest border, while the `orientation` must be `left` or `right` | string \| number | - | |
| plain | Divider text show as plain style | boolean | true | 4.2.0 | | plain | Divider text show as plain style | boolean | true | 4.2.0 |
| style | The style object of container | CSSProperties | - | | | style | The style object of container | CSSProperties | - | |
| type | The direction type of divider | `horizontal` \| `vertical` | `horizontal` | | | type | The direction type of divider | `horizontal` \| `vertical` | `horizontal` | |

View File

@ -6,6 +6,7 @@ export interface DividerProps {
prefixCls?: string; prefixCls?: string;
type?: 'horizontal' | 'vertical'; type?: 'horizontal' | 'vertical';
orientation?: 'left' | 'right' | 'center'; orientation?: 'left' | 'right' | 'center';
orientationMargin?: string | number;
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
dashed?: boolean; dashed?: boolean;
@ -20,6 +21,7 @@ const Divider: React.FC<DividerProps> = props => (
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
type = 'horizontal', type = 'horizontal',
orientation = 'center', orientation = 'center',
orientationMargin,
className, className,
children, children,
dashed, dashed,
@ -29,6 +31,8 @@ const Divider: React.FC<DividerProps> = props => (
const prefixCls = getPrefixCls('divider', customizePrefixCls); const prefixCls = getPrefixCls('divider', customizePrefixCls);
const orientationPrefix = orientation.length > 0 ? `-${orientation}` : orientation; const orientationPrefix = orientation.length > 0 ? `-${orientation}` : orientation;
const hasChildren = !!children; const hasChildren = !!children;
const hasCustomMarginLeft = orientation === 'left' && orientationMargin != null;
const hasCustomMarginRight = orientation === 'right' && orientationMargin != null;
const classString = classNames( const classString = classNames(
prefixCls, prefixCls,
`${prefixCls}-${type}`, `${prefixCls}-${type}`,
@ -38,12 +42,24 @@ const Divider: React.FC<DividerProps> = props => (
[`${prefixCls}-dashed`]: !!dashed, [`${prefixCls}-dashed`]: !!dashed,
[`${prefixCls}-plain`]: !!plain, [`${prefixCls}-plain`]: !!plain,
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-no-default-orientation-margin-left`]: hasCustomMarginLeft,
[`${prefixCls}-no-default-orientation-margin-right`]: hasCustomMarginRight,
}, },
className, className,
); );
const innerStyle = {
...(hasCustomMarginLeft && { marginLeft: orientationMargin }),
...(hasCustomMarginRight && { marginRight: orientationMargin }),
};
return ( return (
<div className={classString} {...restProps} role="separator"> <div className={classString} {...restProps} role="separator">
{children && <span className={`${prefixCls}-inner-text`}>{children}</span>} {children && (
<span className={`${prefixCls}-inner-text`} style={innerStyle}>
{children}
</span>
)}
</div> </div>
); );
}} }}

View File

@ -17,9 +17,11 @@ cover: https://gw.alipayobjects.com/zos/alicdn/5swjECahe/Divider.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| children | 嵌套的标题 | ReactNode | - | |
| className | 分割线样式类 | string | - | | | className | 分割线样式类 | string | - | |
| dashed | 是否虚线 | boolean | false | | | dashed | 是否虚线 | boolean | false | |
| orientation | 分割线标题的位置 | `left` \| `right` \| `center` | `center` | | | orientation | 分割线标题的位置 | `left` \| `right` \| `center` | `center` | |
| orientationMargin | 标题和最近 left/right 边框之间的距离,去除了分割线,同时 `orientation` 必须为 `left``right` | string \| number | - | |
| plain | 文字是否显示为普通正文样式 | boolean | false | 4.2.0 | | plain | 文字是否显示为普通正文样式 | boolean | false | 4.2.0 |
| style | 分割线样式对象 | CSSProperties | - | | | style | 分割线样式对象 | CSSProperties | - | |
| type | 水平还是垂直类型 | `horizontal` \| `vertical` | `horizontal` | | | type | 水平还是垂直类型 | `horizontal` \| `vertical` | `horizontal` | |

View File

@ -103,6 +103,30 @@
font-weight: normal; font-weight: normal;
font-size: @font-size-base; font-size: @font-size-base;
} }
&-horizontal&-with-text-left&-no-default-orientation-margin-left {
&::before {
width: 0;
}
&::after {
width: 100%;
}
.ant-divider-inner-text {
padding-left: 0;
}
}
&-horizontal&-with-text-right&-no-default-orientation-margin-right {
&::before {
width: 100%;
}
&::after {
width: 0;
}
.ant-divider-inner-text {
padding-right: 0;
}
}
} }
@import './rtl'; @import './rtl';

View File

@ -41,6 +41,8 @@ interface EditConfig {
onEnd?: () => void; onEnd?: () => void;
maxLength?: number; maxLength?: number;
autoSize?: boolean | AutoSizeType; autoSize?: boolean | AutoSizeType;
triggerType?: ('icon' | 'text')[];
enterIcon?: React.ReactNode;
} }
export interface EllipsisConfig { export interface EllipsisConfig {
@ -372,12 +374,12 @@ class Base extends React.Component<InternalBlockProps, BaseState> {
const { editable } = this.props; const { editable } = this.props;
if (!editable) return; if (!editable) return;
const { icon, tooltip } = editable as EditConfig; const { icon, tooltip, triggerType = ['icon'] } = editable as EditConfig;
const title = toArray(tooltip)[0] || this.editStr; const title = toArray(tooltip)[0] || this.editStr;
const ariaLabel = typeof title === 'string' ? title : ''; const ariaLabel = typeof title === 'string' ? title : '';
return ( return triggerType.indexOf('icon') !== -1 ? (
<Tooltip key="edit" title={tooltip === false ? '' : title}> <Tooltip key="edit" title={tooltip === false ? '' : title}>
<TransButton <TransButton
ref={this.setEditRef} ref={this.setEditRef}
@ -388,7 +390,7 @@ class Base extends React.Component<InternalBlockProps, BaseState> {
{icon || <EditOutlined role="button" />} {icon || <EditOutlined role="button" />}
</TransButton> </TransButton>
</Tooltip> </Tooltip>
); ) : null;
} }
renderCopy() { renderCopy() {
@ -427,7 +429,7 @@ class Base extends React.Component<InternalBlockProps, BaseState> {
renderEditInput() { renderEditInput() {
const { children, className, style } = this.props; const { children, className, style } = this.props;
const { direction } = this.context; const { direction } = this.context;
const { maxLength, autoSize, onEnd } = this.getEditable(); const { maxLength, autoSize, onEnd, enterIcon } = this.getEditable();
return ( return (
<Editable <Editable
value={typeof children === 'string' ? children : ''} value={typeof children === 'string' ? children : ''}
@ -440,6 +442,7 @@ class Base extends React.Component<InternalBlockProps, BaseState> {
direction={direction} direction={direction}
maxLength={maxLength} maxLength={maxLength}
autoSize={autoSize} autoSize={autoSize}
enterIcon={enterIcon}
/> />
); );
} }
@ -455,6 +458,7 @@ class Base extends React.Component<InternalBlockProps, BaseState> {
const { component, children, className, type, disabled, style, ...restProps } = this.props; const { component, children, className, type, disabled, style, ...restProps } = this.props;
const { direction } = this.context; const { direction } = this.context;
const { rows, suffix, tooltip } = this.getEllipsis(); const { rows, suffix, tooltip } = this.getEllipsis();
const { triggerType = ['icon'] } = this.getEditable() as EditConfig;
const prefixCls = this.getPrefixCls(); const prefixCls = this.getPrefixCls();
@ -549,6 +553,7 @@ class Base extends React.Component<InternalBlockProps, BaseState> {
component={component} component={component}
ref={this.contentRef} ref={this.contentRef}
direction={direction} direction={direction}
onClick={triggerType.indexOf('text') !== -1 ? this.onEditClick : () => {}}
{...textProps} {...textProps}
> >
{textNode} {textNode}

View File

@ -5,6 +5,7 @@ import EnterOutlined from '@ant-design/icons/EnterOutlined';
import { AutoSizeType } from 'rc-textarea/lib/ResizableTextArea'; import { AutoSizeType } from 'rc-textarea/lib/ResizableTextArea';
import TextArea from '../input/TextArea'; import TextArea from '../input/TextArea';
import { DirectionType } from '../config-provider'; import { DirectionType } from '../config-provider';
import { cloneElement } from '../_util/reactNode';
interface EditableProps { interface EditableProps {
prefixCls?: string; prefixCls?: string;
@ -18,6 +19,7 @@ interface EditableProps {
direction?: DirectionType; direction?: DirectionType;
maxLength?: number; maxLength?: number;
autoSize?: boolean | AutoSizeType; autoSize?: boolean | AutoSizeType;
enterIcon?: React.ReactNode;
} }
const Editable: React.FC<EditableProps> = ({ const Editable: React.FC<EditableProps> = ({
@ -32,6 +34,7 @@ const Editable: React.FC<EditableProps> = ({
onSave, onSave,
onCancel, onCancel,
onEnd, onEnd,
enterIcon = <EnterOutlined />,
}) => { }) => {
const ref = React.useRef<any>(); const ref = React.useRef<any>();
@ -129,7 +132,9 @@ const Editable: React.FC<EditableProps> = ({
aria-label={ariaLabel} aria-label={ariaLabel}
autoSize={autoSize} autoSize={autoSize}
/> />
<EnterOutlined className={`${prefixCls}-edit-content-confirm`} /> {enterIcon !== null
? cloneElement(enterIcon, { className: `${prefixCls}-edit-content-confirm` })
: null}
</div> </div>
); );
}; };

View File

@ -436,6 +436,165 @@ Array [
</span> </span>
</div> </div>
</div>, </div>,
"Trigger edit with: ",
<div
class="ant-radio-group ant-radio-group-outline"
>
<label
class="ant-radio-wrapper ant-radio-wrapper-checked"
>
<span
class="ant-radio ant-radio-checked"
>
<input
checked=""
class="ant-radio-input"
type="radio"
value="icon"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
icon
</span>
</label>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio"
>
<input
class="ant-radio-input"
type="radio"
value="text"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
text
</span>
</label>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio"
>
<input
class="ant-radio-input"
type="radio"
value="both"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
both
</span>
</label>
</div>,
<div
class="ant-typography"
>
Text or icon as trigger - click to start editing.
<div
aria-label="click to edit text"
class="ant-typography-edit"
role="button"
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
tabindex="0"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="button"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
</div>
</div>,
<div
class="ant-typography"
>
Editable text with a custom enter icon in edit field.
<div
aria-label="click to edit text"
class="ant-typography-edit"
role="button"
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
tabindex="0"
>
<span
aria-label="highlight"
class="anticon anticon-highlight"
role="img"
>
<svg
aria-hidden="true"
data-icon="highlight"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M957.6 507.4L603.2 158.2a7.9 7.9 0 00-11.2 0L353.3 393.4a8.03 8.03 0 00-.1 11.3l.1.1 40 39.4-117.2 115.3a8.03 8.03 0 00-.1 11.3l.1.1 39.5 38.9-189.1 187H72.1c-4.4 0-8.1 3.6-8.1 8V860c0 4.4 3.6 8 8 8h344.9c2.1 0 4.1-.8 5.6-2.3l76.1-75.6 40.4 39.8a7.9 7.9 0 0011.2 0l117.1-115.6 40.1 39.5a7.9 7.9 0 0011.2 0l238.7-235.2c3.4-3 3.4-8 .3-11.2zM389.8 796.2H229.6l134.4-133 80.1 78.9-54.3 54.1zm154.8-62.1L373.2 565.2l68.6-67.6 171.4 168.9-68.6 67.6zM713.1 658L450.3 399.1 597.6 254l262.8 259-147.3 145z"
/>
</svg>
</span>
</div>
</div>,
<div
class="ant-typography"
>
Editable text with no enter icon in edit field.
<div
aria-label="click to edit text"
class="ant-typography-edit"
role="button"
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
tabindex="0"
>
<span
aria-label="highlight"
class="anticon anticon-highlight"
role="img"
>
<svg
aria-hidden="true"
data-icon="highlight"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M957.6 507.4L603.2 158.2a7.9 7.9 0 00-11.2 0L353.3 393.4a8.03 8.03 0 00-.1 11.3l.1.1 40 39.4-117.2 115.3a8.03 8.03 0 00-.1 11.3l.1.1 39.5 38.9-189.1 187H72.1c-4.4 0-8.1 3.6-8.1 8V860c0 4.4 3.6 8 8 8h344.9c2.1 0 4.1-.8 5.6-2.3l76.1-75.6 40.4 39.8a7.9 7.9 0 0011.2 0l117.1-115.6 40.1 39.5a7.9 7.9 0 0011.2 0l238.7-235.2c3.4-3 3.4-8 .3-11.2zM389.8 796.2H229.6l134.4-133 80.1 78.9-54.3 54.1zm154.8-62.1L373.2 565.2l68.6-67.6 171.4 168.9-68.6 67.6zM713.1 658L450.3 399.1 597.6 254l262.8 259-147.3 145z"
/>
</svg>
</span>
</div>
</div>,
<div <div
class="ant-typography" class="ant-typography"
> >

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { SmileOutlined, LikeOutlined, HighlightOutlined } from '@ant-design/icons'; import { SmileOutlined, LikeOutlined, HighlightOutlined, CheckOutlined } from '@ant-design/icons';
import KeyCode from 'rc-util/lib/KeyCode'; import KeyCode from 'rc-util/lib/KeyCode';
import { resetWarned } from 'rc-util/lib/warning'; import { resetWarned } from 'rc-util/lib/warning';
import { spyElementPrototype } from 'rc-util/lib/test/domHook'; import { spyElementPrototype } from 'rc-util/lib/test/domHook';
@ -395,7 +395,11 @@ describe('Typography', () => {
}); });
describe('editable', () => { describe('editable', () => {
function testStep({ name = '', icon, tooltip } = {}, submitFunc, expectFunc) { function testStep(
{ name = '', icon, tooltip, triggerType, enterIcon } = {},
submitFunc,
expectFunc,
) {
it(name, () => { it(name, () => {
jest.useFakeTimers(); jest.useFakeTimers();
const onStart = jest.fn(); const onStart = jest.fn();
@ -406,7 +410,7 @@ describe('Typography', () => {
const wrapper = mount( const wrapper = mount(
<Paragraph <Paragraph
editable={{ onChange, onStart, icon, tooltip }} editable={{ onChange, onStart, icon, tooltip, triggerType, enterIcon }}
className={className} className={className}
style={style} style={style}
> >
@ -414,12 +418,17 @@ describe('Typography', () => {
</Paragraph>, </Paragraph>,
); );
if (triggerType === undefined || triggerType.indexOf('icon') !== -1) {
if (icon) { if (icon) {
expect(wrapper.find('.anticon-highlight').length).toBeTruthy(); expect(wrapper.find('.anticon-highlight').length).toBeTruthy();
} else { } else {
expect(wrapper.find('.anticon-edit').length).toBeTruthy(); expect(wrapper.find('.anticon-edit').length).toBeTruthy();
} }
if (triggerType === undefined || triggerType.indexOf('text') === -1) {
wrapper.simulate('click');
expect(onStart).not.toHaveBeenCalled();
}
wrapper.find('.ant-typography-edit').first().simulate('mouseenter'); wrapper.find('.ant-typography-edit').first().simulate('mouseenter');
jest.runAllTimers(); jest.runAllTimers();
wrapper.update(); wrapper.update();
@ -435,6 +444,21 @@ describe('Typography', () => {
wrapper.find('.ant-typography-edit').first().simulate('click'); wrapper.find('.ant-typography-edit').first().simulate('click');
expect(onStart).toHaveBeenCalled(); expect(onStart).toHaveBeenCalled();
if (triggerType !== undefined && triggerType.indexOf('text') !== -1) {
wrapper.find('textarea').simulate('keyDown', { keyCode: KeyCode.ESC });
wrapper.find('textarea').simulate('keyUp', { keyCode: KeyCode.ESC });
expect(onChange).not.toHaveBeenCalled();
}
}
if (triggerType !== undefined && triggerType.indexOf('text') !== -1) {
if (triggerType.indexOf('icon') === -1) {
expect(wrapper.find('.anticon-highlight').length).toBeFalsy();
expect(wrapper.find('.anticon-edit').length).toBeFalsy();
}
wrapper.simulate('click');
expect(onStart).toHaveBeenCalled();
}
// Should have className // Should have className
const props = wrapper.find('div').first().props(); const props = wrapper.find('div').first().props();
@ -445,6 +469,18 @@ describe('Typography', () => {
target: { value: 'Bamboo' }, target: { value: 'Bamboo' },
}); });
if (enterIcon === undefined) {
expect(
wrapper.find('span.ant-typography-edit-content-confirm').first().props().className,
).toContain('anticon-enter');
} else if (enterIcon === null) {
expect(wrapper.find('span.ant-typography-edit-content-confirm').length).toBe(0);
} else {
expect(
wrapper.find('span.ant-typography-edit-content-confirm').first().props().className,
).not.toContain('anticon-enter');
}
if (submitFunc) { if (submitFunc) {
submitFunc(wrapper); submitFunc(wrapper);
} else { } else {
@ -492,6 +528,13 @@ describe('Typography', () => {
testStep({ name: 'customize edit show tooltip', tooltip: true }); testStep({ name: 'customize edit show tooltip', tooltip: true });
testStep({ name: 'customize edit hide tooltip', tooltip: false }); testStep({ name: 'customize edit hide tooltip', tooltip: false });
testStep({ name: 'customize edit tooltip text', tooltip: 'click to edit text' }); testStep({ name: 'customize edit tooltip text', tooltip: 'click to edit text' });
testStep({ name: 'enter icon - default', enterIcon: undefined });
testStep({ name: 'enter icon - null', enterIcon: null });
testStep({ name: 'enter icon - custom', enterIcon: <CheckOutlined /> });
testStep({ name: 'trigger by icon', triggerType: ['icon'] });
testStep({ name: 'trigger by text', triggerType: ['text'] });
testStep({ name: 'trigger by both icon and text', triggerType: ['icon', 'text'] });
it('should trigger onEnd when type Enter', () => { it('should trigger onEnd when type Enter', () => {
const onEnd = jest.fn(); const onEnd = jest.fn();

View File

@ -15,19 +15,48 @@ Provide additional interactive capacity of editable and copyable.
```jsx ```jsx
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Typography } from 'antd'; import { Checkbox, Radio, Typography } from 'antd';
import { HighlightOutlined, SmileOutlined, SmileFilled } from '@ant-design/icons'; import { CheckOutlined, HighlightOutlined, SmileOutlined, SmileFilled } from '@ant-design/icons';
const { Paragraph } = Typography; const { Paragraph } = Typography;
const Demo = () => { const Demo = () => {
const [editableStr, setEditableStr] = useState('This is an editable text.'); const [editableStr, setEditableStr] = useState('This is an editable text.');
const [customIconStr, setCustomIconStr] = useState('Custom Edit icon and replace tooltip text.'); const [customIconStr, setCustomIconStr] = useState('Custom Edit icon and replace tooltip text.');
const [clickTriggerStr, setClickTriggerStr] = useState(
'Text or icon as trigger - click to start editing.',
);
const [chooseTrigger, setChooseTrigger] = useState('icon');
const [customEnterIconStr, setCustomEnterIconStr] = useState(
'Editable text with a custom enter icon in edit field.',
);
const [noEnterIconStr, setNoEnterIconStr] = useState(
'Editable text with no enter icon in edit field.',
);
const [hideTooltipStr, setHideTooltipStr] = useState('Hide Edit tooltip.'); const [hideTooltipStr, setHideTooltipStr] = useState('Hide Edit tooltip.');
const [lengthLimitedStr, setLengthLimitedStr] = useState( const [lengthLimitedStr, setLengthLimitedStr] = useState(
'This is an editable text with limited length.', 'This is an editable text with limited length.',
); );
const radioToState = input => {
switch (input) {
case 'text':
return ['text'];
case 'both':
return ['icon', 'text'];
case 'icon':
default:
return ['icon'];
}
};
const stateToRadio = () => {
if (chooseTrigger.indexOf('text') !== -1) {
return chooseTrigger.indexOf('icon') !== -1 ? 'both' : 'text';
}
return 'icon';
};
return ( return (
<> <>
<Paragraph editable={{ onChange: setEditableStr }}>{editableStr}</Paragraph> <Paragraph editable={{ onChange: setEditableStr }}>{editableStr}</Paragraph>
@ -40,6 +69,44 @@ const Demo = () => {
> >
{customIconStr} {customIconStr}
</Paragraph> </Paragraph>
Trigger edit with:{' '}
<Radio.Group
onChange={e => setChooseTrigger(radioToState(e.target.value))}
value={stateToRadio()}
>
<Radio value="icon">icon</Radio>
<Radio value="text">text</Radio>
<Radio value="both">both</Radio>
</Radio.Group>
<Paragraph
editable={{
tooltip: 'click to edit text',
onChange: setClickTriggerStr,
triggerType: chooseTrigger,
}}
>
{clickTriggerStr}
</Paragraph>
<Paragraph
editable={{
icon: <HighlightOutlined />,
tooltip: 'click to edit text',
onChange: setCustomEnterIconStr,
enterIcon: <CheckOutlined />,
}}
>
{customEnterIconStr}
</Paragraph>
<Paragraph
editable={{
icon: <HighlightOutlined />,
tooltip: 'click to edit text',
onChange: setNoEnterIconStr,
enterIcon: null,
}}
>
{noEnterIconStr}
</Paragraph>
<Paragraph editable={{ tooltip: false, onChange: setHideTooltipStr }}> <Paragraph editable={{ tooltip: false, onChange: setHideTooltipStr }}>
{hideTooltipStr} {hideTooltipStr}
</Paragraph> </Paragraph>

View File

@ -95,6 +95,8 @@ Basic text writing, including headings, body text, lists, and more.
onChange: function(string), onChange: function(string),
onCancel: function, onCancel: function,
onEnd: function, onEnd: function,
triggerType: ('icon' | 'text')[],
enterIcon: ReactNode,
} }
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
@ -104,10 +106,12 @@ Basic text writing, including headings, body text, lists, and more.
| icon | Custom editable icon | ReactNode | &lt;EditOutlined /> | 4.6.0 | | icon | Custom editable icon | ReactNode | &lt;EditOutlined /> | 4.6.0 |
| maxLength | `maxLength` attribute of textarea | number | - | 4.4.0 | | maxLength | `maxLength` attribute of textarea | number | - | 4.4.0 |
| tooltip | Custom tooltip text, hide when it is false | boolean \| ReactNode | `Edit` | 4.6.0 | | tooltip | Custom tooltip text, hide when it is false | boolean \| ReactNode | `Edit` | 4.6.0 |
| onCancel | Called when type ESC to exit editable state | function | - | |
| onChange | Called when input at textarea | function(event) | - | |
| onEnd | Called when type ENTER to exit editable state | function | - | 4.14.0 |
| onStart | Called when enter editable state | function | - | | | onStart | Called when enter editable state | function | - | |
| onChange | Called when input at textarea | function(event) | - | |
| onCancel | Called when type ESC to exit editable state | function | - | |
| onEnd | Called when type ENTER to exit editable state | function | - | 4.14.0 |
| triggerType | Edit mode trigger - icon, text or both (not specifying icon as trigger hides it) | Array&lt;`icon`\|`text`> | \[`icon`] | |
| enterIcon | Custom "enter" icon in the edit field (passing `null` removes the icon) | ReactNode | `<EnterOutlined />` | 4.17.0 |
### ellipsis ### ellipsis

View File

@ -96,6 +96,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/GOM1KQ24O/Typography.svg
onChange: function(string), onChange: function(string),
onCancel: function, onCancel: function,
onEnd: function, onEnd: function,
triggerType: ('icon' | 'text')[],
enterIcon: ReactNode,
} }
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
@ -109,6 +111,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/GOM1KQ24O/Typography.svg
| onChange | 文本域编辑时触发 | function(event) | - | | | onChange | 文本域编辑时触发 | function(event) | - | |
| onEnd | 按 ENTER 结束编辑状态时触发 | function | - | 4.14.0 | | onEnd | 按 ENTER 结束编辑状态时触发 | function | - | 4.14.0 |
| onStart | 进入编辑中状态时触发 | function | - | | | onStart | 进入编辑中状态时触发 | function | - | |
| triggerType | Edit mode trigger - icon, text or both (not specifying icon as trigger hides it) | Array&lt;`icon`\|`text`> | \[`icon`] | |
| enterIcon | 在编辑段中自定义“enter”图标传递“null”将删除图标 | ReactNode | `<EnterOutlined />` | 4.17.0 |
### ellipsis ### ellipsis

View File

@ -174,13 +174,13 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
let clone; let clone;
try { try {
clone = (new File([originFileObj], originFileObj.name, { clone = new File([originFileObj], originFileObj.name, {
type: originFileObj.type, type: originFileObj.type,
}) as any) as UploadFile; }) as any as UploadFile;
} catch (e) { } catch (e) {
clone = (new Blob([originFileObj], { clone = new Blob([originFileObj], {
type: originFileObj.type, type: originFileObj.type,
}) as any) as UploadFile; }) as any as UploadFile;
clone.name = originFileObj.name; clone.name = originFileObj.name;
clone.lastModifiedDate = new Date(); clone.lastModifiedDate = new Date();
clone.lastModified = new Date().getTime(); clone.lastModified = new Date().getTime();
@ -326,7 +326,14 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
showUploadList ? ( showUploadList ? (
<LocaleReceiver componentName="Upload" defaultLocale={defaultLocale.Upload}> <LocaleReceiver componentName="Upload" defaultLocale={defaultLocale.Upload}>
{(locale: UploadLocale) => { {(locale: UploadLocale) => {
const { showRemoveIcon, showPreviewIcon, showDownloadIcon, removeIcon, downloadIcon } = const {
showRemoveIcon,
showPreviewIcon,
showDownloadIcon,
removeIcon,
previewIcon,
downloadIcon,
} =
typeof showUploadList === 'boolean' ? ({} as ShowUploadListInterface) : showUploadList; typeof showUploadList === 'boolean' ? ({} as ShowUploadListInterface) : showUploadList;
return ( return (
<UploadList <UploadList
@ -340,6 +347,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
showPreviewIcon={showPreviewIcon} showPreviewIcon={showPreviewIcon}
showDownloadIcon={showDownloadIcon} showDownloadIcon={showDownloadIcon}
removeIcon={removeIcon} removeIcon={removeIcon}
previewIcon={previewIcon}
downloadIcon={downloadIcon} downloadIcon={downloadIcon}
iconRender={iconRender} iconRender={iconRender}
locale={{ ...locale, ...propLocale }} locale={{ ...locale, ...propLocale }}

View File

@ -30,6 +30,7 @@ export interface ListItemProps {
showPreviewIcon?: boolean; showPreviewIcon?: boolean;
removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode); removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode); downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
previewIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
iconRender: (file: UploadFile) => React.ReactNode; iconRender: (file: UploadFile) => React.ReactNode;
actionIconRender: ( actionIconRender: (
customIcon: React.ReactNode, customIcon: React.ReactNode,
@ -62,6 +63,7 @@ const ListItem = React.forwardRef(
showPreviewIcon, showPreviewIcon,
showRemoveIcon, showRemoveIcon,
showDownloadIcon, showDownloadIcon,
previewIcon: customPreviewIcon,
removeIcon: customRemoveIcon, removeIcon: customRemoveIcon,
downloadIcon: customDownloadIcon, downloadIcon: customDownloadIcon,
onPreview, onPreview,
@ -206,7 +208,9 @@ const ListItem = React.forwardRef(
onClick={e => onPreview(file, e)} onClick={e => onPreview(file, e)}
title={locale.previewFile} title={locale.previewFile}
> >
<EyeOutlined /> {typeof customPreviewIcon === 'function'
? customPreviewIcon(file)
: customPreviewIcon || <EyeOutlined />}
</a> </a>
) : null; ) : null;

View File

@ -38,6 +38,7 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
showRemoveIcon, showRemoveIcon,
showDownloadIcon, showDownloadIcon,
removeIcon, removeIcon,
previewIcon,
downloadIcon, downloadIcon,
progress, progress,
appendAction, appendAction,
@ -210,6 +211,7 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
showRemoveIcon={showRemoveIcon} showRemoveIcon={showRemoveIcon}
showDownloadIcon={showDownloadIcon} showDownloadIcon={showDownloadIcon}
removeIcon={removeIcon} removeIcon={removeIcon}
previewIcon={previewIcon}
downloadIcon={downloadIcon} downloadIcon={downloadIcon}
iconRender={internalIconRender} iconRender={internalIconRender}
actionIconRender={actionIconRender} actionIconRender={actionIconRender}

View File

@ -40,7 +40,7 @@ Uploading is the process of publishing information (web pages, text, pictures, v
| openFileDialogOnClick | Click open file dialog | boolean | true | | | openFileDialogOnClick | Click open file dialog | boolean | true | |
| previewFile | Customize preview file logic | (file: File \| Blob) => Promise&lt;dataURL: string> | - | | | previewFile | Customize preview file logic | (file: File \| Blob) => Promise&lt;dataURL: string> | - | |
| progress | Custom progress bar | [ProgressProps](/components/progress/#API) (support `type="line"` only) | { strokeWidth: 2, showInfo: false } | 4.3.0 | | progress | Custom progress bar | [ProgressProps](/components/progress/#API) (support `type="line"` only) | { strokeWidth: 2, showInfo: false } | 4.3.0 |
| showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon` and `downloadIcon` individually | boolean \| { showPreviewIcon?: boolean, showDownloadIcon?: boolean, showRemoveIcon?: boolean, removeIcon?: ReactNode \| (file: UploadFile) => ReactNode, downloadIcon?: ReactNode \| (file: UploadFile) => ReactNode } | true | function: 4.7.0 | | showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon` and `downloadIcon` individually | boolean \| { showPreviewIcon?: boolean, showDownloadIcon?: boolean, showRemoveIcon?: boolean, previewIcon?: ReactNode \| (file: UploadFile) => ReactNode, removeIcon?: ReactNode \| (file: UploadFile) => ReactNode, downloadIcon?: ReactNode \| (file: UploadFile) => ReactNode } | true | function: 4.7.0 |
| withCredentials | The ajax upload with cookie sent | boolean | false | | | withCredentials | The ajax upload with cookie sent | boolean | false | |
| onChange | A callback function, can be executed when uploading state is changing, see [onChange](#onChange) | function | - | | | onChange | A callback function, can be executed when uploading state is changing, see [onChange](#onChange) | function | - | |
| onDrop | A callback function executed when files are dragged and dropped into upload area | (event: React.DragEvent) => void | - | 4.16.0 | | onDrop | A callback function executed when files are dragged and dropped into upload area | (event: React.DragEvent) => void | - | 4.16.0 |

View File

@ -41,7 +41,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg
| openFileDialogOnClick | 点击打开文件对话框 | boolean | true | | | openFileDialogOnClick | 点击打开文件对话框 | boolean | true | |
| previewFile | 自定义文件预览逻辑 | (file: File \| Blob) => Promise&lt;dataURL: string> | - | | | previewFile | 自定义文件预览逻辑 | (file: File \| Blob) => Promise&lt;dataURL: string> | - | |
| progress | 自定义进度条样式 | [ProgressProps](/components/progress/#API)(仅支持 `type="line"` | { strokeWidth: 2, showInfo: false } | 4.3.0 | | progress | 自定义进度条样式 | [ProgressProps](/components/progress/#API)(仅支持 `type="line"` | { strokeWidth: 2, showInfo: false } | 4.3.0 |
| showUploadList | 是否展示文件列表, 可设为一个对象,用于单独设定 `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon``downloadIcon` | boolean \| { showPreviewIcon?: boolean, showRemoveIcon?: boolean, showDownloadIcon?: boolean, removeIcon?: ReactNode \| (file: UploadFile) => ReactNode, downloadIcon?: ReactNode \| (file: UploadFile) => ReactNode } | true | function: 4.7.0 | | showUploadList | 是否展示文件列表, 可设为一个对象,用于单独设定 `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon``downloadIcon` | boolean \| { showPreviewIcon?: boolean, showRemoveIcon?: boolean, showDownloadIcon?: boolean, previewIcon?: ReactNode \| (file: UploadFile) => ReactNode, removeIcon?: ReactNode \| (file: UploadFile) => ReactNode, downloadIcon?: ReactNode \| (file: UploadFile) => ReactNode } | true | function: 4.7.0 |
| withCredentials | 上传请求时是否携带 cookie | boolean | false | | | withCredentials | 上传请求时是否携带 cookie | boolean | false | |
| onChange | 上传文件改变时的状态,详见 [onChange](#onChange) | function | - | | | onChange | 上传文件改变时的状态,详见 [onChange](#onChange) | function | - | |
| onDrop | 当文件被拖入上传区域时执行的回调功能 | (event: React.DragEvent) => void | - | 4.16.0 | | onDrop | 当文件被拖入上传区域时执行的回调功能 | (event: React.DragEvent) => void | - | 4.16.0 |

View File

@ -52,6 +52,7 @@ export interface ShowUploadListInterface {
showDownloadIcon?: boolean; showDownloadIcon?: boolean;
removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode); removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode); downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
previewIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
} }
export interface UploadLocale { export interface UploadLocale {
@ -145,6 +146,7 @@ export interface UploadListProps<T = any> {
showPreviewIcon?: boolean; showPreviewIcon?: boolean;
removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode); removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode); downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
previewIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
locale: UploadLocale; locale: UploadLocale;
previewFile?: PreviewFileHandler; previewFile?: PreviewFileHandler;
iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode; iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode;