fix(webkit): place overlay panels with translate so re-anchors can transition - #932
Open
gabriel-lisboa-azion wants to merge 2 commits into
Open
fix(webkit): place overlay panels with translate so re-anchors can transition#932gabriel-lisboa-azion wants to merge 2 commits into
gabriel-lisboa-azion wants to merge 2 commits into
Conversation
…ansition Three fixes to `usePlacement`, all of them about a panel that moves after it is already up. THE PANEL IS PLACED WITH `translate`, NOT `top`/`left`. A translate is a compositor property, so re-anchoring an OPEN panel can be transitioned into place instead of snapping a layout property. And `translate` is a property of its own, so it composes with the `transform: scale(...)` the open/close keyframes animate rather than overwriting it — which a `transform: translate3d(...)` here would. `anchored` GATES THAT TRANSITION. It turns true one frame AFTER the opening placements land, so the entrance stays the panel's own scale animation and only later re-anchors glide. Without it a panel would travel in from the off-screen seed. It resets on close but the panel is NOT moved off-screen there: it is still in the DOM playing its leave animation. THE PANEL IS MEASURED WITH `offsetWidth`/`offsetHeight`, NOT `getBoundingClientRect`. The rect reports the TRANSFORMED box and the panel opens under `scale(0.9) → scale(1)`: read mid-animation it was up to 10% narrow, which placed a centred tooltip 4.1px off its trigger and left it there. The layout box does not depend on which frame of the entrance it is read on. A ResizeObserver RE-ANCHORS AN OPEN PANEL WHOSE CONTENT CHANGES SIZE. A centred panel's position is computed from its measured width, so a panel that resizes after placement keeps coordinates for the width it used to have — and nothing else catches it, since the window has not resized and the page has not scrolled. Measured on the copy control, whose tooltip swaps "Copy code" for "Copied" while the pointer is still on it: the panel lost 21px of width, slid 4px, and ended 10.4px off centre in one frame. A transform does not affect the observed box and placement only writes position, so this cannot feed back into itself. Preserves #891 (`fix(webkit): stop re-anchoring the popover panel when its own content scrolls`), which landed on main after this work started. The inner-scroll guard and its regression test are byte-identical in behaviour; only the comments differ, and the ones kept here are supersets that add why repositioning on an inner scroll also cost two bounding rects and an inline-style write per frame.
…e translate anchoring
gabriel-lisboa-azion
added a commit
that referenced
this pull request
Aug 26, 2026
Brings in the 17 commits main has since 2026-08-17 — the webkit documentation category (#919), the shared leading column for Select / MultiSelect / Dropdown (#935), resizable-panel (#902), the copy-button tooltip and card-box header inset (#936), --container-site (#923), the page-enter / content-enter arrivals (#896), the colored-icon currentColor fix (#930) and the skills that landed with them. Conflict resolution, all fifteen: Demo wins where it already carries work that is still an OPEN PR on main and main's side is the older shape that PR rewrote — toast-item (#900's `h-[1lh]` line box over #892's `self-center`), use-placement and popover.test (#932's translate anchoring over #891's prose), and the theme animation tokens, where the entry-count canary confirms every main entry survived alongside demo's. Main wins where its side is a superset or the corrected form — DESIGN.md (gains `text-body-prose-md` and `.layout-column-docs`), package.json (keeping demo's footer block would have duplicated the four keys main already merged in above it), and resizable-panel.test, whose named imports are the form that keeps CodeQL from reading a compiler-generated `.vue` default export as undefined. Both sides where each added something: .size-limit.json keeps demo's ratcheted 1.1 KB footer-root budget and takes main's fourteen doc-* entries; the ui-craft skill and the CLI plan keep demo's microcopy routing without duplicating main's entry. Generated artifacts were rebuilt rather than hand-resolved — theme dist via build:tokens, catalog.json via catalog:build — and both carry each side's tokens and all 258 exports.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three fixes to
usePlacement, all about a panel that moves after it is already up.1. Placed with
translate, nottop/leftTwo reasons:
translateis a property of its own, so it composes with thetransform: scale(...)the open/close keyframes animate. Atransform: translate3d(...)here would overwrite them.Note the transition names
translate, nottransform— in Tailwind v4 the translate utilities compile to the standalonetranslateproperty, so atransition-[transform]would animate nothing, silently (.claude/rules/styling.md).2.
anchoredgates that transitionIt turns true one frame after the opening placements land, so the entrance stays the panel's own scale animation and only later re-anchors glide. Without the gate a panel would visibly travel in from the off-screen seed.
It resets on close, but the panel is deliberately not moved off-screen there — it is still in the DOM playing its leave animation, and moving it would replace that animation with a disappearance.
3. Measured with
offsetWidth, notgetBoundingClientRectgetBoundingClientRectreports the transformed box, and the panel opens underscale(0.9) → scale(1). Read mid-animation it was up to 10% narrow, which placed a centred tooltip 4.1px off its trigger — and left it there, because nothing re-measured afterwards.offsetWidth/offsetHeightare the layout box, so placement no longer depends on which frame of the entrance it is read on.4. A
ResizeObserverre-anchors an open panel whose content resizesA centred panel's position is computed from its measured width, so a panel that resizes after placement keeps coordinates for the width it used to have. Nothing else catches this: the window has not resized and the page has not scrolled.
Measured on the copy control, whose tooltip swaps
Copy codeforCopiedwhile the pointer is still on it — the panel lost 21px of width, slid 4px, and ended 10.4px off centre in a single frame. Reads as a glitch, and the same applies to any overlay whose content changes while it is up.Safe from feedback: a transform does not affect the observed box, and
updatePositiononly writes position.#891 (stop re-anchoring the popover panel when its own content scrolls) landed on
mainafter this work started and touches the same two files. Naively replaying this branch would have reverted it.Verified with a real 3-way merge (
git merge-fileagainst the merge base):use-placement/index.js— the only conflict is the comment. The guard code is character-identical on both sides:The comment kept here is a superset: it retains fix(webkit): stop re-anchoring the popover panel when its own content scrolls #891's capture-phase rationale and adds why repositioning on an inner scroll also cost two bounding rects and an inline-style write per scrolled frame.
popover.test.ts— also comment-only. Test-name sets on both sides are identical (commdiff empty in both directions), so fix(webkit): stop re-anchoring the popover panel when its own content scrolls #891'sdoes not re-measure the trigger when the panel scrolls, but does when the page doesregression test survives intact.Also
tooltip.vueconsumesanchoredviadata-anchored, withmotion-reduce:transition-none..specs/tooltip.mdrecords the re-anchor motion row and the measurements.