chore: auto merge branchs (#33889)

chore: merge master into feature
This commit is contained in:
github-actions[bot] 2022-01-29 11:53:19 +00:00 committed by GitHub
commit 0d4e1b1356
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 191 additions and 16 deletions

View File

@ -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 (
<div>
<button type="button" id="toggle_loading" onClick={toggleLoading}>
Toggle Loading
</button>
<button type="button" id="toggle_visible" onClick={toggleVisible}>
Toggle Visible
</button>
{visible && <Button type="text" loading={loading ? { delay: specialDelay } : false} />}
</div>
);
};
it('Delay loading timer in Button component', () => {
const otherTimer: any = 9528;
jest.spyOn(window, 'setTimeout').mockReturnValue(otherTimer);
jest.restoreAllMocks();
const wrapper = mount(<Content />);
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();
});

View File

@ -156,7 +156,6 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false); const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false);
const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext); const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext);
const buttonRef = (ref as any) || React.createRef<HTMLElement>(); const buttonRef = (ref as any) || React.createRef<HTMLElement>();
const delayTimeoutRef = React.useRef<number>();
const isNeedInserted = () => const isNeedInserted = () =>
React.Children.count(children) === 1 && !icon && !isUnborderedButtonType(type); React.Children.count(children) === 1 && !icon && !isUnborderedButtonType(type);
@ -181,14 +180,25 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
typeof loading === 'object' && loading.delay ? loading.delay || true : !!loading; typeof loading === 'object' && loading.delay ? loading.delay || true : !!loading;
React.useEffect(() => { React.useEffect(() => {
clearTimeout(delayTimeoutRef.current); let delayTimer: number | null = null;
if (typeof loadingOrDelay === 'number') { if (typeof loadingOrDelay === 'number') {
delayTimeoutRef.current = window.setTimeout(() => { delayTimer = window.setTimeout(() => {
delayTimer = null;
setLoading(loadingOrDelay); setLoading(loadingOrDelay);
}, loadingOrDelay); }, loadingOrDelay);
} else { } else {
setLoading(loadingOrDelay); 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]); }, [loadingOrDelay]);
React.useEffect(fixTwoCNChar, [buttonRef]); React.useEffect(fixTwoCNChar, [buttonRef]);

View File

@ -256,7 +256,7 @@
letter-spacing: 0.34em; letter-spacing: 0.34em;
} }
&-block { &&-block {
width: 100%; width: 100%;
} }

View File

@ -346,4 +346,22 @@ describe('Tooltip', () => {
); );
expect(wrapper.find('.ant-tooltip-inner').getDOMNode().style.color).toBe('red'); expect(wrapper.find('.ant-tooltip-inner').getDOMNode().style.color).toBe('red');
}); });
it('should work with loading switch', () => {
const onVisibleChange = jest.fn();
const wrapper = mount(
<Tooltip
title="loading tips"
mouseEnterDelay={0}
mouseLeaveDelay={0}
onVisibleChange={onVisibleChange}
>
<Switch loading defaultChecked />
</Tooltip>,
);
const wrapperEl = wrapper.find('.ant-tooltip-disabled-compatible-wrapper');
expect(wrapperEl).toHaveLength(1);
wrapper.find('span').first().simulate('mouseenter');
expect(onVisibleChange).toHaveBeenLastCalledWith(true);
});
}); });

View File

@ -85,10 +85,8 @@ const PresetColorRegex = new RegExp(`^(${PresetColorTypes.join('|')})(-inverse)?
function getDisabledCompatibleChildren(element: React.ReactElement<any>, prefixCls: string) { function getDisabledCompatibleChildren(element: React.ReactElement<any>, prefixCls: string) {
const elementType = element.type as any; const elementType = element.type as any;
if ( if (
(elementType.__ANT_BUTTON === true || ((elementType.__ANT_BUTTON === true || element.type === 'button') && element.props.disabled) ||
elementType.__ANT_SWITCH === true || (elementType.__ANT_SWITCH === true && (element.props.disabled || element.props.loading))
element.type === 'button') &&
element.props.disabled
) { ) {
// Pick some layout related style properties up to span // Pick some layout related style properties up to span
// Prevent layout bugs like https://github.com/ant-design/ant-design/issues/5254 // Prevent layout bugs like https://github.com/ant-design/ant-design/issues/5254

View File

@ -293,12 +293,14 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
const textEle = typographyRef.current; const textEle = typographyRef.current;
if (enableEllipsis && cssEllipsis && textEle) { if (enableEllipsis && cssEllipsis && textEle) {
const currentEllipsis = textEle.offsetWidth < textEle.scrollWidth; const currentEllipsis = cssLineClamp
? textEle.offsetHeight < textEle.scrollHeight
: textEle.offsetWidth < textEle.scrollWidth;
if (isNativeEllipsis !== currentEllipsis) { if (isNativeEllipsis !== currentEllipsis) {
setIsNativeEllipsis(currentEllipsis); setIsNativeEllipsis(currentEllipsis);
} }
} }
}, [enableEllipsis, cssEllipsis, children]); }, [enableEllipsis, cssEllipsis, children, cssLineClamp]);
// ========================== Tooltip =========================== // ========================== Tooltip ===========================
const tooltipTitle = ellipsisConfig.tooltip === true ? children : ellipsisConfig.tooltip; const tooltipTitle = ellipsisConfig.tooltip === true ? children : ellipsisConfig.tooltip;

View File

@ -277,4 +277,34 @@ describe('Typography.Ellipsis', () => {
const tooltipWrapper = mount(<Base ellipsis={{ expandable: true, tooltip: 'little' }} />); const tooltipWrapper = mount(<Base ellipsis={{ expandable: true, tooltip: 'little' }} />);
expect(tooltipWrapper.find('.ant-typography').prop('aria-label')).toEqual('little'); 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(
<Base ellipsis={{ tooltip: 'This is tooltip', rows: 2 }}>
Ant Design, a design language for background applications, is refined by Ant UED Team.
</Base>,
);
expect(wrapper.find('EllipsisTooltip').prop('isEllipsis')).toBeTruthy();
mockRectSpy.mockRestore();
});
}); });

View File

@ -322,7 +322,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
delete rcUploadProps.id; delete rcUploadProps.id;
} }
const renderUploadList = (button?: React.ReactNode) => const renderUploadList = (button?: React.ReactNode, buttonVisible?: boolean) =>
showUploadList ? ( showUploadList ? (
<LocaleReceiver componentName="Upload" defaultLocale={defaultLocale.Upload}> <LocaleReceiver componentName="Upload" defaultLocale={defaultLocale.Upload}>
{(locale: UploadLocale) => { {(locale: UploadLocale) => {
@ -354,6 +354,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
isImageUrl={isImageUrl} isImageUrl={isImageUrl}
progress={progress} progress={progress}
appendAction={button} appendAction={button}
appendActionVisible={buttonVisible}
itemRender={itemRender} itemRender={itemRender}
/> />
); );
@ -400,8 +401,8 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',
}); });
const uploadButton = ( const renderUploadButton = (uploadButtonStyle?: React.CSSProperties) => (
<div className={uploadButtonCls} style={children ? undefined : { display: 'none' }}> <div className={uploadButtonCls} style={uploadButtonStyle}>
<RcUpload {...rcUploadProps} ref={upload} /> <RcUpload {...rcUploadProps} ref={upload} />
</div> </div>
); );
@ -409,14 +410,14 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
if (listType === 'picture-card') { if (listType === 'picture-card') {
return ( return (
<span className={classNames(`${prefixCls}-picture-card-wrapper`, className)}> <span className={classNames(`${prefixCls}-picture-card-wrapper`, className)}>
{renderUploadList(uploadButton)} {renderUploadList(renderUploadButton(), !!children)}
</span> </span>
); );
} }
return ( return (
<span className={className}> <span className={className}>
{uploadButton} {renderUploadButton(children ? undefined : { display: 'none' })}
{renderUploadList()} {renderUploadList()}
</span> </span>
); );

View File

@ -42,6 +42,7 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
downloadIcon, downloadIcon,
progress, progress,
appendAction, appendAction,
appendActionVisible,
itemRender, itemRender,
}, },
ref, ref,
@ -225,12 +226,14 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
{/* Append action */} {/* Append action */}
{appendAction && ( {appendAction && (
<CSSMotion {...motionConfig}> <CSSMotion {...motionConfig} visible={appendActionVisible} forceRender>
{({ className: motionClassName, style: motionStyle }) => {({ className: motionClassName, style: motionStyle }) =>
cloneElement(appendAction, oriProps => ({ cloneElement(appendAction, oriProps => ({
className: classNames(oriProps.className, motionClassName), className: classNames(oriProps.className, motionClassName),
style: { style: {
...motionStyle, ...motionStyle,
// prevent the element has hover css pseudo-class that may cause animation to end prematurely.
pointerEvents: motionClassName ? 'none' : undefined,
...oriProps.style, ...oriProps.style,
}, },
})) }))
@ -254,6 +257,7 @@ UploadList.defaultProps = {
showRemoveIcon: true, showRemoveIcon: true,
showDownloadIcon: false, showDownloadIcon: false,
showPreviewIcon: true, showPreviewIcon: true,
appendActionVisible: true,
previewFile: previewImage, previewFile: previewImage,
isImageUrl, isImageUrl,
}; };

View File

@ -867,4 +867,26 @@ describe('Upload', () => {
expect(onChange.mock.calls[0][0].fileList).toHaveLength(1); 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(
<Upload listType="picture-card">
<button type="button">upload</button>
</Upload>,
);
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');
});
}); });

View File

@ -155,5 +155,6 @@ export interface UploadListProps<T = any> {
iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode; iconRender?: (file: UploadFile<T>, listType?: UploadListType) => React.ReactNode;
isImageUrl?: (file: UploadFile) => boolean; isImageUrl?: (file: UploadFile) => boolean;
appendAction?: React.ReactNode; appendAction?: React.ReactNode;
appendActionVisible?: boolean;
itemRender?: ItemRender<T>; itemRender?: ItemRender<T>;
} }