You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.tsx — TooltipPrimitive.{Provider, Root, Trigger, Portal, Content, Arrow}. Exports both Tooltip (default) and TooltipProvider.
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
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 #568on 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)
⚠️ 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.
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/tooltipPoC): #591 concluded in favor of full Tamagui, and Tamagui's ownTooltipwas 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-primitivesbranch is for this issue now. Findings:openworks directly on Tamagui'sTooltiproot — 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.Tooltip.test.tsxsuite.@tamagui/popper'sPopperprimitive builds its own interaction-lessuseFloating()instance and explicitly discards the focus-aware context Tamagui'sTooltipbuilds viafocus={{ enabled: true }}.Tab-focusing the trigger genuinely never opens the tooltip andEscape-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'sonFocus/onBlurto call the controlledopensetter directly (bypassing the broken path, untested), accept the gap, or wait on upstream.role="tooltip"trap: don't set an explicitroleonTooltip.Content— Tamagui already sets one on its floating portal wrapper, and setting it again breaks everygetByRole('tooltip')query.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.tsx—TooltipPrimitive.{Provider, Root, Trigger, Portal, Content, Arrow}. Exports bothTooltip(default) andTooltipProvider.content,placement(top/bottom/left/right),align,sideOffset,className,disabledTooltipProviderprops:delayDuration(250),skipDelayDuration(100),disableHoverableContent(true)App.tsx:74, and nested again insideMap.tsx:570withdelayDuration={0} skipDelayDuration={0}for instant map tooltips — the replacement must still support per-subtree overridesActivityTimeline.tsx,Map.tsx,TasksPanel.tsx,layout/Infobar/AchievementBadges.tsxTooltip.tsxcurrently carries a mobile workaround (thehandlePointerDownblock, 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 touchpointerdownand callspreventDefault().#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-calloutCSS 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
collisionPadding={8}, side/align resolution) — the hardest part to reproduce, and the part native needs a completely different solution forArrowelement, positioned against the triggeraria-describedbywiring and dismissal on escape/blurAcceptance criteria
@radix-ui/react-tooltipimport remainsTooltipandTooltipProviderunchanged; all 4 consumers work untouchedMap.tsx's zero-delay tooltips specifically)disabledand null/undefined/falsecontentstill short-circuit to rendering children bare#568acceptance criteria are met, or Unify tap/click tooltip behaviour #568 is explicitly confirmed as out of scope and left open with a note explaining whyTooltip.test.tsx,TasksPanel.test.tsx,Map.test.tsxpassNotes
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.