Skip to content

Commit

Permalink
feat(tooltip): add long hover
Browse files Browse the repository at this point in the history
  • Loading branch information
adenvt committed Jul 12, 2024
1 parent b91965c commit 614d17e
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 8 deletions.
9 changes: 8 additions & 1 deletion src/components/tooltip/Tooltip.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div
v-show="show"
v-show="show && !isHidden"
ref="tooltip"
class="tooltip"
data-testid="tooltip"
Expand Down Expand Up @@ -31,6 +31,8 @@ import {
offset,
flip,
shift,
inline,
hide,
} from '@floating-ui/dom'
const props = defineProps({
Expand Down Expand Up @@ -61,6 +63,7 @@ const placement = toRef(props, 'placement')
const target = toRef(props, 'target')
const tooltip = ref<HTMLDivElement>()
const tooltipArrow = ref<HTMLDivElement>()
const isHidden = ref(false)
const classNames = computed(() => {
const result: string[] = []
Expand All @@ -87,7 +90,9 @@ watchEffect((onCleanup) => {
middleware: [
flip(),
shift(),
inline(),
offset(12),
hide(),
arrow({ element: tooltipArrow.value }),
],
}).then(({ x, y, middlewareData, placement }) => {
Expand All @@ -96,6 +101,8 @@ watchEffect((onCleanup) => {
tooltip.value.style.left = `${x || 0}px`
tooltip.value.style.top = `${y || 0}px`
isHidden.value = middlewareData.hide.referenceHidden
}
if (tooltipArrow.value) {
Expand Down
22 changes: 22 additions & 0 deletions src/components/tooltip/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,27 @@ Tooltips can be triggered (opened/closed) using modifiers `.click`, `.hover` and
</template>
```

### Long Hover

Special for `.hover`, it's have additional modifier `.long` which enable **Long Hover** mode.
Duration can be change using `data-tooltip-long` attribute, default is `500` (ms).

<preview>
<div class="flex flex-col space-gap-2 md:flex-row">
<p-button v-p-tooltip.hover title="Hover">Hover</p-button>
<p-button v-p-tooltip.hover.long title="Hover + Long">Long Hover</p-button>
<p-button v-p-tooltip.hover.long title="Hover + Long + Duration" data-tooltip-long="1500">Super Long Hover</p-button>
</div>
</preview>

```vue
<template>
<p-button v-p-tooltip.hover title="Hover">Hover</p-button>
<p-button v-p-tooltip.hover.long title="Hover + Long">Long Hover</p-button>
<p-button v-p-tooltip.hover.long title="Hover + Long + Duration" data-tooltip-long="1500">Super Long Hover</p-button>
</template>
```

### Manual Trigger

If you prefer to trigger manually, add modifiers `.manual` and combine it with some ref.
Expand Down Expand Up @@ -203,6 +224,7 @@ Alternatively, you can manual trigger tooltip using `showTooltip`, `hideToolip`,
| `hover` | Enable hover trigger |
| `click` | Enable click trigger |
| `focus` | Enable focus trigger |
| `long` | Enable long hover mode |

### Events

Expand Down
36 changes: 33 additions & 3 deletions src/components/tooltip/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ it('should able render the tooltip', async () => {
template : `
<div
data-testid="sample"
v-p-tooltip="'Hello World'" />
v-p-tooltip="'Hello World'"
data-tooltip-debounce="0" />
`,
})

Expand All @@ -38,7 +39,7 @@ it('should able render the tooltip', async () => {
expect(tooltip).toHaveTextContent('Hello World')

await fireEvent.mouseLeave(sample)
await delay(0)
await delay(1)

expect(tooltip).not.toBeVisible()
})
Expand Down Expand Up @@ -86,7 +87,8 @@ it('should able to change tooltip trigger using trigger modifiers (.click, .focu
template : `
<div
data-testid="sample"
v-p-tooltip.click="'Hello World'" />
v-p-tooltip.click="'Hello World'"
data-tooltip-debounce="0" />
`,
})

Expand All @@ -97,6 +99,7 @@ it('should able to change tooltip trigger using trigger modifiers (.click, .focu

await fireEvent.mouseEnter(sample)
await delay(0)
await delay(0)

expect(tooltip).toBeInTheDocument()
expect(tooltip).not.toBeVisible()
Expand Down Expand Up @@ -298,6 +301,33 @@ it('should able to manual show/hide tooltip using `toggleTooltip`', async () =>
expect(tooltip).not.toBeVisible()
})

it('should enable long hover if modifier .long provided', async () => {
const screen = render({
directives: { PTooltip: pTooltip },
template : `
<div
data-testid="sample"
v-p-tooltip.hover.long="'Hello World'"
data-tooltip-long="2"
data-tooltip-debounce="0" />
`,
})

await delay(0)

const sample = screen.queryByTestId('sample')
const tooltip = screen.queryByTestId('tooltip')

await fireEvent.mouseEnter(sample)
await delay(0)

expect(tooltip).not.toBeVisible()

await delay(3)

expect(tooltip).toBeVisible()
})

it('should export alias vPTooltip', () => {
expect(pTooltip).toBe(vPTooltip)
})
17 changes: 13 additions & 4 deletions src/components/tooltip/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Placement } from '@floating-ui/dom'
import type { Directive } from 'vue-demi'
import { useSingleton } from '../global/use-singleton'
import createHandler from './utils/create-handler'
import { addHoverListener, removeHoverListener } from './utils/on-hover'
import {
parsePlacement,
parseAction,
Expand Down Expand Up @@ -59,10 +60,18 @@ export const pTooltip: Directive<HTMLElement, string | boolean> = {

el.removeAttribute('title') // remove attribute title, we don't want native-browser's tooltip to shown
el.addEventListener('click', handleClick)
el.addEventListener('mouseenter', handleMouseEnter, { passive: true })
el.addEventListener('mouseleave', handleMouseLeave, { passive: true })
el.addEventListener('focus', handleFocus, { passive: true })
el.addEventListener('blur', handleBlur, { passive: true })

const delay = Number.parseInt(el.dataset.tooltipLong ?? '500')
const debounce = Number.parseInt(el.dataset.tooltipDebounce)

addHoverListener(el, {
onHoverIn : handleMouseEnter,
onHoverOut: handleMouseLeave,
delay : bindings.modifiers.long ? delay : 0,
debounced : debounce,
})
},

async updated (el, bindings) {
Expand Down Expand Up @@ -103,12 +112,12 @@ export const pTooltip: Directive<HTMLElement, string | boolean> = {
tooltip.remove(id)

el.removeEventListener('click', handleClick)
el.removeEventListener('mouseenter', handleMouseEnter)
el.removeEventListener('mouseleave', handleMouseLeave)
el.removeEventListener('focus', handleFocus)
el.removeEventListener('blur', handleBlur)
el.setAttribute('title', text)

removeHoverListener(el)

delete el.dataset.tooltipId
delete el.dataset.tooltipAction
delete el.dataset.tooltipText
Expand Down
121 changes: 121 additions & 0 deletions src/components/tooltip/utils/on-hover.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { vi } from 'vitest'
import { fireEvent } from '@testing-library/dom'
import { addHoverListener } from './on-hover'
import { delay } from 'nanodelay'

it('should fix fast mouseenter - mouseleave', async () => {
const target = document.createElement('span')
const onHoverIn = vi.fn()
const onHoverOut = vi.fn()

document.body.append(target)

addHoverListener(target, { onHoverIn, onHoverOut })

fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)

await delay(0)

expect(onHoverIn).toBeCalledTimes(1)
expect(onHoverOut).toBeCalledTimes(0)
})

it('should trigger hover out after some periodic', async () => {
const target = document.createElement('span')
const onHoverIn = vi.fn()
const onHoverOut = vi.fn()

document.body.append(target)

addHoverListener(target, {
onHoverIn,
onHoverOut,
debounced: 2,
})

fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)

await delay(0)

expect(onHoverIn).toBeCalledTimes(1)
expect(onHoverOut).toBeCalledTimes(0)

fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)

await delay(3)

expect(onHoverIn).toBeCalledTimes(1)
expect(onHoverOut).toBeCalledTimes(1)
})

it('should trigger hover in if delay provided', async () => {
const target = document.createElement('span')
const onHoverIn = vi.fn()
const onHoverOut = vi.fn()

document.body.append(target)

addHoverListener(target, {
onHoverIn,
onHoverOut,
delay: 2,
})

fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)

await delay(0)

expect(onHoverIn).toBeCalledTimes(0)
expect(onHoverOut).toBeCalledTimes(0)

await delay(3)

expect(onHoverIn).toBeCalledTimes(1)
expect(onHoverOut).toBeCalledTimes(0)
})

it('should not trigger hover in if mouseleave before delay', async () => {
const target = document.createElement('span')
const onHoverIn = vi.fn()
const onHoverOut = vi.fn()

document.body.append(target)

addHoverListener(target, {
onHoverIn,
onHoverOut,
delay: 2,
})

fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)
fireEvent.mouseLeave(target)
fireEvent.mouseEnter(target)

await delay(0)

expect(onHoverIn).toBeCalledTimes(0)
expect(onHoverOut).toBeCalledTimes(0)

fireEvent.mouseLeave(target)

await delay(3)

expect(onHoverIn).toBeCalledTimes(0)
expect(onHoverOut).toBeCalledTimes(0)
})
Loading

0 comments on commit 614d17e

Please sign in to comment.