From 388edca10bf18aaa6021cfb28dea3ea03dbc9df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 21 Nov 2020 19:00:11 +0800 Subject: [PATCH] refactor: UploadList use `rc-motion` instead of `rc-animate` (#27923) * chore: Update rc-motion version * refactor: Move item into single file * refactor: Use CSSMotion of progress bar * chore: part style it * chore: slit style of pic card * chore: RM count-x * support appendAction back * chore: Style smooth * fix progress makes shaking * docs: clean up demo * test: Update snapshot * test: fix test case * fix: lint * test: Update snapshot * test: coverage * clean up --- components/_util/reactNode.ts | 5 +- .../__snapshots__/components.test.js.snap | 612 ++-- components/style/themes/default.less | 2 +- components/upload/UploadList.tsx | 373 --- components/upload/UploadList/ListItem.tsx | 273 ++ components/upload/UploadList/index.tsx | 262 ++ .../__tests__/__snapshots__/demo.test.js.snap | 2668 ++++++++--------- .../__snapshots__/uploadlist.test.js.snap | 1990 ++++++------ .../upload/__tests__/uploadlist.test.js | 21 +- components/upload/interface.tsx | 18 +- components/upload/style/index.less | 151 +- package.json | 6 +- 12 files changed, 3274 insertions(+), 3107 deletions(-) delete mode 100644 components/upload/UploadList.tsx create mode 100644 components/upload/UploadList/ListItem.tsx create mode 100644 components/upload/UploadList/index.tsx diff --git a/components/_util/reactNode.ts b/components/_util/reactNode.ts index 872948d94f..ccdc8015be 100644 --- a/components/_util/reactNode.ts +++ b/components/_util/reactNode.ts @@ -13,7 +13,10 @@ export function replaceElement( ): React.ReactNode { if (!isValidElement(element)) return replacement; - return React.cloneElement(element, typeof props === 'function' ? props(element.props) : props); + return React.cloneElement( + element, + typeof props === 'function' ? props(element.props || {}) : props, + ); } export function cloneElement(element: React.ReactNode, props?: RenderProps): React.ReactElement { diff --git a/components/config-provider/__tests__/__snapshots__/components.test.js.snap b/components/config-provider/__tests__/__snapshots__/components.test.js.snap index d81b973e67..17254c77f0 100644 --- a/components/config-provider/__tests__/__snapshots__/components.test.js.snap +++ b/components/config-provider/__tests__/__snapshots__/components.test.js.snap @@ -37674,27 +37674,63 @@ exports[`ConfigProvider components Upload configProvider 1`] = ` class="config-upload-list config-upload-list-text" >
- +
-
- -
+ + + +
+ + xxx.png + + +
- - xxx.png - - - - + -
+
-
+
@@ -37776,27 +37776,63 @@ exports[`ConfigProvider components Upload configProvider componentSize large 1`] class="config-upload-list config-upload-list-text" >
- +
-
- -
+ + + +
+ + xxx.png + + +
- - xxx.png - - - - + -
+
-
+
@@ -37878,27 +37878,63 @@ exports[`ConfigProvider components Upload configProvider componentSize middle 1` class="config-upload-list config-upload-list-text" >
- +
-
- -
+ + + +
+ + xxx.png + + +
- - xxx.png - - - - + -
+
-
+
@@ -37980,27 +37980,63 @@ exports[`ConfigProvider components Upload configProvider virtual and dropdownMat class="ant-upload-list ant-upload-list-text" >
- +
-
- -
+ + + +
+ + xxx.png + + +
- - xxx.png - - - - + -
+
-
+
@@ -38082,27 +38082,63 @@ exports[`ConfigProvider components Upload normal 1`] = ` class="ant-upload-list ant-upload-list-text" >
- +
-
- -
+ + + +
+ + xxx.png + + +
- - xxx.png - - - - + -
+
-
+
@@ -38184,27 +38184,63 @@ exports[`ConfigProvider components Upload prefixCls 1`] = ` class="ant-upload-list ant-upload-list-text" >
- +
-
- -
+ + + +
+ + xxx.png + + +
- - xxx.png - - - - + -
+
-
+
diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 0e1c70938e..c5ffe20700 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -860,7 +860,7 @@ // Skeleton // --- -@skeleton-color: rgba(190, 190, 190, .2); +@skeleton-color: rgba(190, 190, 190, 0.2); @skeleton-to-color: shade(@skeleton-color, 5%); @skeleton-paragraph-margin-top: 28px; @skeleton-paragraph-li-margin-top: @margin-md; diff --git a/components/upload/UploadList.tsx b/components/upload/UploadList.tsx deleted file mode 100644 index ed6357eadd..0000000000 --- a/components/upload/UploadList.tsx +++ /dev/null @@ -1,373 +0,0 @@ -import * as React from 'react'; -import Animate from 'rc-animate'; -import classNames from 'classnames'; -import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; -import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined'; -import PictureTwoTone from '@ant-design/icons/PictureTwoTone'; -import FileTwoTone from '@ant-design/icons/FileTwoTone'; -import EyeOutlined from '@ant-design/icons/EyeOutlined'; -import DeleteOutlined from '@ant-design/icons/DeleteOutlined'; -import DownloadOutlined from '@ant-design/icons/DownloadOutlined'; - -import { cloneElement, isValidElement } from '../_util/reactNode'; -import { UploadListProps, UploadFile, UploadListType } from './interface'; -import { previewImage, isImageUrl } from './utils'; -import Tooltip from '../tooltip'; -import Progress from '../progress'; -import { ConfigContext } from '../config-provider'; -import Button, { ButtonProps } from '../button'; -import useForceUpdate from '../_util/hooks/useForceUpdate'; - -const InternalUploadList: React.ForwardRefRenderFunction = ( - { - listType, - previewFile, - onPreview, - onDownload, - onRemove, - locale, - iconRender, - isImageUrl: isImgUrl, - prefixCls: customizePrefixCls, - items = [], - showPreviewIcon, - showRemoveIcon, - showDownloadIcon, - removeIcon: customRemoveIcon, - downloadIcon: customDownloadIcon, - progress: progressProps, - appendAction, - itemRender, - }, - ref, -) => { - const forceUpdate = useForceUpdate(); - - React.useEffect(() => { - if (listType !== 'picture' && listType !== 'picture-card') { - return; - } - (items || []).forEach(file => { - if ( - typeof document === 'undefined' || - typeof window === 'undefined' || - !(window as any).FileReader || - !(window as any).File || - !(file.originFileObj instanceof File || file.originFileObj instanceof Blob) || - file.thumbUrl !== undefined - ) { - return; - } - file.thumbUrl = ''; - if (previewFile) { - previewFile(file.originFileObj as File).then((previewDataUrl: string) => { - // Need append '' to avoid dead loop - file.thumbUrl = previewDataUrl || ''; - forceUpdate(); - }); - } - }); - }, [listType, items, previewFile]); - - const handlePreview = (file: UploadFile, e: React.SyntheticEvent) => { - if (!onPreview) { - return; - } - e.preventDefault(); - return onPreview(file); - }; - - const handleDownload = (file: UploadFile) => { - if (typeof onDownload === 'function') { - onDownload(file); - } else if (file.url) { - window.open(file.url); - } - }; - - const handleClose = (file: UploadFile) => { - if (onRemove) { - onRemove(file); - } - }; - - const handleIconRender = (file: UploadFile) => { - if (iconRender) { - return iconRender(file, listType); - } - const isLoading = file.status === 'uploading'; - const fileIcon = isImgUrl && isImgUrl(file) ? : ; - let icon: React.ReactNode = isLoading ? : ; - if (listType === 'picture') { - icon = isLoading ? : fileIcon; - } else if (listType === 'picture-card') { - icon = isLoading ? locale.uploading : fileIcon; - } - return icon; - }; - - const handleActionIconRender = ( - customIcon: React.ReactNode, - callback: () => void, - prefixCls: string, - title?: string, - ) => { - const btnProps: ButtonProps = { - type: 'text', - size: 'small', - title, - onClick: (e: React.MouseEvent) => { - callback(); - if (isValidElement(customIcon) && customIcon.props.onClick) { - customIcon.props.onClick(e); - } - }, - className: `${prefixCls}-list-item-card-actions-btn`, - }; - if (isValidElement(customIcon)) { - const btnIcon = cloneElement(customIcon, { - ...customIcon.props, - onClick: () => {}, - }); - - return - ); - }; - - // 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 =
{iconNode}
; - 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 =
{iconNode}
; - } else { - const thumbnail = - isImgUrl && isImgUrl(file) ? ( - {file.name} - ) : ( - iconNode - ); - const aClassName = classNames({ - [`${prefixCls}-list-item-thumbnail`]: true, - [`${prefixCls}-list-item-file`]: isImgUrl && !isImgUrl(file), - }); - icon = ( - handlePreview(file, e)} - href={file.url || file.thumbUrl} - target="_blank" - rel="noopener noreferrer" - > - {thumbnail} - - ); - } - } - - if (file.status === 'uploading') { - // show loading icon if upload progress listener is disabled - const loadingProgress = - 'percent' in file ? ( - - ) : null; - - progress = ( -
- {loadingProgress} -
- ); - } - 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( - (typeof customRemoveIcon === 'function' ? customRemoveIcon(file) : customRemoveIcon) || ( - - ), - () => handleClose(file), - prefixCls, - locale.removeFile, - ) - : null; - - const downloadIcon = - showDownloadIcon && file.status === 'done' - ? handleActionIconRender( - (typeof customDownloadIcon === 'function' - ? customDownloadIcon(file) - : customDownloadIcon) || , - () => handleDownload(file), - prefixCls, - locale.downloadFile, - ) - : null; - const downloadOrDelete = listType !== 'picture-card' && ( - - {downloadIcon} - {removeIcon} - - ); - 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 - ? [ - handlePreview(file, e)} - > - {file.name} - , - downloadOrDelete, - ] - : [ - handlePreview(file, e)} - title={file.name} - > - {file.name} - , - downloadOrDelete, - ]; - const style: React.CSSProperties = { - pointerEvents: 'none', - opacity: 0.5, - }; - const previewIcon = showPreviewIcon ? ( - handlePreview(file, e)} - title={locale.previewFile} - > - - - ) : null; - - const actions = listType === 'picture-card' && file.status !== 'uploading' && ( - - {previewIcon} - {file.status === 'done' && downloadIcon} - {removeIcon} - - ); - - let message; - if (file.response && typeof file.response === 'string') { - message = file.response; - } else { - message = (file.error && file.error.statusText) || locale.uploadError; - } - const iconAndPreview = ( - - {icon} - {preview} - - ); - const dom = ( -
-
{iconAndPreview}
- {actions} - - {progress} - -
- ); - const listContainerNameClass = classNames({ - [`${prefixCls}-list-picture-card-container`]: listType === 'picture-card', - }); - const item = - file.status === 'error' ? ( - node.parentNode as HTMLElement}> - {dom} - - ) : ( - {dom} - ); - return ( -
- {itemRender ? itemRender(item, file, items) : item} -
- ); - }); - const listClassNames = classNames({ - [`${prefixCls}-list`]: true, - [`${prefixCls}-list-${listType}`]: true, - [`${prefixCls}-list-rtl`]: direction === 'rtl', - }); - const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate'; - const transitionName = list.length === 0 ? '' : `${prefixCls}-${animationDirection}`; - return ( - - {list} - {React.isValidElement(appendAction) - ? React.cloneElement(appendAction, { key: 'appendAction' }) - : appendAction} - - ); -}; - -const UploadList = React.forwardRef(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; diff --git a/components/upload/UploadList/ListItem.tsx b/components/upload/UploadList/ListItem.tsx new file mode 100644 index 0000000000..4956fb58c1 --- /dev/null +++ b/components/upload/UploadList/ListItem.tsx @@ -0,0 +1,273 @@ +import * as React from 'react'; +import CSSMotion from 'rc-motion'; +import classNames from 'classnames'; +import EyeOutlined from '@ant-design/icons/EyeOutlined'; +import DeleteOutlined from '@ant-design/icons/DeleteOutlined'; +import DownloadOutlined from '@ant-design/icons/DownloadOutlined'; +import Tooltip from '../../tooltip'; +import Progress from '../../progress'; +import { + ItemRender, + UploadFile, + UploadListProgressProps, + UploadListType, + UploadLocale, +} from '../interface'; + +export interface ListItemProps { + prefixCls: string; + className?: string; + style?: React.CSSProperties; + locale: UploadLocale; + file: UploadFile; + items: UploadFile[]; + listType?: UploadListType; + isImgUrl?: (file: UploadFile) => boolean; + showRemoveIcon?: boolean; + showDownloadIcon?: boolean; + showPreviewIcon?: boolean; + removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode); + downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode); + iconRender: (file: UploadFile) => React.ReactNode; + actionIconRender: ( + customIcon: React.ReactNode, + callback: () => void, + prefixCls: string, + title?: string | undefined, + ) => React.ReactNode; + itemRender?: ItemRender; + onPreview: (file: UploadFile, e: React.SyntheticEvent) => void; + onClose: (file: UploadFile) => void; + onDownload: (file: UploadFile) => void; + progress?: UploadListProgressProps; +} + +const ListItem = React.forwardRef( + ( + { + prefixCls, + className, + style, + locale, + listType, + file, + items, + progress: progressProps, + iconRender, + actionIconRender, + itemRender, + isImgUrl, + showPreviewIcon, + showRemoveIcon, + showDownloadIcon, + removeIcon: customRemoveIcon, + downloadIcon: customDownloadIcon, + onPreview, + onDownload, + onClose, + }: ListItemProps, + ref: React.Ref, + ) => { + // Delay to show the progress bar + const [showProgress, setShowProgress] = React.useState(false); + const progressRafRef = React.useRef(); + React.useEffect(() => { + progressRafRef.current = setTimeout(() => { + setShowProgress(true); + }, 300); + + return () => { + window.clearTimeout(progressRafRef.current); + }; + }, []); + + // This is used for legacy span make scrollHeight the wrong value. + // We will force these to be `display: block` with non `picture-card` + const spanClassName = `${prefixCls}-span`; + + const iconNode = iconRender(file); + let icon =
{iconNode}
; + 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 =
{iconNode}
; + } else { + const thumbnail = isImgUrl?.(file) ? ( + {file.name} + ) : ( + iconNode + ); + const aClassName = classNames({ + [`${prefixCls}-list-item-thumbnail`]: true, + [`${prefixCls}-list-item-file`]: isImgUrl && !isImgUrl(file), + }); + icon = ( + onPreview(file, e)} + href={file.url || file.thumbUrl} + target="_blank" + rel="noopener noreferrer" + > + {thumbnail} + + ); + } + } + + 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 + ? actionIconRender( + (typeof customRemoveIcon === 'function' ? customRemoveIcon(file) : customRemoveIcon) || ( + + ), + () => onClose(file), + prefixCls, + locale.removeFile, + ) + : null; + + const downloadIcon = + showDownloadIcon && file.status === 'done' + ? actionIconRender( + (typeof customDownloadIcon === 'function' + ? customDownloadIcon(file) + : customDownloadIcon) || , + () => onDownload(file), + prefixCls, + locale.downloadFile, + ) + : null; + const downloadOrDelete = listType !== 'picture-card' && ( + + {downloadIcon} + {removeIcon} + + ); + const listItemNameClass = classNames(`${prefixCls}-list-item-name`); + const preview = file.url + ? [ + onPreview(file, e)} + > + {file.name} + , + downloadOrDelete, + ] + : [ + onPreview(file, e)} + title={file.name} + > + {file.name} + , + downloadOrDelete, + ]; + const previewStyle: React.CSSProperties = { + pointerEvents: 'none', + opacity: 0.5, + }; + const previewIcon = showPreviewIcon ? ( + onPreview(file, e)} + title={locale.previewFile} + > + + + ) : null; + + const actions = listType === 'picture-card' && file.status !== 'uploading' && ( + + {previewIcon} + {file.status === 'done' && downloadIcon} + {removeIcon} + + ); + + let message; + if (file.response && typeof file.response === 'string') { + message = file.response; + } else { + message = (file.error && file.error.statusText) || locale.uploadError; + } + const iconAndPreview = ( + + {icon} + {preview} + + ); + + const dom = ( +
+
{iconAndPreview}
+ {actions} + {showProgress && ( + + {({ className: motionClassName }) => { + // show loading icon if upload progress listener is disabled + const loadingProgress = + 'percent' in file ? ( + + ) : null; + + return ( +
+ {loadingProgress} +
+ ); + }} +
+ )} +
+ ); + const listContainerNameClass = classNames(`${prefixCls}-list-${listType}-container`, className); + const item = + file.status === 'error' ? ( + node.parentNode as HTMLElement}> + {dom} + + ) : ( + dom + ); + + return ( +
+ {itemRender ? itemRender(item, file, items) : item} +
+ ); + }, +); + +export default ListItem; diff --git a/components/upload/UploadList/index.tsx b/components/upload/UploadList/index.tsx new file mode 100644 index 0000000000..f639be23d0 --- /dev/null +++ b/components/upload/UploadList/index.tsx @@ -0,0 +1,262 @@ +import * as React from 'react'; +import CSSMotion, { CSSMotionList, CSSMotionListProps } from 'rc-motion'; +import classNames from 'classnames'; +import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; +import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined'; +import PictureTwoTone from '@ant-design/icons/PictureTwoTone'; +import FileTwoTone from '@ant-design/icons/FileTwoTone'; +import { cloneElement, isValidElement } from '../../_util/reactNode'; +import { UploadListProps, UploadFile, UploadListType } from '../interface'; +import { previewImage, isImageUrl } from '../utils'; +import collapseMotion from '../../_util/motion'; +import { ConfigContext } from '../../config-provider'; +import Button, { ButtonProps } from '../../button'; +import useForceUpdate from '../../_util/hooks/useForceUpdate'; +import ListItem from './ListItem'; + +const listItemMotion: Partial = { + ...collapseMotion, +}; + +delete listItemMotion.onAppearEnd; +delete listItemMotion.onEnterEnd; +delete listItemMotion.onLeaveEnd; + +const InternalUploadList: React.ForwardRefRenderFunction = ( + { + listType, + previewFile, + onPreview, + onDownload, + onRemove, + locale, + iconRender, + isImageUrl: isImgUrl, + prefixCls: customizePrefixCls, + items = [], + showPreviewIcon, + showRemoveIcon, + showDownloadIcon, + removeIcon, + downloadIcon, + progress, + appendAction, + itemRender, + }, + ref, +) => { + const forceUpdate = useForceUpdate(); + const [motionAppear, setMotionAppear] = React.useState(false); + + // ============================= Effect ============================= + React.useEffect(() => { + if (listType !== 'picture' && listType !== 'picture-card') { + return; + } + (items || []).forEach(file => { + if ( + typeof document === 'undefined' || + typeof window === 'undefined' || + !(window as any).FileReader || + !(window as any).File || + !(file.originFileObj instanceof File || file.originFileObj instanceof Blob) || + file.thumbUrl !== undefined + ) { + return; + } + file.thumbUrl = ''; + if (previewFile) { + previewFile(file.originFileObj as File).then((previewDataUrl: string) => { + // Need append '' to avoid dead loop + file.thumbUrl = previewDataUrl || ''; + forceUpdate(); + }); + } + }); + }, [listType, items, previewFile]); + + React.useEffect(() => { + setMotionAppear(true); + }, []); + + // ============================= Events ============================= + const onInternalPreview = (file: UploadFile, e: React.SyntheticEvent) => { + if (!onPreview) { + return; + } + e.preventDefault(); + return onPreview(file); + }; + + const onInternalDownload = (file: UploadFile) => { + if (typeof onDownload === 'function') { + onDownload(file); + } else if (file.url) { + window.open(file.url); + } + }; + + const onInternalClose = (file: UploadFile) => { + if (onRemove) { + onRemove(file); + } + }; + + const internalIconRender = (file: UploadFile) => { + if (iconRender) { + return iconRender(file, listType); + } + const isLoading = file.status === 'uploading'; + const fileIcon = isImgUrl && isImgUrl(file) ? : ; + let icon: React.ReactNode = isLoading ? : ; + if (listType === 'picture') { + icon = isLoading ? : fileIcon; + } else if (listType === 'picture-card') { + icon = isLoading ? locale.uploading : fileIcon; + } + return icon; + }; + + const actionIconRender = ( + customIcon: React.ReactNode, + callback: () => void, + prefixCls: string, + title?: string, + ) => { + const btnProps: ButtonProps = { + type: 'text', + size: 'small', + title, + onClick: (e: React.MouseEvent) => { + callback(); + if (isValidElement(customIcon) && customIcon.props.onClick) { + customIcon.props.onClick(e); + } + }, + className: `${prefixCls}-list-item-card-actions-btn`, + }; + if (isValidElement(customIcon)) { + const btnIcon = cloneElement(customIcon, { + ...customIcon.props, + onClick: () => {}, + }); + + return + ); + }; + + // ============================== Ref =============================== + // Test needs + React.useImperativeHandle(ref, () => ({ + handlePreview: onInternalPreview, + handleDownload: onInternalDownload, + })); + + const { getPrefixCls, direction } = React.useContext(ConfigContext); + + // ============================= Render ============================= + const prefixCls = getPrefixCls('upload', customizePrefixCls); + + const listClassNames = classNames({ + [`${prefixCls}-list`]: true, + [`${prefixCls}-list-${listType}`]: true, + [`${prefixCls}-list-rtl`]: direction === 'rtl', + }); + + // >>> Motion config + const motionKeyList = [ + ...items.map(file => ({ + key: file.uid, + file, + })), + ]; + + const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate'; + // const transitionName = list.length === 0 ? '' : `${prefixCls}-${animationDirection}`; + + let motionConfig: Omit = { + motionName: `${prefixCls}-${animationDirection}`, + keys: motionKeyList, + motionAppear, + }; + + if (listType !== 'picture-card') { + motionConfig = { + ...listItemMotion, + ...motionConfig, + }; + } + + return ( +
+ + {({ key, file, className: motionClassName, style: motionStyle }) => { + return ( + + ); + }} + + + {/* Append action */} + {appendAction && ( + + {({ className: motionClassName, style: motionStyle }) => { + return cloneElement(appendAction, oriProps => ({ + className: classNames(oriProps.className, motionClassName), + style: { + ...motionStyle, + ...oriProps.style, + }, + })); + }} + + )} +
+ ); +}; + +const UploadList = React.forwardRef(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; diff --git a/components/upload/__tests__/__snapshots__/demo.test.js.snap b/components/upload/__tests__/__snapshots__/demo.test.js.snap index c7fd82060c..a1bbfa5f6e 100644 --- a/components/upload/__tests__/__snapshots__/demo.test.js.snap +++ b/components/upload/__tests__/__snapshots__/demo.test.js.snap @@ -114,95 +114,95 @@ exports[`renders ./components/upload/demo/crop-image.md correctly 1`] = `
- +
- - - - + image.png - + image.png +
- + + + + + + + + +
- -
-
- -
- - - -
- - xxx.png - - - - -
-
-
-
-
-
- -
-
- -
- - - -
- - yyy.png - - - - -
-
-
-
-
-
- +
@@ -515,7 +361,163 @@ exports[`renders ./components/upload/demo/defaultFileList.md correctly 1`] = `
+ xxx.png + + + + +
+
+
+
+
+
+
+ +
+ + + +
+ + yyy.png + + + + +
+
+
+
+
+
+ - - image1.png - - - - + -
+
- +
- +
-
- -
+ + + +
+ + image2.png + + +
- - image2.png - - - - + -
+
-
+
- +
-
- -
+ + + +
+ + image3.png + + +
- - image3.png - - - - + -
+
-
+
- +
-
- -
+ + + +
+ + image4.png + + +
- - image4.png - - - - + -
+
-
+
- +
@@ -1095,13 +1099,13 @@ exports[`renders ./components/upload/demo/drag-sorting.md correctly 1`] = `
image.png + pdf.pdf +
- + + + + + + + + +
- +
- - + doc.doc +
- + + + + + + + + +
- +
@@ -1437,7 +1443,7 @@ exports[`renders ./components/upload/demo/file-type.md correctly 1`] = `
image.png @@ -1511,7 +1517,9 @@ exports[`renders ./components/upload/demo/file-type.md correctly 1`] = `
- +
@@ -1553,7 +1561,7 @@ exports[`renders ./components/upload/demo/file-type.md correctly 1`] = `
pdf.pdf @@ -1627,7 +1635,9 @@ exports[`renders ./components/upload/demo/file-type.md correctly 1`] = `
- +
@@ -1661,7 +1671,7 @@ exports[`renders ./components/upload/demo/file-type.md correctly 1`] = `
doc.doc @@ -1826,27 +1836,66 @@ exports[`renders ./components/upload/demo/fileList.md correctly 1`] = ` class="ant-upload-list ant-upload-list-text" >
- +
-
- -
+ + + +
+ + xxx.png + + +
- - xxx.png - - - - + -
+
-
+
@@ -1917,424 +1927,404 @@ exports[`renders ./components/upload/demo/picture-card.md correctly 1`] = `
- +
- - - - + image.png - + image.png +
- + + + + + + + + +
- +
- - - - + image.png - + image.png +
- + + + + + + + + +
- +
- - - - + image.png - + image.png +
- + + + + + + + + +
- +
- - - - + image.png - + image.png +
- + + + + + + + + +
- +
-
- -
- Uploading... -
- - image.png - -
-
-
-
-
-
-
-
+ Uploading...
-
+ + image.png + +
- +
- +
@@ -2387,7 +2379,7 @@ exports[`renders ./components/upload/demo/picture-card.md correctly 1`] = `
image.png @@ -2552,74 +2544,74 @@ Array [ class="ant-upload-list ant-upload-list-picture" >
- +
-
- - + xxx.png + + + xxx.png + + + - + + + + -
+
- +
- +
@@ -2669,7 +2663,7 @@ Array [
yyy.png @@ -2761,74 +2755,74 @@ Array [ class="ant-upload-list ant-upload-list-picture" >
- +
-
- - + xxx.png + + + xxx.png + + + - + + + + -
+
- +
- +
@@ -2878,7 +2874,7 @@ Array [
yyy.png @@ -3076,189 +3072,17 @@ exports[`renders ./components/upload/demo/upload-custom-action-icon.md correctly class="ant-upload-list ant-upload-list-text" >
- -
-
- -
- - - -
- - xxx.png - - - - - -
-
-
-
-
-
- -
-
- -
- - - -
- - yyy.png - - - - - -
-
-
-
-
-
- +
@@ -3283,7 +3107,181 @@ exports[`renders ./components/upload/demo/upload-custom-action-icon.md correctly
+ xxx.png + + + + + +
+
+
+
+
+
+
+ +
+ + + +
+ + yyy.png + + + + + +
+
+
+
+
@@ -283,27 +267,64 @@ exports[`Upload List should be uploading when upload a file 2`] = ` class="ant-upload-list ant-upload-list-text" >
- +
-
- -
+ + + +
+ + foo.png + + +
- - foo.png - - - - + -
-
-
-
-
-
-
-
-
-
+
- +
@@ -409,30 +374,74 @@ exports[`Upload List should non-image format file preview 1`] = ` class="ant-upload-list ant-upload-list-picture" >
- +
-
- - - image - - - image - - - - - -
-
- -
-
- -
-
- - + image + + + image + + + - + -
+
-
+
- +
-
- - + + + + + + not-image + + + - + -
+
- +
- +
- +
-
- - + image + + + image + + + - + + + + -
+
- +
- +
-
- - + image + + + image + + + - + + + + -
+
- +
- +
-
- - + image + + + image + + + - + + + + -
+
- +
- +
-
- - + image + + + image + + + - + + + + -
+
- +
- +
-
- - + image + + + image + + + - + + + + -
+
- +
+
+
+
+
+ + + image + + + image + + + + + +
+
@@ -1167,127 +1132,124 @@ exports[`Upload List should support removeIcon and downloadIcon 1`] = ` class="ant-upload-list ant-upload-list-picture" >
- +
-
- -
- - - -
- - image - +
- + + +
+ + image + + + -
-
+
- +
- +
-
- - + image + + + image + + + + - - + + RM + + -
+
- +
@@ -1321,127 +1283,124 @@ exports[`Upload List should support removeIcon and downloadIcon 2`] = ` class="ant-upload-list ant-upload-list-picture" >
- +
-
- -
- - - -
- - image - +
- + + +
+ + image + + + -
-
+
- +
- +
-
- - + image + + + image + + + + - - + + RM + + -
+
- +
@@ -1475,98 +1434,95 @@ exports[`Upload List should support showRemoveIcon and showPreviewIcon 1`] = ` class="ant-upload-list ant-upload-list-picture" >
- +
-
- -
- - - -
- - image - +
- -
-
+ aria-label="loading" + class="anticon anticon-loading anticon-spin" + role="img" + > + + +
+ + image + + +
- +
- +
-
- - - image - - - image - - + image - -
+ + + image + + +
- +
diff --git a/components/upload/__tests__/uploadlist.test.js b/components/upload/__tests__/uploadlist.test.js index b9a5516afe..7b066dfa58 100644 --- a/components/upload/__tests__/uploadlist.test.js +++ b/components/upload/__tests__/uploadlist.test.js @@ -131,9 +131,20 @@ describe('Upload List', () => { ); expect(wrapper.find('.ant-upload-list-item').length).toBe(2); wrapper.find('.ant-upload-list-item').at(0).find('.anticon-delete').simulate('click'); - await sleep(1000); - wrapper.update(); - expect(wrapper.find('.ant-upload-list-item').hostNodes().length).toBe(1); + + await act(async () => { + await sleep(1000); + wrapper.update(); + + const domNode = wrapper.find('.ant-upload-list-text-container').at(0).hostNodes().instance(); + const transitionEndEvent = new Event('transitionend'); + domNode.dispatchEvent(transitionEndEvent); + wrapper.update(); + }); + + // console.log(wrapper.html()); + + expect(wrapper.find('.ant-upload-list-text-container').hostNodes().length).toBe(1); }); it('should be uploading when upload a file', done => { @@ -222,6 +233,10 @@ describe('Upload List', () => { , ); + + // Has error item className + wrapper.find('.ant-upload-list-item-error').simulate('mouseenter'); + expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0); }); diff --git a/components/upload/interface.tsx b/components/upload/interface.tsx index 28464a763f..4debbfbd6a 100755 --- a/components/upload/interface.tsx +++ b/components/upload/interface.tsx @@ -72,6 +72,12 @@ export type UploadType = 'drag' | 'select'; export type UploadListType = 'text' | 'picture' | 'picture-card'; export type UploadListProgressProps = Omit; +export type ItemRender = ( + originNode: React.ReactElement, + file: UploadFile, + fileList?: Array>, +) => React.ReactNode; + type PreviewFileHandler = (file: File | Blob) => PromiseLike; type TransformFileHandler = ( file: RcFile, @@ -111,11 +117,7 @@ export interface UploadProps { iconRender?: (file: UploadFile, listType?: UploadListType) => React.ReactNode; isImageUrl?: (file: UploadFile) => boolean; progress?: UploadListProgressProps; - itemRender?: ( - originNode: React.ReactElement, - file: UploadFile, - fileList?: Array>, - ) => React.ReactNode; + itemRender?: ItemRender; } export interface UploadState { @@ -141,9 +143,5 @@ export interface UploadListProps { iconRender?: (file: UploadFile, listType?: UploadListType) => React.ReactNode; isImageUrl?: (file: UploadFile) => boolean; appendAction?: React.ReactNode; - itemRender?: ( - originNode: React.ReactElement, - file: UploadFile, - fileList?: Array>, - ) => React.ReactNode; + itemRender?: ItemRender; } diff --git a/components/upload/style/index.less b/components/upload/style/index.less index b18613e1b5..b511edb0c0 100644 --- a/components/upload/style/index.less +++ b/components/upload/style/index.less @@ -142,16 +142,8 @@ .reset-component; .clearfix; line-height: @line-height-base; - &-item-list-type-text { - &:hover { - .@{upload-prefix-cls}-list-item-name-icon-count-1 { - padding-right: 14px; - } - .@{upload-prefix-cls}-list-item-name-icon-count-2 { - padding-right: 28px; - } - } - } + + // ============================ Item ============================ &-item { position: relative; height: @line-height-base * @font-size-base; @@ -167,10 +159,6 @@ text-overflow: ellipsis; } - &-name-icon-count-1 { - padding-right: 14px; - } - &-card-actions { position: absolute; right: 0; @@ -200,7 +188,7 @@ &-info { height: 100%; - padding: 0 12px 0 4px; + padding: 0 4px; transition: background-color 0.3s; > span { @@ -273,6 +261,7 @@ } } + // =================== Picture & Picture Card =================== &-picture, &-picture-card { .@{upload-item} { @@ -304,9 +293,6 @@ } .@{upload-item}-thumbnail { - position: absolute; - top: 8px; - left: 8px; width: 48px; height: 48px; line-height: 54px; @@ -370,14 +356,6 @@ transition: all 0.3s; } - .@{upload-item}-name-icon-count-1 { - padding-right: 18px; - } - - .@{upload-item}-name-icon-count-2 { - padding-right: 36px; - } - .@{upload-item}-uploading .@{upload-item}-name { line-height: 28px; } @@ -398,11 +376,8 @@ } } + // ======================== Picture Card ======================== &-picture-card { - &.@{upload-prefix-cls}-list::after { - display: none; - } - &-container { display: inline-block; width: @upload-picture-card-size; @@ -411,6 +386,10 @@ vertical-align: top; } + &.@{upload-prefix-cls}-list::after { + display: none; + } + .@{upload-item} { height: 100%; margin: 0; @@ -492,6 +471,12 @@ display: block; } + // .@{upload-item}-thumbnail { + // position: absolute; + // top: 8px; + // left: 8px; + // } + .@{upload-item}-uploading { &.@{upload-item} { background-color: @background-color-light; @@ -515,27 +500,77 @@ } } - .@{upload-prefix-cls}-success-icon { - color: @success-color; - font-weight: bold; + // ======================= Picture & Text ======================= + &-text, + &-picture { + &-container { + transition: opacity @animation-duration-slow, height @animation-duration-slow; + + &::before { + display: table; + width: 0; + height: 0; + content: ''; + } + + // Don't know why span here, just stretch it + .@{upload-prefix-cls}-span { + display: block; + flex: auto; + } + } + + // text & picture no need this additional element. + // But it used for picture-card, let's keep it. + .@{upload-prefix-cls}-span { + display: flex; + align-items: center; + + > * { + flex: none; + } + } + + .@{upload-item}-name { + flex: auto; + padding: 0 @padding-xs; + } + + .@{upload-item}-card-actions { + position: static; + } } - .@{upload-prefix-cls}-animate-enter, - .@{upload-prefix-cls}-animate-leave, + // ============================ Text ============================ + &-text { + .@{upload-prefix-cls}-text-icon { + .@{iconfont-css-prefix} { + position: static; + } + } + } + + // =========================== Motion =========================== + // .@{upload-prefix-cls}-animate-appear, + // .@{upload-prefix-cls}-animate-enter, + // .@{upload-prefix-cls}-animate-leave, + .@{upload-prefix-cls}-animate-inline-appear, .@{upload-prefix-cls}-animate-inline-enter, .@{upload-prefix-cls}-animate-inline-leave { - animation-duration: 0.3s; + animation-duration: @animation-duration-slow; animation-fill-mode: @ease-in-out-circ; } - .@{upload-prefix-cls}-animate-enter { - animation-name: uploadAnimateIn; - } + // .@{upload-prefix-cls}-animate-appear, + // .@{upload-prefix-cls}-animate-enter { + // animation-name: uploadAnimateIn; + // } - .@{upload-prefix-cls}-animate-leave { - animation-name: uploadAnimateOut; - } + // .@{upload-prefix-cls}-animate-leave { + // animation-name: uploadAnimateOut; + // } + .@{upload-prefix-cls}-animate-inline-appear, .@{upload-prefix-cls}-animate-inline-enter { animation-name: uploadAnimateInlineIn; } @@ -545,23 +580,23 @@ } } -@keyframes uploadAnimateIn { - from { - height: 0; - margin: 0; - padding: 0; - opacity: 0; - } -} +// @keyframes uploadAnimateIn { +// from { +// height: 0; +// margin: 0; +// padding: 0; +// opacity: 0; +// } +// } -@keyframes uploadAnimateOut { - to { - height: 0; - margin: 0; - padding: 0; - opacity: 0; - } -} +// @keyframes uploadAnimateOut { +// to { +// height: 0; +// margin: 0; +// padding: 0; +// opacity: 0; +// } +// } @keyframes uploadAnimateInlineIn { from { diff --git a/package.json b/package.json index 65bd4e931c..49db69450a 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "rc-input-number": "~6.1.0", "rc-mentions": "~1.5.0", "rc-menu": "~8.10.0", - "rc-motion": "^2.2.0", + "rc-motion": "^2.4.0", "rc-notification": "~4.5.2", "rc-pagination": "~3.1.0", "rc-picker": "~2.4.1", @@ -235,7 +235,7 @@ "open": "^7.0.3", "preact": "^10.0.0", "preact-compat": "^3.18.5", - "prettier": "^2.0.1", + "prettier": "^2.2.0", "pretty-quick": "^3.0.0", "querystring": "^0.2.0", "rc-footer": "^0.6.3", @@ -271,7 +271,7 @@ "reqwest": "^2.0.5", "rimraf": "^3.0.0", "scrollama": "^2.0.0", - "simple-git": "^2.0.0", + "simple-git": "^2.23.0", "string-replace-loader": "^2.3.0", "stylelint": "^13.0.0", "stylelint-config-prettier": "^8.0.0",