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:
kiner-tang(文辉) 2023-06-16 15:36:28 +08:00 committed by GitHub
parent 4ae0d6bcf5
commit 378b54281b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 267 additions and 36 deletions

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

View 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];
}

View File

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

View File

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

View File

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

View File

@ -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 | &lt;CloseOutlined /> | |
| closeIcon | Custom close icon. 5.7.0: close button will be hidden when setting to `null` or `false` | boolean \| ReactNode | &lt;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 | &lt;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 | |

View File

@ -48,8 +48,7 @@ demo:
| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button-cn#api) | - | |
| cancelText | 取消按钮文字 | ReactNode | `取消` | |
| centered | 垂直居中展示 Modal | boolean | false | |
| closable | 是否显示右上角的关闭按钮 | boolean | true | |
| closeIcon | 自定义关闭图标 | ReactNode | &lt;CloseOutlined /> | |
| closeIcon | 自定义关闭图标。5.7.0:设置为 `null``false` 时隐藏关闭按钮 | boolean \| ReactNode | &lt;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 | &lt;CloseOutlined /> | |
| content | 内容 | ReactNode | - | |
| footer | 底部内容,当不需要默认底部按钮时,可以设为 `footer: null` | ReactNode | - | 5.1.0 |
| getContainer | 指定 Modal 挂载的 HTML 节点, false 为挂载在当前 dom | HTMLElement \| () => HTMLElement \| Selectors \| false | document.body | |

View File

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