From 31e6477620caacaf3e82b26de4ecbf0577e1b071 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Fri, 15 May 2026 03:15:58 -0700 Subject: [PATCH] fix a regression introduced by fixYogaFlexBasisFitContentInMainAxis feature flag Summary: Limit the FixFlexBasisFitContent height optimization to non-measure container children inside scroll subtrees. This keeps Marketplace Home wrappers outside the ScrollView viewport-bounded while preserving the scroll remeasurement optimization. This regression was discovered during a recent QE where I tried to run enable this optimisation. Reviewed By: christophpurrer Differential Revision: D105167981 --- .../View-flexBasisFitContent-itest.js | 87 +++++++++++++++++++ .../yoga/yoga/algorithm/CalculateLayout.cpp | 18 +++- 2 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 packages/react-native/Libraries/Components/View/__tests__/View-flexBasisFitContent-itest.js diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-flexBasisFitContent-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-flexBasisFitContent-itest.js new file mode 100644 index 000000000000..1d5b6008a48c --- /dev/null +++ b/packages/react-native/Libraries/Components/View/__tests__/View-flexBasisFitContent-itest.js @@ -0,0 +1,87 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @fantom_flags fixYogaFlexBasisFitContentInMainAxis:* + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native'; + +import ensureInstance from '../../../../src/private/__tests__/utilities/ensureInstance'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {createRef} from 'react'; +import {ScrollView, StyleSheet, View} from 'react-native'; +import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; + +const VIEWPORT_WIDTH = 402; +const VIEWPORT_HEIGHT = 760; +const FEED_CONTENT_HEIGHT = 1800; + +test('auto-height wrapper around a feed ScrollView stays bounded by the viewport', () => { + const root = Fantom.createRoot({ + viewportWidth: VIEWPORT_WIDTH, + viewportHeight: VIEWPORT_HEIGHT, + }); + + const wrapperRef = createRef(); + const scrollViewRef = createRef(); + const feedContentRef = createRef(); + + Fantom.runTask(() => { + root.render( + + + + + + + , + ); + }); + + const wrapper = ensureInstance(wrapperRef.current, ReactNativeElement); + const scrollView = ensureInstance(scrollViewRef.current, ReactNativeElement); + const feedContent = ensureInstance( + feedContentRef.current, + ReactNativeElement, + ); + + const wrapperRect = wrapper.getBoundingClientRect(); + const scrollViewRect = scrollView.getBoundingClientRect(); + const feedContentRect = feedContent.getBoundingClientRect(); + + expect(wrapperRect.height).toBe(VIEWPORT_HEIGHT); + expect(scrollViewRect.height).toBe(VIEWPORT_HEIGHT); + expect(feedContentRect.height).toBe(FEED_CONTENT_HEIGHT); +}); + +const styles = StyleSheet.create({ + feed: { + width: VIEWPORT_WIDTH, + }, + feedContent: { + height: FEED_CONTENT_HEIGHT, + width: VIEWPORT_WIDTH, + }, + feedWrapper: { + flexGrow: 1, + }, + screen: { + height: VIEWPORT_HEIGHT, + width: VIEWPORT_WIDTH, + }, +}); diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp index 6c90ddce39c1..4aa56cffa0cb 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp @@ -177,9 +177,11 @@ static void computeFlexBasisForChild( // For height in the main axis (column direction): when the // FixFlexBasisFitContent feature is enabled, skip FitContent for - // non-measure container children. This makes the flex basis independent - // of the parent's content-determined height, preventing unnecessary - // re-measurement cascades when a sibling changes size in a ScrollView. + // non-measure container children inside scroll subtrees. This makes the + // flex basis independent of content-determined heights, preventing + // unnecessary re-measurement cascades when a sibling changes size in a + // ScrollView, while preserving viewport bounds for wrappers outside the + // scroll subtree. // // We only optimize the height (column) axis because text wrapping depends // on width constraints propagating through container nodes. Removing @@ -188,8 +190,16 @@ static void computeFlexBasisForChild( bool applyHeightFitContent = isMainAxisRow || node->style().overflow() != Overflow::Scroll; if (fixFlexBasisFitContent) { + bool nodeHasScrollAncestor = false; + for (auto owner = node->getOwner(); owner != nullptr; + owner = owner->getOwner()) { + if (owner->style().overflow() == Overflow::Scroll) { + nodeHasScrollAncestor = true; + break; + } + } applyHeightFitContent = isMainAxisRow || - (child->hasMeasureFunc() && + ((child->hasMeasureFunc() || !nodeHasScrollAncestor) && node->style().overflow() != Overflow::Scroll); } if (applyHeightFitContent && yoga::isUndefined(childHeight) &&