Browse Source

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
pull/42719/head
二货爱吃白萝卜 1 year ago
committed by GitHub
parent
commit
b072d3a02c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      components/_util/ActionButton.tsx
  2. 25
      components/modal/ConfirmDialog.tsx
  3. 44
      components/modal/__tests__/hook.test.tsx
  4. 4
      components/modal/demo/hooks.md
  5. 9
      components/modal/demo/hooks.tsx
  6. 15
      components/modal/index.en-US.md
  7. 15
      components/modal/index.zh-CN.md
  8. 8
      components/modal/useModal/HookModal.tsx
  9. 35
      components/modal/useModal/index.tsx

12
components/_util/ActionButton.tsx

@ -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);
},
);

25
components/modal/ConfirmDialog.tsx

@ -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`}

44
components/modal/__tests__/hook.test.tsx

@ -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();
});
});

4
components/modal/demo/hooks.md

@ -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.

9
components/modal/demo/hooks.tsx

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

15
components/modal/index.en-US.md

@ -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>

15
components/modal/index.zh-CN.md

@ -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
components/modal/useModal/HookModal.tsx

@ -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}
/>
);
};

35
components/modal/useModal/index.tsx

@ -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…
Cancel
Save