Skip to content
35 changes: 35 additions & 0 deletions packages/react-aria-components/stories/ListBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1146,3 +1146,38 @@ export const DropOntoRoot = () => (
<DroppableListBox />
</div>
);

export const FractionalWidth: StoryFn = () => {
let items = Array.from({length: 50}, (_, i) => ({id: i, name: `Item ${i + 1}`}));
return (
<div
style={{
display: 'flex',
width: 501,
border: '1px solid gray'
}}>
<div style={{width: '50%'}}>
<Virtualizer layout={ListLayout} layoutOptions={{rowHeight: 32}}>
<ListBox
aria-label="Fractional width list 1"
className={styles.menu}
style={{height: 300, width: '100%'}}
items={items}>
{item => <MyListBoxItem>{item.name}</MyListBoxItem>}
</ListBox>
</Virtualizer>
</div>
<div style={{width: '50%'}}>
<Virtualizer layout={ListLayout} layoutOptions={{rowHeight: 32}}>
<ListBox
aria-label="Fractional width list 2"
className={styles.menu}
style={{height: 300, width: '100%'}}
items={items}>
{item => <MyListBoxItem>{item.name}</MyListBoxItem>}
</ListBox>
</Virtualizer>
</div>
</div>
);
};
25 changes: 20 additions & 5 deletions packages/react-aria/src/virtualizer/ScrollView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ interface ScrollViewAria {
contentProps: HTMLAttributes<HTMLElement>;
}

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<HTMLElement | null>
Expand Down Expand Up @@ -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;

Expand All @@ -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);
Expand Down