Skip to content

fix(studio): stop popovers and tooltips clipping at panel edges - #2890

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/studio-popover-clipping
Jul 30, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix/studio-popover-clipping

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jul 30, 2026 •

Copy link
Copy Markdown
Collaborator

What

Two Studio overlays were rendered cut off:

  • Renders tab, Format ? popover — the format guidance box was sliced at the right panel's left edge (~143px of it, at a 1512px window).
  • Timeline toolbar tooltips — "Selection tool (V)" rendered 32px off the left edge of the viewport.

Why

Both are the same defect class: an overlay positioned without accounting for what will clip it.

  • The format popover was an in-flow absolute top-full right-0 w-52 panel. Two ancestors in StudioRightPanel are overflow-hidden, so anything extending past the panel is cut. Anchored right-0 on a control near the panel's left column, the 208px box always reached outside the panel.
  • ui/Tooltip clamped only the bubble's centre point to the viewport, then centred the bubble on it with translate(-50%). A bubble wider than twice its distance from the edge still hangs off-screen.

How

  • FormatInfoTooltip renders in a portal to document.body with position: fixed, placed by the existing resolveFloatingPanelPosition helper (same pattern as the color picker in propertyPanelColor). Hover grace, aria-describedby, and Escape-to-dismiss behaviour are unchanged; the portaled panel carries its own pointer-enter/leave so moving onto it keeps it open.
  • New clampCentredLeft in floatingPanel.ts clamps a centred bubble by its measured width, and ui/Tooltip measures the bubble in useLayoutEffect (before paint, so there is no visible jump) and uses it.

Test plan

Verified in a live Studio (bun run dev, port 5190) against a real project: opened the Renders tab and hovered the Format ? (full box renders, nothing clipped), and hovered the timeline Selection tool (bubble clamped to left: 8).

Swept the other in-flow popovers by measuring their rects against every overflow-hidden ancestor in the real DOM: speed menu, shortcuts panel, snap/grid popover, and the ease-curve dropdown are all unclipped and stay in the viewport. Context menus and the color picker were already portaled.

Not covered: the font dropdown (propertyPanelFont.tsx) is left-0 right-0 so it cannot overhang horizontally, but it may still be clipped at the panel's bottom edge when the field sits low in the inspector. Untested here, separate fix.

  • Unit tests added/updated (4 cases for clampCentredLeft; full studio suite green: 3118 passed)
  • Manual testing performed
  • Documentation updated (if applicable)

after-format-popover.png

The Renders tab format popover rendered as an in-flow absolute panel inside
the right panel, which is overflow-hidden, so it was sliced at the panel
edge. Portal it to the body and position it with the shared floating-panel
helper instead.

The ui/Tooltip bubble clamped only its centre point to the viewport, so a
wide bubble near an edge still hung off-screen (the timeline Selection tool
tooltip lost 32px on the left). Clamp with the measured bubble width.

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — grade A — rubric CORRECT

Root-cause fit: Correctly identifies two separate defects. The Renders ? popover was an in-flow absolute right-0 w-52 inside StudioRightPanel's overflow-hidden ancestors — portal-to-body + position: fixed (via resolveFloatingPanelPosition) is the right escape. ui/Tooltip clamped only the centre point, so translate(-50%) still hung wide bubbles off-screen; measuring bubbleRef.offsetWidth in useLayoutEffect (pre-paint) and clamping via clampCentredLeft(centre, width, viewport, margin) fixes the geometry.

Claims verified (file:line at HEAD 9d2b05f2):

  • floatingPanel.ts:27-35 — clampCentredLeft matches PR body math; 4 unit cases pass by hand.
  • Tooltip.tsx:88 — useLayoutEffect measures before paint, avoids visible jump; clampedX used at :108.
  • RenderQueue.tsx:157-235 — portal to document.body, aria-expanded/aria-describedby={panelId} bridge portal boundary; Escape handler at :175-183.
  • File-list and +114/-31 match.

Adversarial findings:

  • nit — FORMAT_PANEL_SIZE.height=150 (RenderQueue.tsx:143) is estimated; if guidance text grows past ~150px the flip decision will pick "bottom" incorrectly near viewport bottom. Comment acknowledges. Low risk today.
  • nit — Portaled popover positions once and does not reposition on panel/window scroll (comment at RenderQueue.tsx:171). Hover-lived + trigger in flex-shrink-0 header makes this mostly safe; keyboard-open + scroll could drift.
  • nit — Tooltip.tsx:113 puts aria-describedby on a display: contents span wrapper; AT reading the focused child may not surface the tooltip description. Pre-existing shape; consider hoisting to child on next pass.
  • nit — z-[200] on both portals; verify no Modal/Dropdown sits lower next time layers are audited.

CI state: Preflight, Lint, Format, Studio load smoke, Regression, CodeQL green on the latest run. Typecheck + Build + Test still pending at review time; older run had unrelated Producer/Smoke:global-install failures (stale).

Suggested next step: Land after Typecheck + Build clear. File a follow-up for scroll-reposition + panel-height measurement if popover content grows.

— Review by Via

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small, well-scoped fix — the root-cause diagnosis is exactly right (in-flow absolute right-0 inside an overflow-hidden ancestor for the format popover; centre-clamp-then-translate leaves a wide bubble hanging off the edge for the tooltip), and both fixes match patterns the codebase already uses (portal + resolveFloatingPanelPosition for FormatInfoTooltip, clampCentredLeft measuring the bubble in useLayoutEffect for Tooltip). The two-commit pre-paint dance in Tooltip.tsx:75-77 is React 18-safe (setState inside useLayoutEffect triggers a synchronous re-render before browser paint), and the four clampCentredLeft unit tests cover the boundary cases cleanly. The 21+ existing <Tooltip> consumers gain corrected clamping with no API change, so this can't regress its callers.

Nits

  • RenderQueue.tsx:139 FORMAT_PANEL_SIZE = { height: 150 } is load-bearing for the flip decision AND the top clamp — the inline comment at :137-138 says "only the flip decision uses the height, and the clamp keeps the panel on screen either way," but floatingPanel.ts:59 clamps using the estimated height, so if actual > estimated and the anchor is near the viewport bottom, the real bottom can overflow. Current content sits around 130-160px, so this is safe today. Worth a // estimate must stay >= actual panel height line in the comment to prevent someone growing the content past the estimate without noticing.

  • RenderQueue.tsx:227 renders the portal at left: -9999, top: -9999 on the frame between setOpen(true) and the useLayoutEffect at :166 computing position. Pre-paint via useLayoutEffect, so no visible flash — but the pattern is un-remarked, and a reader has to piece it together. A one-liner (// Off-screen until useLayoutEffect at :166 sets position — same commit, no paint at -9999) would make the intent obvious.

  • Tooltip.tsx:89 computes clampedX on every render regardless of visible. When visible=false, bubbleWidth=0 and the computation is discarded. Not a perf issue, but hoisting inside the visible && createPortal(...) branch at :104 would tighten the render tree.

  • floatingPanel.ts:48 — the horizontal clamp Math.max(margin, viewport.width - panel.width - margin) folds panel.width > viewport.width - 2*margin into "stick to left margin" via the outer Math.max, mirroring clampCentredLeft's edge behavior (floatingPanel.test.ts:18-20). Consistent, but a one-line docstring on resolveFloatingPanelPosition calling out this edge case (like the one on clampCentredLeft:25-28) would help.

  • The Tooltip measurement path (two-commit useLayoutEffect dance) doesn't have an integration test — the 4 new unit tests cover clampCentredLeft in isolation, but not the wider Tooltip flow. If a future React upgrade changes useLayoutEffect scheduling, only manual QA catches the regression. A happy-dom test asserting bubbleWidth > 0 after act(() => render(...)) with the tooltip visible would document the invariant.

Questions

  • The PR body flags propertyPanelFont.tsx (font dropdown, left-0 right-0) as "not covered here, separate fix" for potential bottom-edge clipping. Is that scheduled as a follow-up, or landing implicitly when someone else touches the file?

What I didn't verify

  • The 21+ <Tooltip> consumers in a real browser — trusting Miguel's PR-body sweep across speed menu, shortcuts panel, snap/grid, ease-curve dropdown.
  • The bottom-edge clipping on the font dropdown (called out as out of scope).
  • Current-SHA CI — Tests on windows-latest, Build, Test, Typecheck, CLI smoke, Producer: integration tests are still pending on 9d2b05f2a; the two fail entries on Smoke: global install / Producer: matrix.lane tests are on an older run id (30502359506), appear stale.

— Review by Rames D Jusso

@miguel-heygen
miguel-heygen merged commit f81ac74 into main Jul 30, 2026
75 of 102 checks passed
@miguel-heygen
miguel-heygen deleted the fix/studio-popover-clipping branch July 30, 2026 00:27
dahans-msft2 pushed a commit to dahans-msft2/hyperframes that referenced this pull request Aug 6, 2026
…en-com#2890)

The Renders tab format popover rendered as an in-flow absolute panel inside
the right panel, which is overflow-hidden, so it was sliced at the panel
edge. Portal it to the body and position it with the shared floating-panel
helper instead.

The ui/Tooltip bubble clamped only its centre point to the viewport, so a
wide bubble near an edge still hung off-screen (the timeline Selection tool
tooltip lost 32px on the left). Clamp with the measured bubble width.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants