[EuiFlyout] Preserve pixel width on container resize when size is numeric - #9976
[EuiFlyout] Preserve pixel width on container resize when size is numeric#9976clintandrewhall wants to merge 4 commits into
size is numeric#9976Conversation
|
💚 CLA has been signed |
|
👋 Since this is a community submitted pull request, a Buildkite build has not been started automatically. Would an Elastic organization member please verify the contents of this pull request and kick off a build manually? |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ETTN39EVo6iTDT7cztt7T
There was a problem hiding this comment.
🟡 Changes recommended
The callback reset can still allow container-driven onResize calls when the callback identity changes concurrently.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates resizable flyouts to preserve numeric pixel widths during container resizing.
Changes:
- Re-clamps numeric sizes while retaining proportional scaling for named sizes.
- Prevents most container-driven
onResizecallbacks. - Adds regression tests and changelog entries.
File summaries
| File | Description |
|---|---|
use_flyout_resizable.ts |
Implements conditional resize behavior. |
use_flyout_resizable.test.ts |
Tests resizing, clamping, and callbacks. |
9976.md |
Documents both fixes. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…`size` A resizable flyout given a numeric `size` did not keep the width the user dragged it to. When the flyout container's width changed, the current pixel width was multiplied by the reference-width ratio to preserve the flyout's *percentage* of the container. That is correct for named sizes — EUI defines `s`/`m`/`l` as 25%/50%/75% in `flyout.styles.ts` — but a numeric `size` is a pixel contract: the consumer measured, persisted, and re-supplied a pixel value, and scaling discards exactly the information they are trying to preserve. Branch on the `size` type in the constraint-change path: numeric sizes are re-clamped only, named sizes keep scaling. Also reset `callOnResize` when the reference width changes. It is set to `true` on `onMouseUp`/`onKeyDown` and was never reset on this path, so container resizes fired `onResize` with a machine-generated width and consumers that persist that value had the user's stored width overwritten. The reset is gated on the reference width actually changing so it cannot suppress the legitimate `onResize` at the end of a drag. Closes #9969 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ETTN39EVo6iTDT7cztt7T
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ETTN39EVo6iTDT7cztt7T
…ring Resetting `callOnResize` state was not sufficient on its own. The `onResize` effect can re-run in the same render the container resize arrives in — for example when a parent rerender also hands down a new inline `onResize` — and it then reads the render's pre-update `callOnResize === true`, calling the consumer back with the pre-clamp width before the state reset lands. Track the container-driven resize in a ref instead. It is written synchronously by the constraint effect, which is declared before the `onResize` effect and so always runs first within a commit, making the signal visible to the callback effect in the same pass. The ref is cleared by `onMouseUp`/`onKeyDown` — the only callers that set `callOnResize` back to `true` — so a user resize following a container resize is still reported. Reported by the Copilot reviewer on PR #9976. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ETTN39EVo6iTDT7cztt7T
4cf430f to
40cfb8c
Compare
…back Re-clamping the already-clamped `flyoutWidth` was lossy. Shrinking the container past the requested width overwrote the request with the clamped result, so growing the container back left the flyout stuck at the shrunken width instead of returning to the width that was asked for. Track the requested width — the consumer's numeric `size`, or the width the user last dragged / keyed to — separately from the rendered width, and always clamp the request rather than the previous result. Clamping becomes non-destructive, so the flyout narrows while there is not enough room and returns to the requested width once there is. The request is updated wherever intent is expressed: a `size` prop change, the initial measurement, and the drag / keyboard handlers. Named sizes are untouched and still scale proportionally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ETTN39EVo6iTDT7cztt7T
|
@weronikaolejniczak you're right, and thanks for catching it — that was a real regression in this PR. Fixed in 17beead. What was wrong. The numeric branch re-clamped the already-clamped return getFlyoutMinMaxWidth(currentWidth);which is lossy. With The fix. Track the width that was requested separately from the width being rendered, and always clamp the request rather than the previous result: const requestedWidthRef = useRef<number | null>(null);
...
if (typeof _size === 'number') {
return getFlyoutMinMaxWidth(requestedWidthRef.current ?? _size);
}Clamping is now non-destructive: the flyout narrows while there isn't room and returns to the requested width once there is. The request is updated wherever intent is actually expressed — a Two tests, both confirmed failing against 40cfb8c before the fix: Both now pass; full flyout suite is 464/464 and Named sizes are unchanged — the proportional-scaling path is byte-identical to One pre-existing wrinkle I noticed while tracing this, not introduced here and left alone to keep scope tight: for named sizes the scale factor compounds off the clamped result, so if Ready for another look when you have a moment. Generated by Claude Code |
💚 Build Succeeded
History
|
💚 Build Succeeded
History
|
weronikaolejniczak
left a comment
There was a problem hiding this comment.
blocking: Click / arrow while the flyout is clamped still fires onResize with the clamped width (and keyboard also overwrites requestedWidthRef). Discover persists that so the stored width is lost again.
Repro: size={800}, shrink container so it clamps to 540, click the handle (no drag) or press ArrowLeft at max → onResize(540). Grow the window back and keyboard path stays at 540; click path persists 540 for the next open.

Summary
Closes #9969
What: A resizable
EuiFlyoutgiven a numericsizeno longer rescales when the flyout container's width changes. It keeps the user's pixel width and is only re-clamped. Container-driven resizes also no longer fireonResize.Why: When the container width changed,
useEuiFlyoutResizablemultiplied the current pixel width by the reference-width ratio to preserve the flyout's percentage of the container. For atype="push"flyout this moves both panes on a single container resize — the flyout rescales and the pushed content reflows to match.Separately,
callOnResizeis set totrueononMouseUp/onKeyDownand was never reset on the constraint-change path, so these machine-generated rescales calledonResize. Consumers that persist the callback value had the user's stored width permanently overwritten by a window resize.How: Branch on the
sizetype in the constraint-change path ofuse_flyout_resizable.ts, and resetcallOnResizewhen the reference width actually changed.Why the fix is conditional, and why it does not conflict with #9683
#9683 fires on the same trigger — the flyout container's width changing — but asks for the opposite outcome: a manually-resized main+child pair should revert to its coded
s/msize when it goes stacked. Both asks are valid because they describe different consumer contracts, andtypeof size === 'number'is the discriminator:sizeprop's'/'m'/'l'/'fill'544)Scaling is correct for named sizes because EUI literally defines them as percentages in
packages/eui/src/components/flyout/flyout.styles.ts:An
mflyout that stays at 50% through a container resize is behaving as designed, and the pre-existing code comment said so explicitly ("preserves the flyout's percentage position in both directions"). Removing the scale factor unconditionally would be a silent semantic change for every resizable named-size flyout, and would pre-empt #9683's design space. This PR therefore changes the numeric branch only.The test
preserves the percentage for a named "size"inuse_flyout_resizable.test.tsis the regression guard for that: it asserts anmflyout still holds 50% across a reference-width change. It passes both with and without this diff, which is exactly what a guard should do.Notes for reviewers
resizeModeprop deliberately not added. The issue floats an explicitresizeMode: 'pixel' | 'percent'prop as the more discoverable API. This PR implements the inferred (typeof size === 'number') version, which needs no consumer changes and keeps the diff reviewable. Happy to switch to the explicit prop if the team prefers it.callOnResizereset is gated on the reference width having changed (if (_referenceWidth !== prevRefWidth)) rather than firing unconditionally. An unconditional reset in this branch would risk suppressing the legitimateonResizeat the end of a drag when a sibling width or other clamp input changes around the same tick. There is a test covering that a drag-then-release still firesonResizeexactly once with the final width.%round-trip is lossless on this path.referenceWidthcomes fromuseResizeObserver(container, 'width')(which reportsborderBoxSize.inlineSize) andflyout.component.tsxconverts back withcontainerRect.widthfromgetBoundingClientRect()— both border-box. This PR does not change the%output mechanism.container.clientWidthfallback inflyout.component.tsx:365(used when the ResizeObserver hasn't reported yet) is content-box where the rest of the path is border-box, so it is off by the container's padding on the first frame — including the push padding this component applies itself. That's a real but distinct bug and is intentionally not folded in here.Reporting consumer
Kibana's Discover document details flyout. Kibana scopes flyouts to the app workspace container (
#app-main-scroll), so a window resize, a sidebar resize, or opening the AI Assistant all changereferenceWidth; Discover persists theonResizewidth tolocalStorage, which is the user-visible half of the bug. Kibana tracks its own consumer-side fixes (the ones that do not need this change) at elastic/kibana#287943.API Changes
size(numeric)sizeis now treated as a pixel contract: on container resize the flyout is re-clamped rather than rescaled proportionallyonResizeScreenshots
No visual change for named sizes. For numeric sizes the change is that the flyout stops moving on container resize; see the screenshot in #9969 for the reported behavior.
Impact Assessment
size.sizeduring a container resize, and in the direction the issue asks for. Named sizes (s/m/l/fill) are untouched.use_flyout_resizable.test.tshad no coverage of scaling onreferenceWidthchange (every prior test uses a staticreferenceWidth), so nothing was weakened to get green.Impact level: 🟢 Low
Release Readiness
Documentation:no doc-visible API changeFigma:n/aMigration guide:n/aAdoption plan (new features):bug fix; the reporting consumer is DiscoverQA instructions for reviewer
EuiFlyoutwithtype="push", a numericsize(e.g.544), and acontainerelement narrower than the viewport.maxWidth), and that only the pushed content resizes.onResizedoes not fire for the container resize.size="m".mflyout still scales proportionally, holding 50% of the container across the resize (pre-existing behavior, deliberately preserved for [Flyout System] Resizable Flyout Issues #9683).onResizestill fires exactly once with the final width.Checklist before marking Ready for Review
QA: Tested light/dark modes, high contrast, mobile, Chrome/Safari/Edge/Firefox, keyboard-only, screen reader— no rendering or styling changeQA: Tested docs changes— no docs changesflyout_resizable.spec.tsxnever passes acontainerand never changes the container width mid-test, so it does not exercise the branch this PR touches and is unaffected. A Cypress case that resizes a real container would be worthwhile, but I could not validate one in my environment (the Cypress binary host is unreachable there), so I left it out rather than push an unverified spec. Happy to add it on request.Breaking changes: Added— not a breaking changebreaking changelabelGenerated by Claude Code