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;
}