Skip to content

useTooltipTriggerState delay warmup function triggers the tooltip immediately when open() is called twice #8026

@zachbwh

Description

@zachbwh

Provide a general summary of the issue here

In our design system, we had an issue with the delay functionality from the useTooltipTriggerState hook from react-stately

🤔 Expected Behavior?

When a delay is set on useTooltipTriggerState and state.open() is called twice in quick succession, the initial timeout should be respected, and the tooltip is only opened once the delay is lapsed.

😯 Current Behavior

When a delay is set on useTooltipTriggerState and state.open() is called twice in quick succession, the tooltip is immediately opened without any delay.

💁 Possible Solution

I believe there is an idempotency issue with the implementation of warmupTooltip in @react-stately/useTooltipTriggerState.ts. See the snippet below:

let warmupTooltip = () => {
  closeOpenTooltips();
  ensureTooltipEntry();
  if (!isOpen && !globalWarmUpTimeout && !globalWarmedUp) {
    globalWarmUpTimeout = setTimeout(() => {
      globalWarmUpTimeout = null;
      globalWarmedUp = true;
      showTooltip();
    }, delay);
  } else if (!isOpen) {
    showTooltip();
  }
};

If this function is called twice in quick succession. the condition !isOpen && !globalWarmUpTimeout && !globalWarmedUp will evaluate to false on the second call. This is because globalWarmUpTimeout is initialised on the first call and is not falsy on the second call.

A potential fix could look like the following:

let warmupTooltip = () => {
  closeOpenTooltips();
  ensureTooltipEntry();
  if (!isOpen && !globalWarmedUp) {
   // don't let an initialised timeout cause the tooltip to be opened immediately.
    if (!globalWarmUpTimeout) {
      globalWarmUpTimeout = setTimeout(() => {
        globalWarmUpTimeout = null;
        globalWarmedUp = true;
        showTooltip();
      }, delay);
    }
  } else if (!isOpen) {
    showTooltip();
  }
};

🔦 Context

No response

🖥️ Steps to Reproduce

You can repro in our storybook here, but it's somewhat meaningless without seeing our tooltip implementation. Our minimal repro looks something like the following:

import { useTooltipTriggerState } from 'react-stately'
import { useTooltip, useTooltipTrigger } from 'react-aria'

// ... somewhere in a hook

const state = useTooltipTriggerState({ delay: 1000 })
const {triggerProps} = useTooltipTrigger(props, state, triggerRef)
const { tooltipProps } = useTooltip(triggerProps, state)

We fixed it on our end by removing the state from useTooltip like the following:

import { useTooltipTriggerState } from 'react-stately'
import { useTooltip, useTooltipTrigger } from 'react-aria'

// ... somewhere in a hook

const state = useTooltipTriggerState({ delay: 1000 })
const {triggerProps} = useTooltipTrigger(props, state, triggerRef)
// stop double handling of `state.open` by `useTooltipTrigger` and `useTooltip`
const { tooltipProps } = useTooltip(triggerProps)

This works fine, but in my investigation I believe I found an issue with the react-stately implementation of useTooltipTriggerState.

Version

react-stately@3.36.1, react-aria@3.38.1

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

MacOS Sequoia 15.3.2

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions