diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index f0adbb1f0a..96977cddb5 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -15,6 +15,29 @@ timeline: true --- +## 4.6.2 + +`2020-08-31` + +- Upload + - 🐞 Fix Upload list status issue when in control mode. [#26481](https://github.com/ant-design/ant-design/pull/26481) + - 💄 Fix Upload `picture-card` style unexpected margin in Form.Item. [#26367](https://github.com/ant-design/ant-design/pull/26367) +- 💄 Fix Select focus shadow style. [#26465](https://github.com/ant-design/ant-design/pull/26465) [@Rainy](https://github.com/Rainy) +- Table + - 🐞 Fix Table Pagination not hide with empty data when show on top position. [#26143](https://github.com/ant-design/ant-design/pull/26143) [@zhangchen915](https://github.com/zhangchen915) + - 💄 Fix Table expand icon size issue when `@font-size-base` is `12px`. [#26409](https://github.com/ant-design/ant-design/pull/26409) +- Space + - 🐞 Fix Space not support React.Fragment issue. [#26444](https://github.com/ant-design/ant-design/pull/26444) + - 🐞 Fix Space preserve empty dom node when `children` contains empty node. [#26389](https://github.com/ant-design/ant-design/pull/26389) +- 🐞 Fix Badge not work when `status` or `color` is empty. [#26375](https://github.com/ant-design/ant-design/pull/26375) [@zhangchen915](https://github.com/zhangchen915) +- 💄 Fix Tree draggable transition style. [#26387](https://github.com/ant-design/ant-design/pull/26387) +- 🐞 Fix `colorPalette is not defined` when customize theme in some situation. [#26395](https://github.com/ant-design/ant-design/pull/26395) +- TypeScript + - 🐞 Fix TimePicker.RangePicker typescript need `picker` issue. [#26446](https://github.com/ant-design/ant-design/pull/26446) + - 🐞 Upload extended `showUploadList` of Upload with `removeIcon` and `downloadIcon` properties. [#26406](https://github.com/ant-design/ant-design/pull/26406) [@bencallaway](https://github.com/bencallaway) +- RTL + - 🐞 Fix the rtl style of Col. [#26479](https://github.com/ant-design/ant-design/pull/26479) [#26482](https://github.com/ant-design/ant-design/pull/26482) [@TrueMoein](https://github.com/TrueMoein) + ## 4.6.1 `2020-08-24` diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 8dac81652d..a38def3bc7 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -15,6 +15,29 @@ timeline: true --- +## 4.6.2 + +`2020-08-31` + +- Upload + - 🐞 修复 Upload 在受控模式下同步更新导致的状态错误问题。[#26481](https://github.com/ant-design/ant-design/pull/26481) + - 💄 修复 Upload 图片样式在 Form.Item 中有异常 margin 的问题。[#26367](https://github.com/ant-design/ant-design/pull/26367) +- 💄 修复 Select focus 状态样式无阴影。[#26465](https://github.com/ant-design/ant-design/pull/26465) [@Rainy](https://github.com/Rainy) +- Table + - 🐞 修复 Table Pagination 展示于上侧且没有数据时不消失的问题。[#26143](https://github.com/ant-design/ant-design/pull/26143) [@zhangchen915](https://github.com/zhangchen915) + - 💄 修复 Table 展开图标在 `@font-size-base` 为 `12px` 时样式错位的问题。[#26409](https://github.com/ant-design/ant-design/pull/26409) +- Space + - 🐞 修复 Space 不支持 React.Fragment 的问题。[#26444](https://github.com/ant-design/ant-design/pull/26444) + - 🐞 修复 Space 在 `children` 中包含空节点时会出现空 dom 的问题。[#26389](https://github.com/ant-design/ant-design/pull/26389) +- 🐞 修复 Badge 在 `status` 或 `color` 为空时不展示。[#26375](https://github.com/ant-design/ant-design/pull/26375) [@zhangchen915](https://github.com/zhangchen915) +- 💄 修复 Tree `draggable` 切换时样式 transition 变化的问题。[#26387](https://github.com/ant-design/ant-design/pull/26387) +- 🐞 修复使用主题有时会报 `colorPalette is not defined` 的问题。[#26395](https://github.com/ant-design/ant-design/pull/26395) +- TypeScript + - 🐞 修复 TimePicker.RangePicker 定义需要 `picker` 的问题。[#26446](https://github.com/ant-design/ant-design/pull/26446) + - 🐞 Upload 组件 `showUploadList` 类型添加 `removeIcon` 和 `downloadIcon` 属性声明。[#26406](https://github.com/ant-design/ant-design/pull/26406) [@bencallaway](https://github.com/bencallaway) +- RTL + - 🐞 修复 Col RTL 样式。[#26479](https://github.com/ant-design/ant-design/pull/26479) [#26482](https://github.com/ant-design/ant-design/pull/26482) [@TrueMoein](https://github.com/TrueMoein) + ## 4.6.1 `2020-08-24` diff --git a/components/checkbox/index.en-US.md b/components/checkbox/index.en-US.md index fa4f177dc5..e5f1c8dc42 100644 --- a/components/checkbox/index.en-US.md +++ b/components/checkbox/index.en-US.md @@ -34,7 +34,7 @@ Checkbox component. | defaultValue | Default selected value | string\[] | \[] | | | disabled | If disable all checkboxes | boolean | false | | | name | The `name` property of all `input[type="checkbox"]` children | string | - | | -| options | Specifies options | Array[] | \[] | | +| options | Specifies options | string\[] \| Option\[] | \[] | | | value | Used for setting the currently selected value | string\[] | \[] | | | onChange | The callback function that is triggered when the state changes | function(checkedValue) | - | | diff --git a/components/config-provider/__tests__/form-locale.test.js b/components/config-provider/__tests__/form-locale.test.js new file mode 100644 index 0000000000..e4e836bb61 --- /dev/null +++ b/components/config-provider/__tests__/form-locale.test.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import ConfigProvider from '..'; +import zhCN from '../../locale/zh_CN'; +import Form from '../../form'; + +describe('ConfigProvider.Form.Locale', () => { + describe('form validateMessages', () => { + const wrapperComponent = ({ validateMessages }) => { + const formRef = React.createRef(); + + const wrapper = mount( + +
+ + + + + + +
+
, + ); + + return [wrapper, formRef]; + }; + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('set locale zhCN', async () => { + const [wrapper, formRef] = wrapperComponent({}); + + await act(async () => { + try { + await formRef.current.validateFields(); + } catch (e) { + // Do nothing + } + }); + + await act(async () => { + jest.runAllTimers(); + wrapper.update(); + await Promise.resolve(); + }); + + expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('请输入姓名'); + }); + + it('set locale zhCN and set form validateMessages one item, other use default message', async () => { + const [wrapper, formRef] = wrapperComponent({ validateMessages: { required: '必须' } }); + + await act(async () => { + try { + await formRef.current.validateFields(); + } catch (e) { + // Do nothing + } + }); + + await act(async () => { + jest.runAllTimers(); + wrapper.update(); + await Promise.resolve(); + }); + + expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('必须'); + expect(wrapper.find('.ant-form-item-explain').last().text()).toEqual('年龄必须等于17'); + }); + }); +}); diff --git a/components/config-provider/__tests__/locale.test.js b/components/config-provider/__tests__/locale.test.js index 45accd04e2..5ffb49f82c 100644 --- a/components/config-provider/__tests__/locale.test.js +++ b/components/config-provider/__tests__/locale.test.js @@ -6,12 +6,6 @@ import zhCN from '../../locale/zh_CN'; import enUS from '../../locale/en_US'; import TimePicker from '../../time-picker'; import Modal from '../../modal'; -import Form from '../../form'; - -const delay = (timeout = 0) => - new Promise(resolve => { - setTimeout(resolve, timeout); - }); describe('ConfigProvider.Locale', () => { function $$(className) { @@ -113,41 +107,4 @@ describe('ConfigProvider.Locale', () => { testLocale(wrapper); }); }); - - describe('form validateMessages', () => { - const wrapperComponent = ({ validateMessages }) => - mount( - -
- - - - - - -
-
, - ); - - it('set locale zhCN', async () => { - const wrapper = wrapperComponent({}); - - wrapper.find('form').simulate('submit'); - await delay(50); - wrapper.update(); - - expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('请输入姓名'); - }); - - it('set locale zhCN and set form validateMessages one item, other use default message', async () => { - const wrapper = wrapperComponent({ validateMessages: { required: '必须' } }); - - wrapper.find('form').simulate('submit'); - await delay(200); - wrapper.update(); - - expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('必须'); - expect(wrapper.find('.ant-form-item-explain').last().text()).toEqual('年龄必须等于17'); - }); - }); }); diff --git a/components/config-provider/index.zh-CN.md b/components/config-provider/index.zh-CN.md index 4a4779afbe..560245c74a 100644 --- a/components/config-provider/index.zh-CN.md +++ b/components/config-provider/index.zh-CN.md @@ -48,7 +48,7 @@ export default () => ( | getPopupContainer | 弹出框(Select, Tooltip, Menu 等等)渲染父节点,默认渲染到 body 上。 | function(triggerNode) | () => document.body | | | getTargetContainer | 配置 Affix、Anchor 滚动监听容器。 | () => HTMLElement | () => window | 4.2.0 | | locale | 语言包配置,语言包可到 [antd/es/locale](http://unpkg.com/antd/es/locale/) 目录下寻找 | object | - | | -| prefixCls | 设置统一样式前缀。`注意:需要配合 less 变量 [@ant-prefix](https://github.com/ant-design/ant-design/blob/2c6c789e3a9356f96c47aea0083f5a15538315cf/components/style/themes/default.less#L7) 使用` | string | `ant` | | +| prefixCls | 设置统一样式前缀。注意:需要配合 `less` 变量 [@ant-prefix](https://github.com/ant-design/ant-design/blob/2c6c789e3a9356f96c47aea0083f5a15538315cf/components/style/themes/default.less#L7) 使用 | string | `ant` | | | pageHeader | 统一设置 PageHeader 的 ghost,参考 [PageHeader](/components/page-header) | { ghost: boolean } | true | | | direction | 设置文本展示方向。 [示例](#components-config-provider-demo-direction) | `ltr` \| `rtl` | `ltr` | | | space | 设置 Space 的 `size`,参考 [Space](/components/space) | { size: `small` \| `middle` \| `large` \| `number` } | - | 4.1.0 | diff --git a/components/grid/style/rtl.less b/components/grid/style/rtl.less index 230716cc21..5a9e7256b8 100644 --- a/components/grid/style/rtl.less +++ b/components/grid/style/rtl.less @@ -7,13 +7,6 @@ } } -.@{ant-prefix}-col { - &&-rtl { - float: right; - width: 100%; - } -} - // mixin .loop-grid-columns(@index, @class) when (@index > 0) { .@{ant-prefix}-col@{class}-push-@{index} { diff --git a/components/modal/index.en-US.md b/components/modal/index.en-US.md index 2aa4d2700b..b37a2a39cf 100644 --- a/components/modal/index.en-US.md +++ b/components/modal/index.en-US.md @@ -18,6 +18,7 @@ When requiring users to interact with the application, but without jumping to a | afterClose | Specify a function that will be called when modal is closed completely | function | - | | bodyStyle | Body style for modal body element. Such as height, padding etc | CSSProperties | {} | | cancelText | Text of the Cancel button | string \| ReactNode | `Cancel` | +| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#API) | - | | centered | Centered Modal | boolean | false | | closable | Whether a close (x) button is visible on top right of the modal dialog or not | boolean | true | | closeIcon | Custom close icon | ReactNode | <CloseOutlined /> | @@ -26,21 +27,21 @@ When requiring users to interact with the application, but without jumping to a | footer | Footer content, set as `footer={null}` when you don't need default buttons | string \| ReactNode | (OK and Cancel buttons) | | forceRender | Force render Modal | boolean | false | | getContainer | Return the mount node for Modal | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | +| keyboard | Whether support press esc to close | boolean | true | | mask | Whether show mask or not | boolean | true | | maskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | boolean | true | | maskStyle | Style for modal's mask element | object | {} | +| okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - | | okText | Text of the OK button | string \| ReactNode | `OK` | | okType | Button `type` of the OK button | string | `primary` | -| okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - | -| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#API) | - | +| onCancel | Specify a function that will be called when a user clicks mask, close button on top right or Cancel button | function(e) | - | +| onOk | Specify a function that will be called when a user clicks the OK button | function(e) | - | | style | Style of floating layer, typically used at least for adjusting the position | CSSProperties | - | | title | The modal dialog's title | string \| ReactNode | - | | visible | Whether the modal dialog is visible or not | boolean | false | | width | Width of the modal dialog | string \| number | 520 | | wrapClassName | The class name of the container of the modal dialog | string | - | | zIndex | The `z-index` of the Modal | number | 1000 | -| onCancel | Specify a function that will be called when a user clicks mask, close button on top right or Cancel button | function(e) | - | -| onOk | Specify a function that will be called when a user clicks the OK button | function(e) | - | #### Note @@ -63,23 +64,26 @@ The items listed above are all functions, expecting a settings object as paramet | Property | Description | Type | Default | Version | | --- | --- | --- | --- | --- | | autoFocusButton | Specify which button to autofocus | null \| `ok` \| `cancel` | `ok` | | +| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#API) | - | | | cancelText | Text of the Cancel button with Modal.confirm | string | `Cancel` | | | centered | Centered Modal | boolean | false | | | className | The className of container | string | - | | | content | Content | string \| ReactNode | - | | +| getContainer | Return the mount node for Modal | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | | | icon | Custom icon | ReactNode | <QuestionCircle /> | 3.12.0 | | keyboard | Whether support press esc to close | boolean | true | | | mask | Whether show mask or not. | boolean | true | | | maskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | boolean | false | | +| maskStyle | Style for modal's mask element | object | {} | | +| okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - | | | okText | Text of the OK button | string | `OK` | | | okType | Button `type` of the OK button | string | `primary` | | -| okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - | | -| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#API) | - | | +| onCancel | Specify a function that will be called when the user clicks the Cancel button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - | | +| onOk | Specify a function that will be called when the user clicks the OK button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - | | +| style | Style of floating layer, typically used at least for adjusting the position | CSSProperties | - | | | title | Title | string \| ReactNode | - | | | width | Width of the modal dialog | string \| number | 416 | | | zIndex | The `z-index` of the Modal | number | 1000 | | -| onCancel | Specify a function that will be called when the user clicks the Cancel button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - | | -| onOk | Specify a function that will be called when the user clicks the OK button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - | | All the `Modal.method`s will return a reference, and then we can update and close the modal dialog by the reference. diff --git a/components/modal/index.zh-CN.md b/components/modal/index.zh-CN.md index dc7f241f34..2f9418deac 100644 --- a/components/modal/index.zh-CN.md +++ b/components/modal/index.zh-CN.md @@ -20,6 +20,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3StSdUlSH/Modal.svg | --- | --- | --- | --- | | afterClose | Modal 完全关闭后的回调 | function | - | | bodyStyle | Modal body 样式 | object | {} | +| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button/#API) | - | | cancelText | 取消按钮文字 | string \| ReactNode | `取消` | | centered | 垂直居中展示 Modal | boolean | false | | closable | 是否显示右上角的关闭按钮 | boolean | true | @@ -33,18 +34,17 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3StSdUlSH/Modal.svg | mask | 是否展示遮罩 | boolean | true | | maskClosable | 点击蒙层是否允许关闭 | boolean | true | | maskStyle | 遮罩样式 | object | {} | +| okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - | | okText | 确认按钮文字 | string \| ReactNode | `确定` | | okType | 确认按钮类型 | string | `primary` | -| okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - | -| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button/#API) | - | +| onCancel | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | - | +| onOk | 点击确定回调 | function(e) | - | | style | 可用于设置浮层的样式,调整浮层位置等 | CSSProperties | - | | title | 标题 | string \| ReactNode | - | | visible | 对话框是否可见 | boolean | - | | width | 宽度 | string \| number | 520 | | wrapClassName | 对话框外层容器的类名 | string | - | | zIndex | 设置 Modal 的 `z-index` | number | 1000 | -| onCancel | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | - | -| onOk | 点击确定回调 | function(e) | - | #### 注意 @@ -67,21 +67,26 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3StSdUlSH/Modal.svg | 参数 | 说明 | 类型 | 默认值 | 版本 | | --- | --- | --- | --- | --- | | autoFocusButton | 指定自动获得焦点的按钮 | null \| `ok` \| `cancel` | `ok` | | +| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button/#API) | - | | | cancelText | 设置 Modal.confirm 取消按钮文字 | string | `取消` | | | centered | 垂直居中展示 Modal | boolean | false | | | className | 容器类名 | string | - | | | content | 内容 | string \| ReactNode | - | | +| getContainer | 指定 Modal 挂载的 HTML 节点, false 为挂载在当前 dom | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | | | icon | 自定义图标 | ReactNode | <QuestionCircle /> | 3.12.0 | +| keyboard | 是否支持键盘 esc 关闭 | boolean | true | | +| mask | 是否展示遮罩 | boolean | true | | | maskClosable | 点击蒙层是否允许关闭 | boolean | false | | +| maskStyle | 遮罩样式 | object | {} | | +| okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - | | | okText | 确认按钮文字 | string | `确定` | | | okType | 确认按钮类型 | string | `primary` | | -| okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - | | -| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button/#API) | - | | +| onCancel | 取消回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - | | +| onOk | 点击确定回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - | | +| style | 可用于设置浮层的样式,调整浮层位置等 | CSSProperties | - | | | title | 标题 | string \| ReactNode | - | | | width | 宽度 | string \| number | 416 | | | zIndex | 设置 Modal 的 `z-index` | number | 1000 | | -| onCancel | 取消回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - | | -| onOk | 点击确定回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - | | 以上函数调用后,会返回一个引用,可以通过该引用更新和关闭弹窗。 diff --git a/components/upload/Upload.tsx b/components/upload/Upload.tsx index d893338e1a..fbe1eda4c6 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -18,7 +18,6 @@ import LocaleReceiver from '../locale-provider/LocaleReceiver'; import defaultLocale from '../locale/default'; import { ConfigContext } from '../config-provider'; import devWarning from '../_util/devWarning'; -import useSyncState from '../_util/hooks/useSyncState'; import useForceUpdate from '../_util/hooks/useForceUpdate'; export { UploadProps }; @@ -45,15 +44,18 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr style, } = props; - const [getFileList, setFileList] = useSyncState>( - fileListProp || defaultFileList || [], - ); const [dragState, setDragState] = React.useState('drop'); + const forceUpdate = useForceUpdate(); + + // `fileListRef` used for internal state sync to avoid control mode set it back when sync update. + // `visibleFileList` used for display in UploadList instead. + // It's a workaround and not the best solution. + const fileListRef = React.useRef(fileListProp || defaultFileList || []); + const visibleFileList = fileListProp || fileListRef.current; const upload = React.useRef(); React.useEffect(() => { - setFileList(fileListProp || defaultFileList || []); devWarning( 'fileList' in props || !('value' in props), 'Upload', @@ -62,15 +64,15 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr }, []); React.useEffect(() => { - if ('fileList' in props) { - setFileList(fileListProp || []); + if (fileListProp !== undefined && fileListProp !== fileListRef.current) { + fileListRef.current = fileListProp; + forceUpdate(); } }, [fileListProp]); const onChange = (info: UploadChangeParam) => { - if (!('fileList' in props)) { - setFileList(info.fileList); - } + fileListRef.current = info.fileList; + forceUpdate(); const { onChange: onChangeProp } = props; if (onChangeProp) { @@ -85,7 +87,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr const targetItem = fileToObject(file); targetItem.status = 'uploading'; - const nextFileList = getFileList().concat(); + const nextFileList = fileListRef.current.concat(); const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === targetItem.uid); if (fileIndex === -1) { @@ -108,7 +110,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr } catch (e) { /* do nothing */ } - const targetItem = getFileItem(file, getFileList()); + const targetItem = getFileItem(file, fileListRef.current); // removed if (!targetItem) { return; @@ -118,12 +120,12 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr targetItem.xhr = xhr; onChange({ file: { ...targetItem }, - fileList: getFileList().concat(), + fileList: fileListRef.current.concat(), }); }; const onProgress = (e: { percent: number }, file: UploadFile) => { - const targetItem = getFileItem(file, getFileList()); + const targetItem = getFileItem(file, fileListRef.current); // removed if (!targetItem) { return; @@ -132,12 +134,12 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr onChange({ event: e, file: { ...targetItem }, - fileList: getFileList().concat(), + fileList: fileListRef.current.concat(), }); }; const onError = (error: Error, response: any, file: UploadFile) => { - const targetItem = getFileItem(file, getFileList()); + const targetItem = getFileItem(file, fileListRef.current); // removed if (!targetItem) { return; @@ -147,7 +149,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr targetItem.status = 'error'; onChange({ file: { ...targetItem }, - fileList: getFileList().concat(), + fileList: fileListRef.current.concat(), }); }; @@ -158,7 +160,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr return; } - const removedFileList = removeFileItem(file, getFileList()); + const removedFileList = removeFileItem(file, fileListRef.current); if (removedFileList) { file.status = 'removed'; @@ -187,13 +189,11 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr if (result === false) { // Get unique file list const uniqueList: UploadFile[] = []; - getFileList() - .concat(fileListArgs.map(fileToObject)) - .forEach(f => { - if (uniqueList.every(uf => uf.uid !== f.uid)) { - uniqueList.push(f); - } - }); + fileListRef.current.concat(fileListArgs.map(fileToObject)).forEach(f => { + if (uniqueList.every(uf => uf.uid !== f.uid)) { + uniqueList.push(f); + } + }); onChange({ file, @@ -207,13 +207,12 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr return true; }; // Test needs - const forceUpdate = useForceUpdate(); React.useImperativeHandle(ref, () => ({ onStart, onSuccess, onProgress, onError, - fileList: getFileList(), + fileList: fileListRef.current, upload: upload.current, forceUpdate, })); @@ -252,7 +251,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr return ( = (pr prefixCls, { [`${prefixCls}-drag`]: true, - [`${prefixCls}-drag-uploading`]: getFileList().some(file => file.status === 'uploading'), + [`${prefixCls}-drag-uploading`]: fileListRef.current.some( + file => file.status === 'uploading', + ), [`${prefixCls}-drag-hover`]: dragState === 'dragover', [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-rtl`]: direction === 'rtl', diff --git a/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap b/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap index f7c4ee02bd..d13d51562c 100644 --- a/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap +++ b/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap @@ -28,7 +28,7 @@ exports[`Upload List handle error 1`] = ` class="ant-upload-list ant-upload-list-text" >
{ wrapper.find('.ant-upload').at(1).simulate('mouseLeave'); expect(onMouseLeave).toHaveBeenCalled(); }); + + // https://github.com/ant-design/ant-design/issues/26427 + it('should sync file list with control mode', done => { + let callTimes = 0; + + const customRequest = jest.fn(async options => { + options.onProgress({ percent: 0 }); + const url = Promise.resolve('https://ant.design'); + options.onProgress({ percent: 100 }); + options.onSuccess({}, { ...options.file, url }); + }); + + const Demo = () => { + const [fileList, setFileList] = React.useState([]); + + const onChange = e => { + const newFileList = Array.isArray(e) ? e : e.fileList; + setFileList(newFileList); + const file = newFileList[0]; + + callTimes += 1; + + switch (callTimes) { + case 1: + case 2: + expect(file).toEqual(expect.objectContaining({ status: 'uploading', percent: 0 })); + break; + + case 3: + expect(file).toEqual(expect.objectContaining({ status: 'uploading', percent: 100 })); + break; + + case 4: + expect(file).toEqual(expect.objectContaining({ status: 'done', percent: 100 })); + break; + + default: + // Do nothing + } + + if (callTimes >= 4) { + done(); + } + }; + + return ( + + + + ); + }; + + const wrapper = mount(); + + act(() => { + wrapper.find('input').simulate('change', { + target: { + files: [{ file: 'foo.png' }], + }, + }); + }); + }); }); diff --git a/components/upload/__tests__/uploadlist.test.js b/components/upload/__tests__/uploadlist.test.js index 541eac43f0..9b0a82da15 100644 --- a/components/upload/__tests__/uploadlist.test.js +++ b/components/upload/__tests__/uploadlist.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import Upload from '..'; import UploadList from '../UploadList'; @@ -801,14 +802,23 @@ describe('Upload List', () => { const nonImageFile = new File([''], 'foo.7z', { type: 'application/x-7z-compressed' }); it('should render when upload non-image file and configure thumbUrl in onChange', done => { let wrapper; - const onChange = async ({ fileList: files }) => { - const newfileList = files.map(item => ({ + const onChange = async ({ file, fileList: files }) => { + const newFileList = files.map(item => ({ ...item, thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', })); - wrapper.setProps({ fileList: newfileList }); - await sleep(); + wrapper.setProps({ fileList: newFileList }); + + if (file.status === 'uploading') { + return; + } + + act(async () => { + await sleep(); + wrapper.update(); + }); + const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); expect(imgNode.length).toBe(1); done(); diff --git a/package.json b/package.json index d21adbeb9f..c43b1eed11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "antd", - "version": "4.6.1", + "version": "4.6.2", "description": "An enterprise-class UI design language and React components implementation", "title": "Ant Design", "keywords": [ @@ -147,7 +147,7 @@ "rc-tree": "~3.9.0", "rc-tree-select": "~4.1.1", "rc-trigger": "~4.4.0", - "rc-upload": "~3.2.0", + "rc-upload": "~3.2.1", "rc-util": "^5.1.0", "scroll-into-view-if-needed": "^2.2.25", "warning": "^4.0.3"