feat(module:popconfirm): support closing based on promise (#30871)

* feat(module:popconfirm): support closing based on promise

fix: typos

fix: fix tests

fix: fix ActionButton for Popconfirm

* chore: cleanup

* test: add test
This commit is contained in:
Wendell 2021-06-09 12:18:52 +08:00 committed by GitHub
parent 2e65f276de
commit 5ddd9e6b97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 109 additions and 28 deletions

View File

@ -5,10 +5,16 @@ import { LegacyButtonType, ButtonProps, convertLegacyProps } from '../button/but
export interface ActionButtonProps {
type?: LegacyButtonType;
actionFn?: (...args: any[]) => any | PromiseLike<any>;
closeModal: Function;
close: Function;
autoFocus?: boolean;
prefixCls: string;
buttonProps?: ButtonProps;
emitEvent?: boolean;
quitOnNullishReturnValue?: boolean;
}
function isThenable(thing?: PromiseLike<any>): boolean {
return !!(thing && !!thing.then);
}
const ActionButton: React.FC<ActionButtonProps> = props => {
@ -30,16 +36,16 @@ const ActionButton: React.FC<ActionButtonProps> = props => {
}, []);
const handlePromiseOnOk = (returnValueOfOnOk?: PromiseLike<any>) => {
const { closeModal } = props;
if (!returnValueOfOnOk || !returnValueOfOnOk.then) {
const { close } = props;
if (!isThenable(returnValueOfOnOk)) {
return;
}
setLoading(true);
returnValueOfOnOk.then(
returnValueOfOnOk!.then(
(...args: any[]) => {
// It's unnecessary to set loading=false, for the Modal will be unmounted after close.
// setState({ loading: false });
closeModal(...args);
setLoading(false);
close(...args);
clickedRef.current = false;
},
(e: Error) => {
// Emit error when catch promise reject
@ -52,25 +58,32 @@ const ActionButton: React.FC<ActionButtonProps> = props => {
);
};
const onClick = () => {
const { actionFn, closeModal } = props;
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const { actionFn, close } = props;
if (clickedRef.current) {
return;
}
clickedRef.current = true;
if (!actionFn) {
closeModal();
close();
return;
}
let returnValueOfOnOk;
if (actionFn.length) {
returnValueOfOnOk = actionFn(closeModal);
if (props.emitEvent) {
returnValueOfOnOk = actionFn(e);
if (props.quitOnNullishReturnValue && !isThenable(returnValueOfOnOk)) {
clickedRef.current = false;
close(e);
return;
}
} else if (actionFn.length) {
returnValueOfOnOk = actionFn(close);
// https://github.com/ant-design/ant-design/issues/23358
clickedRef.current = false;
} else {
returnValueOfOnOk = actionFn();
if (!returnValueOfOnOk) {
closeModal();
close();
return;
}
}

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import classNames from 'classnames';
import Dialog, { ModalFuncProps } from './Modal';
import ActionButton from './ActionButton';
import ActionButton from '../_util/ActionButton';
import devWarning from '../_util/devWarning';
import ConfigProvider from '../config-provider';
import { getTransitionName } from '../_util/motion';
@ -68,7 +68,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
const cancelButton = okCancel && (
<ActionButton
actionFn={onCancel}
closeModal={close}
close={close}
autoFocus={autoFocusButton === 'cancel'}
buttonProps={cancelButtonProps}
prefixCls={`${rootPrefixCls}-btn`}
@ -118,7 +118,7 @@ const ConfirmDialog = (props: ConfirmDialogProps) => {
<ActionButton
type={okType}
actionFn={onOk}
closeModal={close}
close={close}
autoFocus={autoFocusButton === 'ok'}
buttonProps={okButtonProps}
prefixCls={`${rootPrefixCls}-btn`}

View File

@ -11,7 +11,7 @@ title:
## en-US
Asynchronously close a modal dialog when a the OK button is pressed. For example, you can use this pattern when you submit a form.
Asynchronously close a modal dialog when the OK button is pressed. For example, you can use this pattern when you submit a form.
```jsx
import { Modal, Button } from 'antd';

View File

@ -9,7 +9,6 @@ import confirm, {
modalGlobalConfig,
} from './confirm';
export { ActionButtonProps } from './ActionButton';
export { ModalProps, ModalFuncProps } from './Modal';
function modalWarn(props: ModalFuncProps) {

View File

@ -179,3 +179,14 @@ exports[`renders ./components/popconfirm/demo/placement.md correctly 1`] = `
</div>
</div>
`;
exports[`renders ./components/popconfirm/demo/promise.md correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Popconfirm with Promise
</span>
</button>
`;

View File

@ -131,6 +131,24 @@ describe('Popconfirm', () => {
expect(onVisibleChange).toHaveBeenLastCalledWith(false, eventObject);
});
it('should support onConfirm to return Promise', async () => {
const confirm = () => new Promise(res => setTimeout(res, 300));
const onVisibleChange = jest.fn();
const popconfirm = mount(
<Popconfirm title="code" onConfirm={confirm} onVisibleChange={onVisibleChange}>
<span>show me your code</span>
</Popconfirm>,
);
const triggerNode = popconfirm.find('span').at(0);
triggerNode.simulate('click');
expect(onVisibleChange).toHaveBeenCalledTimes(1);
popconfirm.find('.ant-btn').at(0).simulate('click');
await sleep(400);
expect(onVisibleChange).toHaveBeenCalledWith(false, eventObject);
});
it('should support customize icon', () => {
const wrapper = mount(
<Popconfirm title="code" icon={<span className="customize-icon">custom-icon</span>}>

View File

@ -0,0 +1,37 @@
---
order: 7
title:
zh-CN: 基于 Promise 的异步关闭
en-US: Asynchronously close on Promise
---
## zh-CN
点击确定后异步关闭 Popconfirm例如提交表单。
## en-US
Asynchronously close a popconfirm when the OK button is pressed. For example, you can use this pattern when you submit a form.
```jsx
import { Button, Popconfirm } from 'antd';
const App = () => {
const confirm = () =>
new Promise(resolve => {
setTimeout(() => resolve(), 3000);
});
return (
<Popconfirm
title="Title"
onConfirm={confirm}
onVisibleChange={() => console.log('visible change')}
>
<Button type="primary">Open Popconfirm with Promise</Button>
</Popconfirm>
);
};
ReactDOM.render(<App />, mountNode);
```

View File

@ -12,6 +12,7 @@ import { ConfigContext } from '../config-provider';
import { getRenderPropValue, RenderFunction } from '../_util/getRenderPropValue';
import { cloneElement } from '../_util/reactNode';
import { getTransitionName } from '../_util/motion';
import ActionButton from '../_util/ActionButton';
export interface PopconfirmProps extends AbstractTooltipProps {
title: React.ReactNode | RenderFunction;
@ -40,6 +41,7 @@ export interface PopconfirmLocale {
}
const Popconfirm = React.forwardRef<unknown, PopconfirmProps>((props, ref) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const [visible, setVisible] = useMergedState(false, {
value: props.visible,
defaultValue: props.defaultVisible,
@ -54,11 +56,12 @@ const Popconfirm = React.forwardRef<unknown, PopconfirmProps>((props, ref) => {
props.onVisibleChange?.(value, e);
};
const onConfirm = (e: React.MouseEvent<HTMLButtonElement>) => {
const close = (e: React.MouseEvent<HTMLButtonElement>) => {
settingVisible(false, e);
props.onConfirm?.call(this, e);
};
const onConfirm = (e: React.MouseEvent<HTMLButtonElement>) => props.onConfirm?.call(this, e);
const onCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
settingVisible(false, e);
props.onCancel?.call(this, e);
@ -90,21 +93,21 @@ const Popconfirm = React.forwardRef<unknown, PopconfirmProps>((props, ref) => {
<Button onClick={onCancel} size="small" {...cancelButtonProps}>
{cancelText || popconfirmLocale.cancelText}
</Button>
<Button
onClick={onConfirm}
{...convertLegacyProps(okType)}
size="small"
{...okButtonProps}
<ActionButton
buttonProps={{ size: 'small', ...convertLegacyProps(okType), ...okButtonProps }}
actionFn={onConfirm}
close={close}
prefixCls={getPrefixCls('btn')}
quitOnNullishReturnValue
emitEvent
>
{okText || popconfirmLocale.okText}
</Button>
</ActionButton>
</div>
</div>
);
};
const { getPrefixCls } = React.useContext(ConfigContext);
const {
prefixCls: customizePrefixCls,
placement,