From 451d0988342abb80e9c533fc3ed9369b7ab9cf79 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 11 Jun 2026 15:26:52 -0700 Subject: [PATCH 1/3] fix: make sure virtualized grid layouts dont disappear when hidden and reshown --- .../stories/GridList.stories.tsx | 26 ++++++++- .../test/GridList.browser.test.tsx | 58 ++++++++++++++++++- .../src/virtualizer/useVirtualizerItem.ts | 5 ++ vitest.browser.config.ts | 4 ++ 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/stories/GridList.stories.tsx b/packages/react-aria-components/stories/GridList.stories.tsx index 7c42b76baf4..2ec148f3a38 100644 --- a/packages/react-aria-components/stories/GridList.stories.tsx +++ b/packages/react-aria-components/stories/GridList.stories.tsx @@ -393,7 +393,7 @@ const VirtualizedGridListRender = (args: GridListProps & {isLoading: boolea className={styles.menu} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} - style={{height: 400}} + style={{height: 400, width: 400}} aria-label="virtualized gridlist" items={list.items}> @@ -953,3 +953,27 @@ export const AsyncGridListGridVirtualized: StoryObj + +
+ +
+ + ); +} + +export const VirtualizedDisplayNoneToggle: StoryObj = { + render: VirtualizedDisplayNoneToggleRender, + parameters: { + description: { + data: 'toggling hide and show should not cause the items to disappear' + } + } +}; diff --git a/packages/react-aria-components/test/GridList.browser.test.tsx b/packages/react-aria-components/test/GridList.browser.test.tsx index d7b944e1f73..9ff6e8869a7 100644 --- a/packages/react-aria-components/test/GridList.browser.test.tsx +++ b/packages/react-aria-components/test/GridList.browser.test.tsx @@ -11,10 +11,13 @@ */ import {expect, it} from 'vitest'; +import {GridLayout} from '../src/GridLayout'; import {GridList, GridListItem} from '../src/GridList'; -import React from 'react'; +import React, {useState} from 'react'; import {render} from 'vitest-browser-react'; +import {Size} from 'react-stately/useVirtualizerState'; import {User} from '@react-aria/test-utils'; +import {Virtualizer} from '../src/Virtualizer'; function Grid() { return ( @@ -36,6 +39,37 @@ function Grid() { ); } +function VirtualizedDisplayNone() { + let [visible, setVisible] = useState(true); + let items = Array.from({length: 100}, (_, i) => ({id: i, name: `Item ${i}`})); + return ( +
+ +
+ + + {(item: {id: number; name: string}) => ( + {item.name} + )} + + +
+
+ ); +} + it.each` interactionType ${'mouse'} @@ -64,3 +98,25 @@ it.each` expect(rows[8].getAttribute('aria-selected')).toBe('true'); expect(document.activeElement).toBe(rows[8]); }); + +it('virtualizer renders items after toggling display:none', async () => { + let testUtilUser = new User(); + let {container} = await render(); + + let gridlist = container.querySelector('[role=grid]') as HTMLElement; + let tester = testUtilUser.createTester('GridList', { + root: gridlist, + layout: 'grid' + }); + + await expect.poll(() => tester.getRows().length).toBeGreaterThan(0); + let button = container.querySelector('[data-testid=toggle]') as HTMLElement; + + await button.click(); + await button.click(); + await expect.poll(() => tester.getRows().length).toBeGreaterThan(0); + + await button.click(); + await button.click(); + await expect.poll(() => tester.getRows().length).toBeGreaterThan(0); +}); diff --git a/packages/react-aria/src/virtualizer/useVirtualizerItem.ts b/packages/react-aria/src/virtualizer/useVirtualizerItem.ts index a76a6e74431..2bbf940daf3 100644 --- a/packages/react-aria/src/virtualizer/useVirtualizerItem.ts +++ b/packages/react-aria/src/virtualizer/useVirtualizerItem.ts @@ -31,6 +31,11 @@ export function useVirtualizerItem(options: VirtualizerItemOptions): {updateSize let updateSize = useCallback(() => { if (key != null && ref.current) { + // offsetParent is null if element or ancestor has display: none + // see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent + if (ref.current.offsetParent === null) { + return; + } let size = getSize(ref.current); virtualizer.updateItemSize(key, size); } diff --git a/vitest.browser.config.ts b/vitest.browser.config.ts index 96a154b7e1b..abc08a79db4 100644 --- a/vitest.browser.config.ts +++ b/vitest.browser.config.ts @@ -162,6 +162,10 @@ function iconWrapperPlugin(): Plugin { } export default defineConfig({ + define: { + // make sure virtualizer actually runs since this is in browser + 'process.env.VIRT_ON': '"1"' + }, plugins: [ // @ts-expect-error macros.vite(), // Must be first! From a2f7df969cbb1b3ea1198ba9468c29b3672dd554 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 11 Jun 2026 15:27:09 -0700 Subject: [PATCH 2/3] unecessary change --- packages/react-aria-components/stories/GridList.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/stories/GridList.stories.tsx b/packages/react-aria-components/stories/GridList.stories.tsx index 2ec148f3a38..606838205ad 100644 --- a/packages/react-aria-components/stories/GridList.stories.tsx +++ b/packages/react-aria-components/stories/GridList.stories.tsx @@ -393,7 +393,7 @@ const VirtualizedGridListRender = (args: GridListProps & {isLoading: boolea className={styles.menu} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} - style={{height: 400, width: 400}} + style={{height: 400}} aria-label="virtualized gridlist" items={list.items}> From e9e52700468b7c410c1b10bbb6cce27e705fe25e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 11 Jun 2026 17:15:18 -0700 Subject: [PATCH 3/3] fix tests --- packages/react-aria/src/virtualizer/useVirtualizerItem.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react-aria/src/virtualizer/useVirtualizerItem.ts b/packages/react-aria/src/virtualizer/useVirtualizerItem.ts index 2bbf940daf3..64c6d124e22 100644 --- a/packages/react-aria/src/virtualizer/useVirtualizerItem.ts +++ b/packages/react-aria/src/virtualizer/useVirtualizerItem.ts @@ -31,9 +31,11 @@ export function useVirtualizerItem(options: VirtualizerItemOptions): {updateSize let updateSize = useCallback(() => { if (key != null && ref.current) { - // offsetParent is null if element or ancestor has display: none - // see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent - if (ref.current.offsetParent === null) { + // offsetParent is null if element or ancestor has display: none, see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent + // for that case we want to avoid reporting size 0 otherwise we get into a state + // where the virtualizer renders 0 items when it is hidden and thus won't remeasure when it is is unhidden + // in jsdom tests, offsetParent can be null, so skip the check there. + if (!navigator.userAgent.includes('jsdom') && ref.current.offsetParent === null) { return; } let size = getSize(ref.current);