Skip to content

Commit 614d17e

Browse files
committed
feat(tooltip): add long hover
1 parent b91965c commit 614d17e

File tree

6 files changed

+286
-8
lines changed

6 files changed

+286
-8
lines changed

src/components/tooltip/Tooltip.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div
3-
v-show="show"
3+
v-show="show && !isHidden"
44
ref="tooltip"
55
class="tooltip"
66
data-testid="tooltip"
@@ -31,6 +31,8 @@ import {
3131
offset,
3232
flip,
3333
shift,
34+
inline,
35+
hide,
3436
} from '@floating-ui/dom'
3537
3638
const props = defineProps({
@@ -61,6 +63,7 @@ const placement = toRef(props, 'placement')
6163
const target = toRef(props, 'target')
6264
const tooltip = ref<HTMLDivElement>()
6365
const tooltipArrow = ref<HTMLDivElement>()
66+
const isHidden = ref(false)
6467
6568
const classNames = computed(() => {
6669
const result: string[] = []
@@ -87,7 +90,9 @@ watchEffect((onCleanup) => {
8790
middleware: [
8891
flip(),
8992
shift(),
93+
inline(),
9094
offset(12),
95+
hide(),
9196
arrow({ element: tooltipArrow.value }),
9297
],
9398
}).then(({ x, y, middlewareData, placement }) => {
@@ -96,6 +101,8 @@ watchEffect((onCleanup) => {
96101
97102
tooltip.value.style.left = `${x || 0}px`
98103
tooltip.value.style.top = `${y || 0}px`
104+
105+
isHidden.value = middlewareData.hide.referenceHidden
99106
}
100107
101108
if (tooltipArrow.value) {

src/components/tooltip/index.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,27 @@ Tooltips can be triggered (opened/closed) using modifiers `.click`, `.hover` and
130130
</template>
131131
```
132132

133+
### Long Hover
134+
135+
Special for `.hover`, it's have additional modifier `.long` which enable **Long Hover** mode.
136+
Duration can be change using `data-tooltip-long` attribute, default is `500` (ms).
137+
138+
<preview>
139+
<div class="flex flex-col space-gap-2 md:flex-row">
140+
<p-button v-p-tooltip.hover title="Hover">Hover</p-button>
141+
<p-button v-p-tooltip.hover.long title="Hover + Long">Long Hover</p-button>
142+
<p-button v-p-tooltip.hover.long title="Hover + Long + Duration" data-tooltip-long="1500">Super Long Hover</p-button>
143+
</div>
144+
</preview>
145+
146+
```vue
147+
<template>
148+
<p-button v-p-tooltip.hover title="Hover">Hover</p-button>
149+
<p-button v-p-tooltip.hover.long title="Hover + Long">Long Hover</p-button>
150+
<p-button v-p-tooltip.hover.long title="Hover + Long + Duration" data-tooltip-long="1500">Super Long Hover</p-button>
151+
</template>
152+
```
153+
133154
### Manual Trigger
134155

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

207229
### Events
208230

src/components/tooltip/index.spec.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ it('should able render the tooltip', async () => {
2020
template : `
2121
<div
2222
data-testid="sample"
23-
v-p-tooltip="'Hello World'" />
23+
v-p-tooltip="'Hello World'"
24+
data-tooltip-debounce="0" />
2425
`,
2526
})
2627

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

4041
await fireEvent.mouseLeave(sample)
41-
await delay(0)
42+
await delay(1)
4243

4344
expect(tooltip).not.toBeVisible()
4445
})
@@ -86,7 +87,8 @@ it('should able to change tooltip trigger using trigger modifiers (.click, .focu
8687
template : `
8788
<div
8889
data-testid="sample"
89-
v-p-tooltip.click="'Hello World'" />
90+
v-p-tooltip.click="'Hello World'"
91+
data-tooltip-debounce="0" />
9092
`,
9193
})
9294

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

98100
await fireEvent.mouseEnter(sample)
99101
await delay(0)
102+
await delay(0)
100103

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

304+
it('should enable long hover if modifier .long provided', async () => {
305+
const screen = render({
306+
directives: { PTooltip: pTooltip },
307+
template : `
308+
<div
309+
data-testid="sample"
310+
v-p-tooltip.hover.long="'Hello World'"
311+
data-tooltip-long="2"
312+
data-tooltip-debounce="0" />
313+
`,
314+
})
315+
316+
await delay(0)
317+
318+
const sample = screen.queryByTestId('sample')
319+
const tooltip = screen.queryByTestId('tooltip')
320+
321+
await fireEvent.mouseEnter(sample)
322+
await delay(0)
323+
324+
expect(tooltip).not.toBeVisible()
325+
326+
await delay(3)
327+
328+
expect(tooltip).toBeVisible()
329+
})
330+
301331
it('should export alias vPTooltip', () => {
302332
expect(pTooltip).toBe(vPTooltip)
303333
})

src/components/tooltip/index.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Placement } from '@floating-ui/dom'
33
import type { Directive } from 'vue-demi'
44
import { useSingleton } from '../global/use-singleton'
55
import createHandler from './utils/create-handler'
6+
import { addHoverListener, removeHoverListener } from './utils/on-hover'
67
import {
78
parsePlacement,
89
parseAction,
@@ -59,10 +60,18 @@ export const pTooltip: Directive<HTMLElement, string | boolean> = {
5960

6061
el.removeAttribute('title') // remove attribute title, we don't want native-browser's tooltip to shown
6162
el.addEventListener('click', handleClick)
62-
el.addEventListener('mouseenter', handleMouseEnter, { passive: true })
63-
el.addEventListener('mouseleave', handleMouseLeave, { passive: true })
6463
el.addEventListener('focus', handleFocus, { passive: true })
6564
el.addEventListener('blur', handleBlur, { passive: true })
65+
66+
const delay = Number.parseInt(el.dataset.tooltipLong ?? '500')
67+
const debounce = Number.parseInt(el.dataset.tooltipDebounce)
68+
69+
addHoverListener(el, {
70+
onHoverIn : handleMouseEnter,
71+
onHoverOut: handleMouseLeave,
72+
delay : bindings.modifiers.long ? delay : 0,
73+
debounced : debounce,
74+
})
6675
},
6776

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

105114
el.removeEventListener('click', handleClick)
106-
el.removeEventListener('mouseenter', handleMouseEnter)
107-
el.removeEventListener('mouseleave', handleMouseLeave)
108115
el.removeEventListener('focus', handleFocus)
109116
el.removeEventListener('blur', handleBlur)
110117
el.setAttribute('title', text)
111118

119+
removeHoverListener(el)
120+
112121
delete el.dataset.tooltipId
113122
delete el.dataset.tooltipAction
114123
delete el.dataset.tooltipText
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { vi } from 'vitest'
2+
import { fireEvent } from '@testing-library/dom'
3+
import { addHoverListener } from './on-hover'
4+
import { delay } from 'nanodelay'
5+
6+
it('should fix fast mouseenter - mouseleave', async () => {
7+
const target = document.createElement('span')
8+
const onHoverIn = vi.fn()
9+
const onHoverOut = vi.fn()
10+
11+
document.body.append(target)
12+
13+
addHoverListener(target, { onHoverIn, onHoverOut })
14+
15+
fireEvent.mouseEnter(target)
16+
fireEvent.mouseLeave(target)
17+
fireEvent.mouseEnter(target)
18+
fireEvent.mouseLeave(target)
19+
fireEvent.mouseEnter(target)
20+
21+
await delay(0)
22+
23+
expect(onHoverIn).toBeCalledTimes(1)
24+
expect(onHoverOut).toBeCalledTimes(0)
25+
})
26+
27+
it('should trigger hover out after some periodic', async () => {
28+
const target = document.createElement('span')
29+
const onHoverIn = vi.fn()
30+
const onHoverOut = vi.fn()
31+
32+
document.body.append(target)
33+
34+
addHoverListener(target, {
35+
onHoverIn,
36+
onHoverOut,
37+
debounced: 2,
38+
})
39+
40+
fireEvent.mouseEnter(target)
41+
fireEvent.mouseLeave(target)
42+
fireEvent.mouseEnter(target)
43+
fireEvent.mouseLeave(target)
44+
fireEvent.mouseEnter(target)
45+
46+
await delay(0)
47+
48+
expect(onHoverIn).toBeCalledTimes(1)
49+
expect(onHoverOut).toBeCalledTimes(0)
50+
51+
fireEvent.mouseLeave(target)
52+
fireEvent.mouseEnter(target)
53+
fireEvent.mouseLeave(target)
54+
55+
await delay(3)
56+
57+
expect(onHoverIn).toBeCalledTimes(1)
58+
expect(onHoverOut).toBeCalledTimes(1)
59+
})
60+
61+
it('should trigger hover in if delay provided', async () => {
62+
const target = document.createElement('span')
63+
const onHoverIn = vi.fn()
64+
const onHoverOut = vi.fn()
65+
66+
document.body.append(target)
67+
68+
addHoverListener(target, {
69+
onHoverIn,
70+
onHoverOut,
71+
delay: 2,
72+
})
73+
74+
fireEvent.mouseEnter(target)
75+
fireEvent.mouseLeave(target)
76+
fireEvent.mouseEnter(target)
77+
fireEvent.mouseLeave(target)
78+
fireEvent.mouseEnter(target)
79+
80+
await delay(0)
81+
82+
expect(onHoverIn).toBeCalledTimes(0)
83+
expect(onHoverOut).toBeCalledTimes(0)
84+
85+
await delay(3)
86+
87+
expect(onHoverIn).toBeCalledTimes(1)
88+
expect(onHoverOut).toBeCalledTimes(0)
89+
})
90+
91+
it('should not trigger hover in if mouseleave before delay', async () => {
92+
const target = document.createElement('span')
93+
const onHoverIn = vi.fn()
94+
const onHoverOut = vi.fn()
95+
96+
document.body.append(target)
97+
98+
addHoverListener(target, {
99+
onHoverIn,
100+
onHoverOut,
101+
delay: 2,
102+
})
103+
104+
fireEvent.mouseEnter(target)
105+
fireEvent.mouseLeave(target)
106+
fireEvent.mouseEnter(target)
107+
fireEvent.mouseLeave(target)
108+
fireEvent.mouseEnter(target)
109+
110+
await delay(0)
111+
112+
expect(onHoverIn).toBeCalledTimes(0)
113+
expect(onHoverOut).toBeCalledTimes(0)
114+
115+
fireEvent.mouseLeave(target)
116+
117+
await delay(3)
118+
119+
expect(onHoverIn).toBeCalledTimes(0)
120+
expect(onHoverOut).toBeCalledTimes(0)
121+
})

0 commit comments

Comments
 (0)