mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-07 17:44:35 +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;
|
emitEvent?: boolean;
|
||||||
quitOnNullishReturnValue?: boolean;
|
quitOnNullishReturnValue?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not throw if is await mode
|
||||||
|
*/
|
||||||
|
isSilent?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isThenable<T extends any>(thing?: PromiseLike<T>): boolean {
|
function isThenable<T extends any>(thing?: PromiseLike<T>): boolean {
|
||||||
@ -29,6 +34,7 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
|||||||
close,
|
close,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
emitEvent,
|
emitEvent,
|
||||||
|
isSilent,
|
||||||
quitOnNullishReturnValue,
|
quitOnNullishReturnValue,
|
||||||
actionFn,
|
actionFn,
|
||||||
} = props;
|
} = props;
|
||||||
@ -70,6 +76,12 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
|||||||
// See: https://github.com/ant-design/ant-design/issues/6183
|
// See: https://github.com/ant-design/ant-design/issues/6183
|
||||||
setLoading(false, true);
|
setLoading(false, true);
|
||||||
clickedRef.current = false;
|
clickedRef.current = false;
|
||||||
|
|
||||||
|
// Do not throw if is `await` mode
|
||||||
|
if (isSilent?.()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(e);
|
return Promise.reject(e);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -16,6 +16,12 @@ import type { ModalFuncProps, ModalLocale } from './interface';
|
|||||||
interface ConfirmDialogProps extends ModalFuncProps {
|
interface ConfirmDialogProps extends ModalFuncProps {
|
||||||
afterClose?: () => void;
|
afterClose?: () => void;
|
||||||
close?: (...args: any[]) => 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';
|
autoFocusButton?: null | 'ok' | 'cancel';
|
||||||
rootPrefixCls: string;
|
rootPrefixCls: string;
|
||||||
iconPrefixCls?: string;
|
iconPrefixCls?: string;
|
||||||
@ -23,6 +29,11 @@ interface ConfirmDialogProps extends ModalFuncProps {
|
|||||||
|
|
||||||
/** @private Internal Usage. Do not override this */
|
/** @private Internal Usage. Do not override this */
|
||||||
locale?: ModalLocale;
|
locale?: ModalLocale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not throw if is await mode
|
||||||
|
*/
|
||||||
|
isSilent?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfirmContent(
|
export function ConfirmContent(
|
||||||
@ -35,6 +46,8 @@ export function ConfirmContent(
|
|||||||
onCancel,
|
onCancel,
|
||||||
onOk,
|
onOk,
|
||||||
close,
|
close,
|
||||||
|
onConfirm,
|
||||||
|
isSilent,
|
||||||
okText,
|
okText,
|
||||||
okButtonProps,
|
okButtonProps,
|
||||||
cancelText,
|
cancelText,
|
||||||
@ -89,8 +102,12 @@ export function ConfirmContent(
|
|||||||
|
|
||||||
const cancelButton = mergedOkCancel && (
|
const cancelButton = mergedOkCancel && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
isSilent={isSilent}
|
||||||
actionFn={onCancel}
|
actionFn={onCancel}
|
||||||
close={close}
|
close={(...args: any[]) => {
|
||||||
|
close?.(...args);
|
||||||
|
onConfirm?.(false);
|
||||||
|
}}
|
||||||
autoFocus={autoFocusButton === 'cancel'}
|
autoFocus={autoFocusButton === 'cancel'}
|
||||||
buttonProps={cancelButtonProps}
|
buttonProps={cancelButtonProps}
|
||||||
prefixCls={`${rootPrefixCls}-btn`}
|
prefixCls={`${rootPrefixCls}-btn`}
|
||||||
@ -112,9 +129,13 @@ export function ConfirmContent(
|
|||||||
<div className={`${confirmPrefixCls}-btns`}>
|
<div className={`${confirmPrefixCls}-btns`}>
|
||||||
{cancelButton}
|
{cancelButton}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
isSilent={isSilent}
|
||||||
type={okType}
|
type={okType}
|
||||||
actionFn={onOk}
|
actionFn={onOk}
|
||||||
close={close}
|
close={(...args: any[]) => {
|
||||||
|
close?.(...args);
|
||||||
|
onConfirm?.(true);
|
||||||
|
}}
|
||||||
autoFocus={autoFocusButton === 'ok'}
|
autoFocus={autoFocusButton === 'ok'}
|
||||||
buttonProps={okButtonProps}
|
buttonProps={okButtonProps}
|
||||||
prefixCls={`${rootPrefixCls}-btn`}
|
prefixCls={`${rootPrefixCls}-btn`}
|
||||||
|
@ -367,4 +367,48 @@ describe('Modal.hook', () => {
|
|||||||
|
|
||||||
expect(afterClose).toHaveBeenCalledTimes(1);
|
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
|
## zh-CN
|
||||||
|
|
||||||
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`。
|
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`。其中仅有 hooks 方法支持 Promise `await` 操作。
|
||||||
|
|
||||||
## en-US
|
## 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">
|
<ReachableContext.Provider value="Light">
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
modal.confirm(config);
|
const confirmed = await modal.confirm(config);
|
||||||
|
console.log('Confirmed: ', confirmed);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
@ -36,14 +37,14 @@ const App: React.FC = () => {
|
|||||||
Warning
|
Warning
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
modal.info(config);
|
modal.info(config);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Info
|
Info
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
modal.error(config);
|
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/basic.tsx">Basic</code>
|
||||||
<code src="./demo/async.tsx">Asynchronously close</code>
|
<code src="./demo/async.tsx">Asynchronously close</code>
|
||||||
<code src="./demo/footer.tsx">Customized Footer</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/locale.tsx">Internationalization</code>
|
||||||
<code src="./demo/manual.tsx">Manual to update destroy</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/position.tsx">To customize the position of modal</code>
|
||||||
<code src="./demo/dark.tsx" debug>Dark Bg</code>
|
<code src="./demo/dark.tsx" debug>Dark Bg</code>
|
||||||
<code src="./demo/button-props.tsx">Customize footer buttons props</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/modal-render.tsx">Custom modal content render</code>
|
||||||
<code src="./demo/width.tsx">To customize the width of modal</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/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/confirm-router.tsx">destroy confirmation modal dialog</code>
|
||||||
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</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>
|
<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>;
|
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
|
## Design Token
|
||||||
|
|
||||||
<ComponentTokenTable component="Modal"></ComponentTokenTable>
|
<ComponentTokenTable component="Modal"></ComponentTokenTable>
|
||||||
|
@ -23,16 +23,16 @@ demo:
|
|||||||
<code src="./demo/basic.tsx">基本</code>
|
<code src="./demo/basic.tsx">基本</code>
|
||||||
<code src="./demo/async.tsx">异步关闭</code>
|
<code src="./demo/async.tsx">异步关闭</code>
|
||||||
<code src="./demo/footer.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/locale.tsx">国际化</code>
|
||||||
<code src="./demo/manual.tsx">手动更新和移除</code>
|
<code src="./demo/manual.tsx">手动更新和移除</code>
|
||||||
<code src="./demo/position.tsx">自定义位置</code>
|
<code src="./demo/position.tsx">自定义位置</code>
|
||||||
<code src="./demo/dark.tsx" debug>暗背景</code>
|
<code src="./demo/dark.tsx" debug>暗背景</code>
|
||||||
<code src="./demo/button-props.tsx">自定义页脚按钮属性</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/modal-render.tsx">自定义渲染对话框</code>
|
||||||
<code src="./demo/width.tsx">自定义模态的宽度</code>
|
<code src="./demo/width.tsx">自定义模态的宽度</code>
|
||||||
<code src="./demo/static-info.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/confirm-router.tsx">销毁确认对话框</code>
|
||||||
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
|
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
|
||||||
<code src="./demo/custom-mouse-position.tsx" debug>控制弹框动画原点</code>
|
<code src="./demo/custom-mouse-position.tsx" debug>控制弹框动画原点</code>
|
||||||
@ -168,6 +168,17 @@ React.useEffect(() => {
|
|||||||
return <div>{contextHolder}</div>;
|
return <div>{contextHolder}</div>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`modal.confirm` 返回方法:
|
||||||
|
|
||||||
|
- `destroy`:销毁当前窗口
|
||||||
|
- `update`:更新当前窗口
|
||||||
|
- `then`:Promise 链式调用,支持 `await` 操作。该方法为 Hooks 仅有
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
//点击 `onOk` 时返回 `true`,点击 `onCancel` 时返回 `false`
|
||||||
|
const confirmed = await modal.confirm({ ... });
|
||||||
|
```
|
||||||
|
|
||||||
## Design Token
|
## Design Token
|
||||||
|
|
||||||
<ComponentTokenTable component="Modal"></ComponentTokenTable>
|
<ComponentTokenTable component="Modal"></ComponentTokenTable>
|
||||||
|
@ -8,6 +8,11 @@ import type { ModalFuncProps } from '../interface';
|
|||||||
export interface HookModalProps {
|
export interface HookModalProps {
|
||||||
afterClose: () => void;
|
afterClose: () => void;
|
||||||
config: ModalFuncProps;
|
config: ModalFuncProps;
|
||||||
|
onConfirm?: (confirmed: boolean) => void;
|
||||||
|
/**
|
||||||
|
* Do not throw if is await mode
|
||||||
|
*/
|
||||||
|
isSilent?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HookModalRef {
|
export interface HookModalRef {
|
||||||
@ -16,7 +21,7 @@ export interface HookModalRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HookModal: React.ForwardRefRenderFunction<HookModalRef, HookModalProps> = (
|
const HookModal: React.ForwardRefRenderFunction<HookModalRef, HookModalProps> = (
|
||||||
{ afterClose: hookAfterClose, config },
|
{ afterClose: hookAfterClose, config, ...restProps },
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const [open, setOpen] = React.useState(true);
|
const [open, setOpen] = React.useState(true);
|
||||||
@ -66,6 +71,7 @@ const HookModal: React.ForwardRefRenderFunction<HookModalRef, HookModalProps> =
|
|||||||
}
|
}
|
||||||
direction={innerConfig.direction || direction}
|
direction={innerConfig.direction || direction}
|
||||||
cancelText={innerConfig.cancelText || contextLocale?.cancelText}
|
cancelText={innerConfig.cancelText || contextLocale?.cancelText}
|
||||||
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import usePatchElement from '../../_util/hooks/usePatchElement';
|
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 { withConfirm, withError, withInfo, withSuccess, withWarn } from '../confirm';
|
||||||
import destroyFns from '../destroyFns';
|
import destroyFns from '../destroyFns';
|
||||||
import type { ModalFuncProps } from '../interface';
|
import type { ModalFuncProps } from '../interface';
|
||||||
@ -13,6 +13,13 @@ interface ElementsHolderRef {
|
|||||||
patchElement: ReturnType<typeof usePatchElement>[1];
|
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(
|
const ElementsHolder = React.memo(
|
||||||
React.forwardRef<ElementsHolderRef>((_props, ref) => {
|
React.forwardRef<ElementsHolderRef>((_props, ref) => {
|
||||||
const [elements, patchElement] = usePatchElement();
|
const [elements, patchElement] = usePatchElement();
|
||||||
@ -28,10 +35,7 @@ const ElementsHolder = React.memo(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
function useModal(): readonly [
|
function useModal(): readonly [instance: HookAPI, contextHolder: React.ReactElement] {
|
||||||
instance: Omit<ModalStaticFunctions, 'warn'>,
|
|
||||||
contextHolder: React.ReactElement,
|
|
||||||
] {
|
|
||||||
const holderRef = React.useRef<ElementsHolderRef>(null);
|
const holderRef = React.useRef<ElementsHolderRef>(null);
|
||||||
|
|
||||||
// ========================== Effect ==========================
|
// ========================== Effect ==========================
|
||||||
@ -56,6 +60,13 @@ function useModal(): readonly [
|
|||||||
|
|
||||||
const modalRef = React.createRef<HookModalRef>();
|
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;
|
let closeFunc: Function | undefined;
|
||||||
const modal = (
|
const modal = (
|
||||||
<HookModal
|
<HookModal
|
||||||
@ -65,6 +76,10 @@ function useModal(): readonly [
|
|||||||
afterClose={() => {
|
afterClose={() => {
|
||||||
closeFunc?.();
|
closeFunc?.();
|
||||||
}}
|
}}
|
||||||
|
isSilent={() => silent}
|
||||||
|
onConfirm={(confirmed) => {
|
||||||
|
resolvePromise(confirmed);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -74,7 +89,7 @@ function useModal(): readonly [
|
|||||||
destroyFns.push(closeFunc);
|
destroyFns.push(closeFunc);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const instance: ReturnType<ModalFuncWithPromise> = {
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
function destroyAction() {
|
function destroyAction() {
|
||||||
modalRef.current?.destroy();
|
modalRef.current?.destroy();
|
||||||
@ -97,12 +112,18 @@ function useModal(): readonly [
|
|||||||
setActionQueue((prev) => [...prev, updateAction]);
|
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),
|
info: getConfirmFunc(withInfo),
|
||||||
success: getConfirmFunc(withSuccess),
|
success: getConfirmFunc(withSuccess),
|
||||||
|
Loading…
Reference in New Issue
Block a user