Skip to content

fix(virtualizer): preserve subpixel precision in ScrollView to preven… - #10489

Open
devo-id wants to merge 7 commits into
adobe:mainfrom
devo-id:fix/virtualizer-fractional-width
Open

fix(virtualizer): preserve subpixel precision in ScrollView to preven…#10489
devo-id wants to merge 7 commits into
adobe:mainfrom
devo-id:fix/virtualizer-fractional-width

Conversation

@devo-id

@devo-id devo-id commented Aug 20, 2026

Copy link
Copy Markdown

What was changed

When a Virtualizer container has a fractional layout width (e.g. 250.5px, common when two components sit side-by-side in a 50% flex layout), the browser's integer dom.clientWidth rounds up to 251px per the CSSOM View spec. ScrollView was passing this rounded integer to the layout engine, which sized the content wrapper to 251px inside a 250.5px container — producing an unwanted horizontal scrollbar.

Why it happened

ScrollView.tsx read container dimensions via dom.clientWidth / dom.clientHeight, which are always integers in the browser. The Virtualizer's layout engine then sized the content element to those rounded values, causing a 0.5px overflow that triggered overflow: auto to show a scrollbar.

How it's fixed

Introduced getClientSize(dom) in ScrollView.tsx that derives exact subpixel dimensions from getBoundingClientRect() while correctly subtracting borders and scrollbars (dom.offsetWidth - dom.clientWidth). This gives the layout engine the true floating-point scrollport dimensions.

Fixing at the measurement source means all layouts (ListLayout, GridLayout, WaterfallLayout, custom layouts) automatically benefit — no per-layout patching needed.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices
  • I understand every change in this PR and can explain why it's there.
  • If AI-assisted, I followed our AI contribution guidance and pointed my assistant at CLAUDE.md.

📝 Test Instructions:

  1. Run yarn jest packages/react-aria-components/test/ListBox.test.js -t "fractional container" — both tests should pass.
  2. Render a virtualized ListBox inside a 501px container split into two 50% columns (so each column is 250.5px). Confirm no horizontal scrollbar appears.

Closes #10471

@devo-id devo-id closed this Aug 20, 2026
@devo-id devo-id reopened this Aug 20, 2026
@devo-id
devo-id force-pushed the fix/virtualizer-fractional-width branch from cab6ae5 to 62225e1 Compare August 20, 2026 19:35
@devo-id
devo-id marked this pull request as draft August 20, 2026 19:45

@snowystinger snowystinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems like a better approach than the already existing PR. While the unit test is kind of useful, it'd be more useful to have a storybook setup with the 50% widths you mention, then we can put it in a chromatic story and get visual regression testing that actually uses the browser instead of mocking everything in jsdom. Would you mind putting that together?

Is there a reason you have left it as a draft?

Thank you!

@devo-id
devo-id force-pushed the fix/virtualizer-fractional-width branch from 62225e1 to 22f0f72 Compare August 21, 2026 05:26
@devo-id devo-id closed this Aug 21, 2026
@devo-id devo-id reopened this Aug 21, 2026
@devo-id
devo-id marked this pull request as ready for review August 21, 2026 05:28
@devo-id

devo-id commented Aug 21, 2026

Copy link
Copy Markdown
Author

Hi @snowystinger,

Thanks for taking a look and for the feedback!

About the draft status: I temporarily moved it to draft while looking into a CircleCI failure in ListViewDnd.test.js. In jsdom, drag-and-drop mocks were returning item coordinates that getClientSize was picking up, which shrunk the mock container height in that test. I’ve refined the test environment handling in ScrollView.tsx, and all test suites are passing cleanly now.

Storybook / Chromatic: I've added a FractionalWidth story to packages/react-aria-components/stories/ListBox.stories.tsx that renders two side-by-side virtualized ListBoxes at 50% width inside a 501px container (giving 250.5px per list).

I've marked the PR as ready for review.

Thanks!

@devo-id devo-id closed this Aug 23, 2026
@devo-id devo-id reopened this Aug 23, 2026
@devo-id

devo-id commented Aug 23, 2026

Copy link
Copy Markdown
Author

recheck

@devo-id

devo-id commented Aug 23, 2026

Copy link
Copy Markdown
Author

I’ve resubmitted the Adobe CLA with my GitHub login, and the submission has been verified successfully.

@snowystinger

Copy link
Copy Markdown
Member

Hey, so I'm running the story you added, but I'm not seeing a horizontal scrollbar either before or after. How did you verify this in the browser?

What it looks like to me both with and without your ScrollView changes:
Screenshot 2026-08-24 at 2 54 40 pm

snowystinger
snowystinger previously approved these changes Aug 24, 2026

@snowystinger snowystinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like a width 100% was missing.
I attempted a chromatic story instead, but it appears that they turn off scrollbars. So there isn't a way with our current setups to actually test this. For now I'll leave it in the storybook.
Thanks!

@devo-id

devo-id commented Aug 24, 2026

Copy link
Copy Markdown
Author

@snowystinger, You're right. I initially verified the issue using the reported StackBlitz reproduction rather than the exact Storybook story. The Storybook story had a separate issue because styles.menu was preventing the ListBox from filling the 250.5px column.

That was corrected with width: 100%, and I verified the updated story against both the baseline and the fix.

Thanks for the feedback and for fixing the story.

Comment on lines +73 to +78
let rect = dom.getBoundingClientRect?.();
if (rect && rect.width > 0 && rect.height > 0) {
clientWidth = rect.width - Math.max(0, dom.offsetWidth - dom.clientWidth);
clientHeight = rect.height - Math.max(0, dom.offsetHeight - dom.clientHeight);
}
return {clientWidth, clientHeight};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This isn't correct, because getBoundingClientRect() returns the border-box of an element, which is fine for normal elements but can easily break window scrolling, because neither border-box nor offset sizes work for the viewport. We should be using something similar to Modal.tsx instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@nwidynski, thanks to pointing this out.
I traced the viewport case and confirmed the previous getClientSize() logic could use the document content height instead of the actual viewport height for root scrolling elements.

I updated it to keep the native clientWidth/clientHeight for document.documentElement, body, and document.scrollingElement, while keeping the fractional measurement for normal elements unchanged. I also added a regression test covering the document root case.

@nwidynski nwidynski Aug 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, I'm not sure of how pedantic we want to be about root elements but there is a lot more complexity that goes into which element is responsible for viewport overflow. For example, the body element can be an independent scroll container, so bailing out unconditionally may not be right. I will let @snowystinger weigh in there.

I also noticed that this is potentially introducing a breaking change to test environments, since the NODE_ENV flag is now required. This may be okay, but clientWidth and clientHeight are documented public API for this use case as far as i know, so I wanted to bring it up.

@snowystinger
snowystinger dismissed their stale review August 26, 2026 03:15

@nwidynski brought up a good point that should be looked at more closely

@devo-id
devo-id force-pushed the fix/virtualizer-fractional-width branch from 3bea096 to 4e2acc4 Compare August 26, 2026 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Virtualizer rounding of width causes a horizontal scrollbar

3 participants