Skip to content

Commit

Permalink
feat(tooltip): support hoverable tooltip
Browse files Browse the repository at this point in the history
  • Loading branch information
yqrashawn committed Oct 15, 2020
1 parent 45aeef2 commit 4cade3f
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 61 deletions.
104 changes: 104 additions & 0 deletions components/tooltip/__test__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('Tooltip', () => {
wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent)
await updateWrapper(wrapper, 400)
expectTooltipIsHidden(wrapper)
expect(() => wrapper.unmount()).not.toThrow()
})

it('should call custom mouse event handler when is controlled', async () => {
Expand Down Expand Up @@ -82,6 +83,7 @@ describe('Tooltip', () => {

await clickAway(wrapper)
expect(onClickAway).toBeCalledTimes(1)
expect(() => wrapper.unmount()).not.toThrow()
})

it('should render text when hover it', async () => {
Expand All @@ -97,6 +99,7 @@ describe('Tooltip', () => {
wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent)
await updateWrapper(wrapper, 400)
expectTooltipIsHidden(wrapper)
expect(() => wrapper.unmount()).not.toThrow()
})

it('should render react-node when click it', async () => {
Expand Down Expand Up @@ -124,6 +127,7 @@ describe('Tooltip', () => {

await updateWrapper(wrapper, 400)
expectTooltipIsHidden(wrapper)
expect(() => wrapper.unmount()).not.toThrow()
})

it('should render inner components', async () => {
Expand All @@ -135,6 +139,7 @@ describe('Tooltip', () => {
</Tooltip>,
)
expect(wrapper.find('#test').length).not.toBe(0)
expect(() => wrapper.unmount()).not.toThrow()
})

it('should render correctly by visible', async () => {
Expand All @@ -148,6 +153,7 @@ describe('Tooltip', () => {

await updateWrapper(wrapper, 150)
expect(wrapper.find('#visible').length).toBe(1)
expect(() => wrapper.unmount()).not.toThrow()
})

it('should render correctly by using wrong placement', async () => {
Expand All @@ -162,5 +168,103 @@ describe('Tooltip', () => {
</div>,
)
expect(wrapper.find('#default-visible').length).toBe(1)
expect(() => wrapper.unmount()).not.toThrow()
})

it('should have the hoverable effect', async () => {
const onVisibleChange = jest.fn()
const onMouseLeave = jest.fn()

const wrapper = mount(
<Tooltip
placement="top"
text="some text"
hoverable
hoverableTimeout={200}
offset={[0, 0]}
onMouseLeave={onMouseLeave}>
tooltip
</Tooltip>,
)

// normal show hide
// on
await act(async () => {
wrapper.find('.tooltip').simulate('mouseEnter', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsShow(wrapper)

// off
await act(async () => {
wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsShow(wrapper)
await updateWrapper(wrapper, 250)
expectTooltipIsHidden(wrapper)

wrapper.setProps({ onVisibleChange })

// hover tooltip -> leave t and hover content -> leave content
// on
await act(async () => {
wrapper.find('.tooltip').simulate('mouseEnter', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsShow(wrapper)

// off
await act(async () => {
wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsShow(wrapper)

// on content
await act(async () => {
wrapper.find('.tooltip-content').simulate('mouseEnter', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsShow(wrapper)

// off content
await act(async () => {
wrapper.find('.tooltip-content').simulate('mouseLeave', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsHidden(wrapper)

// hover t -> hover c and leave t -> leave c
// on
await act(async () => {
wrapper.find('.tooltip').simulate('mouseEnter', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsShow(wrapper)

// on content
await act(async () => {
wrapper.find('.tooltip-content').simulate('mouseEnter', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsShow(wrapper)

// off
await act(async () => {
wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent)
await updateWrapper(wrapper, 0)
})
await updateWrapper(wrapper, 250)
expectTooltipIsShow(wrapper)

// off content
await act(async () => {
wrapper.find('.tooltip-content').simulate('mouseLeave', nativeEvent)
await updateWrapper(wrapper, 0)
})
expectTooltipIsHidden(wrapper)

expect(() => wrapper.unmount()).not.toThrow()
})
})
67 changes: 36 additions & 31 deletions components/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useMemo, useEffect, useRef, useState, MouseEvent } from 'react'
import React, { useMemo, useEffect, useRef, useState, MouseEvent, useCallback } from 'react'
import withDefaults from '../utils/with-defaults'
import { usePopper } from 'react-popper'
import useTheme from '../styles/use-theme'
import { getColors } from './styles'
import { TriggerTypes, Placement, SnippetColors } from '../utils/prop-types'
// import CSSTransition from '../shared/css-transition'
import useClickAway from '../utils/use-click-away'
import type { Options } from '@popperjs/core/lib/modifiers/offset'

Expand All @@ -21,23 +20,23 @@ export const defaultProps = {
color: 'default' as SnippetColors,
trigger: 'hover' as TriggerTypes,
placement: 'auto' as Placement,
// enterDelay: 100,
// leaveDelay: 200,
offset: [0, 10] as Options['offset'],
className: '',
contentClassName: '',
hoverable: false,
hoverableTimeout: 200,
}

export interface Props extends Omit<React.HTMLAttributes<any>, 'onMouseEnter' | 'onMouseLeave'> {
hoverable?: boolean
hoverableTimeout?: number
text: string | React.ReactNode
color?: SnippetColors
placement?: Placement
visible?: boolean
defaultVisible?: boolean
hideArrow?: boolean
trigger?: TriggerTypes
// enterDelay?: number
// leaveDelay?: number
offset?: Options['offset']
contentClassName?: string
onVisibleChange?: TooltipOnVisibleChange
Expand All @@ -50,14 +49,14 @@ export interface Props extends Omit<React.HTMLAttributes<any>, 'onMouseEnter' |
export type TooltipProps = React.PropsWithChildren<Props>

const Tooltip: React.FC<TooltipProps> = ({
hoverable,
hoverableTimeout,
children,
defaultVisible,
text,
offset,
placement,
contentClassName,
// enterDelay,
// leaveDelay,
trigger,
color,
className,
Expand All @@ -70,7 +69,8 @@ const Tooltip: React.FC<TooltipProps> = ({
onClickAway,
...props
}: TooltipProps & typeof defaultProps) => {
// const timer = useRef<number>()
const hoverableTimer = useRef<number>()
const contentHovering = useRef<boolean>()
const theme = useTheme()
const parentRef = useRef(null)
const contentRef = useRef(null)
Expand All @@ -96,29 +96,27 @@ const Tooltip: React.FC<TooltipProps> = ({

const colors = useMemo(() => getColors(color, theme.palette), [color, theme.palette])

const changeVisible = (visible: boolean) => {
if (typeof onVisibleChange === 'function') onVisibleChange(visible)
setVisible(visible)
const changeVisible = (newVisible: boolean) => {
if (typeof onVisibleChange === 'function') onVisibleChange(newVisible)
setVisible(newVisible)
}
// const changeVisible = (nextState: boolean) => {
// const clear = () => {
// clearTimeout(timer.current)
// timer.current = undefined
// }
// const handler = (nextState: boolean) => {
// setVisible(nextState)
// onVisibleChange(nextState)
// clear()
// }
// clear()
// if (nextState) {
// timer.current = window.setTimeout(() => handler(true), enterDelay)
// return
// }
// timer.current = window.setTimeout(() => handler(false), leaveDelay)
// }

const mouseEventHandler = (e: MouseEvent, next: boolean) => {

const realHoverMouseLeaveHandler = useCallback((e: MouseEvent) => {
if (contentHovering?.current) return
customVisible === undefined && changeVisible(false)
onMouseLeave && onMouseLeave(e, changeVisible)
}, [])

const mouseEventHandler = (e: MouseEvent, next: boolean, fromContent?: boolean) => {
if (hoverable && trigger === 'hover' && !next && !fromContent) {
if (hoverableTimer.current) clearTimeout(hoverableTimer.current)
hoverableTimer.current = window.setTimeout(
() => realHoverMouseLeaveHandler(e),
hoverableTimeout,
)
return
}

if (customVisible === undefined) trigger === 'hover' && changeVisible(next)
if (next) onMouseEnter && onMouseEnter(e, changeVisible)
else onMouseLeave && onMouseLeave(e, changeVisible)
Expand Down Expand Up @@ -153,6 +151,13 @@ const Tooltip: React.FC<TooltipProps> = ({
className={`tooltip-content ${contentClassName}`}
ref={contentRef}
style={styles.popper}
onMouseEnter={() => {
contentHovering.current = true
}}
onMouseLeave={e => {
contentHovering.current = false
mouseEventHandler(e, false, true)
}}
{...attributes.popper}>
{/* @ts-ignore*/}
{!hideArrow && <div className="tooltip-arrow" ref={setArrowRef} style={styles.arrow} />}
Expand Down
38 changes: 23 additions & 15 deletions pages/en-us/components/tooltip.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ Displays additional information on hover.
`}
/>

<Playground
title="Hoverable tooltip content"
scope={{ Tooltip }}
code={`
<Tooltip hoverable text={'Try hover on me'}>Tooltip</Tooltip>
`}
/>

<Playground
title="Trigger"
desc="Trigger by click."
Expand Down Expand Up @@ -189,21 +197,21 @@ Displays additional information on hover.
<Attributes edit="/pages/en-us/components/tooltip.mdx">
<Attributes.Title>Tooltip.Props</Attributes.Title>

| Attribute | Description | Type | Accepted values | Default |
| -------------------- | ---------------------------------------------- | ---------------------------------------------------------------- | -------------------------------- | --------- |
| **text** | text of pop-up | `string` `React.ReactNode` | - | - |
| **visible** | visible or not | `boolean` | - | `false` |
| **defaultVisible** | visible on initial | `boolean` | - | `false` |
| **hideArrow** | hide arrow icon | `boolean` | - | `false` |
| **color** | preset style color | [TooltipColors](#tooltipcolors) | - | `default` |
| **placement** | position of the tooltip relative to the target | [Placement](#placement) | - | `top` |
| **trigger** | tooltip trigger mode | `'click' / 'hover'` | - | `hover` |
| **enterDelay**(ms) | delay before tooltip is shown | `number` | - | `100` |
| **leaveDelay**(ms) | delay before tooltip is hidden | `number` | - | `0` |
| **offset**(px) | skidding and distance, | check the doc at https://popper.js.org/docs/v2/modifiers/offset/ | - | `[0, 10]` |
| **contentClassName** | className of pop-up box | `string` | - | - |
| **onVisibleChange** | call when visibility of the tooltip is changed | `(visible: boolean) => void` | - | - |
| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
| Attribute | Description | Type | Accepted values | Default |
| ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------- | --------- |
| **text** | text of pop-up | `string` `React.ReactNode` | - | - |
| **visible** | visible or not | `boolean` | - | `false` |
| **defaultVisible** | visible on initial | `boolean` | - | `false` |
| **hideArrow** | hide arrow icon | `boolean` | - | `false` |
| **color** | preset style color | [TooltipColors](#tooltipcolors) | - | `default` |
| **placement** | position of the tooltip relative to the target | [Placement](#placement) | - | `top` |
| **trigger** | tooltip trigger mode | `'click' / 'hover'` | - | `hover` |
| **hoverable** | make the tooltip content hoverable, only works when `trigger==='hover'` | `boolean` | - | `false` |
| **hoverableTimeout**(ms) | timeout after which tooltip content get removed | `number` | - | `200` |
| **offset**(px) | skidding and distance, | check the doc at https://popper.js.org/docs/v2/modifiers/offset/ | - | `[0, 10]` |
| **contentClassName** | className of pop-up box | `string` | - | - |
| **onVisibleChange** | call when visibility of the tooltip is changed | `(visible: boolean) => void` | - | - |
| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |

<Attributes.Title>TooltipColors</Attributes.Title>

Expand Down
Loading

0 comments on commit 4cade3f

Please sign in to comment.