refactor(upload): rewrite with hooks (#26196)

* refactor(upload): rewrite with hooks

* refactor: optimize code style;
chore(dragger): add forwardRef;

* fix: lint

* fix: trigger ci

* fix: lint

* chore: add test case
This commit is contained in:
Kermit Xuan 2020-08-18 15:44:31 +08:00 committed by GitHub
parent 517d2eeccb
commit ee50fe7952
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 571 additions and 521 deletions

View File

@ -4,12 +4,15 @@ import { UploadProps } from './interface';
export type DraggerProps = UploadProps & { height?: number };
// stick class comoponent to avoid React ref warning inside Form
// https://github.com/ant-design/ant-design/issues/18707
// eslint-disable-next-line react/prefer-stateless-function
export default class Dragger extends React.Component<DraggerProps, any> {
render() {
const { style, height, ...restProps } = this.props;
return <Upload {...restProps} type="drag" style={{ ...style, height }} />;
}
}
const InternalDragger: React.ForwardRefRenderFunction<unknown, DraggerProps> = (
{ style, height, ...restProps },
ref,
) => {
return <Upload ref={ref} {...restProps} type="drag" style={{ ...style, height }} />;
};
const Dragger = React.forwardRef(InternalDragger) as React.FC<DraggerProps>;
Dragger.displayName = 'Dragger';
export default Dragger;

View File

@ -6,7 +6,6 @@ import UploadList from './UploadList';
import {
RcFile,
UploadProps,
UploadState,
UploadFile,
UploadLocale,
UploadChangeParam,
@ -16,72 +15,76 @@ import {
import { T, fileToObject, getFileItem, removeFileItem } from './utils';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale/default';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning';
import useSyncState from './hooks/useSyncState';
import useForceUpdate from './hooks/useForceUpdate';
export { UploadProps };
class Upload extends React.Component<UploadProps, UploadState> {
static Dragger: typeof Dragger;
const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (props, ref) => {
const {
fileList: fileListProp,
defaultFileList,
onRemove,
showUploadList,
listType,
onPreview,
onDownload,
previewFile,
disabled,
locale: propLocale,
iconRender,
isImageUrl,
progress,
prefixCls: customizePrefixCls,
className,
type,
children,
style,
} = props;
static defaultProps = {
type: 'select' as UploadType,
multiple: false,
action: '',
data: {},
accept: '',
beforeUpload: T,
showUploadList: true,
listType: 'text' as UploadListType, // or picture
className: '',
disabled: false,
supportServerRender: true,
};
const [getFileList, setFileList] = useSyncState<Array<UploadFile>>(
fileListProp || defaultFileList || [],
);
const [dragState, setDragState] = React.useState<string>('drop');
static getDerivedStateFromProps(nextProps: UploadProps) {
if ('fileList' in nextProps) {
return {
fileList: nextProps.fileList || [],
};
}
return null;
}
recentUploadStatus: boolean | PromiseLike<any>;
progressTimer: any;
upload: any;
constructor(props: UploadProps) {
super(props);
this.state = {
fileList: props.fileList || props.defaultFileList || [],
dragState: 'drop',
};
const upload = React.useRef<any>();
React.useEffect(() => {
setFileList(fileListProp || defaultFileList || []);
devWarning(
'fileList' in props || !('value' in props),
'Upload',
'`value` is not a valid prop, do you mean `fileList`?',
);
}
}, []);
componentWillUnmount() {
this.clearProgressTimer();
}
React.useEffect(() => {
if ('fileList' in props) {
setFileList(fileListProp || []);
}
}, [fileListProp]);
saveUpload = (node: any) => {
this.upload = node;
const onChange = (info: UploadChangeParam) => {
if (!('fileList' in props)) {
setFileList(info.fileList);
}
const { onChange: onChangeProp } = props;
if (onChangeProp) {
onChangeProp({
...info,
fileList: [...info.fileList],
});
}
};
onStart = (file: RcFile) => {
const { fileList } = this.state;
const onStart = (file: RcFile) => {
const targetItem = fileToObject(file);
targetItem.status = 'uploading';
const nextFileList = fileList.concat();
const nextFileList = getFileList().concat();
const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === targetItem.uid);
if (fileIndex === -1) {
@ -90,14 +93,13 @@ class Upload extends React.Component<UploadProps, UploadState> {
nextFileList[fileIndex] = targetItem;
}
this.onChange({
onChange({
file: targetItem,
fileList: nextFileList,
});
};
onSuccess = (response: any, file: UploadFile, xhr: any) => {
this.clearProgressTimer();
const onSuccess = (response: any, file: UploadFile, xhr: any) => {
try {
if (typeof response === 'string') {
response = JSON.parse(response);
@ -105,8 +107,7 @@ class Upload extends React.Component<UploadProps, UploadState> {
} catch (e) {
/* do nothing */
}
const { fileList } = this.state;
const targetItem = getFileItem(file, fileList);
const targetItem = getFileItem(file, getFileList());
// removed
if (!targetItem) {
return;
@ -114,31 +115,28 @@ class Upload extends React.Component<UploadProps, UploadState> {
targetItem.status = 'done';
targetItem.response = response;
targetItem.xhr = xhr;
this.onChange({
onChange({
file: { ...targetItem },
fileList,
fileList: getFileList().concat(),
});
};
onProgress = (e: { percent: number }, file: UploadFile) => {
const { fileList } = this.state;
const targetItem = getFileItem(file, fileList);
const onProgress = (e: { percent: number }, file: UploadFile) => {
const targetItem = getFileItem(file, getFileList());
// removed
if (!targetItem) {
return;
}
targetItem.percent = e.percent;
this.onChange({
onChange({
event: e,
file: { ...targetItem },
fileList,
fileList: getFileList().concat(),
});
};
onError = (error: Error, response: any, file: UploadFile) => {
this.clearProgressTimer();
const { fileList } = this.state;
const targetItem = getFileItem(file, fileList);
const onError = (error: Error, response: any, file: UploadFile) => {
const targetItem = getFileItem(file, getFileList());
// removed
if (!targetItem) {
return;
@ -146,32 +144,28 @@ class Upload extends React.Component<UploadProps, UploadState> {
targetItem.error = error;
targetItem.response = response;
targetItem.status = 'error';
this.onChange({
onChange({
file: { ...targetItem },
fileList,
fileList: getFileList().concat(),
});
};
handleRemove = (file: UploadFile) => {
const { onRemove } = this.props;
const { fileList } = this.state;
const handleRemove = (file: UploadFile) => {
Promise.resolve(typeof onRemove === 'function' ? onRemove(file) : onRemove).then(ret => {
// Prevent removing file
if (ret === false) {
return;
}
const removedFileList = removeFileItem(file, fileList);
const removedFileList = removeFileItem(file, getFileList());
if (removedFileList) {
file.status = 'removed';
if (this.upload) {
this.upload.abort(file);
if (upload.current) {
upload.current.abort(file);
}
this.onChange({
onChange({
file,
fileList: removedFileList,
});
@ -179,43 +173,28 @@ class Upload extends React.Component<UploadProps, UploadState> {
});
};
onChange = (info: UploadChangeParam) => {
if (!('fileList' in this.props)) {
this.setState({ fileList: info.fileList });
}
const { onChange } = this.props;
if (onChange) {
onChange({
...info,
fileList: [...info.fileList],
});
}
const onFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
setDragState(e.type);
};
onFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
this.setState({
dragState: e.type,
});
};
beforeUpload = (file: RcFile, fileList: RcFile[]) => {
const { beforeUpload } = this.props;
const { fileList: stateFileList } = this.state;
if (!beforeUpload) {
const beforeUpload = (file: RcFile, fileListArgs: RcFile[]) => {
const { beforeUpload: beforeUploadProp } = props;
if (!beforeUploadProp) {
return true;
}
const result = beforeUpload(file, fileList);
const result = beforeUploadProp(file, fileListArgs);
if (result === false) {
// Get unique file list
const uniqueList: UploadFile<any>[] = [];
stateFileList.concat(fileList.map(fileToObject)).forEach(f => {
if (uniqueList.every(uf => uf.uid !== f.uid)) {
uniqueList.push(f);
}
});
getFileList()
.concat(fileListArgs.map(fileToObject))
.forEach(f => {
if (uniqueList.every(uf => uf.uid !== f.uid)) {
uniqueList.push(f);
}
});
this.onChange({
onChange({
file,
fileList: uniqueList,
});
@ -226,24 +205,19 @@ class Upload extends React.Component<UploadProps, UploadState> {
}
return true;
};
// Test needs
const forceUpdate = useForceUpdate();
React.useImperativeHandle(ref, () => ({
onStart,
onSuccess,
onProgress,
onError,
fileList: getFileList(),
upload: upload.current,
forceUpdate,
}));
clearProgressTimer() {
clearInterval(this.progressTimer);
}
renderUploadList = (locale: UploadLocale) => {
const {
showUploadList,
listType,
onPreview,
onDownload,
previewFile,
disabled,
locale: propLocale,
iconRender,
isImageUrl,
progress,
} = this.props;
const renderUploadList = (locale: UploadLocale) => {
const {
showRemoveIcon,
showPreviewIcon,
@ -251,15 +225,14 @@ class Upload extends React.Component<UploadProps, UploadState> {
removeIcon,
downloadIcon,
} = showUploadList as any;
const { fileList } = this.state;
return (
<UploadList
listType={listType}
items={fileList}
items={getFileList()}
previewFile={previewFile}
onPreview={onPreview}
onDownload={onDownload}
onRemove={this.handleRemove}
onRemove={handleRemove}
showRemoveIcon={!disabled && showRemoveIcon}
showPreviewIcon={showPreviewIcon}
showDownloadIcon={showDownloadIcon}
@ -273,111 +246,120 @@ class Upload extends React.Component<UploadProps, UploadState> {
);
};
renderUpload = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
className,
showUploadList,
listType,
type,
disabled,
children,
style,
} = this.props;
const { fileList, dragState } = this.state;
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('upload', customizePrefixCls);
const prefixCls = getPrefixCls('upload', customizePrefixCls);
const rcUploadProps = {
onStart: this.onStart,
onError: this.onError,
onProgress: this.onProgress,
onSuccess: this.onSuccess,
...this.props,
const rcUploadProps = {
onStart,
onError,
onProgress,
onSuccess,
...props,
prefixCls,
beforeUpload,
};
delete rcUploadProps.className;
delete rcUploadProps.style;
// Remove id to avoid open by label when trigger is hidden
// !children: https://github.com/ant-design/ant-design/issues/14298
// disabled: https://github.com/ant-design/ant-design/issues/16478
// https://github.com/ant-design/ant-design/issues/24197
if (!children || disabled) {
delete rcUploadProps.id;
}
const uploadList = showUploadList ? (
<LocaleReceiver componentName="Upload" defaultLocale={defaultLocale.Upload}>
{renderUploadList}
</LocaleReceiver>
) : null;
if (type === 'drag') {
const dragCls = classNames(
prefixCls,
beforeUpload: this.beforeUpload,
};
delete rcUploadProps.className;
delete rcUploadProps.style;
// Remove id to avoid open by label when trigger is hidden
// !children: https://github.com/ant-design/ant-design/issues/14298
// disabled: https://github.com/ant-design/ant-design/issues/16478
// https://github.com/ant-design/ant-design/issues/24197
if (!children || disabled) {
delete rcUploadProps.id;
}
const uploadList = showUploadList ? (
<LocaleReceiver componentName="Upload" defaultLocale={defaultLocale.Upload}>
{this.renderUploadList}
</LocaleReceiver>
) : null;
if (type === 'drag') {
const dragCls = classNames(
prefixCls,
{
[`${prefixCls}-drag`]: true,
[`${prefixCls}-drag-uploading`]: fileList.some(file => file.status === 'uploading'),
[`${prefixCls}-drag-hover`]: dragState === 'dragover',
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
return (
<span>
<div
className={dragCls}
onDrop={this.onFileDrop}
onDragOver={this.onFileDrop}
onDragLeave={this.onFileDrop}
style={style}
>
<RcUpload {...rcUploadProps} ref={this.saveUpload} className={`${prefixCls}-btn`}>
<div className={`${prefixCls}-drag-container`}>{children}</div>
</RcUpload>
</div>
{uploadList}
</span>
);
}
const uploadButtonCls = classNames(prefixCls, {
[`${prefixCls}-select`]: true,
[`${prefixCls}-select-${listType}`]: true,
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl',
});
const uploadButton = (
<div className={uploadButtonCls} style={children ? undefined : { display: 'none' }}>
<RcUpload {...rcUploadProps} ref={this.saveUpload} />
</div>
{
[`${prefixCls}-drag`]: true,
[`${prefixCls}-drag-uploading`]: getFileList().some(file => file.status === 'uploading'),
[`${prefixCls}-drag-hover`]: dragState === 'dragover',
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
if (listType === 'picture-card') {
return (
<span className={classNames(className, `${prefixCls}-picture-card-wrapper`)}>
{uploadList}
{uploadButton}
</span>
);
}
return (
<span className={className}>
{uploadButton}
<span>
<div
className={dragCls}
onDrop={onFileDrop}
onDragOver={onFileDrop}
onDragLeave={onFileDrop}
style={style}
>
<RcUpload {...rcUploadProps} ref={upload} className={`${prefixCls}-btn`}>
<div className={`${prefixCls}-drag-container`}>{children}</div>
</RcUpload>
</div>
{uploadList}
</span>
);
};
render() {
return <ConfigConsumer>{this.renderUpload}</ConfigConsumer>;
}
const uploadButtonCls = classNames(prefixCls, {
[`${prefixCls}-select`]: true,
[`${prefixCls}-select-${listType}`]: true,
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl',
});
const uploadButton = (
<div className={uploadButtonCls} style={children ? undefined : { display: 'none' }}>
<RcUpload {...rcUploadProps} ref={upload} />
</div>
);
if (listType === 'picture-card') {
return (
<span className={classNames(className, `${prefixCls}-picture-card-wrapper`)}>
{uploadList}
{uploadButton}
</span>
);
}
return (
<span className={className}>
{uploadButton}
{uploadList}
</span>
);
};
interface CompoundedComponent
extends React.ForwardRefExoticComponent<UploadProps & React.RefAttributes<any>> {
Dragger: typeof Dragger;
}
const Upload = React.forwardRef<unknown, UploadProps>(InternalUpload) as CompoundedComponent;
Upload.Dragger = Dragger;
Upload.displayName = 'Upload';
Upload.defaultProps = {
type: 'select' as UploadType,
multiple: false,
action: '',
data: {},
accept: '',
beforeUpload: T,
showUploadList: true,
listType: 'text' as UploadListType, // or picture
className: '',
disabled: false,
supportServerRender: true,
};
export default Upload;

View File

@ -14,25 +14,34 @@ import { UploadListProps, UploadFile, UploadListType } from './interface';
import { previewImage, isImageUrl } from './utils';
import Tooltip from '../tooltip';
import Progress from '../progress';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import Button, { ButtonProps } from '../button';
import useForceUpdate from './hooks/useForceUpdate';
export default class UploadList extends React.Component<UploadListProps, any> {
static defaultProps = {
listType: 'text' as UploadListType, // or picture
progress: {
strokeWidth: 2,
showInfo: false,
},
showRemoveIcon: true,
showDownloadIcon: false,
showPreviewIcon: true,
previewFile: previewImage,
isImageUrl,
};
const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProps> = (
{
listType,
previewFile,
onPreview,
onDownload,
onRemove,
locale,
iconRender,
isImageUrl: isImgUrl,
prefixCls: customizePrefixCls,
items = [],
showPreviewIcon,
showRemoveIcon,
showDownloadIcon,
removeIcon: customRemoveIcon,
downloadIcon: customDownloadIcon,
progress: progressProps,
},
ref,
) => {
const forceUpdate = useForceUpdate();
componentDidUpdate() {
const { listType, items, previewFile } = this.props;
React.useEffect(() => {
if (listType !== 'picture' && listType !== 'picture-card') {
return;
}
@ -52,14 +61,13 @@ export default class UploadList extends React.Component<UploadListProps, any> {
previewFile(file.originFileObj as File).then((previewDataUrl: string) => {
// Need append '' to avoid dead loop
file.thumbUrl = previewDataUrl || '';
this.forceUpdate();
forceUpdate();
});
}
});
}
}, [listType, items, previewFile]);
handlePreview = (file: UploadFile, e: React.SyntheticEvent<HTMLElement>) => {
const { onPreview } = this.props;
const handlePreview = (file: UploadFile, e: React.SyntheticEvent<HTMLElement>) => {
if (!onPreview) {
return;
}
@ -67,8 +75,7 @@ export default class UploadList extends React.Component<UploadListProps, any> {
return onPreview(file);
};
handleDownload = (file: UploadFile) => {
const { onDownload } = this.props;
const handleDownload = (file: UploadFile) => {
if (typeof onDownload === 'function') {
onDownload(file);
} else if (file.url) {
@ -76,15 +83,13 @@ export default class UploadList extends React.Component<UploadListProps, any> {
}
};
handleClose = (file: UploadFile) => {
const { onRemove } = this.props;
const handleClose = (file: UploadFile) => {
if (onRemove) {
onRemove(file);
}
};
handleIconRender = (file: UploadFile) => {
const { listType, locale, iconRender, isImageUrl: isImgUrl } = this.props;
const handleIconRender = (file: UploadFile) => {
if (iconRender) {
return iconRender(file, listType);
}
@ -99,7 +104,7 @@ export default class UploadList extends React.Component<UploadListProps, any> {
return icon;
};
handleActionIconRender = (
const handleActionIconRender = (
customIcon: React.ReactNode,
callback: () => void,
prefixCls: string,
@ -132,223 +137,229 @@ export default class UploadList extends React.Component<UploadListProps, any> {
);
};
renderUploadList = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
items = [],
listType,
showPreviewIcon,
showRemoveIcon,
showDownloadIcon,
removeIcon: customRemoveIcon,
downloadIcon: customDownloadIcon,
locale,
progress: progressProps,
isImageUrl: isImgUrl,
} = this.props;
const prefixCls = getPrefixCls('upload', customizePrefixCls);
const list = items.map(file => {
let progress;
const iconNode = this.handleIconRender(file);
let icon = <div className={`${prefixCls}-text-icon`}>{iconNode}</div>;
if (listType === 'picture' || listType === 'picture-card') {
if (file.status === 'uploading' || (!file.thumbUrl && !file.url)) {
const uploadingClassName = classNames({
[`${prefixCls}-list-item-thumbnail`]: true,
[`${prefixCls}-list-item-file`]: file.status !== 'uploading',
});
icon = <div className={uploadingClassName}>{iconNode}</div>;
} else {
const thumbnail =
isImgUrl && isImgUrl(file) ? (
<img
src={file.thumbUrl || file.url}
alt={file.name}
className={`${prefixCls}-list-item-image`}
/>
) : (
iconNode
);
const aClassName = classNames({
[`${prefixCls}-list-item-thumbnail`]: true,
[`${prefixCls}-list-item-file`]: isImgUrl && !isImgUrl(file),
});
icon = (
<a
className={aClassName}
onClick={e => this.handlePreview(file, e)}
href={file.url || file.thumbUrl}
target="_blank"
rel="noopener noreferrer"
>
{thumbnail}
</a>
// Test needs
React.useImperativeHandle(ref, () => ({
handlePreview,
handleDownload,
}));
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('upload', customizePrefixCls);
const list = items.map(file => {
let progress;
const iconNode = handleIconRender(file);
let icon = <div className={`${prefixCls}-text-icon`}>{iconNode}</div>;
if (listType === 'picture' || listType === 'picture-card') {
if (file.status === 'uploading' || (!file.thumbUrl && !file.url)) {
const uploadingClassName = classNames({
[`${prefixCls}-list-item-thumbnail`]: true,
[`${prefixCls}-list-item-file`]: file.status !== 'uploading',
});
icon = <div className={uploadingClassName}>{iconNode}</div>;
} else {
const thumbnail =
isImgUrl && isImgUrl(file) ? (
<img
src={file.thumbUrl || file.url}
alt={file.name}
className={`${prefixCls}-list-item-image`}
/>
) : (
iconNode
);
}
}
if (file.status === 'uploading') {
// show loading icon if upload progress listener is disabled
const loadingProgress =
'percent' in file ? (
<Progress {...progressProps} type="line" percent={file.percent} />
) : null;
progress = (
<div className={`${prefixCls}-list-item-progress`} key="progress">
{loadingProgress}
</div>
const aClassName = classNames({
[`${prefixCls}-list-item-thumbnail`]: true,
[`${prefixCls}-list-item-file`]: isImgUrl && !isImgUrl(file),
});
icon = (
<a
className={aClassName}
onClick={e => handlePreview(file, e)}
href={file.url || file.thumbUrl}
target="_blank"
rel="noopener noreferrer"
>
{thumbnail}
</a>
);
}
const infoUploadingClass = classNames({
[`${prefixCls}-list-item`]: true,
[`${prefixCls}-list-item-${file.status}`]: true,
[`${prefixCls}-list-item-list-type-${listType}`]: true,
});
const linkProps =
typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps;
}
const removeIcon = showRemoveIcon
? this.handleActionIconRender(
customRemoveIcon || <DeleteOutlined />,
() => this.handleClose(file),
if (file.status === 'uploading') {
// show loading icon if upload progress listener is disabled
const loadingProgress =
'percent' in file ? (
<Progress {...progressProps} type="line" percent={file.percent} />
) : null;
progress = (
<div className={`${prefixCls}-list-item-progress`} key="progress">
{loadingProgress}
</div>
);
}
const infoUploadingClass = classNames({
[`${prefixCls}-list-item`]: true,
[`${prefixCls}-list-item-${file.status}`]: true,
[`${prefixCls}-list-item-list-type-${listType}`]: true,
});
const linkProps =
typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps;
const removeIcon = showRemoveIcon
? handleActionIconRender(
customRemoveIcon || <DeleteOutlined />,
() => handleClose(file),
prefixCls,
locale.removeFile,
)
: null;
const downloadIcon =
showDownloadIcon && file.status === 'done'
? handleActionIconRender(
customDownloadIcon || <DownloadOutlined />,
() => handleDownload(file),
prefixCls,
locale.removeFile,
locale.downloadFile,
)
: null;
const downloadIcon =
showDownloadIcon && file.status === 'done'
? this.handleActionIconRender(
customDownloadIcon || <DownloadOutlined />,
() => this.handleDownload(file),
prefixCls,
locale.downloadFile,
)
: null;
const downloadOrDelete = listType !== 'picture-card' && (
<span
key="download-delete"
className={`${prefixCls}-list-item-card-actions ${
listType === 'picture' ? 'picture' : ''
}`}
>
{downloadIcon}
{removeIcon}
</span>
);
const listItemNameClass = classNames({
[`${prefixCls}-list-item-name`]: true,
[`${prefixCls}-list-item-name-icon-count-${
[downloadIcon, removeIcon].filter(x => x).length
}`]: true,
});
const preview = file.url
? [
<a
key="view"
target="_blank"
rel="noopener noreferrer"
className={listItemNameClass}
title={file.name}
{...linkProps}
href={file.url}
onClick={e => this.handlePreview(file, e)}
>
{file.name}
</a>,
downloadOrDelete,
]
: [
<span
key="view"
className={listItemNameClass}
onClick={e => this.handlePreview(file, e)}
title={file.name}
>
{file.name}
</span>,
downloadOrDelete,
];
const style: React.CSSProperties = {
pointerEvents: 'none',
opacity: 0.5,
};
const previewIcon = showPreviewIcon ? (
<a
href={file.url || file.thumbUrl}
target="_blank"
rel="noopener noreferrer"
style={file.url || file.thumbUrl ? undefined : style}
onClick={e => this.handlePreview(file, e)}
title={locale.previewFile}
>
<EyeOutlined />
</a>
) : null;
const actions = listType === 'picture-card' && file.status !== 'uploading' && (
<span className={`${prefixCls}-list-item-actions`}>
{previewIcon}
{file.status === 'done' && downloadIcon}
{removeIcon}
</span>
);
let message;
if (file.response && typeof file.response === 'string') {
message = file.response;
} else {
message = (file.error && file.error.statusText) || locale.uploadError;
}
const iconAndPreview = (
<span>
{icon}
{preview}
</span>
);
const dom = (
<div className={infoUploadingClass}>
<div className={`${prefixCls}-list-item-info`}>{iconAndPreview}</div>
{actions}
<Animate transitionName="fade" component="">
{progress}
</Animate>
</div>
);
const listContainerNameClass = classNames({
[`${prefixCls}-list-picture-card-container`]: listType === 'picture-card',
});
return (
<div key={file.uid} className={listContainerNameClass}>
{file.status === 'error' ? (
<Tooltip title={message} getPopupContainer={node => node.parentNode as HTMLElement}>
{dom}
</Tooltip>
) : (
<span>{dom}</span>
)}
</div>
);
});
const listClassNames = classNames({
[`${prefixCls}-list`]: true,
[`${prefixCls}-list-${listType}`]: true,
[`${prefixCls}-list-rtl`]: direction === 'rtl',
});
const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate';
return (
<Animate
transitionName={`${prefixCls}-${animationDirection}`}
component="div"
className={listClassNames}
const downloadOrDelete = listType !== 'picture-card' && (
<span
key="download-delete"
className={`${prefixCls}-list-item-card-actions ${listType === 'picture' ? 'picture' : ''}`}
>
{list}
</Animate>
{downloadIcon}
{removeIcon}
</span>
);
};
const listItemNameClass = classNames({
[`${prefixCls}-list-item-name`]: true,
[`${prefixCls}-list-item-name-icon-count-${
[downloadIcon, removeIcon].filter(x => x).length
}`]: true,
});
const preview = file.url
? [
<a
key="view"
target="_blank"
rel="noopener noreferrer"
className={listItemNameClass}
title={file.name}
{...linkProps}
href={file.url}
onClick={e => handlePreview(file, e)}
>
{file.name}
</a>,
downloadOrDelete,
]
: [
<span
key="view"
className={listItemNameClass}
onClick={e => handlePreview(file, e)}
title={file.name}
>
{file.name}
</span>,
downloadOrDelete,
];
const style: React.CSSProperties = {
pointerEvents: 'none',
opacity: 0.5,
};
const previewIcon = showPreviewIcon ? (
<a
href={file.url || file.thumbUrl}
target="_blank"
rel="noopener noreferrer"
style={file.url || file.thumbUrl ? undefined : style}
onClick={e => handlePreview(file, e)}
title={locale.previewFile}
>
<EyeOutlined />
</a>
) : null;
render() {
return <ConfigConsumer>{this.renderUploadList}</ConfigConsumer>;
}
}
const actions = listType === 'picture-card' && file.status !== 'uploading' && (
<span className={`${prefixCls}-list-item-actions`}>
{previewIcon}
{file.status === 'done' && downloadIcon}
{removeIcon}
</span>
);
let message;
if (file.response && typeof file.response === 'string') {
message = file.response;
} else {
message = (file.error && file.error.statusText) || locale.uploadError;
}
const iconAndPreview = (
<span>
{icon}
{preview}
</span>
);
const dom = (
<div className={infoUploadingClass}>
<div className={`${prefixCls}-list-item-info`}>{iconAndPreview}</div>
{actions}
<Animate transitionName="fade" component="">
{progress}
</Animate>
</div>
);
const listContainerNameClass = classNames({
[`${prefixCls}-list-picture-card-container`]: listType === 'picture-card',
});
return (
<div key={file.uid} className={listContainerNameClass}>
{file.status === 'error' ? (
<Tooltip title={message} getPopupContainer={node => node.parentNode as HTMLElement}>
{dom}
</Tooltip>
) : (
<span>{dom}</span>
)}
</div>
);
});
const listClassNames = classNames({
[`${prefixCls}-list`]: true,
[`${prefixCls}-list-${listType}`]: true,
[`${prefixCls}-list-rtl`]: direction === 'rtl',
});
const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate';
return (
<Animate
transitionName={`${prefixCls}-${animationDirection}`}
component="div"
className={listClassNames}
>
{list}
</Animate>
);
};
const UploadList = React.forwardRef<unknown, UploadListProps>(InternalUploadList);
UploadList.displayName = 'UploadList';
UploadList.defaultProps = {
listType: 'text' as UploadListType, // or picture
progress: {
strokeWidth: 2,
showInfo: false,
},
showRemoveIcon: true,
showDownloadIcon: false,
showPreviewIcon: true,
previewFile: previewImage,
isImageUrl,
};
export default UploadList;

View File

@ -1,6 +1,7 @@
/* eslint-disable react/no-string-refs, react/prefer-es6-class */
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import Upload from '..';
import { setup, teardown } from './mock';
import mountTest from '../../../tests/shared/mountTest';
@ -12,6 +13,7 @@ describe('Upload.Dragger', () => {
afterEach(() => teardown());
it('support drag file with over style', () => {
jest.useFakeTimers();
const wrapper = mount(
<Upload.Dragger action="http://upload.com">
<div />
@ -23,6 +25,14 @@ describe('Upload.Dragger', () => {
files: [{ file: 'foo.png' }],
},
});
act(() => {
jest.runAllTimers();
});
wrapper.update();
expect(wrapper.find('.ant-upload-drag').hasClass('ant-upload-drag-hover')).toBe(true);
jest.useRealTimers();
});
});

View File

@ -55,7 +55,6 @@ describe('Upload', () => {
<button type="button">upload</button>
</Upload>,
);
wrapper.find('input').simulate('change', {
target: {
files: [{ file: 'foo.png' }],
@ -263,10 +262,11 @@ describe('Upload', () => {
url: 'http://www.baidu.com/xxx.png',
},
];
const wrapper = mount(<Upload />);
expect(wrapper.instance().state.fileList).toEqual([]);
const ref = React.createRef();
const wrapper = mount(<Upload ref={ref} />);
expect(ref.current.fileList).toEqual([]);
wrapper.setProps({ fileList });
expect(wrapper.instance().state.fileList).toEqual(fileList);
expect(ref.current.fileList).toEqual(fileList);
});
describe('util', () => {
@ -464,25 +464,13 @@ describe('Upload', () => {
// https://github.com/ant-design/ant-design/issues/14439
it('should allow call abort function through upload instance', () => {
const wrapper = mount(
<Upload>
const ref = React.createRef();
mount(
<Upload ref={ref}>
<button type="button">upload</button>
</Upload>,
);
expect(typeof wrapper.instance().upload.abort).toBe('function');
});
it('unmount', () => {
const wrapper = mount(
<Upload>
<button type="button">upload</button>
</Upload>,
);
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
expect(clearIntervalSpy).not.toHaveBeenCalled();
wrapper.unmount();
expect(clearIntervalSpy).toHaveBeenCalled();
clearIntervalSpy.mockRestore();
expect(typeof ref.current.upload.abort).toBe('function');
});
it('correct dragCls when type is drag', () => {
@ -497,14 +485,34 @@ describe('Upload', () => {
it('return when targetItem is null', () => {
const fileList = [{ uid: 'file' }];
const wrapper = mount(
<Upload type="drag" fileList={fileList}>
const ref = React.createRef();
mount(
<Upload ref={ref} type="drag" fileList={fileList}>
<button type="button">upload</button>
</Upload>,
).instance();
expect(wrapper.onSuccess('', { uid: 'fileItem' })).toBe(undefined);
expect(wrapper.onProgress('', { uid: 'fileItem' })).toBe(undefined);
expect(wrapper.onError('', '', { uid: 'fileItem' })).toBe(undefined);
);
expect(ref.current.onSuccess('', { uid: 'fileItem' })).toBe(undefined);
expect(ref.current.onProgress('', { uid: 'fileItem' })).toBe(undefined);
expect(ref.current.onError('', '', { uid: 'fileItem' })).toBe(undefined);
});
it('should replace file when targetItem already exists', () => {
const fileList = [{ uid: 'file', name: 'file' }];
const ref = React.createRef();
mount(
<Upload ref={ref} defaultFileList={fileList}>
<button type="button">upload</button>
</Upload>,
);
ref.current.onStart({
uid: 'file',
name: 'file1',
});
expect(ref.current.fileList.length).toBe(1);
expect(ref.current.fileList[0].originFileObj).toEqual({
name: 'file1',
uid: 'file',
});
});
it('warning if set `value`', () => {

View File

@ -184,8 +184,10 @@ describe('Upload List', () => {
it('does concat filelist when beforeUpload returns false', () => {
const handleChange = jest.fn();
const ref = React.createRef();
const wrapper = mount(
<Upload
ref={ref}
listType="picture"
defaultFileList={fileList}
onChange={handleChange}
@ -194,14 +196,13 @@ describe('Upload List', () => {
<button type="button">upload</button>
</Upload>,
);
wrapper.find('input').simulate('change', {
target: {
files: [{ name: 'foo.png' }],
},
});
expect(wrapper.state().fileList.length).toBe(fileList.length + 1);
expect(ref.current.fileList.length).toBe(fileList.length + 1);
expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3);
});
@ -334,15 +335,21 @@ describe('Upload List', () => {
};
delete newFile.thumbUrl;
newFileList.push(newFile);
const wrapper = mount(
<Upload listType="picture-card" defaultFileList={newFileList} onPreview={handlePreview}>
const ref = React.createRef();
mount(
<Upload
ref={ref}
listType="picture-card"
defaultFileList={newFileList}
onPreview={handlePreview}
>
<button type="button">upload</button>
</Upload>,
);
wrapper.setState({});
ref.current.forceUpdate();
await sleep();
expect(wrapper.state().fileList[2].thumbUrl).not.toBe(undefined);
expect(ref.current.fileList[2].thumbUrl).not.toBe(undefined);
expect(onDrawImage).toHaveBeenCalled();
// Offset check
@ -580,21 +587,24 @@ describe('Upload List', () => {
});
it('return when prop onPreview not exists', () => {
const wrapper = mount(<UploadList />).instance();
expect(wrapper.handlePreview()).toBe(undefined);
const ref = React.createRef();
mount(<UploadList ref={ref} />);
expect(ref.current.handlePreview()).toBe(undefined);
});
it('return when prop onDownload not exists', () => {
const file = new File([''], 'test.txt', { type: 'text/plain' });
const items = [{ uid: 'upload-list-item', url: '' }];
const wrapper = mount(
const ref = React.createRef();
mount(
<UploadList
ref={ref}
items={items}
locale={{ downloadFile: '' }}
showUploadList={{ showDownloadIcon: true }}
/>,
).instance();
expect(wrapper.handleDownload(file)).toBe(undefined);
);
expect(ref.current.handleDownload(file)).toBe(undefined);
});
it('previewFile should work correctly', async () => {
@ -602,8 +612,8 @@ describe('Upload List', () => {
const items = [{ uid: 'upload-list-item', url: '' }];
const wrapper = mount(
<UploadList listType="picture-card" items={items} locale={{ previewFile: '' }} />,
).instance();
return wrapper.props.previewFile(file);
);
return wrapper.props().previewFile(file);
});
it('downloadFile should work correctly', async () => {
@ -617,8 +627,8 @@ describe('Upload List', () => {
locale={{ downloadFile: '' }}
showUploadList={{ showDownloadIcon: true }}
/>,
).instance();
return wrapper.props.onDownload(file);
);
return wrapper.props().onDownload(file);
});
it('extname should work correctly when url not exists', () => {
@ -683,10 +693,12 @@ describe('Upload List', () => {
const wrapper = mount(
<UploadList listType="picture-card" items={fileList} locale={{ uploading: 'uploading' }} />,
);
const instance = wrapper.instance();
return instance.props.previewFile(mockFile).then(dataUrl => {
expect(dataUrl).toEqual('data:image/png;base64,');
});
return wrapper
.props()
.previewFile(mockFile)
.then(dataUrl => {
expect(dataUrl).toEqual('data:image/png;base64,');
});
});
it("upload non image file shouldn't be converted to the base64", async () => {
@ -697,10 +709,12 @@ describe('Upload List', () => {
const wrapper = mount(
<UploadList listType="picture-card" items={fileList} locale={{ uploading: 'uploading' }} />,
);
const instance = wrapper.instance();
return instance.props.previewFile(mockFile).then(dataUrl => {
expect(dataUrl).toBe('');
});
return wrapper
.props()
.previewFile(mockFile)
.then(dataUrl => {
expect(dataUrl).toBe('');
});
});
describe('customize previewFile support', () => {
@ -715,22 +729,20 @@ describe('Upload List', () => {
originFileObj: renderInstance(),
};
delete file.thumbUrl;
const ref = React.createRef();
const wrapper = mount(
<Upload listType="picture" defaultFileList={[file]} previewFile={previewFile}>
<Upload ref={ref} listType="picture" defaultFileList={[file]} previewFile={previewFile}>
<button type="button">button</button>
</Upload>,
);
wrapper.setState({});
await sleep();
ref.current.forceUpdate();
expect(previewFile).toHaveBeenCalledWith(file.originFileObj);
await sleep(100);
wrapper.update();
expect(wrapper.find('.ant-upload-list-item-thumbnail img').prop('src')).toBe(mockThumbnail);
});
}
test('File', () => new File([], 'xxx.png'));
test('Blob', () => new Blob());
});

View File

@ -0,0 +1,6 @@
import * as React from 'react';
export default function useForceUpdate() {
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
return forceUpdate;
}

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import useForceUpdate from './useForceUpdate';
type UseSyncStateProps<T> = [() => T, (newValue: T) => void];
export default function useSyncState<T>(initialValue: T): UseSyncStateProps<T> {
const ref = React.useRef<T>(initialValue);
const forceUpdate = useForceUpdate();
return [
() => ref.current,
(newValue: T) => {
ref.current = newValue;
// re-render
forceUpdate();
},
];
}