mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-30 02:59:04 +08:00
refactor(Modal): refactor closeIcon (#43017)
* refactor: refactor closeIcon * docs: update docs * refactor(Drawer): refactor drawer closeIcon (#42993) * feat: optimize closeIcon * refactor: refactor closeIcon * docs: update docs * feat: optimize code * feat: update test case * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code * docs: update docs * ✨ feat: migrate less to token for Slider (#42428) * ✨ feat: migrate less to token for Slider * ✨ feat: update snap * ✨ feat: update style * ✨ feat: update style * ✨ feat: test ci * ✨ feat: test ci * ✨ feat: test ci * ✨ feat: test ci * ✨ feat: update * ✨ feat: update snap * ✨ feat: update * ✨ feat: update * ✨ feat: 删除未使用token * ✨ feat: update doc * ✨ feat: update dome * ✨ feat: update * ✨ feat: test ci * 📝 doc: update doc * ✨ feat: update * ✨ feat: update * ✨ feat: update * ✨ feat: update * ✨ feat: add demo * ✨ feat: add demo * ✨ feat: update for reviewer * ✨ feat: update for reviewer * ✨ feat: update for reviewer * ✨ feat: update for reviewer * ✨ feat: update for reviewer * ✨ feat: update for reviewer * ✨ feat: update for reviewer * ✨ feat: update for reviewer * Apply suggestions from code review --------- Co-authored-by: MadCcc <1075746765@qq.com> * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code * docs: update docs --------- Co-authored-by: 黑雨 <wangning4567@163.com> Co-authored-by: MadCcc <1075746765@qq.com>
This commit is contained in:
parent
4ae0d6bcf5
commit
378b54281b
181
components/_util/__tests__/hooks.test.tsx
Normal file
181
components/_util/__tests__/hooks.test.tsx
Normal file
@ -0,0 +1,181 @@
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { render } from '@testing-library/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import type { UseClosableParams } from '../hooks/useClosable';
|
||||
import useClosable from '../hooks/useClosable';
|
||||
|
||||
type ParamsOfUseClosable = [
|
||||
UseClosableParams['closable'],
|
||||
UseClosableParams['closeIcon'],
|
||||
UseClosableParams['defaultClosable'],
|
||||
];
|
||||
|
||||
describe('hooks test', () => {
|
||||
const useClosableParams: { params: ParamsOfUseClosable; res: [boolean, string] }[] = [
|
||||
// test case like: <Component />
|
||||
{
|
||||
params: [undefined, undefined, undefined],
|
||||
res: [false, ''],
|
||||
},
|
||||
{
|
||||
params: [undefined, undefined, true],
|
||||
res: [true, 'anticon-close'],
|
||||
},
|
||||
{
|
||||
params: [undefined, undefined, false],
|
||||
res: [false, ''],
|
||||
},
|
||||
|
||||
// test case like: <Component closable={false | true} />
|
||||
{
|
||||
params: [false, undefined, undefined],
|
||||
res: [false, ''],
|
||||
},
|
||||
{
|
||||
params: [true, undefined, true],
|
||||
res: [true, 'anticon-close'],
|
||||
},
|
||||
{
|
||||
params: [true, undefined, false],
|
||||
res: [true, 'anticon-close'],
|
||||
},
|
||||
|
||||
// test case like: <Component closable={false | true} closeIcon={null | false | element} />
|
||||
{
|
||||
params: [false, null, undefined],
|
||||
res: [false, ''],
|
||||
},
|
||||
{
|
||||
params: [false, false, undefined],
|
||||
res: [false, ''],
|
||||
},
|
||||
{
|
||||
params: [true, null, true],
|
||||
res: [true, 'anticon-close'],
|
||||
},
|
||||
{
|
||||
params: [true, false, true],
|
||||
res: [true, 'anticon-close'],
|
||||
},
|
||||
{
|
||||
params: [true, null, false],
|
||||
res: [true, 'anticon-close'],
|
||||
},
|
||||
{
|
||||
params: [true, false, false],
|
||||
res: [true, 'anticon-close'],
|
||||
},
|
||||
{
|
||||
params: [
|
||||
true,
|
||||
<div className="custom-close" key="close">
|
||||
close
|
||||
</div>,
|
||||
false,
|
||||
],
|
||||
res: [true, 'custom-close'],
|
||||
},
|
||||
{
|
||||
params: [false, <div key="close">close</div>, false],
|
||||
res: [false, ''],
|
||||
},
|
||||
|
||||
// test case like: <Component closeIcon={null | false | element} />
|
||||
{
|
||||
params: [undefined, null, undefined],
|
||||
res: [false, ''],
|
||||
},
|
||||
{
|
||||
params: [undefined, false, undefined],
|
||||
res: [false, ''],
|
||||
},
|
||||
{
|
||||
params: [
|
||||
undefined,
|
||||
<div className="custom-close" key="close">
|
||||
close
|
||||
</div>,
|
||||
undefined,
|
||||
],
|
||||
res: [true, 'custom-close'],
|
||||
},
|
||||
{
|
||||
params: [
|
||||
undefined,
|
||||
<div className="custom-close" key="close">
|
||||
close
|
||||
</div>,
|
||||
true,
|
||||
],
|
||||
res: [true, 'custom-close'],
|
||||
},
|
||||
{
|
||||
params: [
|
||||
undefined,
|
||||
<div className="custom-close" key="close">
|
||||
close
|
||||
</div>,
|
||||
false,
|
||||
],
|
||||
res: [true, 'custom-close'],
|
||||
},
|
||||
];
|
||||
|
||||
useClosableParams.forEach(({ params, res }) => {
|
||||
it(`useClosable with closable=${params[0]},closeIcon=${
|
||||
React.isValidElement(params[1]) ? 'element' : params[1]
|
||||
},defaultClosable=${params[2]}. the result should be ${res}`, () => {
|
||||
const App = () => {
|
||||
const [closable, closeIcon] = useClosable(
|
||||
params[0],
|
||||
params[1],
|
||||
undefined,
|
||||
undefined,
|
||||
params[2],
|
||||
);
|
||||
useEffect(() => {
|
||||
expect(closable).toBe(res[0]);
|
||||
}, [closable]);
|
||||
return <div>hooks test {closeIcon}</div>;
|
||||
};
|
||||
const { container } = render(<App />);
|
||||
if (res[1] === '') {
|
||||
expect(container.querySelector('.anticon-close')).toBeFalsy();
|
||||
} else {
|
||||
expect(container.querySelector(`.${res[1]}`)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('useClosable with defaultCloseIcon', () => {
|
||||
const App = () => {
|
||||
const [closable, closeIcon] = useClosable(
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
<CloseOutlined className="custom-close-icon" />,
|
||||
);
|
||||
useEffect(() => {
|
||||
expect(closable).toBe(true);
|
||||
}, [closable]);
|
||||
return <div>hooks test {closeIcon}</div>;
|
||||
};
|
||||
const { container } = render(<App />);
|
||||
expect(container.querySelector('.custom-close-icon')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('useClosable with customCloseIconRender', () => {
|
||||
const App = () => {
|
||||
const customCloseIconRender = (icon: React.ReactNode) => (
|
||||
<span className="custom-close-wrapper">{icon}</span>
|
||||
);
|
||||
const [closable, closeIcon] = useClosable(true, undefined, customCloseIconRender);
|
||||
useEffect(() => {
|
||||
expect(closable).toBe(true);
|
||||
}, [closable]);
|
||||
return <div>hooks test {closeIcon}</div>;
|
||||
};
|
||||
const { container } = render(<App />);
|
||||
expect(container.querySelector('.custom-close-wrapper')).toBeTruthy();
|
||||
});
|
||||
});
|
43
components/_util/hooks/useClosable.tsx
Normal file
43
components/_util/hooks/useClosable.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
function useInnerClosable(
|
||||
closable?: boolean,
|
||||
closeIcon?: boolean | ReactNode,
|
||||
defaultClosable?: boolean,
|
||||
): boolean {
|
||||
if (typeof closable === 'boolean') {
|
||||
return closable;
|
||||
}
|
||||
if (closeIcon === undefined) {
|
||||
return !!defaultClosable;
|
||||
}
|
||||
return closeIcon !== false && closeIcon !== null;
|
||||
}
|
||||
|
||||
export type UseClosableParams = {
|
||||
closable?: boolean;
|
||||
closeIcon?: boolean | ReactNode;
|
||||
defaultClosable?: boolean;
|
||||
defaultCloseIcon?: ReactNode;
|
||||
customCloseIconRender?: (closeIcon: ReactNode) => ReactNode;
|
||||
};
|
||||
|
||||
export default function useClosable(
|
||||
closable?: boolean,
|
||||
closeIcon?: boolean | ReactNode,
|
||||
customCloseIconRender?: (closeIcon: ReactNode) => ReactNode,
|
||||
defaultCloseIcon: ReactNode = <CloseOutlined />,
|
||||
defaultClosable = false,
|
||||
): [closable: boolean, closeIcon: React.ReactNode | null] {
|
||||
const mergedClosable = useInnerClosable(closable, closeIcon, defaultClosable);
|
||||
if (!mergedClosable) {
|
||||
return [false, null];
|
||||
}
|
||||
const mergedCloseIcon =
|
||||
typeof closeIcon === 'boolean' || closeIcon === undefined || closeIcon === null
|
||||
? defaultCloseIcon
|
||||
: closeIcon;
|
||||
return [true, customCloseIconRender ? customCloseIconRender(mergedCloseIcon) : mergedCloseIcon];
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import classNames from 'classnames';
|
||||
import type { DrawerProps as RCDrawerProps } from 'rc-drawer';
|
||||
import * as React from 'react';
|
||||
import useClosable from '../_util/hooks/useClosable';
|
||||
|
||||
export interface DrawerPanelProps {
|
||||
prefixCls: string;
|
||||
@ -44,28 +44,20 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const mergedClosable = React.useMemo(() => {
|
||||
if (typeof closable === 'boolean') {
|
||||
return closable;
|
||||
}
|
||||
|
||||
return closeIcon !== null && closeIcon !== false;
|
||||
}, [closable, closeIcon]);
|
||||
|
||||
const mergedCloseIcon = React.useMemo(() => {
|
||||
if (!mergedClosable) {
|
||||
return null;
|
||||
}
|
||||
if (closeIcon === undefined || closeIcon === true) {
|
||||
return <CloseOutlined />;
|
||||
}
|
||||
return closeIcon;
|
||||
}, [closeIcon, mergedClosable]);
|
||||
|
||||
const closeIconNode = mergedClosable && (
|
||||
const customCloseIconRender = React.useCallback(
|
||||
(icon: React.ReactNode) => (
|
||||
<button type="button" onClick={onClose} aria-label="Close" className={`${prefixCls}-close`}>
|
||||
{mergedCloseIcon}
|
||||
{icon}
|
||||
</button>
|
||||
),
|
||||
[onClose],
|
||||
);
|
||||
const [mergedClosable, mergedCloseIcon] = useClosable(
|
||||
closable,
|
||||
closeIcon,
|
||||
customCloseIconRender,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const headerNode = React.useMemo<React.ReactNode>(() => {
|
||||
@ -80,13 +72,13 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
|
||||
})}
|
||||
>
|
||||
<div className={`${prefixCls}-header-title`}>
|
||||
{closeIconNode}
|
||||
{mergedCloseIcon}
|
||||
{title && <div className={`${prefixCls}-title`}>{title}</div>}
|
||||
</div>
|
||||
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
|
||||
</div>
|
||||
);
|
||||
}, [mergedClosable, closeIconNode, extra, headerStyle, prefixCls, title]);
|
||||
}, [mergedClosable, mergedCloseIcon, extra, headerStyle, prefixCls, title]);
|
||||
|
||||
const footerNode = React.useMemo<React.ReactNode>(() => {
|
||||
if (!footer) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import Dialog from 'rc-dialog';
|
||||
import * as React from 'react';
|
||||
import useClosable from '../_util/hooks/useClosable';
|
||||
import { getTransitionName } from '../_util/motion';
|
||||
import { canUseDocElement } from '../_util/styleChecker';
|
||||
import warning from '../_util/warning';
|
||||
@ -64,6 +66,7 @@ const Modal: React.FC<ModalProps> = (props) => {
|
||||
centered,
|
||||
getContainer,
|
||||
closeIcon,
|
||||
closable,
|
||||
focusTriggerAfterClose = true,
|
||||
|
||||
// Deprecated
|
||||
@ -91,6 +94,14 @@ const Modal: React.FC<ModalProps> = (props) => {
|
||||
const dialogFooter =
|
||||
footer === undefined ? <Footer {...props} onOk={handleOk} onCancel={handleCancel} /> : footer;
|
||||
|
||||
const [mergedClosable, mergedCloseIcon] = useClosable(
|
||||
closable,
|
||||
closeIcon,
|
||||
(icon) => renderCloseIcon(prefixCls, icon),
|
||||
<CloseOutlined className={`${prefixCls}-close-icon`} />,
|
||||
true,
|
||||
);
|
||||
|
||||
return wrapSSR(
|
||||
<NoCompactStyle>
|
||||
<NoFormStyle status override>
|
||||
@ -105,7 +116,8 @@ const Modal: React.FC<ModalProps> = (props) => {
|
||||
visible={open ?? visible}
|
||||
mousePosition={restProps.mousePosition ?? mousePosition}
|
||||
onClose={handleCancel}
|
||||
closeIcon={renderCloseIcon(prefixCls, closeIcon)}
|
||||
closable={mergedClosable}
|
||||
closeIcon={mergedCloseIcon}
|
||||
focusTriggerAfterClose={focusTriggerAfterClose}
|
||||
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
|
||||
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
|
||||
|
@ -33,6 +33,13 @@ describe('Modal', () => {
|
||||
expect(document.body.querySelectorAll('.ant-modal-root')[0]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('support hide close button when setting closeIcon to null or false', () => {
|
||||
const { baseElement, rerender } = render(<Modal closeIcon={null} open />);
|
||||
expect(baseElement.querySelector('.ant-modal-close')).toBeFalsy();
|
||||
rerender(<Modal closeIcon={false} open />);
|
||||
expect(baseElement.querySelector('.ant-modal-close')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('render correctly', () => {
|
||||
const { asFragment } = render(<ModalTester />);
|
||||
expect(asFragment().firstChild).toMatchSnapshot();
|
||||
|
@ -47,8 +47,7 @@ Additionally, if you need show a simple confirmation dialog, you can use [`App.u
|
||||
| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#api) | - | |
|
||||
| cancelText | Text of the Cancel button | ReactNode | `Cancel` | |
|
||||
| centered | Centered Modal | boolean | false | |
|
||||
| closable | Whether a close (x) button is visible on top right of the modal dialog or not | boolean | true | |
|
||||
| closeIcon | Custom close icon | ReactNode | <CloseOutlined /> | |
|
||||
| closeIcon | Custom close icon. 5.7.0: close button will be hidden when setting to `null` or `false` | boolean \| ReactNode | <CloseOutlined /> | |
|
||||
| confirmLoading | Whether to apply loading visual effect for OK button or not | 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 |
|
||||
@ -100,8 +99,7 @@ The items listed above are all functions, expecting a settings object as paramet
|
||||
| cancelText | Text of the Cancel button with Modal.confirm | string | `Cancel` | |
|
||||
| centered | Centered Modal | boolean | false | |
|
||||
| className | The className of container | string | - | |
|
||||
| closable | Whether a close (x) button is visible on top right of the confirm dialog or not | boolean | false | 4.9.0 |
|
||||
| closeIcon | Custom close icon | ReactNode | undefined | 4.9.0 |
|
||||
| closeIcon | Custom close icon. 5.7.0: close button will be hidden when setting to `null` or `false` | boolean \| ReactNode | <CloseOutlined /> | |
|
||||
| content | Content | ReactNode | - | |
|
||||
| footer | Footer content, set as `footer: null` when you don't need default buttons | ReactNode | - | 5.1.0 |
|
||||
| getContainer | Return the mount node for Modal | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | |
|
||||
|
@ -48,8 +48,7 @@ demo:
|
||||
| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button-cn#api) | - | |
|
||||
| cancelText | 取消按钮文字 | ReactNode | `取消` | |
|
||||
| centered | 垂直居中展示 Modal | boolean | false | |
|
||||
| closable | 是否显示右上角的关闭按钮 | boolean | true | |
|
||||
| closeIcon | 自定义关闭图标 | ReactNode | <CloseOutlined /> | |
|
||||
| closeIcon | 自定义关闭图标。5.7.0:设置为 `null` 或 `false` 时隐藏关闭按钮 | boolean \| ReactNode | <CloseOutlined /> | |
|
||||
| confirmLoading | 确定按钮 loading | boolean | false | |
|
||||
| destroyOnClose | 关闭时销毁 Modal 里的子元素 | boolean | false | |
|
||||
| focusTriggerAfterClose | 对话框关闭后是否需要聚焦触发元素 | boolean | true | 4.9.0 |
|
||||
@ -101,8 +100,7 @@ demo:
|
||||
| cancelText | 设置 Modal.confirm 取消按钮文字 | string | `取消` | |
|
||||
| centered | 垂直居中展示 Modal | boolean | false | |
|
||||
| className | 容器类名 | string | - | |
|
||||
| closable | 是否显示右上角的关闭按钮 | boolean | false | 4.9.0 |
|
||||
| closeIcon | 自定义关闭图标 | ReactNode | undefined | 4.9.0 |
|
||||
| closeIcon | 自定义关闭图标。5.7.0:设置为 `null` 或 `false` 时隐藏关闭按钮 | boolean \| ReactNode | <CloseOutlined /> | |
|
||||
| content | 内容 | ReactNode | - | |
|
||||
| footer | 底部内容,当不需要默认底部按钮时,可以设为 `footer: null` | ReactNode | - | 5.1.0 |
|
||||
| getContainer | 指定 Modal 挂载的 HTML 节点, false 为挂载在当前 dom | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | |
|
||||
|
@ -8,7 +8,7 @@ export interface ModalProps {
|
||||
confirmLoading?: boolean;
|
||||
/** The modal dialog's title */
|
||||
title?: React.ReactNode;
|
||||
/** Whether a close (x) button is visible on top right of the modal dialog or not */
|
||||
/** Whether a close (x) button is visible on top right of the modal dialog or not. Advised to use closeIcon instead. */
|
||||
closable?: boolean;
|
||||
/** Specify a function that will be called when a user clicks the OK button */
|
||||
onOk?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
@ -50,7 +50,7 @@ export interface ModalProps {
|
||||
keyboard?: boolean;
|
||||
wrapProps?: any;
|
||||
prefixCls?: string;
|
||||
closeIcon?: React.ReactNode;
|
||||
closeIcon?: boolean | React.ReactNode;
|
||||
modalRender?: (node: React.ReactNode) => React.ReactNode;
|
||||
focusTriggerAfterClose?: boolean;
|
||||
children?: React.ReactNode;
|
||||
|
Loading…
Reference in New Issue
Block a user