Skip to content

Commit 6066521

Browse files
Copilotvinpogo
authored andcommitted
fix: support v-if on tooltip element
1 parent 18b7060 commit 6066521

File tree

4 files changed

+204
-23
lines changed

4 files changed

+204
-23
lines changed

.changeset/open-poets-visit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vingy/vueltip": patch
3+
---
4+
5+
When using v-if to control visibility of the tooltip element, hovering the tooltip, now keeps it visible.

demo/src/Tooltip.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ const { tooltipStyles, arrowStyles, show, content } =
1515
</script>
1616

1717
<template>
18+
<!-- v-show also works -->
1819
<div
19-
v-show="show"
20+
v-if="show"
2021
ref="tooltipElement"
2122
class="relative isolate"
2223
:style="[tooltipStyles]"
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
createApp,
4+
defineComponent,
5+
h,
6+
nextTick,
7+
ref,
8+
} from 'vue'
9+
import { useVueltip } from './composables'
10+
import {
11+
debouncedHoveredElement,
12+
hoveredElement,
13+
} from './state'
14+
15+
const TooltipVIf = defineComponent({
16+
setup() {
17+
const tooltipEl = ref<HTMLElement | null>(null)
18+
const { show, tooltipStyles, content } = useVueltip({
19+
tooltipElement: tooltipEl,
20+
})
21+
return () =>
22+
show.value
23+
? h(
24+
'div',
25+
{ ref: tooltipEl, style: tooltipStyles.value },
26+
content.value?.text ?? undefined,
27+
)
28+
: null
29+
},
30+
})
31+
32+
const TooltipVShow = defineComponent({
33+
setup() {
34+
const tooltipEl = ref<HTMLElement | null>(null)
35+
const { show, tooltipStyles, content } = useVueltip({
36+
tooltipElement: tooltipEl,
37+
})
38+
return () =>
39+
h(
40+
'div',
41+
{
42+
ref: tooltipEl,
43+
style: {
44+
...tooltipStyles.value,
45+
display: show.value ? '' : 'none',
46+
},
47+
},
48+
content.value?.text ?? undefined,
49+
)
50+
},
51+
})
52+
53+
const mountTooltip = (
54+
component: ReturnType<typeof defineComponent>,
55+
) => {
56+
const app = createApp(component)
57+
const container = document.createElement('div')
58+
document.body.appendChild(container)
59+
app.mount(container)
60+
return {
61+
container,
62+
unmount: () => {
63+
app.unmount()
64+
container.remove()
65+
},
66+
}
67+
}
68+
69+
describe('useVueltip listener lifecycle', () => {
70+
it('v-show: attaches mouseenter/mouseleave listeners when show becomes true', async () => {
71+
debouncedHoveredElement.value = undefined
72+
hoveredElement.value = undefined
73+
74+
const { container, unmount } =
75+
mountTooltip(TooltipVShow)
76+
await nextTick()
77+
78+
const refEl = document.createElement('div')
79+
debouncedHoveredElement.value = refEl
80+
await nextTick()
81+
82+
const el = container.firstElementChild as HTMLElement
83+
el.dispatchEvent(new MouseEvent('mouseenter'))
84+
expect(hoveredElement.value).toBe(refEl)
85+
86+
el.dispatchEvent(new MouseEvent('mouseleave'))
87+
expect(hoveredElement.value).toBeUndefined()
88+
89+
debouncedHoveredElement.value = undefined
90+
unmount()
91+
})
92+
93+
it('v-if: attaches listeners when element enters the DOM', async () => {
94+
debouncedHoveredElement.value = undefined
95+
hoveredElement.value = undefined
96+
97+
const { container, unmount } = mountTooltip(TooltipVIf)
98+
await nextTick()
99+
expect(container.firstElementChild).toBeNull()
100+
101+
const refEl = document.createElement('div')
102+
debouncedHoveredElement.value = refEl
103+
await nextTick()
104+
105+
const el = container.firstElementChild as HTMLElement
106+
expect(el).not.toBeNull()
107+
108+
el.dispatchEvent(new MouseEvent('mouseenter'))
109+
expect(hoveredElement.value).toBe(refEl)
110+
111+
el.dispatchEvent(new MouseEvent('mouseleave'))
112+
expect(hoveredElement.value).toBeUndefined()
113+
114+
debouncedHoveredElement.value = undefined
115+
unmount()
116+
})
117+
118+
it('v-if: removes listeners when element leaves the DOM (show=false)', async () => {
119+
debouncedHoveredElement.value = undefined
120+
hoveredElement.value = undefined
121+
122+
const { container, unmount } = mountTooltip(TooltipVIf)
123+
124+
const refEl = document.createElement('div')
125+
debouncedHoveredElement.value = refEl
126+
await nextTick()
127+
128+
const el = container.firstElementChild as HTMLElement
129+
const removedTypes: string[] = []
130+
el.removeEventListener = (type: string) => {
131+
removedTypes.push(type)
132+
}
133+
134+
debouncedHoveredElement.value = undefined
135+
await nextTick()
136+
137+
expect(removedTypes).toContain('mouseenter')
138+
expect(removedTypes).toContain('mouseleave')
139+
140+
unmount()
141+
})
142+
143+
it('v-show: removes listeners on component unmount', async () => {
144+
debouncedHoveredElement.value = undefined
145+
hoveredElement.value = undefined
146+
147+
const { container, unmount } =
148+
mountTooltip(TooltipVShow)
149+
await nextTick()
150+
151+
const refEl = document.createElement('div')
152+
debouncedHoveredElement.value = refEl
153+
await nextTick()
154+
155+
const el = container.firstElementChild as HTMLElement
156+
const removedTypes: string[] = []
157+
el.removeEventListener = (type: string) => {
158+
removedTypes.push(type)
159+
}
160+
161+
unmount()
162+
163+
expect(removedTypes).toContain('mouseenter')
164+
expect(removedTypes).toContain('mouseleave')
165+
166+
debouncedHoveredElement.value = undefined
167+
})
168+
})

packages/vueltip/src/composables.ts

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@ import {
77
useFloating,
88
} from '@floating-ui/vue'
99
import type { Maybe } from '@vingy/shared/types'
10-
import {
11-
computed,
12-
onMounted,
13-
type StyleValue,
14-
watch,
15-
} from 'vue'
10+
import { computed, type StyleValue, watch } from 'vue'
1611
import { getOption } from './options'
1712
import {
1813
debouncedHoveredElement,
@@ -38,19 +33,35 @@ export const useVueltip = ({
3833
floatingOptions,
3934
}: UseTooltipOptions) => {
4035
let initialParent: Maybe<HTMLElement>
41-
onMounted(() => {
42-
initialParent = tooltipElement.value?.parentElement
43-
tooltipElement.value?.addEventListener(
44-
'mouseenter',
45-
() =>
36+
37+
const show = computed(
38+
() => !!debouncedHoveredElement.value,
39+
)
40+
41+
watch(
42+
show,
43+
(value, _, onCleanup) => {
44+
if (!value) return
45+
const el = tooltipElement.value
46+
if (!el) return
47+
initialParent = el.parentElement
48+
49+
const onEnter = () =>
4650
(hoveredElement.value =
47-
debouncedHoveredElement.value),
48-
)
49-
tooltipElement.value?.addEventListener(
50-
'mouseleave',
51-
() => (hoveredElement.value = undefined),
52-
)
53-
})
51+
debouncedHoveredElement.value)
52+
const onLeave = () =>
53+
(hoveredElement.value = undefined)
54+
55+
el.addEventListener('mouseenter', onEnter)
56+
el.addEventListener('mouseleave', onLeave)
57+
58+
onCleanup(() => {
59+
el.removeEventListener('mouseenter', onEnter)
60+
el.removeEventListener('mouseleave', onLeave)
61+
})
62+
},
63+
{ flush: 'post' },
64+
)
5465
const middleware = [
5566
offset(_offset),
5667
flip(),
@@ -91,10 +102,6 @@ export const useVueltip = ({
91102
}
92103
})
93104

94-
const show = computed(
95-
() => !!debouncedHoveredElement.value,
96-
)
97-
98105
if (getOption('handleDialogModals')) {
99106
watch(show, (value) => {
100107
if (

0 commit comments

Comments
 (0)