diff --git a/components/button/__tests__/delay-timer.test.tsx b/components/button/__tests__/delay-timer.test.tsx new file mode 100644 index 0000000000..c0fb924051 --- /dev/null +++ b/components/button/__tests__/delay-timer.test.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import Button from '../button'; + +const specialDelay = 9529; +const Content = () => { + const [loading, setLoading] = useState(false); + const toggleLoading = () => { + setLoading(!loading); + }; + + const [visible, setVisible] = useState(true); + const toggleVisible = () => { + setVisible(!visible); + }; + + return ( +
+ + + {visible &&
+ ); +}; + +it('Delay loading timer in Button component', () => { + const otherTimer: any = 9528; + jest.spyOn(window, 'setTimeout').mockReturnValue(otherTimer); + jest.restoreAllMocks(); + + const wrapper = mount(); + + const btnTimer: any = 9527; + jest.spyOn(window, 'setTimeout').mockReturnValue(btnTimer); + jest.spyOn(window, 'clearTimeout'); + const setTimeoutMock = window.setTimeout as any as jest.Mock; + const clearTimeoutMock = window.clearTimeout as any as jest.Mock; + + // other component may call setTimeout or clearTimeout + const setTimeoutCount = () => { + const items = setTimeoutMock.mock.calls.filter(item => item[1] === specialDelay); + return items.length; + }; + const clearTimeoutCount = () => { + const items = clearTimeoutMock.mock.calls.filter(item => item[0] === btnTimer); + return items.length; + }; + + // switch loading state to true + wrapper.find('#toggle_loading').at(0).simulate('click'); + expect(setTimeoutCount()).toBe(1); + expect(clearTimeoutCount()).toBe(0); + + // trigger timer handler + act(() => { + setTimeoutMock.mock.calls[0][0](); + }); + expect(setTimeoutCount()).toBe(1); + expect(clearTimeoutCount()).toBe(0); + + // switch loading state to false + wrapper.find('#toggle_loading').at(0).simulate('click'); + expect(setTimeoutCount()).toBe(1); + expect(clearTimeoutCount()).toBe(0); + + // switch loading state to true + wrapper.find('#toggle_loading').at(0).simulate('click'); + expect(setTimeoutCount()).toBe(2); + expect(clearTimeoutCount()).toBe(0); + + // switch loading state to false + wrapper.find('#toggle_loading').at(0).simulate('click'); + expect(setTimeoutCount()).toBe(2); + expect(clearTimeoutCount()).toBe(1); + + // switch loading state to true + wrapper.find('#toggle_loading').at(0).simulate('click'); + // remove Button component + wrapper.find('#toggle_visible').at(0).simulate('click'); + expect(setTimeoutCount()).toBe(3); + expect(clearTimeoutCount()).toBe(2); + + jest.restoreAllMocks(); +}); diff --git a/components/button/button.tsx b/components/button/button.tsx index 9bc2b2423f..215025ba7b 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -156,7 +156,6 @@ const InternalButton: React.ForwardRefRenderFunction = (pr const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false); const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext); const buttonRef = (ref as any) || React.createRef(); - const delayTimeoutRef = React.useRef(); const isNeedInserted = () => React.Children.count(children) === 1 && !icon && !isUnborderedButtonType(type); @@ -181,14 +180,25 @@ const InternalButton: React.ForwardRefRenderFunction = (pr typeof loading === 'object' && loading.delay ? loading.delay || true : !!loading; React.useEffect(() => { - clearTimeout(delayTimeoutRef.current); + let delayTimer: number | null = null; + if (typeof loadingOrDelay === 'number') { - delayTimeoutRef.current = window.setTimeout(() => { + delayTimer = window.setTimeout(() => { + delayTimer = null; setLoading(loadingOrDelay); }, loadingOrDelay); } else { setLoading(loadingOrDelay); } + + return () => { + if (delayTimer) { + // in order to not perform a React state update on an unmounted component + // and clear timer after 'loadingOrDelay' updated. + window.clearTimeout(delayTimer); + delayTimer = null; + } + }; }, [loadingOrDelay]); React.useEffect(fixTwoCNChar, [buttonRef]); diff --git a/components/button/style/index.less b/components/button/style/index.less index 357ca5ba87..cdb67c8d0e 100644 --- a/components/button/style/index.less +++ b/components/button/style/index.less @@ -256,7 +256,7 @@ letter-spacing: 0.34em; } - &-block { + &&-block { width: 100%; } diff --git a/components/tooltip/__tests__/tooltip.test.js b/components/tooltip/__tests__/tooltip.test.js index ad26de36b0..2eb67f1356 100644 --- a/components/tooltip/__tests__/tooltip.test.js +++ b/components/tooltip/__tests__/tooltip.test.js @@ -346,4 +346,22 @@ describe('Tooltip', () => { ); expect(wrapper.find('.ant-tooltip-inner').getDOMNode().style.color).toBe('red'); }); + + it('should work with loading switch', () => { + const onVisibleChange = jest.fn(); + const wrapper = mount( + + + , + ); + const wrapperEl = wrapper.find('.ant-tooltip-disabled-compatible-wrapper'); + expect(wrapperEl).toHaveLength(1); + wrapper.find('span').first().simulate('mouseenter'); + expect(onVisibleChange).toHaveBeenLastCalledWith(true); + }); }); diff --git a/components/tooltip/index.tsx b/components/tooltip/index.tsx index 0e8b6e0562..6c8e103390 100644 --- a/components/tooltip/index.tsx +++ b/components/tooltip/index.tsx @@ -85,10 +85,8 @@ const PresetColorRegex = new RegExp(`^(${PresetColorTypes.join('|')})(-inverse)? function getDisabledCompatibleChildren(element: React.ReactElement, prefixCls: string) { const elementType = element.type as any; if ( - (elementType.__ANT_BUTTON === true || - elementType.__ANT_SWITCH === true || - element.type === 'button') && - element.props.disabled + ((elementType.__ANT_BUTTON === true || element.type === 'button') && element.props.disabled) || + (elementType.__ANT_SWITCH === true && (element.props.disabled || element.props.loading)) ) { // Pick some layout related style properties up to span // Prevent layout bugs like https://github.com/ant-design/ant-design/issues/5254 diff --git a/components/typography/Base/index.tsx b/components/typography/Base/index.tsx index c3838330ba..c6eea42bf8 100644 --- a/components/typography/Base/index.tsx +++ b/components/typography/Base/index.tsx @@ -293,12 +293,14 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => { const textEle = typographyRef.current; if (enableEllipsis && cssEllipsis && textEle) { - const currentEllipsis = textEle.offsetWidth < textEle.scrollWidth; + const currentEllipsis = cssLineClamp + ? textEle.offsetHeight < textEle.scrollHeight + : textEle.offsetWidth < textEle.scrollWidth; if (isNativeEllipsis !== currentEllipsis) { setIsNativeEllipsis(currentEllipsis); } } - }, [enableEllipsis, cssEllipsis, children]); + }, [enableEllipsis, cssEllipsis, children, cssLineClamp]); // ========================== Tooltip =========================== const tooltipTitle = ellipsisConfig.tooltip === true ? children : ellipsisConfig.tooltip; diff --git a/components/typography/__tests__/ellipsis.test.js b/components/typography/__tests__/ellipsis.test.js index 372c206039..34173480db 100644 --- a/components/typography/__tests__/ellipsis.test.js +++ b/components/typography/__tests__/ellipsis.test.js @@ -277,4 +277,34 @@ describe('Typography.Ellipsis', () => { const tooltipWrapper = mount(); expect(tooltipWrapper.find('.ant-typography').prop('aria-label')).toEqual('little'); }); + + it('should display tooltip if line clamp', () => { + mockRectSpy = spyElementPrototypes(HTMLElement, { + scrollHeight: { + get() { + let html = this.innerHTML; + html = html.replace(/<[^>]*>/g, ''); + const lines = Math.ceil(html.length / LINE_STR_COUNT); + return lines * 16; + }, + }, + offsetHeight: { + get: () => 32, + }, + offsetWidth: { + get: () => 100, + }, + scrollWidth: { + get: () => 100, + }, + }); + + const wrapper = mount( + + Ant Design, a design language for background applications, is refined by Ant UED Team. + , + ); + expect(wrapper.find('EllipsisTooltip').prop('isEllipsis')).toBeTruthy(); + mockRectSpy.mockRestore(); + }); }); diff --git a/components/upload/Upload.tsx b/components/upload/Upload.tsx index 628f36afd8..305caad216 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -322,7 +322,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr delete rcUploadProps.id; } - const renderUploadList = (button?: React.ReactNode) => + const renderUploadList = (button?: React.ReactNode, buttonVisible?: boolean) => showUploadList ? ( {(locale: UploadLocale) => { @@ -354,6 +354,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr isImageUrl={isImageUrl} progress={progress} appendAction={button} + appendActionVisible={buttonVisible} itemRender={itemRender} /> ); @@ -400,8 +401,8 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr [`${prefixCls}-rtl`]: direction === 'rtl', }); - const uploadButton = ( -
+ const renderUploadButton = (uploadButtonStyle?: React.CSSProperties) => ( +
); @@ -409,14 +410,14 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr if (listType === 'picture-card') { return ( - {renderUploadList(uploadButton)} + {renderUploadList(renderUploadButton(), !!children)} ); } return ( - {uploadButton} + {renderUploadButton(children ? undefined : { display: 'none' })} {renderUploadList()} ); diff --git a/components/upload/UploadList/index.tsx b/components/upload/UploadList/index.tsx index 89955c2127..11f7083ea6 100644 --- a/components/upload/UploadList/index.tsx +++ b/components/upload/UploadList/index.tsx @@ -42,6 +42,7 @@ const InternalUploadList: React.ForwardRefRenderFunction + {({ className: motionClassName, style: motionStyle }) => cloneElement(appendAction, oriProps => ({ className: classNames(oriProps.className, motionClassName), style: { ...motionStyle, + // prevent the element has hover css pseudo-class that may cause animation to end prematurely. + pointerEvents: motionClassName ? 'none' : undefined, ...oriProps.style, }, })) @@ -254,6 +257,7 @@ UploadList.defaultProps = { showRemoveIcon: true, showDownloadIcon: false, showPreviewIcon: true, + appendActionVisible: true, previewFile: previewImage, isImageUrl, }; diff --git a/components/upload/__tests__/upload.test.js b/components/upload/__tests__/upload.test.js index c096327999..71865b951e 100644 --- a/components/upload/__tests__/upload.test.js +++ b/components/upload/__tests__/upload.test.js @@ -867,4 +867,26 @@ describe('Upload', () => { expect(onChange.mock.calls[0][0].fileList).toHaveLength(1); }); + + // https://github.com/ant-design/ant-design/issues/33819 + it('should show the animation of the upload children leaving when the upload children becomes null', async () => { + const wrapper = mount( + + + , + ); + wrapper.setProps({ children: null }); + expect(wrapper.find('.ant-upload-select-picture-card').getDOMNode().style.display).not.toBe( + 'none', + ); + await act(async () => { + await sleep(100); + wrapper + .find('.ant-upload-select-picture-card') + .getDOMNode() + .dispatchEvent(new Event('animationend')); + await sleep(20); + }); + expect(wrapper.find('.ant-upload-select-picture-card').getDOMNode().style.display).toBe('none'); + }); }); diff --git a/components/upload/interface.tsx b/components/upload/interface.tsx index 7c11a8dd22..ff47c3c1b6 100755 --- a/components/upload/interface.tsx +++ b/components/upload/interface.tsx @@ -155,5 +155,6 @@ export interface UploadListProps { iconRender?: (file: UploadFile, listType?: UploadListType) => React.ReactNode; isImageUrl?: (file: UploadFile) => boolean; appendAction?: React.ReactNode; + appendActionVisible?: boolean; itemRender?: ItemRender; }