fix(react-headless-components-preview): reserve the scrollbar gutter while a dialog locks scroll - #36666
Open
Ray Knight (ArrayKnight) wants to merge 2 commits into
Conversation
…while a dialog locks scroll lockDocumentScroll sets body.style.overflow = 'visible clip' and nothing else, so the page scrollbar disappears without its space being reserved and every fixed or centred element on the page shifts sideways by half the scrollbar width when a modal opens. Measured on a scrolling 1280px page: a centred marker sits at x 332.5 with the dialog closed and jumps to 340 when it opens, where the Griffel equivalent stays at 332.5 because react-dialog's useDisableBodyScroll also reserves the gutter. The reservation goes on the document element, not on body. `overflow` propagates from body to the viewport — which is why the lock works at all with `<html>` left `visible` — but `scrollbar-gutter` does not: measured, `scrollbar-gutter: stable` on body alongside the clip reserves nothing and leaves the same 7.5px jump. The docblock's promise to leave `<html>` untouched cannot be kept and still fix this; padding body instead was measured too and does not help, because it narrows body rather than the initial containing block a top-layer surface resolves against. `stable` reserves a gutter whether or not the page had a scrollbar, so writing it unconditionally would introduce the mirror-image defect: a 7.5px shift on a page that never scrolled. It is written only when the scrollbar is actually taking layout width, read before the lock removes it. That reads 0 under overlay scrollbars as well, where nothing needs reserving and Griffel's own height-based guard over-reserves. Unlock restores the previous inline gutter next to the previous overflow, so a host application that had already set one gets it back, and the existing lockCount reference count scopes both to the outermost dialog. Non-modal dialogs never take the lock and are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aj9uA3rCVgosnh2zNn8qkc
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.
The headless Dialog's
lockDocumentScrollclipsbodyoverflow and reserves nothing in the scrollbar's place, so on a page that scrolls, opening a modal dialog removes the scrollbar's layout width and every fixed and centred element shifts sideways for as long as the dialog is open (measured at a 7.5px jump with classic scrollbars; the Griffel Dialog on the same page does not move).The fix reserves the gutter while the lock is held, with three details that were measured rather than assumed: the
scrollbar-gutterwrite must go ondocumentElement(overflow propagates from body to the viewport,scrollbar-gutterdoes not); it must be guarded on the scrollbar actually taking layout width, read before the lock removes it, so pages that never scroll — and overlay-scrollbar environments — do not get the mirror-image shift; and unlock restores the previous inline gutter alongside the previous overflow. Non-modal dialogs never take the lock and are unaffected. Four regression tests are added; the two behavioural ones were verified to fail without the fix.Note for verification: Puppeteer passes
--hide-scrollbarsby default, under which this defect cannot reproduce at all — measurements were taken withignoreDefaultArgs: ['--hide-scrollbars'].Fixes #36648.
Extracted from #36656 per maintainer request — each in-tree fix from that PR as an isolated change.