diff --git a/packages/react-aria-components/stories/GridList.stories.tsx b/packages/react-aria-components/stories/GridList.stories.tsx index 7c42b76baf4..606838205ad 100644 --- a/packages/react-aria-components/stories/GridList.stories.tsx +++ b/packages/react-aria-components/stories/GridList.stories.tsx @@ -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..64c6d124e22 100644 --- a/packages/react-aria/src/virtualizer/useVirtualizerItem.ts +++ b/packages/react-aria/src/virtualizer/useVirtualizerItem.ts @@ -31,6 +31,13 @@ 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 + // 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); 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!