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
This commit is contained in:
Tim Yung
2025-09-02 16:28:02 -07:00
committed by Facebook GitHub Bot
parent 1604232e8d
commit d6ed32f8d6
2 changed files with 26 additions and 21 deletions
@@ -56,6 +56,7 @@ const VirtualViewNativeComponent: typeof VirtualViewClassicNativeComponent =
type VirtualViewComponent = component(
children?: React.Node,
hiddenStyle?: (targetRect: Rect) => ViewStyleProp,
nativeID?: string,
ref?: ?React.RefSetter<React.ElementRef<typeof VirtualViewNativeComponent>>,
style?: ?ViewStyleProp,
@@ -63,16 +64,21 @@ type VirtualViewComponent = component(
removeClippedSubviews?: boolean,
);
type HiddenHeight = number;
const NotHidden = null;
type HiddenStyle = Exclude<ViewStyleProp, typeof NotHidden>;
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<React.ElementRef<typeof VirtualViewNativeComponent>>,
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<State>} = {};
@@ -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(<VirtualView />);
});
expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
<rn-virtualView />,
);
expect(
root.getRenderedOutput({props: ['minHeight', 'minWidth']}).toJSX(),
).toEqual(<rn-virtualView />);
});
test('does not set height when prerendered', () => {
test('does not set styles when prerendered', () => {
const root = Fantom.createRoot();
const viewRef = createRef<React.RefOf<VirtualView>>();
@@ -141,12 +141,12 @@ describe('styles', () => {
dispatchModeChangeEvent(viewRef.current, VirtualViewMode.Prerender);
expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
<rn-virtualView />,
);
expect(
root.getRenderedOutput({props: ['minHeight', 'minWidth']}).toJSX(),
).toEqual(<rn-virtualView />);
});
test('sets height when hidden', () => {
test('sets styles when hidden', () => {
const root = Fantom.createRoot();
const viewRef = createRef<React.RefOf<VirtualView>>();
@@ -156,9 +156,9 @@ describe('styles', () => {
dispatchModeChangeEvent(viewRef.current, VirtualViewMode.Hidden);
expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
<rn-virtualView height="100.000000" />,
);
expect(
root.getRenderedOutput({props: ['minHeight', 'minWidth']}).toJSX(),
).toEqual(<rn-virtualView minHeight="100.000000" minWidth="100.000000" />);
});
});