mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 02:59:58 +08:00
feat: Modal footer support custom render function (#44318)
This commit is contained in:
parent
ef61160942
commit
e7c7601bc0
@ -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}
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
54
components/modal/components/ConfirmCancelBtn.tsx
Normal file
54
components/modal/components/ConfirmCancelBtn.tsx
Normal 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;
|
52
components/modal/components/ConfirmOkBtn.tsx
Normal file
52
components/modal/components/ConfirmOkBtn.tsx
Normal 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;
|
26
components/modal/components/NormalCancelBtn.tsx
Normal file
26
components/modal/components/NormalCancelBtn.tsx
Normal 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;
|
33
components/modal/components/NormalOkBtn.tsx
Normal file
33
components/modal/components/NormalOkBtn.tsx
Normal 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;
|
15
components/modal/context.ts
Normal file
15
components/modal/context.ts
Normal 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;
|
7
components/modal/demo/footer-render.md
Normal file
7
components/modal/demo/footer-render.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
## zh-CN
|
||||||
|
|
||||||
|
自定义页脚渲染函数,支持在原有基础上进行扩展。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
|
||||||
|
Customize the footer rendering function to support extensions on top of the original.
|
65
components/modal/demo/footer-render.tsx
Normal file
65
components/modal/demo/footer-render.tsx
Normal 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;
|
@ -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 | <CloseOutlined /> | |
|
| closeIcon | Custom close icon. 5.7.0: close button will be hidden when setting to `null` or `false` | boolean \| ReactNode | <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 | <ExclamationCircleFilled /> | |
|
| icon | Custom icon | ReactNode | <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>
|
||||||
|
@ -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 | <CloseOutlined /> | |
|
| closeIcon | 自定义关闭图标。5.7.0:设置为 `null` 或 `false` 时隐藏关闭按钮 | boolean \| ReactNode | <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 | <ExclamationCircleFilled /> | |
|
| icon | 自定义图标 | ReactNode | <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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user