diff --git a/.dumi/theme/common/styles/Markdown.tsx b/.dumi/theme/common/styles/Markdown.tsx
index f6c8bac06..811e22a78 100644
--- a/.dumi/theme/common/styles/Markdown.tsx
+++ b/.dumi/theme/common/styles/Markdown.tsx
@@ -324,6 +324,12 @@ const GlobalStyle: React.FC = () => {
max-width: unset;
}
}
+
+ td {
+ &:nth-child(3) {
+ color: ${token.magenta7};
+ }
+ }
th {
color: #5c6b77;
diff --git a/packages/design/src/tooltip/__tests__/index.test.tsx b/packages/design/src/tooltip/__tests__/index.test.tsx
new file mode 100644
index 000000000..a974ea529
--- /dev/null
+++ b/packages/design/src/tooltip/__tests__/index.test.tsx
@@ -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(
+
+ Hello world!
+
+ );
+
+ 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(
+ }>
+
Hello world!
+
+ );
+
+ 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(
+
+ Hello world!
+
+ );
+
+ 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();
+ });
+});
diff --git a/packages/design/src/tooltip/demo/close-icon.tsx b/packages/design/src/tooltip/demo/close-icon.tsx
new file mode 100644
index 000000000..808a77a8f
--- /dev/null
+++ b/packages/design/src/tooltip/demo/close-icon.tsx
@@ -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) => {
+ console.log(e);
+ };
+
+ return (
+
+
+
+
+
+ {
+ setOpen(false)
+ }}
+ onOpenChange={(v) => {
+ setOpen(v)
+ }}
+ >
+
+
+ } onClose={log}>
+
+
+
+ );
+}
+export default App;
diff --git a/packages/design/src/tooltip/index.md b/packages/design/src/tooltip/index.md
index faf95f866..5aebb889b 100644
--- a/packages/design/src/tooltip/index.md
+++ b/packages/design/src/tooltip/index.md
@@ -9,11 +9,15 @@ nav:
- 💄 定制主题和样式,符合 OceanBase Design 设计规范。
- 🆕 新增 `type` 属性,支持 `default`、`light`、`info`、`success`、`warning` 和 `error` 五种类型的 Tooltip。
- 🆕 新增 `mouseFollow` 属性,支持鼠标跟随。
+- 🆕 新增 `closeIcon` 属性,支持展示关闭按钮。
+- 🆕 新增 `onClose` 属性,关闭按钮被点击时调用此函数,可以配合 `open` 和 `onOpenChange` 属性来控制 `Tooltip` 展示。
## 代码演示
+
+
@@ -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
diff --git a/packages/design/src/tooltip/index.tsx b/packages/design/src/tooltip/index.tsx
index 27069f499..cef2699dc 100644
--- a/packages/design/src/tooltip/index.tsx
+++ b/packages/design/src/tooltip/index.tsx
@@ -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';
@@ -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) => void;
}
export const getTooltipTypeList = () => [
@@ -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) => {
+ e.stopPropagation();
+ onClose?.(e);
+
+ if (e.defaultPrevented) {
+ return;
+ }
+
+ setInnerOpen(false);
+ };
+
+ const hasCloseIcon = !!closeIcon
+ const CloseIconNode = useMemo(() => {
+ if (!hasCloseIcon) {
+ return null
+ }
+
+ return closeIcon === true ? :
+ {closeIcon}
+
+ }, [closeIcon])
+
+ const titleNode = typeof title === 'function' ? title() : title
+ const titleWithCloseIcon = (
+
+ {titleNode}
+ {CloseIconNode}
+
+ )
+
const typeList = getTooltipTypeList();
const typeItem = typeList.find(item => item.type === type);
- return mouseFollow ? (
+ return wrapSSR(mouseFollow ? (
{children}
) : (
{
+ setInnerOpen(open)
+ }}
overlayInnerStyle={{
color: typeItem?.color,
...overlayInnerStyle,
}}
+ className={tooltipCls}
{...restProps}
>
{children}
- );
+ ));
};
if (process.env.NODE_ENV !== 'production') {
diff --git a/packages/design/src/tooltip/style/index.ts b/packages/design/src/tooltip/style/index.ts
new file mode 100644
index 000000000..0b36c271f
--- /dev/null
+++ b/packages/design/src/tooltip/style/index.ts
@@ -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 = (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);
+};
diff --git a/tests/util.ts b/tests/util.ts
new file mode 100644
index 000000000..7d09ea60c
--- /dev/null
+++ b/tests/util.ts
@@ -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();
+ }
+ });
+ }
+}
\ No newline at end of file