Skip to content

Fix tooltip skipDelayDuration with disableHoverableContent #3562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

jamiebuilds
Copy link

Background

Scenario:

  • Multiple <Tooltip.Trigger>'s are 0px apart
  • <Tooltip.Provider skipDelayDuration={0}>
  • <Tooltip.Root disableHoverableContent> (not sure why this is required to reproduce)

Problem:

  • The delayDuration will be skipped, even though skipDelayDuration is 0

Solution

Before:

  • Even when skipDelayDuration was set to 0:
    • onOpen sets skipDelayTimerRef.current = false
    • onClose waits to set skipDelayTimerRef.current = true for 0ms in setTimeout()
  • So there's a very brief tick where skipDelayTimerRef.current is false
onOpen={() => {
  clearTimeout(skipDelayTimerRef.current)
  isOpenDelayedRef.current = false
}}
onClose={() => {
  setTimeout(() => {
    isOpenDelayedRef.current = true
  }, skipDelayDuration /* 0ms */)
}}

After:

  • When skipDelayDuration is <= 0:
    • onOpen never set skipDelayTimerRef.current = false
    • onClose instantly set skipDelayTimerRef.current = true
      • Just in case skipDelayDuration changed between onOpen and onClose
onOpen={() => {
  if (skipDelayDuration > 0) {
    isOpenDelayedRef.current = false
  }
}}
onClose={() => {
  if (skipDelayDuration > 0) {
    // ...setTimeout...
  } else {
    isOpenDelayedRef.current = false
  }
}}

Additional Fix

The cleanup code for skipDelayTimerRef is always using the initial value even as many different timers may be set/cleared.

const skipDelayTimerRef = useRef(0)

useEffect(() => {
  const skipDelayTimer = skipDelayTimerRef.current
  return () => {
    // `skipDelayTimer` will always be `0`
    // unless `onClose` was somehow called before this effect fired
    // but even then, it will be the initial timer forever
    clearTimeout(skipDelayTimer)
  }
}, [])

After: Always clear the last value of skipDelayTimerRef

useEffect(() => {
  return () => clearTimeout(skipDelayTimerRef.current);
}, [])

Copy link

changeset-bot bot commented May 23, 2025

⚠️ No Changeset found

Latest commit: a1ef3b2

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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.

1 participant