Skip to content

Commit

Permalink
Merge pull request #63 from linhf123/feat-tooltip-close
Browse files Browse the repository at this point in the history
feat(Tooltip): Tooltip support for closeIcon and onClose
  • Loading branch information
dengfuping authored Sep 5, 2023
2 parents 51d0193 + bf2f702 commit 9899353
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .dumi/theme/common/styles/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ const GlobalStyle: React.FC = () => {
max-width: unset;
}
}
td {
&:nth-child(3) {
color: ${token.magenta7};
}
}
th {
color: #5c6b77;
Expand Down
71 changes: 71 additions & 0 deletions packages/design/src/tooltip/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { Tooltip } from '@oceanbase/design';
import { waitFakeTimer } from '../../../../../tests/util';
import { CloseCircleOutlined } from '@oceanbase/icons';

describe('Tooltip', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
jest.clearAllTimers();
});

it('default close icon should render correctly', async () => {
const { container } = render(
<Tooltip title="This is prompt text" closeIcon={true}>
<div id="hello">Hello world!</div>
</Tooltip>
);

const divElement = container.querySelector('#hello');
fireEvent.mouseEnter(divElement!);
await waitFakeTimer();
expect(container.querySelector('.ant-tooltip-open')).not.toBeNull();
expect(document.querySelectorAll('.ant-tooltip-close-icon').length).toBe(1);

// After clicking the close icon, the tooltip disappears.
fireEvent.click(document.querySelector('.ant-tooltip-close-icon'));
await waitFakeTimer();
expect(container.querySelector('.ant-tooltip-open')).toBeNull();
});

it('custom close icon should render correctly', async () => {
const { container } = render(
<Tooltip title="This is prompt text" closeIcon={<CloseCircleOutlined />}>
<div id="hello">Hello world!</div>
</Tooltip>
);

const divElement = container.querySelector('#hello');
fireEvent.mouseEnter(divElement!);
await waitFakeTimer();
expect(document.querySelectorAll('.anticon-close-circle').length).toBe(1);

// After clicking the close icon, the tooltip disappears.
fireEvent.click(document.querySelector('.ant-tooltip-close-icon'));
await waitFakeTimer();
expect(container.querySelector('.ant-tooltip-open')).toBeNull();
});

it('check `onOpenChange` arguments', async () => {
const onClose = jest.fn();
const { container } = render(
<Tooltip title="This is prompt text" closeIcon={true} onClose={onClose}>
<div id="hello">Hello world!</div>
</Tooltip>
);

const divElement = container.querySelector('#hello');
fireEvent.mouseEnter(divElement!);
await waitFakeTimer();

fireEvent.click(document.querySelector('.ant-tooltip-close-icon'));
expect(onClose).toHaveBeenCalled();

await waitFakeTimer();
expect(container.querySelector('.ant-tooltip-open')).toBeNull();
});
});
37 changes: 37 additions & 0 deletions packages/design/src/tooltip/demo/close-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Space, Tooltip, Button } from '@oceanbase/design';
import { CloseCircleOutlined } from '@oceanbase/icons';
import React, { useState } from 'react';

const App: React.FC = () => {
const [open, setOpen] = useState(true)
const log = (e: React.MouseEvent<HTMLElement>) => {
console.log(e);
};

return (
<Space>
<Tooltip title="This is prompt text"
closeIcon={true}
>
<Button>Default Close Tooltip</Button>
</Tooltip>

<Tooltip title="This is prompt text"
open={open}
closeIcon={true}
onClose={() => {
setOpen(false)
}}
onOpenChange={(v) => {
setOpen(v)
}}
>
<Button>Set open</Button>
</Tooltip>
<Tooltip title="This is prompt text This is prompt text This is prompt text This is prompt text" closeIcon={<CloseCircleOutlined />} onClose={log}>
<Button>Customize closeIcon</Button>
</Tooltip>
</Space>
);
}
export default App;
9 changes: 8 additions & 1 deletion packages/design/src/tooltip/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ nav:
- 💄 定制主题和样式,符合 OceanBase Design 设计规范。
- 🆕 新增 `type` 属性,支持 `default``light``info``success``warning``error` 五种类型的 Tooltip。
- 🆕 新增 `mouseFollow` 属性,支持鼠标跟随。
- 🆕 新增 `closeIcon` 属性,支持展示关闭按钮。
- 🆕 新增 `onClose` 属性,关闭按钮被点击时调用此函数,可以配合 `open``onOpenChange` 属性来控制 `Tooltip` 展示。

## 代码演示

<code src="./demo/basic.tsx" title="基本"></code>

<code src="./demo/close-icon.tsx" title="关闭按钮" description="Tooltip 可以通过设置 closeIcon 变为可关闭 Tooltip, 并支持自定义关闭按钮,设置为 true 时将使用默认关闭按钮。可关闭 Tooltip 具有 onClose 事件"></code>

<code src="./demo/type.tsx" title="Tooltip 类型" description="Tooltip 有五种类型:default、light、info、success、warning 和 error,以满足不同场景的提示需求。"></code>

<code src="./demo/mouse-follow.tsx" title="鼠标跟随" description="设置 `mouseFollow: true` 可开启鼠标跟随,此时会去掉箭头,并且 `placement`、`open` 和 `trigger` 等属性也将失效。"></code>
Expand All @@ -23,6 +27,9 @@ nav:
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| :-- | :-- | :-- | :-- | :-- |
| type | 类型 | default \| light \| info \| success \| warning \| error | default | - |
| mouseFollow | 是否跟随鼠标移动,开启后会去掉箭头,并且 `placement``open``trigger` 等属性也将失效 | boolean | false | - |
| mouseFollow | 是否跟随鼠标移动,开启后会去掉箭头,并且 `placement``open``closeIcon``trigger` 等属性也将失效 | boolean | false | - |
| closeIcon | 自定义关闭按钮 | boolean \| ReactNode | false | - |
| closeTitle | 自定义关闭标题 | ReactNode | - | - |
| onClose | 关闭时的回调(可通过 e.preventDefault() 来阻止默认行为) | (e) => void | - | - |

- 更多 API 详见 antd Tooltip 文档: https://ant.design/components/tooltip-cn
69 changes: 65 additions & 4 deletions packages/design/src/tooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Tooltip as AntTooltip } from 'antd';
import { Tooltip as AntTooltip, Space } from 'antd';
import type { TooltipPropsWithTitle as AntTooltipPropsWithTitle } from 'antd/es/tooltip';
import React from 'react';
import React, { useContext, useMemo, useState } from 'react';
import { CloseOutlined } from '@oceanbase/icons';
import { isNil } from 'lodash';
import { token } from '../static-function';
import MouseTooltip from './MouseTooltip';
import ConfigProvider from '../config-provider';
import useStyle from './style';
import classNames from 'classnames';

export * from 'antd/es/tooltip';

Expand All @@ -11,6 +16,8 @@ export type TooltipType = 'default' | 'light' | 'success' | 'info' | 'warning' |
export interface TooltipProps extends AntTooltipPropsWithTitle {
type?: TooltipType;
mouseFollow?: boolean;
closeIcon?: boolean | React.ReactNode;
onClose?: (e: React.MouseEvent<HTMLElement>) => void;
}

export const getTooltipTypeList = () => [
Expand Down Expand Up @@ -52,33 +59,87 @@ const Tooltip: CompoundedComponent = ({
color,
overlayInnerStyle,
mouseFollow,
closeIcon = false,
onClose,
title,
className,
open: propOpen,
...restProps
}) => {
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext);

const { prefixCls: customizePrefixCls } = restProps
const prefixCls = getPrefixCls('tooltip', customizePrefixCls);
const { wrapSSR, hashId } = useStyle(prefixCls);

const tooltipCls = classNames(className, hashId);
const [innerOpen, setInnerOpen] = useState(undefined)

const open = isNil(propOpen) ? innerOpen : propOpen

const handleCloseClick = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
onClose?.(e);

if (e.defaultPrevented) {
return;
}

setInnerOpen(false);
};

const hasCloseIcon = !!closeIcon
const CloseIconNode = useMemo(() => {
if (!hasCloseIcon) {
return null
}

return closeIcon === true ? <CloseOutlined className={`${prefixCls}-close-icon`} onClick={handleCloseClick} /> : <span className={`${prefixCls}-close-icon`} onClick={handleCloseClick}>
{closeIcon}
</span>
}, [closeIcon])

const titleNode = typeof title === 'function' ? title() : title
const titleWithCloseIcon = (
<Space className={`${prefixCls}-close-icon-wrap`}>
{titleNode}
{CloseIconNode}
</Space >
)

const typeList = getTooltipTypeList();
const typeItem = typeList.find(item => item.type === type);
return mouseFollow ? (
return wrapSSR(mouseFollow ? (
<MouseTooltip
title={title}
color={color || typeItem?.backgroundColor}
overlayInnerStyle={{
color: typeItem?.color,
...overlayInnerStyle,
}}
className={tooltipCls}
{...restProps}
>
{children}
</MouseTooltip>
) : (
<AntTooltip
title={hasCloseIcon ? titleWithCloseIcon : title}
color={color || typeItem?.backgroundColor}
open={open}
onOpenChange={(open) => {
setInnerOpen(open)
}}
overlayInnerStyle={{
color: typeItem?.color,
...overlayInnerStyle,
}}
className={tooltipCls}
{...restProps}
>
{children}
</AntTooltip>
);
));
};

if (process.env.NODE_ENV !== 'production') {
Expand Down
33 changes: 33 additions & 0 deletions packages/design/src/tooltip/style/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { CSSObject } from '@ant-design/cssinjs';
import type { FullToken, AliasToken, GenerateStyle } from 'antd/es/theme/internal';
import { genComponentStyleHook } from '../../_util/genComponentStyleHook';

export type TooltipToken = FullToken<'Tooltip'>;

export const genTooltipStyle: GenerateStyle<TooltipToken> = (token: TooltipToken): CSSObject => {
const { componentCls } = token;

return {
[componentCls]: {
[`${componentCls}-close-icon-wrap`]: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
wordBreak: 'break-all',
[`${componentCls}-close-icon`]: {
cursor: 'pointer',
},
},
},
};
};

export default (prefixCls: string) => {
const useStyle = genComponentStyleHook('Tooltip', token => {
return [genTooltipStyle(token as TooltipToken)];
}, ({ zIndexPopupBase, colorBgSpotlight }) => ({
zIndexPopup: zIndexPopupBase + 70,
colorBgDefault: colorBgSpotlight,
}),);
return useStyle(prefixCls);
};
22 changes: 22 additions & 0 deletions tests/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { act } from '@testing-library/react';

/**
* Wait for a time delay. Will wait `advanceTime * times` ms.
*
* @param advanceTime Default 1000
* @param times Default 20
*/
export async function waitFakeTimer(advanceTime = 1000, times = 20) {
for (let i = 0; i < times; i += 1) {
// eslint-disable-next-line no-await-in-loop
await act(async () => {
await Promise.resolve();

if (advanceTime > 0) {
jest.advanceTimersByTime(advanceTime);
} else {
jest.runAllTimers();
}
});
}
}

0 comments on commit 9899353

Please sign in to comment.