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);