Skip to content

Replace Radix Tooltip — coordinate with the tap/click unification in #568 #583

Description

@gaidheal1

Update: #591 concluded in favor of Tamagui — Tooltip has its own completed PoC (#629), not just #579's

Superseding the earlier update (which pointed at #579's @rn-primitives/tooltip PoC): #591 concluded in favor of full Tamagui, and Tamagui's own Tooltip was separately PoC'd end to end in #629 (.claude/plans/issue-629-tamagui-poc.md) — this is a more direct head start than the @rn-primitives branch is for this issue now. Findings:

  • Controlled open works directly on Tamagui's Tooltip root — no ref/imperative workaround needed, an advantage over what Spike: choose the cross-platform primitive strategy (and resolve the ds-entry contract) #579 found with @rn-primitives.
  • Hover show/hide passes against this branch's real Tooltip.test.tsx suite.
  • Focus-to-open is a confirmed, real upstream bug, not a test artifact — verified in an actual Chromium browser via Playwright, traced to source. @tamagui/popper's Popper primitive builds its own interaction-less useFloating() instance and explicitly discards the focus-aware context Tamagui's Tooltip builds via focus={{ enabled: true }}. Tab-focusing the trigger genuinely never opens the tooltip and Escape-driven dismiss fails for the same reason. Filed upstream: tamagui/tamagui#4152. Not fixable at the app level within Tamagui's current API — the options are: hand-roll the trigger's onFocus/onBlur to call the controlled open setter directly (bypassing the broken path, untested), accept the gap, or wait on upstream.
  • Watch for a duplicate-role="tooltip" trap: don't set an explicit role on Tooltip.Content — Tamagui already sets one on its floating portal wrapper, and setting it again breaks every getByRole('tooltip') query.

Note on sources: the committed PoC doc .claude/plans/issue-629-tamagui-poc.md still ends with a "stay with @rn-primitives" recommendation section — that line is stale. The actual decision (#591's decision comment, posted after that doc, weighing additional Vitest-incompatibility findings from the restyle/gluestack PoCs that aren't in the Tamagui doc) is full Tamagui. Trust the issue comment, not that doc's closing paragraph.

This issue must decide, as an explicit acceptance item, how to handle the focus-to-open gap before shipping — not silently ship a keyboard-inaccessible tooltip.

Summary

Replace the Radix tooltip primitives. This one has a live behavioural change already queued against it in #568, and the two should almost certainly be done as one piece of work.

Relevant code

frontend/src/components/Tooltip/Tooltip.tsxTooltipPrimitive.{Provider, Root, Trigger, Portal, Content, Arrow}. Exports both Tooltip (default) and TooltipProvider.

  • Props: content, placement (top/bottom/left/right), align, sideOffset, className, disabled
  • TooltipProvider props: delayDuration (250), skipDelayDuration (100), disableHoverableContent (true)
  • Provider mounted at App.tsx:74, and nested again inside Map.tsx:570 with delayDuration={0} skipDelayDuration={0} for instant map tooltips — the replacement must still support per-subtree overrides
  • Consumers (4): ActivityTimeline.tsx, Map.tsx, TasksPanel.tsx, layout/Infobar/AchievementBadges.tsx

⚠️ Overlaps #568 — read that first

Tooltip.tsx currently carries a mobile workaround (the handlePointerDown block, with a long comment explaining it): Radix's hover logic ignores touch pointers, so a tap opened and immediately re-closed the tooltip. The fix toggles controlled state on touch pointerdown and calls preventDefault().

#568 says this didn't fully work and proposes replacing the hover/tap split entirely with a single click/tap-to-toggle model on both platforms, plus touch-action/-webkit-touch-callout CSS to stop the native long-press text selection that the current fix doesn't address.

That is a strong argument for merging the two: #568 wants to discard Radix's hover behaviour anyway, and a click-to-toggle tooltip is materially simpler to implement than the hover-with-delay model Radix gives us. Doing #568 on top of Radix and then re-doing it during this swap would be wasted work.

Recommendation: resolve #568 as part of this issue, and design the replacement around click/tap-to-toggle from the start rather than porting the hover model and then changing it. Confirm with the issue author before starting, since #568 is currently framed as independent and "not blocking the current staging→main release".

What Radix is providing

  • Positioning with collision detection (collisionPadding={8}, side/align resolution) — the hardest part to reproduce, and the part native needs a completely different solution for
  • The Arrow element, positioned against the trigger
  • Delay/skip-delay grouping across multiple tooltips via the Provider
  • aria-describedby wiring and dismissal on escape/blur

Acceptance criteria

  • No @radix-ui/react-tooltip import remains
  • Public props of Tooltip and TooltipProvider unchanged; all 4 consumers work untouched
  • Per-subtree provider overrides still work (verify Map.tsx's zero-delay tooltips specifically)
  • Placement and collision-avoidance behaviour preserved — tooltips near a viewport edge must not overflow
  • disabled and null/undefined/false content still short-circuit to rendering children bare
  • The #568 acceptance criteria are met, or Unify tap/click tooltip behaviour #568 is explicitly confirmed as out of scope and left open with a note explaining why
  • Trigger remains a focusable interactive element with the tooltip associated for screen readers (the component's docstring commits to this)
  • Tooltip.test.tsx, TasksPanel.test.tsx, Map.test.tsx pass
  • Verified on a real touch device, not just responsive emulation (Unify tap/click tooltip behaviour #568 calls this out explicitly)

Notes

⚠️ Also exported from src/ds-entry.js — resolved: it's WIP, not a published contract, so no prop-compatibility obligation follows from that.

Positioning/collision is the genuinely hard part and is worth checking against the spike's chosen approach early; if the library picked doesn't handle collision detection well, that's worth knowing before the overlay work too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions