fix: measure overlay natural width before positioning#10295
Conversation
Reset the overlay's left/right insets before measuring in useOverlayPosition, mirroring the existing top/bottom reset. A stale inset from the previous positioning pass clamps an auto-width overlay's shrink-to-fit width, so overlays near the right page edge opened too narrow and visibly widened step by step as each ResizeObserver pass grew them a little further. Both insets must be reset together, since physical-left placements position via a right inset and a leftover value would stretch the overlay during measurement instead. Closes adobe#10050, closes adobe#7017
| // a single pass (360 + 100 - 300). With a stale left of 400px the measurement | ||
| // is clamped to 100px and the overlay only creeps toward its final position. |
There was a problem hiding this comment.
| // a single pass (360 + 100 - 300). With a stale left of 400px the measurement | |
| // is clamped to 100px and the overlay only creeps toward its final position. | |
| // a single pass (360 + 100 - 300). |
What did that sentence mean?
| // Pin the trigger to the right viewport edge regardless of story decorators, | ||
| // so the overlay has to fit its width against the page boundary (issue #10050). | ||
| <div style={{position: 'fixed', top: 8, right: 8}}> | ||
| <button ref={targetRef} {...triggerProps} onClick={() => state.toggle()}> |
There was a problem hiding this comment.
This file is pretty old, it looks like we never updated it after some changes because the new stories all render an error overlay due to onPress/etc coming through triggerProps. Easiest thing to do would be to make button a component using useButton and forward ref
Something like:
const Button = React.forwardRef(
(props: {children: React.ReactNode}, ref: React.RefObject<HTMLButtonElement>): JSX.Element => {
const {buttonProps} = useButton({...props, ref});
return (
<button ref={ref} {...buttonProps}>
{props.children}
</button>
);
}
);
|
|
||
| res.rerender(<AutoWidthExample placement="bottom end" triggerLeft={180} />); | ||
|
|
||
| // 180 + 100 - 60 = 220. The stale right inset from the previous placement must |
There was a problem hiding this comment.
The stale right inset
Was that 320? or what is this referring to?
| overlay.style.maxHeight = (window.visualViewport?.height ?? window.innerHeight) + 'px'; | ||
| } | ||
|
|
||
| // Reset the horizontal insets before measuring as well. An auto-width overlay is otherwise |
There was a problem hiding this comment.
Are there any cases where we don't want to do the style changes? It looks like we only do it sometimes for top/bottom.
What if a width is already specified for the overlay, then we shouldn't need to do this?
Or what if the position is already left?
In general this seems more correct compared to the previous attempt.
|
Looks like One of our chromatic stories is acting weirdly as a result of this. I cannot horizontally scroll this page anymore and it doesn't close the tooltip. |
Closes #10050
Closes #7017
What this does
Menus, popovers, and tooltips that open near the right edge of the page no longer open too narrow and visibly stretch wider step by step. They now open at their correct width in one clean move.
Why
When an overlay is repositioned, its width was measured while the previous position was still applied to it. Being pinned near the right page edge, the browser only gave it the leftover space up to that edge, so the measured width came out too small. Each reposition pass then let it grow only a little further, which users saw as a jittery widening animation (and, for tooltips, as dozens of reposition passes with
ResizeObserverloop errors — #7017).The fix mirrors what
useOverlayPositionalready does vertically before measuring (top/bottom/maxHeightreset): the horizontal insets are now reset too, so the overlay lays out at its natural width before its position is calculated.Two details matter here, and both are covered by tests:
rightmust be cleared together withleft. Physical-left placements (left ...,startin LTR,endin RTL, flipped submenus) position via arightinset. Settingleft: 0pxwhile a stalerightremains stretches the overlay between both insets and over-measures it, causing spurious flips. This is the difference from the earlier attempt in Fix resize loop issue #8164, which set onlyleftand was never merged.!maxHeightguard. Width measurement is unrelated to the user-providedmaxHeightprop, so overlays that pass one get the fix too.Before / after
Before (menu at the right page edge, item descriptions render after the initial positioning pass):

After:

What changed
packages/react-aria/src/overlays/useOverlayPosition.ts— resetleft/rightbefore the measurement pass (8 lines).packages/react-aria/test/overlays/useOverlayPosition.test.tsx— 5 new tests with a shrink-to-fitoffsetWidthmock that models how a stale inset clamps an auto-width absolutely-positioned element: single-pass convergence when content grows, the stale-rightstretch guard,maxHeight-prop independence, RTL (endplacements), and placement-change inset cleanup.packages/react-aria/stories/overlays/UseOverlayPosition.stories.tsx— two repro stories (bottom endandstart top) with a right-edge trigger whose content grows 1.5s after opening.Notes for reviewers
--trigger-widthmatching is unaffected (computed from the trigger, not the overlay).if (!position.position) return;between the measurement reset and the style re-apply is currently unreachable (calculatePositionalways returns a position), but if an exception ever escapedcalculatePosition, the overlay would be stranded at the reset position. Flagging rather than fixing to keep this diff minimal — happy to harden it here if preferred.✅ Pull Request Checklist:
📝 Test Instructions:
yarn jest packages/react-aria/test/overlays/useOverlayPosition.test.tsx— includes the newauto-width overlay (shrink-to-fit)suite.yarn start), open UseOverlayPosition → growing content at right page edge (Spectrum 2 ActionMenu renders too narrow for descriptions on page right #10050), click the trigger, and wait ~1.5s for the descriptions to render. The menu should snap to its full width in a single move with no incremental widening. Repeat with the start top variant and with an RTL locale.ResizeObserver loop completed with undelivered notificationsconsole errors.🧢 Your Project: