From d6ed32f8d64bd7e0e20b7ea8fa4ac6be76041ff0 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Tue, 2 Sep 2025 16:28:02 -0700 Subject: [PATCH] VirtualView: Configurable Hidden Layout (#53571) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53571 Changes `VirtualView` so that its layout when hidden can be configured by call sites. Previously, it was hardcoded to only retain the last known height. However, this logic only works for `VirtualView` children oriented in a column layout. This change enables the use of `VirtualView` in more flexible abstractions that require different hidden styles (e.g. row or grid orientations). Also, this changes the default behavior to set `minWidth` and `minHeight`, so that the default behavior is more general and more likely to work in a reasonable manner in more use cases. NOTE: Ideally, we would be able to default to using `flexBasis` instead. However, the `hiddenStyle` function receives a `Rect` and does not know whether the parent's flex direction is row or column to influence whether to use `targetRect.width` or `targetRect.height`. This is an opportunity for future improvement. Changelog: [Internal] Reviewed By: lunaleaps Differential Revision: D81344126 fbshipit-source-id: 33d9e81601b671059f97b4590816243cbd24734a --- .../components/virtualview/VirtualView.js | 23 +++++++++++------- .../__tests__/VirtualView-itest.js | 24 +++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/react-native/src/private/components/virtualview/VirtualView.js b/packages/react-native/src/private/components/virtualview/VirtualView.js index c8652578eae..51183386877 100644 --- a/packages/react-native/src/private/components/virtualview/VirtualView.js +++ b/packages/react-native/src/private/components/virtualview/VirtualView.js @@ -56,6 +56,7 @@ const VirtualViewNativeComponent: typeof VirtualViewClassicNativeComponent = type VirtualViewComponent = component( children?: React.Node, + hiddenStyle?: (targetRect: Rect) => ViewStyleProp, nativeID?: string, ref?: ?React.RefSetter>, style?: ?ViewStyleProp, @@ -63,16 +64,21 @@ type VirtualViewComponent = component( removeClippedSubviews?: boolean, ); -type HiddenHeight = number; const NotHidden = null; +type HiddenStyle = Exclude; -type State = HiddenHeight | typeof NotHidden; +type State = HiddenStyle | typeof NotHidden; + +function defaultHiddenStyle(targetRect: Rect): ViewStyleProp { + return {minHeight: targetRect.height, minWidth: targetRect.width}; +} function createVirtualView(initialState: State): VirtualViewComponent { const initialHidden = initialState !== NotHidden; component VirtualView( children?: React.Node, + hiddenStyle: (targetRect: Rect) => ViewStyleProp = defaultHiddenStyle, nativeID?: string, ref?: ?React.RefSetter>, style?: ?ViewStyleProp, @@ -112,9 +118,8 @@ function createVirtualView(initialState: State): VirtualViewComponent { }); } VirtualViewMode.Hidden => { - const {height} = event.nativeEvent.targetRect; startTransition(() => { - setState(height as HiddenHeight); + setState(hiddenStyle(event.nativeEvent.targetRect) ?? {}); emitModeChange?.(); }); } @@ -134,9 +139,7 @@ function createVirtualView(initialState: State): VirtualViewComponent { } style={ isHidden - ? StyleSheet.compose(style, { - height: Math.abs(nullthrows(state) as HiddenHeight), - }) + ? StyleSheet.compose(style, nullthrows(state) as HiddenStyle) : style } onModeChange={handleModeChange}> @@ -159,8 +162,10 @@ function createVirtualView(initialState: State): VirtualViewComponent { export default createVirtualView(NotHidden) as VirtualViewComponent; -export function createHiddenVirtualView(height: number): VirtualViewComponent { - return createVirtualView(height as HiddenHeight); +export function createHiddenVirtualView( + style: ViewStyleProp, +): VirtualViewComponent { + return createVirtualView((style ?? {}) as HiddenStyle); } export const _logs: {states?: Array} = {}; diff --git a/packages/react-native/src/private/components/virtualview/__tests__/VirtualView-itest.js b/packages/react-native/src/private/components/virtualview/__tests__/VirtualView-itest.js index a7b5516f77b..9a861bc3c89 100644 --- a/packages/react-native/src/private/components/virtualview/__tests__/VirtualView-itest.js +++ b/packages/react-native/src/private/components/virtualview/__tests__/VirtualView-itest.js @@ -119,19 +119,19 @@ describe('mode changes', () => { }); describe('styles', () => { - test('does not set height when visible', () => { + test('does not set styles when visible', () => { const root = Fantom.createRoot(); Fantom.runTask(() => { root.render(); }); - expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual( - , - ); + expect( + root.getRenderedOutput({props: ['minHeight', 'minWidth']}).toJSX(), + ).toEqual(); }); - test('does not set height when prerendered', () => { + test('does not set styles when prerendered', () => { const root = Fantom.createRoot(); const viewRef = createRef>(); @@ -141,12 +141,12 @@ describe('styles', () => { dispatchModeChangeEvent(viewRef.current, VirtualViewMode.Prerender); - expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual( - , - ); + expect( + root.getRenderedOutput({props: ['minHeight', 'minWidth']}).toJSX(), + ).toEqual(); }); - test('sets height when hidden', () => { + test('sets styles when hidden', () => { const root = Fantom.createRoot(); const viewRef = createRef>(); @@ -156,9 +156,9 @@ describe('styles', () => { dispatchModeChangeEvent(viewRef.current, VirtualViewMode.Hidden); - expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual( - , - ); + expect( + root.getRenderedOutput({props: ['minHeight', 'minWidth']}).toJSX(), + ).toEqual(); }); });