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