Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/@react-spectrum/s2/chromatic/ListView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,35 @@ export const EmptyState: Story = {
)
};

// Renders two virtualized ListViews inside a 501px container split into 50%
// columns (250.5px each). Exercises the Virtualizer ScrollView's handling of
// fractional container widths so VRT catches subpixel overflow regressions.
export const FractionalWidth: Story = {
render: () => {
let fractionalItems = 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%'}}>
<ListView
aria-label="Fractional width list 1"
styles={style({height: 300, width: 'full'})}
items={fractionalItems}>
{item => <ListViewItem textValue={item.name}>{item.name}</ListViewItem>}
</ListView>
</div>
<div style={{width: '50%'}}>
<ListView
aria-label="Fractional width list 2"
styles={style({height: 300, width: 'full'})}
items={fractionalItems}>
{item => <ListViewItem textValue={item.name}>{item.name}</ListViewItem>}
</ListView>
</div>
</div>
);
}
};

export const InsertionIndicator: Story = {
...Reorderable,
play: async ({canvasElement}) => {
Expand Down
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