Skip to content

Commit 918dac3

Browse files
authored
Resolve "Tooltip does not hide when value is set to null dynamically" (baserow#4061)
1 parent 108c3ce commit 918dac3

File tree

4 files changed

+132
-17
lines changed

4 files changed

+132
-17
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"type": "bug",
3+
"message": "Hide tooltip when value dynamically changes to null",
4+
"domain": "database",
5+
"issue_number": 3501,
6+
"bullet_points": [],
7+
"created_at": "2025-10-30"
8+
}

web-frontend/modules/core/directives/tooltip.js

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export default {
6565
el.tooltipOptions = getOptions(el, binding)
6666
el.onClickOutsideCallback = null
6767

68+
if (el._tooltipEventsAdded) {
69+
return
70+
}
71+
el._tooltipEventsAdded = true
72+
6873
el.updatePositionEvent = (event) => {
6974
const rect = el.getBoundingClientRect()
7075
const position = el.getAttribute('tooltip-position') || 'bottom'
@@ -162,6 +167,11 @@ export default {
162167
if (el.tooltipOptions.contentIsHtml) {
163168
el.tooltipContentElement.innerHTML = el.tooltipOptions.value
164169
} else {
170+
while (el.tooltipContentElement.firstChild) {
171+
el.tooltipContentElement.removeChild(
172+
el.tooltipContentElement.firstChild
173+
)
174+
}
165175
el.tooltipContentElement.appendChild(
166176
document.createTextNode(el.tooltipOptions.value)
167177
)
@@ -179,17 +189,20 @@ export default {
179189
el.removeTimeout()
180190

181191
// make tooltip content preserved if pointer hovers
182-
el.tooltipContentElement.addEventListener('mouseenter', el.removeTimeout)
183-
el.tooltipContentElement.addEventListener(
184-
'mouseleave',
185-
el.tooltipMoveLeaveEvent
186-
)
192+
// Only add event listeners if they haven't been added yet
193+
if (!el._tooltipContentEventsAdded) {
194+
el._tooltipContentEventsAdded = true
195+
el.tooltipContentElement.addEventListener(
196+
'mouseenter',
197+
el.removeTimeout
198+
)
199+
el.tooltipContentElement.addEventListener(
200+
'mouseleave',
201+
el.tooltipMoveLeaveEvent
202+
)
203+
}
187204

188205
window.addEventListener('mousemove', el.tooltipMouseMoveEvent)
189-
190-
// When the user scrolls or resizes the window it could be possible that the
191-
// element where the tooltip is anchored to has moved, so then the position
192-
// needs to be updated. We only want to do this when the tooltip is visible.
193206
window.addEventListener('scroll', el.updatePositionEvent, true)
194207
window.addEventListener('resize', el.updatePositionEvent)
195208

@@ -211,12 +224,9 @@ export default {
211224
* visible, if duration is > 0.
212225
*/
213226
el.tooltipMoveLeaveEvent = () => {
214-
// we should remove any pending timeout before setting new one, because timeout
215-
// should be counted from the last mouse leave event.
216227
el.removeTimeout()
217228
el.tooltipTimeout = setTimeout(
218229
el.tooltipClose,
219-
// timeout from caller is in seconds. remember to convert to mseconds
220230
el.tooltipOptions.duration * 1000
221231
)
222232
}
@@ -230,21 +240,23 @@ export default {
230240
* actually closing the tooltip here
231241
*/
232242
el.tooltipClose = () => {
233-
// cleanup actions: remove window handlers set with onClickOutside()
234243
el.removeTooltipOutsideClickCallback()
235244

236245
if (el.tooltipElement) {
237-
el.tooltipElement.parentNode.removeChild(el.tooltipElement)
246+
if (el.tooltipElement.parentNode) {
247+
el.tooltipElement.parentNode.removeChild(el.tooltipElement)
248+
}
238249
el.tooltipElement = null
239250
el.tooltipContentElement = null
251+
el._tooltipContentEventsAdded = false
240252
}
241253

242254
window.removeEventListener('mousemove', el.tooltipMouseMoveEvent)
243255
window.removeEventListener('scroll', el.updatePositionEvent, true)
244256
window.removeEventListener('resize', el.updatePositionEvent)
245257
el.removeTimeout()
246258
}
247-
// those event listeners should be bind all the time to the el element
259+
248260
el.addEventListener('mouseenter', el.tooltipMouseEnterEvent)
249261
el.addEventListener('mouseleave', el.tooltipMoveLeaveEvent)
250262
},
@@ -257,10 +269,17 @@ export default {
257269
if (el.tooltipElement && el.tooltipElement.parentNode) {
258270
el.tooltipElement.parentNode.removeChild(el.tooltipElement)
259271
}
272+
273+
if (el._tooltipEventsAdded) {
274+
el.removeEventListener('mouseenter', el.tooltipMouseEnterEvent)
275+
el.removeEventListener('mouseleave', el.tooltipMoveLeaveEvent)
276+
el._tooltipEventsAdded = false
277+
}
278+
260279
el.tooltipElement = null
261280
el.tooltipContentElement = null
262-
el.removeEventListener('mouseenter', el.tooltipMouseEnterEvent)
263-
el.removeEventListener('mouseleave', el.tooltipMoveLeaveEvent)
281+
el._tooltipContentEventsAdded = false
282+
264283
window.removeEventListener('scroll', el.updatePositionEvent, true)
265284
window.removeEventListener('resize', el.updatePositionEvent)
266285
},

web-frontend/stories/ButtonText.stories.mdx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { config, withDesign } from 'storybook-addon-designs'
33
import { action } from '@storybook/addon-actions'
44

55
import ButtonText from '@baserow/modules/core/components/ButtonText'
6+
import tooltip from '@baserow/modules/core/directives/tooltip'
67

78
<Meta
89
title="Baserow/Buttons/Text"
@@ -111,3 +112,30 @@ export const designConfig = {
111112
## Props
112113

113114
<Props of={ButtonText} />
115+
116+
## Tooltip Test
117+
118+
export const TooltipTemplate = (args, { argTypes }) => ({
119+
components: { ButtonText },
120+
directives: { tooltip },
121+
data() {
122+
return {
123+
test: false
124+
}
125+
},
126+
template: `
127+
<div>
128+
<ButtonText v-tooltip="test ? null : 'hello'">
129+
test {{ test ? 'without tooltip' : 'with tooltip' }}
130+
</ButtonText>
131+
<br><br>
132+
<button @click="test = !test">Toggle Tooltip</button>
133+
</div>
134+
`,
135+
})
136+
137+
<Canvas>
138+
<Story name="With Tooltip">
139+
{TooltipTemplate.bind({})}
140+
</Story>
141+
</Canvas>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { shallowMount } from '@vue/test-utils'
2+
import ButtonText from '@baserow/modules/core/components/ButtonText'
3+
import tooltip from '@baserow/modules/core/directives/tooltip'
4+
5+
describe('Tooltip directive', () => {
6+
const Component = {
7+
template: `
8+
<ButtonText v-tooltip="tooltipValue">
9+
test {{ tooltipValue ? 'with tooltip' : 'without tooltip' }}
10+
</ButtonText>
11+
`,
12+
components: { ButtonText },
13+
directives: { tooltip },
14+
data() {
15+
return {
16+
tooltipValue: 'hello',
17+
}
18+
},
19+
}
20+
21+
it('shows tooltip when value is provided', async () => {
22+
const wrapper = shallowMount(Component)
23+
const buttonText = wrapper.findComponent(ButtonText)
24+
25+
await buttonText.trigger('mouseenter')
26+
27+
expect(document.querySelector('.tooltip')).toBeTruthy()
28+
})
29+
30+
it('hides tooltip when value is null', async () => {
31+
const wrapper = shallowMount(Component)
32+
const buttonText = wrapper.findComponent(ButtonText)
33+
34+
await wrapper.setData({ tooltipValue: null })
35+
36+
await buttonText.trigger('mouseenter')
37+
38+
expect(document.querySelector('.tooltip')).toBeFalsy()
39+
})
40+
41+
it('dynamically shows/hides tooltip when value changes', async () => {
42+
const wrapper = shallowMount(Component)
43+
const buttonText = wrapper.findComponent(ButtonText)
44+
45+
await buttonText.trigger('mouseenter')
46+
expect(document.querySelector('.tooltip')).toBeTruthy()
47+
48+
await wrapper.setData({ tooltipValue: null })
49+
expect(document.querySelector('.tooltip')).toBeFalsy()
50+
51+
await wrapper.setData({ tooltipValue: 'new tooltip' })
52+
await buttonText.trigger('mouseenter')
53+
expect(document.querySelector('.tooltip')).toBeTruthy()
54+
})
55+
56+
afterEach(() => {
57+
const tooltips = document.querySelectorAll('.tooltip')
58+
tooltips.forEach((tooltip) => tooltip.remove())
59+
})
60+
})

0 commit comments

Comments
 (0)