mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-06 00:44:17 +08:00
feat: Modal hooks confirm function support await (#43470)
* docs: move demo to useModal * docs: reorder * chore: update ts def * test: test case * chore: fix compile
This commit is contained in:
parent
46db582197
commit
b072d3a02c
@ -14,6 +14,11 @@ export interface ActionButtonProps {
|
||||
emitEvent?: boolean;
|
||||
quitOnNullishReturnValue?: boolean;
|
||||
children?: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Do not throw if is await mode
|
||||
*/
|
||||
isSilent?: () => boolean;
|
||||
}
|
||||
|
||||
function isThenable<T extends any>(thing?: PromiseLike<T>): boolean {
|
||||
@ -29,6 +34,7 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
||||
close,
|
||||
autoFocus,
|
||||
emitEvent,
|
||||
isSilent,
|
||||
quitOnNullishReturnValue,
|
||||
actionFn,
|
||||
} = props;
|
||||
@ -70,6 +76,12 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
||||
// See: https://github.com/ant-design/ant-design/issues/6183
|
||||
setLoading(false, true);
|
||||
clickedRef.current = false;
|
||||
|
||||
// Do not throw if is `await` mode
|
||||
if (isSilent?.()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject(e);
|
||||
},
|
||||
);
|
||||
|
@ -16,6 +16,12 @@ import type { ModalFuncProps, ModalLocale } from './interface';
|
||||
interface ConfirmDialogProps extends ModalFuncProps {
|
||||
afterClose?: () => void;
|
||||
close?: (...args: any[]) => void;
|
||||
/**
|
||||
* `close` prop support `...args` that pass to the developer
|
||||
* that we can not break this.
|
||||
* Provider `onClose` for internal usage
|
||||
*/
|
||||
onConfirm?: (confirmed: boolean) => void;
|
||||
autoFocusButton?: null | 'ok' | 'cancel';
|
||||
rootPrefixCls: string;
|
||||
iconPrefixCls?: string;
|
||||
@ -23,6 +29,11 @@ interface ConfirmDialogProps extends ModalFuncProps {
|
||||
|
||||
/** @private Internal Usage. Do not override this */
|
||||
locale?: ModalLocale;
|
||||
|
||||
/**
|
||||
* Do not throw if is await mode
|
||||
*/
|
||||
isSilent?: () => boolean;
|
||||
}
|
||||
|
||||
export function ConfirmContent(
|
||||
@ -35,6 +46,8 @@ export function ConfirmContent(
|
||||
onCancel,
|
||||
onOk,
|
||||
close,
|
||||
onConfirm,
|
||||
isSilent,
|
||||
okText,
|
||||
okButtonProps,
|
||||
cancelText,
|
||||
@ -89,8 +102,12 @@ export function ConfirmContent(
|
||||
|
||||
const cancelButton = mergedOkCancel && (
|
||||
<ActionButton
|
||||
isSilent={isSilent}
|
||||
actionFn={onCancel}
|
||||
close={close}
|
||||
close={(...args: any[]) => {
|
||||
close?.(...args);
|
||||
onConfirm?.(false);
|
||||
}}
|
||||
autoFocus={autoFocusButton === 'cancel'}
|
||||
buttonProps={cancelButtonProps}
|
||||
prefixCls={`${rootPrefixCls}-btn`}
|
||||
@ -112,9 +129,13 @@ export function ConfirmContent(
|
||||
<div className={`${confirmPrefixCls}-btns`}>
|
||||
{cancelButton}
|
||||
<ActionButton
|
||||
isSilent={isSilent}
|
||||
type={okType}
|
||||
actionFn={onOk}
|
||||
close={close}
|
||||
close={(...args: any[]) => {
|
||||
close?.(...args);
|
||||
onConfirm?.(true);
|
||||
}}
|
||||
autoFocus={autoFocusButton === 'ok'}
|
||||
buttonProps={okButtonProps}
|
||||
prefixCls={`${rootPrefixCls}-btn`}
|
||||
|
@ -367,4 +367,48 @@ describe('Modal.hook', () => {
|
||||
|
||||
expect(afterClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('support await', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
let notReady = true;
|
||||
let lastResult: boolean | null = null;
|
||||
|
||||
const Demo = () => {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
lastResult = await modal.confirm({
|
||||
content: <Input />,
|
||||
onOk: async () => {
|
||||
if (notReady) {
|
||||
notReady = false;
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return contextHolder;
|
||||
};
|
||||
|
||||
render(<Demo />);
|
||||
|
||||
// Wait for modal show
|
||||
await waitFakeTimer();
|
||||
|
||||
// First time click should not close
|
||||
fireEvent.click(document.querySelector('.ant-btn-primary')!);
|
||||
await waitFakeTimer();
|
||||
expect(lastResult).toBeFalsy();
|
||||
|
||||
// Second time click to close
|
||||
fireEvent.click(document.querySelector('.ant-btn-primary')!);
|
||||
await waitFakeTimer();
|
||||
expect(lastResult).toBeTruthy();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`。
|
||||
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`。其中仅有 hooks 方法支持 Promise `await` 操作。
|
||||
|
||||
## en-US
|
||||
|
||||
Use `Modal.useModal` to get `contextHolder` with context accessible issue.
|
||||
Use `Modal.useModal` to get `contextHolder` with context accessible issue. Only hooks method support Promise `await` operation.
|
||||
|
@ -22,8 +22,9 @@ const App: React.FC = () => {
|
||||
<ReachableContext.Provider value="Light">
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modal.confirm(config);
|
||||
onClick={async () => {
|
||||
const confirmed = await modal.confirm(config);
|
||||
console.log('Confirmed: ', confirmed);
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
@ -36,14 +37,14 @@ const App: React.FC = () => {
|
||||
Warning
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
modal.info(config);
|
||||
}}
|
||||
>
|
||||
Info
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
modal.error(config);
|
||||
}}
|
||||
>
|
||||
|
@ -22,16 +22,16 @@ Additionally, if you need show a simple confirmation dialog, you can use [`App.u
|
||||
<code src="./demo/basic.tsx">Basic</code>
|
||||
<code src="./demo/async.tsx">Asynchronously close</code>
|
||||
<code src="./demo/footer.tsx">Customized Footer</code>
|
||||
<code src="./demo/confirm.tsx">Confirmation modal dialog</code>
|
||||
<code src="./demo/hooks.tsx">Use hooks to get context</code>
|
||||
<code src="./demo/locale.tsx">Internationalization</code>
|
||||
<code src="./demo/manual.tsx">Manual to update destroy</code>
|
||||
<code src="./demo/position.tsx">To customize the position of modal</code>
|
||||
<code src="./demo/dark.tsx" debug>Dark Bg</code>
|
||||
<code src="./demo/button-props.tsx">Customize footer buttons props</code>
|
||||
<code src="./demo/hooks.tsx">Use hooks to get context</code>
|
||||
<code src="./demo/modal-render.tsx">Custom modal content render</code>
|
||||
<code src="./demo/width.tsx">To customize the width of modal</code>
|
||||
<code src="./demo/static-info.tsx">Static Method</code>
|
||||
<code src="./demo/confirm.tsx">Static confirmation</code>
|
||||
<code src="./demo/confirm-router.tsx">destroy confirmation modal dialog</code>
|
||||
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
|
||||
<code src="./demo/custom-mouse-position.tsx" debug>Control modal's animation origin position</code>
|
||||
@ -167,6 +167,17 @@ React.useEffect(() => {
|
||||
return <div>{contextHolder}</div>;
|
||||
```
|
||||
|
||||
`modal.confirm` return method:
|
||||
|
||||
- `destroy`: Destroy current modal
|
||||
- `update`: Update current modal
|
||||
- `then`: (Hooks only) Promise chain call, support `await` operation
|
||||
|
||||
```tsx
|
||||
// Return `true` when click `onOk` and `false` when click `onCancel`
|
||||
const confirmed = await modal.confirm({ ... });
|
||||
```
|
||||
|
||||
## Design Token
|
||||
|
||||
<ComponentTokenTable component="Modal"></ComponentTokenTable>
|
||||
|
@ -23,16 +23,16 @@ demo:
|
||||
<code src="./demo/basic.tsx">基本</code>
|
||||
<code src="./demo/async.tsx">异步关闭</code>
|
||||
<code src="./demo/footer.tsx">自定义页脚</code>
|
||||
<code src="./demo/confirm.tsx">确认对话框</code>
|
||||
<code src="./demo/hooks.tsx">使用 hooks 获得上下文</code>
|
||||
<code src="./demo/locale.tsx">国际化</code>
|
||||
<code src="./demo/manual.tsx">手动更新和移除</code>
|
||||
<code src="./demo/position.tsx">自定义位置</code>
|
||||
<code src="./demo/dark.tsx" debug>暗背景</code>
|
||||
<code src="./demo/button-props.tsx">自定义页脚按钮属性</code>
|
||||
<code src="./demo/hooks.tsx">使用 hooks 获得上下文</code>
|
||||
<code src="./demo/modal-render.tsx">自定义渲染对话框</code>
|
||||
<code src="./demo/width.tsx">自定义模态的宽度</code>
|
||||
<code src="./demo/static-info.tsx">静态方法</code>
|
||||
<code src="./demo/confirm.tsx">静态确认对话框</code>
|
||||
<code src="./demo/confirm-router.tsx">销毁确认对话框</code>
|
||||
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
|
||||
<code src="./demo/custom-mouse-position.tsx" debug>控制弹框动画原点</code>
|
||||
@ -168,6 +168,17 @@ React.useEffect(() => {
|
||||
return <div>{contextHolder}</div>;
|
||||
```
|
||||
|
||||
`modal.confirm` 返回方法:
|
||||
|
||||
- `destroy`:销毁当前窗口
|
||||
- `update`:更新当前窗口
|
||||
- `then`:Promise 链式调用,支持 `await` 操作。该方法为 Hooks 仅有
|
||||
|
||||
```tsx
|
||||
//点击 `onOk` 时返回 `true`,点击 `onCancel` 时返回 `false`
|
||||
const confirmed = await modal.confirm({ ... });
|
||||
```
|
||||
|
||||
## Design Token
|
||||
|
||||
<ComponentTokenTable component="Modal"></ComponentTokenTable>
|
||||
|
@ -8,6 +8,11 @@ import type { ModalFuncProps } from '../interface';
|
||||
export interface HookModalProps {
|
||||
afterClose: () => void;
|
||||
config: ModalFuncProps;
|
||||
onConfirm?: (confirmed: boolean) => void;
|
||||
/**
|
||||
* Do not throw if is await mode
|
||||
*/
|
||||
isSilent?: () => boolean;
|
||||
}
|
||||
|
||||
export interface HookModalRef {
|
||||
@ -16,7 +21,7 @@ export interface HookModalRef {
|
||||
}
|
||||
|
||||
const HookModal: React.ForwardRefRenderFunction<HookModalRef, HookModalProps> = (
|
||||
{ afterClose: hookAfterClose, config },
|
||||
{ afterClose: hookAfterClose, config, ...restProps },
|
||||
ref,
|
||||
) => {
|
||||
const [open, setOpen] = React.useState(true);
|
||||
@ -66,6 +71,7 @@ const HookModal: React.ForwardRefRenderFunction<HookModalRef, HookModalProps> =
|
||||
}
|
||||
direction={innerConfig.direction || direction}
|
||||
cancelText={innerConfig.cancelText || contextLocale?.cancelText}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import usePatchElement from '../../_util/hooks/usePatchElement';
|
||||
import type { ModalStaticFunctions } from '../confirm';
|
||||
import type { ModalFunc, ModalStaticFunctions } from '../confirm';
|
||||
import { withConfirm, withError, withInfo, withSuccess, withWarn } from '../confirm';
|
||||
import destroyFns from '../destroyFns';
|
||||
import type { ModalFuncProps } from '../interface';
|
||||
@ -13,6 +13,13 @@ interface ElementsHolderRef {
|
||||
patchElement: ReturnType<typeof usePatchElement>[1];
|
||||
}
|
||||
|
||||
// Add `then` field for `ModalFunc` return instance.
|
||||
export type ModalFuncWithPromise = (...args: Parameters<ModalFunc>) => ReturnType<ModalFunc> & {
|
||||
then<T>(resolve: (confirmed: boolean) => T, reject: VoidFunction): Promise<T>;
|
||||
};
|
||||
|
||||
export type HookAPI = Omit<Record<keyof ModalStaticFunctions, ModalFuncWithPromise>, 'warn'>;
|
||||
|
||||
const ElementsHolder = React.memo(
|
||||
React.forwardRef<ElementsHolderRef>((_props, ref) => {
|
||||
const [elements, patchElement] = usePatchElement();
|
||||
@ -28,10 +35,7 @@ const ElementsHolder = React.memo(
|
||||
}),
|
||||
);
|
||||
|
||||
function useModal(): readonly [
|
||||
instance: Omit<ModalStaticFunctions, 'warn'>,
|
||||
contextHolder: React.ReactElement,
|
||||
] {
|
||||
function useModal(): readonly [instance: HookAPI, contextHolder: React.ReactElement] {
|
||||
const holderRef = React.useRef<ElementsHolderRef>(null);
|
||||
|
||||
// ========================== Effect ==========================
|
||||
@ -56,6 +60,13 @@ function useModal(): readonly [
|
||||
|
||||
const modalRef = React.createRef<HookModalRef>();
|
||||
|
||||
// Proxy to promise with `onClose`
|
||||
let resolvePromise: (confirmed: boolean) => void;
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
let silent = false;
|
||||
|
||||
let closeFunc: Function | undefined;
|
||||
const modal = (
|
||||
<HookModal
|
||||
@ -65,6 +76,10 @@ function useModal(): readonly [
|
||||
afterClose={() => {
|
||||
closeFunc?.();
|
||||
}}
|
||||
isSilent={() => silent}
|
||||
onConfirm={(confirmed) => {
|
||||
resolvePromise(confirmed);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -74,7 +89,7 @@ function useModal(): readonly [
|
||||
destroyFns.push(closeFunc);
|
||||
}
|
||||
|
||||
return {
|
||||
const instance: ReturnType<ModalFuncWithPromise> = {
|
||||
destroy: () => {
|
||||
function destroyAction() {
|
||||
modalRef.current?.destroy();
|
||||
@ -97,12 +112,18 @@ function useModal(): readonly [
|
||||
setActionQueue((prev) => [...prev, updateAction]);
|
||||
}
|
||||
},
|
||||
then: (resolve) => {
|
||||
silent = true;
|
||||
return promise.then(resolve);
|
||||
},
|
||||
};
|
||||
|
||||
return instance;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fns = React.useMemo<Omit<ModalStaticFunctions, 'warn'>>(
|
||||
const fns = React.useMemo<HookAPI>(
|
||||
() => ({
|
||||
info: getConfirmFunc(withInfo),
|
||||
success: getConfirmFunc(withSuccess),
|
||||
|
Loading…
Reference in New Issue
Block a user