Skip to content

fix(react): match Base UI Tooltip behaviour - #871

Merged
Ernxst merged 9 commits into
mainfrom
tooltip-base-ui
Oct 2, 2026
Merged

Ernxst merged 9 commits into
mainfrom
tooltip-base-ui

Conversation

@Ernxst

@Ernxst Ernxst commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Harness: Codex
Agent: none
Model: gpt-6-astra

Task statements
  1. Port Base UI 1.8.0 Tooltip tests to GPU-IX and record observable differences. (Working-directory and workflow instructions omitted.)

  2. Fix the reported Tooltip differences and land the parity cases with verification. (Working-directory and workflow instructions omitted.)

Changes

Tooltip now matches Base UI's observable trigger, popup and portal behaviour across controlled state, event details, accessibility, detached handles and positioning. A Tooltip Positioner also reports the required Portal composition error, and the viewport exposes and remounts its current container when the active trigger changes.

Nested-trigger behaviour remains unresolved under #859 — Tooltip.Trigger drops nested children when rendering a custom element. The attempted implementation still failed Windows React shard 2/3: focusing a nested Trigger left the outer popup mounted. The nested-trigger implementation and tests are excluded from this PR, and #859 remains open.

Previous-content morphing, transition cleanup and transition-direction behaviour remain out of scope because the native renderer does not expose retained rendered-subtree snapshots or animation-lifecycle events; issue #879 — Tooltip.Viewport drops previous content during animated trigger changes tracks those cases.

Baseline differences

Issue or case On main On this branch
#845 — Tooltip.Popup renders without its required Positioner Popup children rendered without a Positioner. Tooltip reports that Tooltip.Positioner is missing.
#846 — Tooltip change event details lack cancel and allowPropagation details.cancel and details.allowPropagation were not functions. Change details expose both methods and their propagation state.
#847 — Tooltip emits controlled no-op changes and mounts closed content Closed controlled content remained mounted, and repeating open emitted another change. Closed content follows the requested state, and a controlled no-op emits no change.
#848 — Tooltip.Arrow is not hidden from assistive technology aria-hidden was absent. Tooltip.Arrow is hidden from assistive technology.
#849 — Tooltip handles silently ignore unregistered trigger IDs handle.open("missing-trigger") did not report a missing trigger. The handle reports the missing trigger.
#851 — Tooltip.Positioner offsets do not use resolved placement With Base UI geometry, both sideOffset and alignOffset reported expected end, received start. The Tooltip Positioner defaults alignment collision handling to flip, preserving the resolved end placement.
#852 — Tooltip handles omit warnings for missing or duplicate Roots The three warning scenarios emitted no warning. Missing and duplicate Roots report the expected warnings.
#853 — Tooltip Positioner ignores disableHoverablePopup pointerEvents was absent. The popup disables pointer events when hoverable content is disabled.
#856 — Tooltip handle does not switch payload while open Handoffs timed out or kept the previous payload. An open handle switches to the new trigger payload.
#857 — Tooltip stays open after its active detached trigger unmounts The active popup remained mounted after its trigger unmounted. The popup closes when its active detached trigger unmounts.
#859 — Tooltip.Trigger drops nested children when rendering a custom element The nested Trigger is absent when its parent Trigger renders a custom element. Not fixed in this PR; the nested-trigger changes were withdrawn after Windows React shard 2/3 left the outer popup open when focus moved to a nested Trigger.
#862 — Tooltip handle loses its active Root during an overlapping handoff An overlapping handoff left handle.isOpen false after the outgoing Root unmounted. The active Root remains open during the handoff.
#885 — Tooltip.Positioner renders outside Tooltip.Portal without reporting the missing Portal Rendering a Positioner outside the Portal produced no composition error. Tooltip reports Base UI: <Tooltip.Portal> is missing.

The Tooltip parity ledger has 131 applicable cases: 84 passed on main and 47 failed. This PR fixes 24 of those failures; the 23 nested-trigger cases tracked by #859 remain unresolved and are excluded. Eight cases do not apply to GPU-IX. Nineteen cases remain out because the native renderer lacks the support they need.

The regression coverage also includes the focus-leaves-triggers behaviour from #834, hover close delays, provider handoff and the Tooltip Positioner offset cases. The focus test does not reproduce the consumer's reported Tab sequence; its exact command and output are recorded in the parity table.

Cases that need renderer support

  1. TooltipPositioner.test.tsx#throws a descriptive error when rendered outside <Tooltip.Root> — the native renderer suppresses render-time React errors instead of exposing a thrown render() error.
  2. TooltipRoot.detached-triggers.test.tsx#keeps a default-open root open while a detached trigger migrates after the initial commit — GPU-IX has no server-rendering or hydration support.
  3. TooltipRoot.detached-triggers.test.tsx#should switch to a rendered disabled button trigger when trigger hover is enabled — the native test renderer cannot dispatch hover to a disabled button.
  4. TooltipRoot.test.tsx#should not open the outer tooltip when hovering over a nested trigger inside a shadow root — the native renderer has no ShadowRoot support.
  5. TooltipRoot.test.tsx#handles a composed path that starts with a ShadowRoot — the native renderer has no ShadowRoot or composedPath event support.
  6. TooltipRoot.test.tsx#handles a composed path that is empty — the native renderer has no ShadowRoot or composedPath event support.
  7. TooltipRoot.test.tsx#should not reopen the outer tooltip via the local reopen path for touch pointers — the native renderer has no pointer-event simulator or pointer-type parameter.
  8. TooltipRoot.test.tsx#should allow mouse hover after leaving a touch interaction — the native renderer has no pointer-event simulator or pointer-type parameter.
  9. TooltipTrigger.test.tsx#throws a descriptive error when rendered without a root or a handle — the native renderer suppresses render-time React errors instead of exposing a thrown render() error.
  10. TooltipViewport.test.tsx#should create morphing containers during transitions — the native renderer does not expose retained rendered-subtree snapshots or animation-completion events.
  11. TooltipViewport.test.tsx#keeps the latest transition active during rapid trigger changes — the native renderer does not expose retained rendered-subtree snapshots or animation-completion events.
  12. TooltipViewport.test.tsx#cleans up the transition when a lagging payload remounts the current container — the native renderer does not expose retained rendered-subtree snapshots or animation-completion events.
  13. TooltipViewport.test.tsx#should calculate right down direction — the native renderer does not expose the transition lifecycle needed to observe prior and current trigger geometry.
  14. TooltipViewport.test.tsx#should calculate left up direction — the native renderer does not expose the transition lifecycle needed to observe prior and current trigger geometry.
  15. TooltipViewport.test.tsx#should calculate right (horizontal only) direction — the native renderer does not expose the transition lifecycle needed to observe prior and current trigger geometry.
  16. TooltipViewport.test.tsx#should calculate down (vertical only) direction — the native renderer does not expose the transition lifecycle needed to observe prior and current trigger geometry.
  17. TooltipViewport.test.tsx#should handle tolerance for small differences — the native renderer does not expose the transition lifecycle needed to observe prior and current trigger geometry.
  18. TooltipViewport.test.tsx#should calculate left down direction — the native renderer does not expose the transition lifecycle needed to observe prior and current trigger geometry.
  19. TooltipViewport.test.tsx#should calculate right up direction — the native renderer does not expose the transition lifecycle needed to observe prior and current trigger geometry.

GitHub's pull_request CI run 37066776451 passed on macOS, Linux and Windows for this head. Nested-trigger behaviour tracked by #859 remains unverified: the previous Windows React shard 2/3 run failed its focus test because the outer popup stayed mounted (run 37064016778), so the nested-trigger tests and implementation are excluded from this PR.

Closes #845
Closes #846
Closes #847
Closes #848
Closes #849
Closes #851
Closes #852
Closes #853
Closes #856
Closes #857
Closes #862
Closes #885

@Ernxst
Ernxst merged commit 072594a into main Oct 2, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment