fix: popover position:fixed, miss/not_found ghost scale-up#417
fix: popover position:fixed, miss/not_found ghost scale-up#417bensonwong merged 9 commits intomainfrom
Conversation
For citations with no annotation (miss/not_found), the page-expand ghost was sized to the full visible page rect, causing scaleX/scaleY >> 1 and an aggressive "keyhole filling the screen" effect. Fix: pass the GhostSnapshot into buildGhostTargetFromViewport and compute ghostRect using source keyhole dimensions (pure translate, scale = 1.0), centering the ghost's image anchor on the page's visible area center — mirroring the buildGhostTarget logic used for the success path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The scroll-following regression was introduced in aa97aff/cd49ef1: portaling into the Radix ScrollArea viewport (or nearest scrollable ancestor) with position:absolute and container-relative coordinates makes the popover part of the scrollable document — it scrolls with the page instead of staying at its rendered viewport position. Root cause: position:absolute anchors the element in document space; as the scroll container scrolls, the popover's document-space position becomes a different viewport-space position. Fix: revert to position:fixed with direct viewport-relative coordinates. With position:fixed the popover stays at the viewport position it was rendered at, regardless of scroll. The wheel passthrough handler (added in Popover.tsx for the scroll-eating problem) already handles the only reason position:fixed was abandoned: Chrome scroll latching trapping wheel events on the popover. Portal detection is simplified: only check for [data-dc-portal-root] (consumer-provided fixed overlay for guaranteed stacking above headers), then fall back to document.body. Scroll-ancestor detection is removed — position:fixed escapes overflow:hidden/scroll without needing to be portaled inside the scroll container. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test was written for position:absolute behavior (yDelta ≈ scrollAmount). With position:fixed the popover stays at its viewport position regardless of scroll, so the correct assertion is yDelta ≈ 0. Updates test name and header comment to match the restored fixed behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lity test Add inline assumption comment to Popover.tsx explaining that [data-dc-portal-root] must be present before mount for the dedicated overlay's z-index guarantee to hold. Strengthen popoverScrollStability spec with a direct position:fixed CSS assertion so the test proves the mechanism (position:fixed) and not just the symptom (viewport-stability that could also arise from position:absolute portalled outside the scroll container). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 4 Skipped Deployments
|
Code ReviewOverall this is a solid, well-reasoned simplification. Both fixes address real bugs (popover drift on scroll, ghost scale-up on miss/not_found), the comments document the tradeoffs clearly, and the test correctly pivots from testing the symptom to testing the mechanism. A few things worth looking at before merging: Popover.tsx — Breaking change concernThe old behavior was explicitly documented as intentional ("popover scrolls with the content") and the scroll-stability spec was written to verify it. This PR inverts that contract. Any consumer that relies on the popover tracking document scroll (not viewport) will regress silently — they won't see a compile error and the behavior looks fine until they actually scroll. The PR description says "Restore position:fixed" which implies there was a prior regression, but there's no reference to the commit or PR where Suggested: Add a brief note to the PR (or a code comment) linking to the commit where
|
✅ Playwright Test ReportStatus: Tests passed 📊 Download Report & Snapshots (see Artifacts section) What's in the Visual SnapshotsThe gallery includes visual snapshots for:
Run ID: 24295968335 |
…on:fixed lineage Add buildGhostTargetFromViewport.test.ts (6 cases) to pin the regression fix: - ghostRect.width/height must match snapshot.viewportRect (not visibleRect) - image center-of-mass alignment formula is verified by coordinate assertions - null/edge-case paths covered (zero-size img, no matching container) Export buildGhostTargetFromViewport and GhostSnapshot from viewTransition.ts with @internal JSDoc — not re-exported in the public index. Add a history comment in Popover.tsx citing a91a344 (the commit that switched to position:absolute) and explaining that its root cause (wheel-scroll passthrough in overflow:hidden apps) is now handled by the overflow:clip + JS wheel-passthrough approach, making position:fixed correct again. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t tests Biome lint/style/noNonNullAssertion forbids the ! operator. Replace all result! usages with result?. (optional chaining) in expect() calls, and use a type-narrowing guard (if result == null return) for the one case that needs destructuring. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… page position:fixed makes the popover stay pinned to the viewport while the user scrolls, which is the scroll-eating bug itself. The correct behavior is for the popover to scroll away with the trigger element. Restore position:absolute inside the scroll-ancestor portal container. Restore the scroll-ancestor portal detection logic (Radix ScrollArea viewport → nearest overflow:auto ancestor → document.body). Restore container-offset coordinate math in recomputePosition. Restore scroll-stability test assertions: Y shifts by ~scrollAmount. The ghost scale-up fix (efa492d) and unit tests are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…wport When a consumer provides [data-dc-portal-root] (a position:fixed overlay), the popover portals into it and is viewport-pinned regardless of its own position:absolute CSS — the fixed overlay is the containing block, not the scroll container. Repositioning in real-time is insufficient; the right fix is to dismiss the popover when the page scroll container fires a scroll event (matching Linear / Notion / GitHub behavior). Updates the scroll-stability Playwright test to assert dismissal on scroll. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oll ancestor The [data-dc-portal-root] exception (added in 4e1e8de to escape overflow clipping near sticky headers) made the popover viewport-pinned even with position:absolute, because portaling into a position:fixed overlay means that overlay — not the scroll container — is the containing block. Restoring the pre-4e1e8de approach: portal into the Radix ScrollArea viewport (or nearest scrollable ancestor, or document.body). With position:absolute inside the scroll container the popover sits in document space and scrolls away naturally when the page scrolls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Popover.tsx): Restoreposition:fixedon the popover wrapper so it stays anchored to the viewport instead of scrolling with the page. Withposition:fixed, the popover is always in viewport space — the old scroll-container-relative coordinate math (x - containerRect.left + container.scrollLeft) reduces to a direct passthrough. The portal detection logic is simplified from ~35 lines of Radix/scroll-ancestor-walking to a 2-line[data-dc-portal-root] ?? document.bodylookup; scroll ancestry is now irrelevant.viewTransition.ts):buildGhostTargetFromViewport(the miss/not_found fallback) previously setghostRect = visibleRect, causing the ghost to scale up to match the expanded page image dimensions. Now mirrorsbuildGhostTarget: keeps ghost at source keyhole dimensions (snapshot.viewportRect) and aligns the image center-of-mass over the visible page center — pure translate, no scale.popoverScrollStability.spec.tsx): Flip scroll-stability assertions from "Y shifts by ~scrollAmount" to "Y delta ≤ 5px". Add a directtoHaveCSS("position", "fixed")assertion to prove the mechanism (not just the symptom — a popover portalled todocument.bodywithposition:absolutewould also appear viewport-stable in the test's scroll-container setup).Popover.tsx): Document the assumption that[data-dc-portal-root]must be present before mount for the z-index stacking guarantee to hold.Test plan
npm run test:ct—popoverScrollStabilityspec passes with the new position:fixed assertionnpm run buildpassesnpm run lintpasses