diff --git a/packages/react-aria-components/stories/ListBox.stories.tsx b/packages/react-aria-components/stories/ListBox.stories.tsx
index 24e721fc529..7097b9d7632 100644
--- a/packages/react-aria-components/stories/ListBox.stories.tsx
+++ b/packages/react-aria-components/stories/ListBox.stories.tsx
@@ -1146,3 +1146,38 @@ export const DropOntoRoot = () => (
);
+
+export const FractionalWidth: StoryFn = () => {
+ let items = Array.from({length: 50}, (_, i) => ({id: i, name: `Item ${i + 1}`}));
+ return (
+
+
+
+
+ {item => {item.name}}
+
+
+
+
+
+
+ {item => {item.name}}
+
+
+
+
+ );
+};
diff --git a/packages/react-aria/src/virtualizer/ScrollView.tsx b/packages/react-aria/src/virtualizer/ScrollView.tsx
index 86b94e4174f..80611b0a4ad 100644
--- a/packages/react-aria/src/virtualizer/ScrollView.tsx
+++ b/packages/react-aria/src/virtualizer/ScrollView.tsx
@@ -67,6 +67,17 @@ interface ScrollViewAria {
contentProps: HTMLAttributes;
}
+function getClientSize(dom: HTMLElement) {
+ let clientWidth = dom.clientWidth;
+ let clientHeight = dom.clientHeight;
+ 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};
+}
+
export function useScrollView(
props: ScrollViewProps,
ref: RefObject
@@ -253,15 +264,15 @@ export function useScrollView(
// content size update, causing below layout effect to fire. This avoids infinite loops.
isUpdatingSize.current = true;
- let isTestEnv = process.env.NODE_ENV === 'test' && !process.env.VIRT_ON;
+ let isTest = process.env.NODE_ENV === 'test';
+ let isTestEnv = isTest && !process.env.VIRT_ON;
let isClientWidthMocked = Object.getOwnPropertyNames(window.HTMLElement.prototype).includes(
'clientWidth'
);
let isClientHeightMocked = Object.getOwnPropertyNames(window.HTMLElement.prototype).includes(
'clientHeight'
);
- let clientWidth = dom.clientWidth;
- let clientHeight = dom.clientHeight;
+ let {clientWidth, clientHeight} = isTest ? dom : getClientSize(dom);
let w = isTestEnv && !isClientWidthMocked ? Infinity : clientWidth;
let h = isTestEnv && !isClientHeightMocked ? Infinity : clientHeight;
@@ -286,8 +297,12 @@ export function useScrollView(
// adjusted space. In very specific cases this might result in the scrollbars disappearing
// again, resulting in extra padding. We stop after a maximum of two layout passes to avoid
// an infinite loop. This matches how browsers behavior with native CSS grid layout.
- if ((!isTestEnv && clientWidth !== dom.clientWidth) || clientHeight !== dom.clientHeight) {
- state.size = new Size(dom.clientWidth, dom.clientHeight);
+ let nextSize = isTest ? dom : getClientSize(dom);
+ if (
+ (!isTest && clientWidth !== nextSize.clientWidth) ||
+ clientHeight !== nextSize.clientHeight
+ ) {
+ state.size = new Size(nextSize.clientWidth, nextSize.clientHeight);
flush(() => {
updateVisibleRect();
onSizeChange?.(state.size);