feat: Modal footer support custom render function (#44318)

This commit is contained in:
红果汁 2023-08-28 11:54:43 +08:00 committed by GitHub
parent ef61160942
commit e7c7601bc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 482 additions and 76 deletions

View File

@ -1,19 +1,23 @@
import * as React from 'react';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled'; import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled'; import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled'; import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
import classNames from 'classnames'; import classNames from 'classnames';
import * as React from 'react';
import ActionButton from '../_util/ActionButton';
import { getTransitionName } from '../_util/motion'; import { getTransitionName } from '../_util/motion';
import warning from '../_util/warning'; import warning from '../_util/warning';
import type { ThemeConfig } from '../config-provider'; import type { ThemeConfig } from '../config-provider';
import ConfigProvider from '../config-provider'; import ConfigProvider from '../config-provider';
import { useLocale } from '../locale'; import { useLocale } from '../locale';
import Dialog from './Modal'; import CancelBtn from './components/ConfirmCancelBtn';
import OkBtn from './components/ConfirmOkBtn';
import type { ModalContextProps } from './context';
import { ModalContextProvider } from './context';
import type { ModalFuncProps, ModalLocale } from './interface'; import type { ModalFuncProps, ModalLocale } from './interface';
import Dialog from './Modal';
interface ConfirmDialogProps extends ModalFuncProps { export interface ConfirmDialogProps extends ModalFuncProps {
afterClose?: () => void; afterClose?: () => void;
close?: (...args: any[]) => void; close?: (...args: any[]) => void;
/** /**
@ -23,7 +27,7 @@ interface ConfirmDialogProps extends ModalFuncProps {
*/ */
onConfirm?: (confirmed: boolean) => void; onConfirm?: (confirmed: boolean) => void;
autoFocusButton?: null | 'ok' | 'cancel'; autoFocusButton?: null | 'ok' | 'cancel';
rootPrefixCls: string; rootPrefixCls?: string;
iconPrefixCls?: string; iconPrefixCls?: string;
theme?: ThemeConfig; theme?: ThemeConfig;
@ -43,22 +47,15 @@ export function ConfirmContent(
) { ) {
const { const {
icon, icon,
onCancel,
onOk,
close,
onConfirm,
isSilent,
okText, okText,
okButtonProps,
cancelText, cancelText,
cancelButtonProps,
confirmPrefixCls, confirmPrefixCls,
rootPrefixCls,
type, type,
okCancel, okCancel,
footer, footer,
// Legacy for static function usage // Legacy for static function usage
locale: staticLocale, locale: staticLocale,
...resetProps
} = props; } = props;
warning( warning(
@ -90,7 +87,6 @@ export function ConfirmContent(
} }
} }
const okType = props.okType || 'primary';
// 默认为 true保持向下兼容 // 默认为 true保持向下兼容
const mergedOkCancel = okCancel ?? type === 'confirm'; const mergedOkCancel = okCancel ?? type === 'confirm';
@ -100,20 +96,26 @@ export function ConfirmContent(
const mergedLocale = staticLocale || locale; const mergedLocale = staticLocale || locale;
const cancelButton = mergedOkCancel && ( // ================== Locale Text ==================
<ActionButton const okTextLocale = okText || (mergedOkCancel ? mergedLocale?.okText : mergedLocale?.justOkText);
isSilent={isSilent} const cancelTextLocale = cancelText || mergedLocale?.cancelText;
actionFn={onCancel}
close={(...args: any[]) => { // ================= Context Value =================
close?.(...args); const btnCtxValue: ModalContextProps = {
onConfirm?.(false); autoFocusButton,
}} cancelTextLocale,
autoFocus={autoFocusButton === 'cancel'} okTextLocale,
buttonProps={cancelButtonProps} mergedOkCancel,
prefixCls={`${rootPrefixCls}-btn`} ...resetProps,
> };
{cancelText || mergedLocale?.cancelText} const btnCtxValueMemo = React.useMemo(() => btnCtxValue, [...Object.values(btnCtxValue)]);
</ActionButton>
// ====================== Footer Origin Node ======================
const footerOriginNode = (
<>
<CancelBtn />
<OkBtn />
</>
); );
return ( return (
@ -125,24 +127,18 @@ export function ConfirmContent(
)} )}
<div className={`${confirmPrefixCls}-content`}>{props.content}</div> <div className={`${confirmPrefixCls}-content`}>{props.content}</div>
</div> </div>
{footer === undefined ? (
<div className={`${confirmPrefixCls}-btns`}> {footer === undefined || typeof footer === 'function' ? (
{cancelButton} <ModalContextProvider value={btnCtxValueMemo}>
<ActionButton <div className={`${confirmPrefixCls}-btns`}>
isSilent={isSilent} {typeof footer === 'function'
type={okType} ? footer(footerOriginNode, {
actionFn={onOk} OkBtn,
close={(...args: any[]) => { CancelBtn,
close?.(...args); })
onConfirm?.(true); : footerOriginNode}
}} </div>
autoFocus={autoFocusButton === 'ok'} </ModalContextProvider>
buttonProps={okButtonProps}
prefixCls={`${rootPrefixCls}-btn`}
>
{okText || (mergedOkCancel ? mergedLocale?.okText : mergedLocale?.justOkText)}
</ActionButton>
</div>
) : ( ) : (
footer footer
)} )}
@ -215,8 +211,12 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = (props) => {
open={open} open={open}
title="" title=""
footer={null} footer={null}
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)} transitionName={getTransitionName(rootPrefixCls || '', 'zoom', props.transitionName)}
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)} maskTransitionName={getTransitionName(
rootPrefixCls || '',
'fade',
props.maskTransitionName,
)}
mask={mask} mask={mask}
maskClosable={maskClosable} maskClosable={maskClosable}
maskStyle={maskStyle} maskStyle={maskStyle}

View File

@ -1,7 +1,8 @@
import * as React from 'react';
import CloseOutlined from '@ant-design/icons/CloseOutlined'; import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import Dialog from 'rc-dialog'; import Dialog from 'rc-dialog';
import * as React from 'react';
import useClosable from '../_util/hooks/useClosable'; import useClosable from '../_util/hooks/useClosable';
import { getTransitionName } from '../_util/motion'; import { getTransitionName } from '../_util/motion';
import { canUseDocElement } from '../_util/styleChecker'; import { canUseDocElement } from '../_util/styleChecker';
@ -9,10 +10,10 @@ import warning from '../_util/warning';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import { NoFormStyle } from '../form/context'; import { NoFormStyle } from '../form/context';
import { NoCompactStyle } from '../space/Compact'; import { NoCompactStyle } from '../space/Compact';
import { usePanelRef } from '../watermark/context';
import type { ModalProps, MousePosition } from './interface'; import type { ModalProps, MousePosition } from './interface';
import { Footer, renderCloseIcon } from './shared'; import { Footer, renderCloseIcon } from './shared';
import useStyle from './style'; import useStyle from './style';
import { usePanelRef } from '../watermark/context';
let mousePosition: MousePosition; let mousePosition: MousePosition;
@ -93,9 +94,9 @@ const Modal: React.FC<ModalProps> = (props) => {
warning(!('visible' in props), 'Modal', '`visible` is deprecated, please use `open` instead.'); warning(!('visible' in props), 'Modal', '`visible` is deprecated, please use `open` instead.');
} }
const dialogFooter = const dialogFooter = footer !== null && (
footer === undefined ? <Footer {...props} onOk={handleOk} onCancel={handleCancel} /> : footer; <Footer {...props} onOk={handleOk} onCancel={handleCancel} />
);
const [mergedClosable, mergedCloseIcon] = useClosable( const [mergedClosable, mergedCloseIcon] = useClosable(
closable, closable,
closeIcon, closeIcon,

View File

@ -1,10 +1,11 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import type { ModalProps } from '..'; import type { ModalProps } from '..';
import Modal from '..'; import Modal from '..';
import { resetWarned } from '../../_util/warning';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils'; import { fireEvent, render } from '../../../tests/utils';
import { resetWarned } from '../../_util/warning';
jest.mock('rc-util/lib/Portal'); jest.mock('rc-util/lib/Portal');
@ -133,4 +134,20 @@ describe('Modal', () => {
render(<Modal open footer={<div className="custom-footer">footer</div>} />); render(<Modal open footer={<div className="custom-footer">footer</div>} />);
expect(document.querySelector('.custom-footer')).toBeTruthy(); expect(document.querySelector('.custom-footer')).toBeTruthy();
}); });
it('Should custom footer function work', () => {
render(
<Modal
open
footer={(_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />
<CancelBtn />
<div className="custom-footer-ele">footer-ele</div>
</>
)}
/>,
);
expect(document.querySelector('.custom-footer-ele')).toBeTruthy();
});
}); });

View File

@ -483,6 +483,40 @@ exports[`renders components/modal/demo/footer.tsx extend context correctly 1`] =
exports[`renders components/modal/demo/footer.tsx extend context correctly 2`] = `[]`; exports[`renders components/modal/demo/footer.tsx extend context correctly 2`] = `[]`;
exports[`renders components/modal/demo/footer-render.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right: 8px;"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Modal
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Modal Confirm
</span>
</button>
</div>
</div>
`;
exports[`renders components/modal/demo/footer-render.tsx extend context correctly 2`] = `[]`;
exports[`renders components/modal/demo/hooks.tsx extend context correctly 1`] = ` exports[`renders components/modal/demo/hooks.tsx extend context correctly 1`] = `
<div <div
class="ant-space ant-space-horizontal ant-space-align-center" class="ant-space ant-space-horizontal ant-space-align-center"

View File

@ -465,6 +465,38 @@ exports[`renders components/modal/demo/footer.tsx correctly 1`] = `
</button> </button>
`; `;
exports[`renders components/modal/demo/footer-render.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Modal
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open Modal Confirm
</span>
</button>
</div>
</div>
`;
exports[`renders components/modal/demo/hooks.tsx correctly 1`] = ` exports[`renders components/modal/demo/hooks.tsx correctly 1`] = `
<div <div
class="ant-space ant-space-horizontal ant-space-align-center" class="ant-space ant-space-horizontal ant-space-align-center"

View File

@ -1,10 +1,11 @@
import * as React from 'react';
import { SmileOutlined } from '@ant-design/icons'; import { SmileOutlined } from '@ant-design/icons';
import CSSMotion from 'rc-motion'; import CSSMotion from 'rc-motion';
import { genCSSMotion } from 'rc-motion/lib/CSSMotion'; import { genCSSMotion } from 'rc-motion/lib/CSSMotion';
import KeyCode from 'rc-util/lib/KeyCode'; import KeyCode from 'rc-util/lib/KeyCode';
import { resetWarned } from 'rc-util/lib/warning'; import { resetWarned } from 'rc-util/lib/warning';
import * as React from 'react';
import TestUtils from 'react-dom/test-utils'; import TestUtils from 'react-dom/test-utils';
import type { ModalFuncProps } from '..'; import type { ModalFuncProps } from '..';
import Modal from '..'; import Modal from '..';
import { act, waitFakeTimer } from '../../../tests/utils'; import { act, waitFakeTimer } from '../../../tests/utils';
@ -845,4 +846,21 @@ describe('Modal.confirm triggers callbacks correctly', () => {
warnSpy.mockRestore(); warnSpy.mockRestore();
}); });
it('Should custom footer function work width confirm', async () => {
Modal.confirm({
content: 'hai',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />
<CancelBtn />
<div className="custom-footer-ele">footer-ele</div>
</>
),
});
await waitFakeTimer();
expect(document.querySelector('.custom-footer-ele')).toBeTruthy();
});
}); });

View File

@ -0,0 +1,54 @@
import type { FC } from 'react';
import React, { useContext } from 'react';
import ActionButton from '../../_util/ActionButton';
import type { ConfirmDialogProps } from '../ConfirmDialog';
import { ModalContext } from '../context';
export interface ConfirmCancelBtnProps
extends Pick<
ConfirmDialogProps,
'cancelButtonProps' | 'isSilent' | 'rootPrefixCls' | 'close' | 'onConfirm' | 'onCancel'
> {
autoFocusButton?: false | 'ok' | 'cancel' | null;
cancelTextLocale?:
| string
| number
| true
| React.ReactElement<any, string | React.JSXElementConstructor<any>>
| Iterable<React.ReactNode>;
mergedOkCancel?: boolean;
}
const ConfirmCancelBtn: FC = () => {
const {
autoFocusButton,
cancelButtonProps,
cancelTextLocale,
isSilent,
mergedOkCancel,
rootPrefixCls,
close,
onCancel,
onConfirm,
} = useContext(ModalContext);
return (
mergedOkCancel && (
<ActionButton
isSilent={isSilent}
actionFn={onCancel}
close={(...args: any[]) => {
close?.(...args);
onConfirm?.(false);
}}
autoFocus={autoFocusButton === 'cancel'}
buttonProps={cancelButtonProps}
prefixCls={`${rootPrefixCls}-btn`}
>
{cancelTextLocale}
</ActionButton>
)
);
};
export default ConfirmCancelBtn;

View File

@ -0,0 +1,52 @@
import type { FC } from 'react';
import React, { useContext } from 'react';
import ActionButton from '../../_util/ActionButton';
import type { ConfirmDialogProps } from '../ConfirmDialog';
import { ModalContext } from '../context';
export interface ConfirmOkBtnProps
extends Pick<
ConfirmDialogProps,
'close' | 'isSilent' | 'okType' | 'okButtonProps' | 'rootPrefixCls' | 'onConfirm' | 'onOk'
> {
autoFocusButton?: false | 'ok' | 'cancel' | null;
okTextLocale?:
| string
| number
| true
| React.ReactElement<any, string | React.JSXElementConstructor<any>>
| Iterable<React.ReactNode>;
}
const ConfirmOkBtn: FC = () => {
const {
autoFocusButton,
close,
isSilent,
okButtonProps,
rootPrefixCls,
okTextLocale,
okType,
onConfirm,
onOk,
} = useContext(ModalContext);
return (
<ActionButton
isSilent={isSilent}
type={okType || 'primary'}
actionFn={onOk}
close={(...args: any[]) => {
close?.(...args);
onConfirm?.(true);
}}
autoFocus={autoFocusButton === 'ok'}
buttonProps={okButtonProps}
prefixCls={`${rootPrefixCls}-btn`}
>
{okTextLocale}
</ActionButton>
);
};
export default ConfirmOkBtn;

View File

@ -0,0 +1,26 @@
import type { FC } from 'react';
import React, { useContext } from 'react';
import Button from '../../button';
import { ModalContext } from '../context';
import type { ModalProps } from '../interface';
export interface NormalCancelBtnProps extends Pick<ModalProps, 'cancelButtonProps' | 'onCancel'> {
cancelTextLocale?:
| string
| number
| true
| React.ReactElement<any, string | React.JSXElementConstructor<any>>
| Iterable<React.ReactNode>;
}
const NormalCancelBtn: FC = () => {
const { cancelButtonProps, cancelTextLocale, onCancel } = useContext(ModalContext);
return (
<Button onClick={onCancel} {...cancelButtonProps}>
{cancelTextLocale}
</Button>
);
};
export default NormalCancelBtn;

View File

@ -0,0 +1,33 @@
import type { FC } from 'react';
import React, { useContext } from 'react';
import Button from '../../button';
import { convertLegacyProps } from '../../button/button';
import { ModalContext } from '../context';
import type { ModalProps } from '../interface';
export interface NormalOkBtnProps
extends Pick<ModalProps, 'confirmLoading' | 'okType' | 'okButtonProps' | 'onOk'> {
okTextLocale?:
| string
| number
| true
| React.ReactElement<any, string | React.JSXElementConstructor<any>>
| Iterable<React.ReactNode>;
}
const NormalOkBtn: FC = () => {
const { confirmLoading, okButtonProps, okType, okTextLocale, onOk } = useContext(ModalContext);
return (
<Button
{...convertLegacyProps(okType)}
loading={confirmLoading}
onClick={onOk}
{...okButtonProps}
>
{okTextLocale}
</Button>
);
};
export default NormalOkBtn;

View File

@ -0,0 +1,15 @@
import React from 'react';
import type { ConfirmCancelBtnProps } from './components/ConfirmCancelBtn';
import type { ConfirmOkBtnProps } from './components/ConfirmOkBtn';
import type { NormalCancelBtnProps } from './components/NormalCancelBtn';
import type { NormalOkBtnProps } from './components/NormalOkBtn';
export type ModalContextProps = NormalCancelBtnProps &
NormalOkBtnProps &
ConfirmOkBtnProps &
ConfirmCancelBtnProps;
export const ModalContext = React.createContext<ModalContextProps>({} as ModalContextProps);
export const { Provider: ModalContextProvider } = ModalContext;

View File

@ -0,0 +1,7 @@
## zh-CN
自定义页脚渲染函数,支持在原有基础上进行扩展。
## en-US
Customize the footer rendering function to support extensions on top of the original.

View File

@ -0,0 +1,65 @@
import React, { useState } from 'react';
import { Button, Modal, Space } from 'antd';
const App: React.FC = () => {
const [open, setOpen] = useState(false);
const showModal = () => {
setOpen(true);
};
const handleOk = () => {
setOpen(false);
};
const handleCancel = () => {
setOpen(false);
};
return (
<>
<Space>
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
<Button
type="primary"
onClick={() => {
Modal.confirm({
title: 'Confirm',
content: 'Bla bla ...',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<Button>Custom Button</Button>
<CancelBtn />
<OkBtn />
</>
),
});
}}
>
Open Modal Confirm
</Button>
</Space>
<Modal
open={open}
title="Title"
onOk={handleOk}
onCancel={handleCancel}
footer={(_, { OkBtn, CancelBtn }) => (
<>
<Button>Custom Button</Button>
<CancelBtn />
<OkBtn />
</>
)}
>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
</>
);
};
export default App;

View File

@ -22,6 +22,7 @@ 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/footer-render.tsx">Customized Footer render function</code>
<code src="./demo/hooks.tsx">Use hooks to get context</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>
@ -53,7 +54,7 @@ Common props ref[Common props](/docs/react/common-props)
| confirmLoading | Whether to apply loading visual effect for OK button or not | boolean | false | | | confirmLoading | Whether to apply loading visual effect for OK button or not | boolean | false | |
| destroyOnClose | Whether to unmount child components on onClose | boolean | false | | | destroyOnClose | Whether to unmount child components on onClose | boolean | false | |
| focusTriggerAfterClose | Whether need to focus trigger element after dialog is closed | boolean | true | 4.9.0 | | focusTriggerAfterClose | Whether need to focus trigger element after dialog is closed | boolean | true | 4.9.0 |
| footer | Footer content, set as `footer={null}` when you don't need default buttons | ReactNode | (OK and Cancel buttons) | | | footer | Footer content, set as `footer={null}` when you don't need default buttons | (params:[footerRenderParams](/components/modal-cn#footerrenderparams))=> React.ReactNode \| React.ReactNode | (OK and Cancel buttons) | |
| forceRender | Force render Modal | boolean | false | | | forceRender | Force render Modal | boolean | false | |
| getContainer | The mounted node for Modal but still display at fullscreen | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | | | getContainer | The mounted node for Modal but still display at fullscreen | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | |
| keyboard | Whether support press esc to close | boolean | true | | | keyboard | Whether support press esc to close | boolean | true | |
@ -103,7 +104,7 @@ The items listed above are all functions, expecting a settings object as paramet
| className | The className of container | string | - | | | className | The className of container | string | - | |
| closeIcon | Custom close icon. 5.7.0: close button will be hidden when setting to `null` or `false` | boolean \| ReactNode | &lt;CloseOutlined /> | | | closeIcon | Custom close icon. 5.7.0: close button will be hidden when setting to `null` or `false` | boolean \| ReactNode | &lt;CloseOutlined /> | |
| content | Content | ReactNode | - | | | content | Content | ReactNode | - | |
| footer | Footer content, set as `footer: null` when you don't need default buttons | ReactNode | - | 5.1.0 | | footer | Footer content, set as `footer: null` when you don't need default buttons | (params:[footerRenderParams](/components/modal-cn#footerrenderparams))=> React.ReactNode \| React.ReactNode | - | 5.9.0 |
| getContainer | Return the mount node for Modal | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | | | getContainer | Return the mount node for Modal | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | |
| icon | Custom icon | ReactNode | &lt;ExclamationCircleFilled /> | | | icon | Custom icon | ReactNode | &lt;ExclamationCircleFilled /> | |
| keyboard | Whether support press esc to close | boolean | true | | | keyboard | Whether support press esc to close | boolean | true | |
@ -180,6 +181,14 @@ return <div>{contextHolder}</div>;
const confirmed = await modal.confirm({ ... }); const confirmed = await modal.confirm({ ... });
``` ```
## footerRenderParams
<!-- prettier-ignore -->
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| originNode | default node | React.ReactNode | - |
| extra | extended options | { OkBtn: FC; CancelBtn: FC } | - |
## Design Token ## Design Token
<ComponentTokenTable component="Modal"></ComponentTokenTable> <ComponentTokenTable component="Modal"></ComponentTokenTable>

View File

@ -23,6 +23,7 @@ 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/footer-render.tsx">自定义页脚渲染函数</code>
<code src="./demo/hooks.tsx">使用 hooks 获得上下文</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>
@ -54,7 +55,7 @@ demo:
| confirmLoading | 确定按钮 loading | boolean | false | | | confirmLoading | 确定按钮 loading | boolean | false | |
| destroyOnClose | 关闭时销毁 Modal 里的子元素 | boolean | false | | | destroyOnClose | 关闭时销毁 Modal 里的子元素 | boolean | false | |
| focusTriggerAfterClose | 对话框关闭后是否需要聚焦触发元素 | boolean | true | 4.9.0 | | focusTriggerAfterClose | 对话框关闭后是否需要聚焦触发元素 | boolean | true | 4.9.0 |
| footer | 底部内容,当不需要默认底部按钮时,可以设为 `footer={null}` | ReactNode | (确定取消按钮) | | | footer | 底部内容,当不需要默认底部按钮时,可以设为 `footer={null}` | (params:[footerRenderParams](/components/modal-cn#footerrenderparams))=> React.ReactNode \| React.ReactNode | (确定取消按钮) | 5.9.0 |
| forceRender | 强制渲染 Modal | boolean | false | | | forceRender | 强制渲染 Modal | boolean | false | |
| getContainer | 指定 Modal 挂载的节点,但依旧为全屏展示,`false` 为挂载在当前位置 | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | | | getContainer | 指定 Modal 挂载的节点,但依旧为全屏展示,`false` 为挂载在当前位置 | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | |
| keyboard | 是否支持键盘 esc 关闭 | boolean | true | | | keyboard | 是否支持键盘 esc 关闭 | boolean | true | |
@ -104,7 +105,7 @@ demo:
| className | 容器类名 | string | - | | | className | 容器类名 | string | - | |
| closeIcon | 自定义关闭图标。5.7.0:设置为 `null``false` 时隐藏关闭按钮 | boolean \| ReactNode | &lt;CloseOutlined /> | | | closeIcon | 自定义关闭图标。5.7.0:设置为 `null``false` 时隐藏关闭按钮 | boolean \| ReactNode | &lt;CloseOutlined /> | |
| content | 内容 | ReactNode | - | | | content | 内容 | ReactNode | - | |
| footer | 底部内容,当不需要默认底部按钮时,可以设为 `footer: null` | ReactNode | - | 5.1.0 | | footer | 底部内容,当不需要默认底部按钮时,可以设为 `footer: null` | (params:[footerRenderParams](/components/modal-cn#footerrenderparams))=> React.ReactNode \| React.ReactNode | - | 5.9.0 |
| getContainer | 指定 Modal 挂载的 HTML 节点, false 为挂载在当前 dom | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | | | getContainer | 指定 Modal 挂载的 HTML 节点, false 为挂载在当前 dom | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | |
| icon | 自定义图标 | ReactNode | &lt;ExclamationCircleFilled /> | | | icon | 自定义图标 | ReactNode | &lt;ExclamationCircleFilled /> | |
| keyboard | 是否支持键盘 esc 关闭 | boolean | true | | | keyboard | 是否支持键盘 esc 关闭 | boolean | true | |
@ -181,6 +182,14 @@ return <div>{contextHolder}</div>;
const confirmed = await modal.confirm({ ... }); const confirmed = await modal.confirm({ ... });
``` ```
## footerRenderParams
<!-- prettier-ignore -->
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| originNode | 默认节点 | React.ReactNode | - |
| extra | 扩展选项 | { OkBtn: FC; CancelBtn: FC } | - |
## Design Token ## Design Token
<ComponentTokenTable component="Modal"></ComponentTokenTable> <ComponentTokenTable component="Modal"></ComponentTokenTable>

View File

@ -1,6 +1,12 @@
import type { FC } from 'react';
import type { ButtonProps, LegacyButtonType } from '../button/button'; import type { ButtonProps, LegacyButtonType } from '../button/button';
import type { DirectionType } from '../config-provider'; import type { DirectionType } from '../config-provider';
export type ModalFooterRender = (
originNode: React.ReactNode,
extra: { OkBtn: FC; CancelBtn: FC },
) => React.ReactNode;
export interface ModalProps { export interface ModalProps {
/** Whether the modal dialog is visible or not */ /** Whether the modal dialog is visible or not */
open?: boolean; open?: boolean;
@ -22,7 +28,7 @@ export interface ModalProps {
/** Width of the modal dialog */ /** Width of the modal dialog */
width?: string | number; width?: string | number;
/** Footer content */ /** Footer content */
footer?: React.ReactNode; footer?: ModalFooterRender | React.ReactNode;
/** Text of the OK button */ /** Text of the OK button */
okText?: React.ReactNode; okText?: React.ReactNode;
/** Button `type` of the OK button */ /** Button `type` of the OK button */
@ -101,7 +107,7 @@ export interface ModalFuncProps {
direction?: DirectionType; direction?: DirectionType;
bodyStyle?: React.CSSProperties; bodyStyle?: React.CSSProperties;
closeIcon?: React.ReactNode; closeIcon?: React.ReactNode;
footer?: React.ReactNode; footer?: ModalProps['footer'];
modalRender?: (node: React.ReactNode) => React.ReactNode; modalRender?: (node: React.ReactNode) => React.ReactNode;
focusTriggerAfterClose?: boolean; focusTriggerAfterClose?: boolean;
} }

View File

@ -1,9 +1,12 @@
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import React from 'react'; import React from 'react';
import Button from '../button'; import CloseOutlined from '@ant-design/icons/CloseOutlined';
import { convertLegacyProps } from '../button/button';
import { DisabledContextProvider } from '../config-provider/DisabledContext'; import { DisabledContextProvider } from '../config-provider/DisabledContext';
import { useLocale } from '../locale'; import { useLocale } from '../locale';
import NormalCancelBtn from './components/NormalCancelBtn';
import NormalOkBtn from './components/NormalOkBtn';
import type { ModalContextProps } from './context';
import { ModalContextProvider } from './context';
import type { ModalProps } from './interface'; import type { ModalProps } from './interface';
import { getConfirmLocale } from './locale'; import { getConfirmLocale } from './locale';
@ -42,23 +45,48 @@ export const Footer: React.FC<
onCancel, onCancel,
okButtonProps, okButtonProps,
cancelButtonProps, cancelButtonProps,
footer,
} = props; } = props;
const [locale] = useLocale('Modal', getConfirmLocale()); const [locale] = useLocale('Modal', getConfirmLocale());
return ( // ================== Locale Text ==================
const okTextLocale = okText || locale?.okText;
const cancelTextLocale = cancelText || locale?.cancelText;
// ================= Context Value =================
const btnCtxValue: ModalContextProps = {
confirmLoading,
okButtonProps,
cancelButtonProps,
okTextLocale,
cancelTextLocale,
okType,
onOk,
onCancel,
};
const btnCtxValueMemo = React.useMemo(() => btnCtxValue, [...Object.values(btnCtxValue)]);
const footerOriginNode = (
<>
<NormalCancelBtn />
<NormalOkBtn />
</>
);
return footer === undefined || typeof footer === 'function' ? (
<DisabledContextProvider disabled={false}> <DisabledContextProvider disabled={false}>
<Button onClick={onCancel} {...cancelButtonProps}> <ModalContextProvider value={btnCtxValueMemo}>
{cancelText || locale?.cancelText} {typeof footer === 'function'
</Button> ? footer(footerOriginNode, {
<Button OkBtn: NormalOkBtn,
{...convertLegacyProps(okType)} CancelBtn: NormalCancelBtn,
loading={confirmLoading} })
onClick={onOk} : footerOriginNode}
{...okButtonProps} </ModalContextProvider>
>
{okText || locale?.okText}
</Button>
</DisabledContextProvider> </DisabledContextProvider>
) : (
footer
); );
}; };