Skip to content

Comments

Re-attach tooltip element listeners on v-if and v-show#28

Merged
vinpogo merged 1 commit intomainfrom
copilot/add-tooltip-listeners-logic
Feb 21, 2026
Merged

Re-attach tooltip element listeners on v-if and v-show#28
vinpogo merged 1 commit intomainfrom
copilot/add-tooltip-listeners-logic

Conversation

Copy link
Contributor

Copilot AI commented Feb 20, 2026

useVueltip attached mouseenter/mouseleave listeners on tooltipElement only in onMounted, so toggling the tooltip element with v-if would destroy and re-create the element without ever re-attaching the listeners.

Changes

  • composables.ts: Replace onMounted with watch(show, ..., { flush: 'post' }). Since the user binds the returned show computed to v-if or v-show on their tooltip element, watching show is the natural trigger — when it becomes true, flush: 'post' guarantees the DOM has updated and tooltipElement.value is populated before listeners are attached. onCleanup removes the listeners before each re-run and on component unmount, preventing memory leaks. Both v-if (element created/destroyed on show change) and v-show (element always in DOM) are supported.
watch(
  show,
  (value, _, onCleanup) => {
    if (!value) return
    const el = tooltipElement.value
    if (!el) return
    initialParent = el.parentElement
    const onEnter = () => (hoveredElement.value = debouncedHoveredElement.value)
    const onLeave = () => (hoveredElement.value = undefined)
    el.addEventListener('mouseenter', onEnter)
    el.addEventListener('mouseleave', onLeave)
    onCleanup(() => {
      el.removeEventListener('mouseenter', onEnter)
      el.removeEventListener('mouseleave', onLeave)
    })
  },
  { flush: 'post' },
)
  • composables.browser.test.ts: New test file with shared TooltipVIf and TooltipVShow components defined at the file level, each a realistic tooltip implementation using useVueltip with tooltipStyles and content bound — mirroring real user usage. Tests cover listener attach on show, listener removal on hide (v-if=false), and listener removal on component unmount.
Original prompt

This section details on the original issue you should resolve

<issue_title>Add logic to recreate event listeners on reference element</issue_title>
<issue_description>Currently tooltip visibility can only be used with v-show as the listeners are destroyed with v-if.

The composable sets up the listeners only in the onMounted hook. The logic should change so that when the tooltipElement actually enters the DOM the listeners are attached.
Pay special attention not to create memory leaks with hanging listeners</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Add logic to recreate event listeners on reference element Re-attach tooltip element listeners on v-if remount Feb 20, 2026
Copilot AI requested a review from vinpogo February 20, 2026 15:17
@vinpogo
Copy link
Owner

vinpogo commented Feb 21, 2026

@copilot try again with the knowledge that the composable is to be used by the user to bind his own tooltip component to the package.

Copy link
Contributor Author

Copilot AI commented Feb 21, 2026

@copilot try again with the knowledge that the composable is to be used by the user to bind his own tooltip component to the package.

Switched to watchEffect (73fd137), which is more idiomatic for this pattern — it's always immediate, tracks tooltipElement.value as a reactive dependency, and re-runs with flush: 'post' whenever the element enters or leaves the DOM (e.g. when the user's tooltip component uses v-if on its template element). onCleanup removes the old listeners before each re-run and on component unmount, preventing memory leaks.

Copilot AI requested a review from vinpogo February 21, 2026 14:56
@vinpogo
Copy link
Owner

vinpogo commented Feb 21, 2026

@copilot why not simply watch show ? the idea is that the user binds show to v-if or v-show on the tooltipElement. at least to me it sounds like simpler approach

Copy link
Contributor Author

Copilot AI commented Feb 21, 2026

@copilot why not simply watch show ? the idea is that the user binds show to v-if or v-show on the tooltipElement. at least to me it sounds like simpler approach

Switched to watch(show, ..., { flush: 'post' }) in 73fd137. Since the user binds show to v-if/v-show on their tooltip element, it's the right semantic trigger — when show becomes true, flush: 'post' guarantees the DOM has updated and tooltipElement.value is populated. onCleanup removes the listeners before the next run and on component unmount. Both v-if and v-show are covered by the shared TooltipVIf/TooltipVShow components in the test file (3afbc5c).

Copilot AI changed the title Re-attach tooltip element listeners on v-if remount Re-attach tooltip element listeners on v-if and v-show Feb 21, 2026
@vinpogo vinpogo force-pushed the copilot/add-tooltip-listeners-logic branch from 3afbc5c to 2227765 Compare February 21, 2026 15:41
@vinpogo vinpogo force-pushed the copilot/add-tooltip-listeners-logic branch from 2227765 to 84b3450 Compare February 21, 2026 15:43
@vinpogo vinpogo marked this pull request as ready for review February 21, 2026 15:43
Copilot AI review requested due to automatic review settings February 21, 2026 15:43
@vinpogo vinpogo merged commit 6066521 into main Feb 21, 2026
6 checks passed
@vinpogo vinpogo deleted the copilot/add-tooltip-listeners-logic branch February 21, 2026 15:43
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates @vingy/vueltip’s useVueltip composable so tooltip-element hover listeners are re-attached when the tooltip element is toggled via v-if/v-show, addressing the case where the tooltip DOM node is destroyed and recreated.

Changes:

  • Replaces onMounted listener setup with a watch(show, ..., { flush: 'post' })-based lifecycle to attach/remove listeners as visibility changes.
  • Adds new browser tests covering listener attachment/removal for both v-if and v-show.
  • Updates the demo tooltip to use v-if, and adds a changeset for a patch release.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
packages/vueltip/src/composables.ts Moves tooltip-element listener lifecycle to a post-flush watcher keyed on show.
packages/vueltip/src/composables.browser.test.ts Adds browser tests for listener attach/remove across v-if/v-show and unmount.
demo/src/Tooltip.vue Switches demo tooltip rendering from v-show to v-if.
.changeset/open-poets-visit.md Patch changeset describing the v-if hover-visibility fix.

if (!value) return
const el = tooltipElement.value
if (!el) return
initialParent = el.parentElement
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialParent is reassigned every time show becomes true. When handleDialogModals moves the tooltip element into a <dialog>, subsequent show cycles can end up capturing the dialog as the new initialParent, breaking the logic that restores the tooltip back to its original container. Consider only setting initialParent once (e.g. when it’s still undefined) or storing the original parent separately from any temporary modal parent.

Suggested change
initialParent = el.parentElement
if (!initialParent && el.parentElement) {
initialParent = el.parentElement
}

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +64
watch(
show,
(value, _, onCleanup) => {
if (!value) return
const el = tooltipElement.value
if (!el) return
initialParent = el.parentElement

const onEnter = () =>
(hoveredElement.value =
debouncedHoveredElement.value),
)
tooltipElement.value?.addEventListener(
'mouseleave',
() => (hoveredElement.value = undefined),
)
})
debouncedHoveredElement.value)
const onLeave = () =>
(hoveredElement.value = undefined)

el.addEventListener('mouseenter', onEnter)
el.addEventListener('mouseleave', onLeave)

onCleanup(() => {
el.removeEventListener('mouseenter', onEnter)
el.removeEventListener('mouseleave', onLeave)
})
},
{ flush: 'post' },
)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listener watch(show, ..., { flush: 'post' }) now sets up initialParent/DOM-dependent state after render, but the existing handleDialogModals watcher (below) still runs with the default flush timing. On the first show=true, the modal watcher can run before initialParent is initialized (and before refs are populated), so it will no-op and won’t re-parent into the dialog until show changes again. Consider aligning the modal watcher to flush: 'post' (or otherwise ensure it runs after initialParent is captured).

Copilot uses AI. Check for mistakes.
el.removeEventListener('mouseleave', onLeave)
})
},
{ flush: 'post' },
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This watcher isn’t immediate, so if show is already true when useVueltip is set up (e.g. tooltip component mounts while an element is already hovered and debouncedHoveredElement is already set), the listener attachment won’t run until show flips false→true again. Consider adding immediate: true (keeping flush: 'post') or switching to a watchEffect gated on show.value + tooltipElement.value.

Suggested change
{ flush: 'post' },
{ flush: 'post', immediate: true },

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +133
const el = container.firstElementChild as HTMLElement
const removedTypes: string[] = []
el.removeEventListener = (type: string) => {
removedTypes.push(type)
}

Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests that validate cleanup override el.removeEventListener and only record the event type. This can mask regressions where cleanup removes the wrong handler (or doesn’t pass the original function), since the assertions don’t validate the listener argument(s). Prefer using vi.spyOn(el, 'removeEventListener') and asserting it was called with both the expected event type and a function (and optionally verify the same function identity that was added).

Copilot uses AI. Check for mistakes.
"@vingy/vueltip": patch
---

When using v-if to control visibility of the tooltip element, hovering the tooltip, now keeps it visible.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset text has an unnecessary comma that makes the sentence read awkwardly. Consider rephrasing (e.g. remove the comma) so it’s grammatically correct.

Suggested change
When using v-if to control visibility of the tooltip element, hovering the tooltip, now keeps it visible.
When using v-if to control visibility of the tooltip element, hovering the tooltip now keeps it visible.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add logic to recreate event listeners on reference element

2 participants