diff --git a/patches/react-native/details.md b/patches/react-native/details.md index 2dfc6e9d96df..f410dadb1ec4 100644 --- a/patches/react-native/details.md +++ b/patches/react-native/details.md @@ -314,3 +314,11 @@ - E/App issue: https://github.com/Expensify/App/issues/97471 - PR introducing patch: https://github.com/Expensify/App/pull/97496 - 0.86.0 migration note: the `fixFindShadowNodeByTagRaceCondition` flag still defaults to `false` in RN 0.86.0 (unchanged from 0.85.3), and the surrounding code in `UIManager.cpp` is byte-for-byte identical, so the original diff applies with zero fuzz. Only the patch-package filename was renumbered from `0.85.3+040` to `0.86.0+036`; no content changes were needed. + +### [react-native+0.86.0+037+fix-stale-font-scale.patch](react-native+0.86.0+037+fix-stale-font-scale.patch) + +- Reason: Fixes Fabric reusing shadow nodes that hold a stale font scale, leaving text at its old measured size after the OS font size changes. RN 0.86.0 dirties measurable nodes from `SurfaceHandler::constraintLayout` only on the commit where the multiplier changes, and only compares against the *root's* value — so a node cloned from a parent still carrying an obsolete `fontSizeMultiplier` is never re-dirtied. The upstream rewrite stores `fontSizeMultiplier` on `LayoutMetrics` and threads it through `YogaLayoutableShadowNode::configureYogaTree` alongside `pointScaleFactor`, so every commit re-checks each node's own value and calls `markDirtyAndPropagate()` when it is out of date; the now-redundant `dirtyMeasurableNodes`/`dirtyMeasurableNodesRecursive` helpers are removed from `SurfaceHandler`. Gated by RN's existing `enableFontScaleChangesUpdatingLayout` flag, which defaults to `true` in 0.86.0. +- Upstream PR/issue: https://github.com/react/react-native/pull/57246 (fixes https://github.com/react/react-native/issues/52895) +- E/App issue: 🛑 — backport of an upstream fix, no separate E/App issue was filed. +- PR introducing patch: https://github.com/Expensify/App/pull/98507 +- 0.86.0 migration note: **drop this patch with the RN 0.87 upgrade** — upstream commit `45904c8` is absent from every 0.86.x release but ships in `v0.87.0`, and the patch will not apply against it. Two deviations from the upstream commit: the `scripts/cxx-api/api-snapshots/*.api` hunks are omitted (those files are not shipped in the npm package), and `fontSizeMultiplier` is declared *last* in `LayoutMetrics` rather than after `pointScaleFactor`, because `@rnmapbox/maps` initializes that struct positionally and inserting a field mid-struct breaks its iOS build. diff --git a/patches/react-native/react-native+0.86.0+037+fix-stale-font-scale.patch b/patches/react-native/react-native+0.86.0+037+fix-stale-font-scale.patch new file mode 100644 index 000000000000..5957f12794d1 --- /dev/null +++ b/patches/react-native/react-native+0.86.0+037+fix-stale-font-scale.patch @@ -0,0 +1,246 @@ +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +index c0f5dab..0a09558 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +@@ -469,6 +469,7 @@ void YogaLayoutableShadowNode::updateYogaProps() { + + void YogaLayoutableShadowNode::configureYogaTree( + float pointScaleFactor, ++ Float fontSizeMultiplier, + YGErrata defaultErrata, + bool swapLeftAndRight) { + ensureUnsealed(); +@@ -478,6 +479,18 @@ void YogaLayoutableShadowNode::configureYogaTree( + YGConfigSetErrata(&yogaConfig_, errata); + YGConfigSetPointScaleFactor(&yogaConfig_, pointScaleFactor); + ++ // A measurable node's measurement depends on `fontSizeMultiplier`, but unlike ++ // `pointScaleFactor` it is not part of the Yoga config, so a change does not ++ // invalidate Yoga's layout cache. Dirty the node when it changes to force ++ // re-measurement. We propagate up to the root so an unchanged, still-cached ++ // ancestor isn't skipped by `calculateLayoutInternal` before reaching us. ++ if (ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout() && ++ getTraits().check(ShadowNodeTraits::Trait::MeasurableYogaNode) && ++ !floatEquality( ++ getLayoutMetrics().fontSizeMultiplier, fontSizeMultiplier)) { ++ yogaNode_.markDirtyAndPropagate(); ++ } ++ + // TODO: `swapLeftAndRight` modified backing props and cannot be undone + if (swapLeftAndRight) { + swapStyleLeftAndRight(); +@@ -496,6 +509,8 @@ void YogaLayoutableShadowNode::configureYogaTree( + + if (child.yogaTreeHasBeenConfigured_ && + childLayoutMetrics.pointScaleFactor == pointScaleFactor && ++ floatEquality( ++ childLayoutMetrics.fontSizeMultiplier, fontSizeMultiplier) && + childLayoutMetrics.wasLeftAndRightSwapped == swapLeftAndRight && + childErrata == child.resolveErrata(errata)) { + continue; +@@ -504,10 +519,13 @@ void YogaLayoutableShadowNode::configureYogaTree( + if (doesOwn(child)) { + auto& mutableChild = const_cast(child); + mutableChild.configureYogaTree( +- pointScaleFactor, child.resolveErrata(errata), swapLeftAndRight); ++ pointScaleFactor, ++ fontSizeMultiplier, ++ child.resolveErrata(errata), ++ swapLeftAndRight); + } else { + cloneChildInPlace(i).configureYogaTree( +- pointScaleFactor, errata, swapLeftAndRight); ++ pointScaleFactor, fontSizeMultiplier, errata, swapLeftAndRight); + } + } + } +@@ -604,6 +622,7 @@ void YogaLayoutableShadowNode::layoutTree( + TraceSection s2("YogaLayoutableShadowNode::configureYogaTree"); + configureYogaTree( + layoutContext.pointScaleFactor, ++ layoutContext.fontSizeMultiplier, + YGErrataAll /*defaultErrata*/, + swapLeftAndRight); + } +@@ -669,6 +688,7 @@ void YogaLayoutableShadowNode::layoutTree( + if (yogaNode_.getHasNewLayout()) { + auto layoutMetrics = layoutMetricsFromYogaNode(yogaNode_); + layoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; ++ layoutMetrics.fontSizeMultiplier = layoutContext.fontSizeMultiplier; + layoutMetrics.wasLeftAndRightSwapped = swapLeftAndRight; + setLayoutMetrics(layoutMetrics); + yogaNode_.setHasNewLayout(false); +@@ -716,6 +736,7 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { + + auto newLayoutMetrics = layoutMetricsFromYogaNode(*childYogaNode); + newLayoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; ++ newLayoutMetrics.fontSizeMultiplier = layoutContext.fontSizeMultiplier; + newLayoutMetrics.wasLeftAndRightSwapped = + layoutContext.swapLeftAndRightInRTL && + newLayoutMetrics.layoutDirection == LayoutDirection::RightToLeft; +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h b/node_modules/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h +index 25714d5..fff9c00 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h +@@ -137,7 +137,8 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { + * ShadowTree has been constructed, but before it has been is laid out or + * committed. + */ +- void configureYogaTree(float pointScaleFactor, YGErrata defaultErrata, bool swapLeftAndRight); ++ void ++ configureYogaTree(float pointScaleFactor, Float fontSizeMultiplier, YGErrata defaultErrata, bool swapLeftAndRight); + + /** + * Return an errata based on a `layoutConformance` prop if given, otherwise +diff --git a/node_modules/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h b/node_modules/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h +index 794f899..cb38378 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h +@@ -45,6 +45,10 @@ struct LayoutMetrics { + // Negative values indicate that children are clipped inside the node + // (like when using `overflow: clip` on Web). + EdgeInsets overflowInset{}; ++ // Surface font scale this node was last laid out with. Declared last so that ++ // positional aggregate initialization of the fields above (as done by ++ // @rnmapbox/maps) still binds each initializer to the intended field. ++ Float fontSizeMultiplier{1.0}; + + // Origin: the outer border of the node. + // Size: includes content only. +@@ -120,6 +124,7 @@ struct hash { + layoutMetrics.displayType, + layoutMetrics.layoutDirection, + layoutMetrics.pointScaleFactor, ++ layoutMetrics.fontSizeMultiplier, + layoutMetrics.overflowInset); + } + }; +diff --git a/node_modules/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp b/node_modules/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp +index 3d4adc6..930472f 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.cpp +@@ -9,7 +9,6 @@ + + #include + #include +-#include + #include + + namespace facebook::react { +@@ -213,78 +212,6 @@ Size SurfaceHandler::measure( + return rootShadowNode->getLayoutMetrics().frame.size; + } + +-std::shared_ptr SurfaceHandler::dirtyMeasurableNodesRecursive( +- std::shared_ptr node) const { +- const auto nodeHasChildren = !node->getChildren().empty(); +- const auto isMeasurableYogaNode = +- node->getTraits().check(ShadowNodeTraits::Trait::MeasurableYogaNode); +- +- // Node is not measurable and has no children, its layout will not be affected +- if (!nodeHasChildren && !isMeasurableYogaNode) { +- return nullptr; +- } +- +- std::shared_ptr>> +- newChildren = ShadowNodeFragment::childrenPlaceholder(); +- +- if (nodeHasChildren) { +- std::shared_ptr>> +- newChildrenMutable = nullptr; +- for (size_t i = 0; i < node->getChildren().size(); i++) { +- const auto& child = node->getChildren()[i]; +- +- if (const auto& layoutableNode = +- std::dynamic_pointer_cast( +- child)) { +- auto newChild = dirtyMeasurableNodesRecursive(layoutableNode); +- +- if (newChild != nullptr) { +- if (newChildrenMutable == nullptr) { +- newChildrenMutable = std::make_shared< +- std::vector>>( +- node->getChildren()); +- newChildren = newChildrenMutable; +- } +- +- (*newChildrenMutable)[i] = newChild; +- } +- } +- } +- +- // Node is not measurable and its children were not dirtied, its layout will +- // not be affected +- if (!isMeasurableYogaNode && newChildrenMutable == nullptr) { +- return nullptr; +- } +- } +- +- const auto newNode = node->getComponentDescriptor().cloneShadowNode( +- *node, +- { +- .children = newChildren, +- // Preserve the original state of the node +- .state = node->getState(), +- }); +- +- if (isMeasurableYogaNode) { +- std::static_pointer_cast(newNode)->dirtyLayout(); +- } +- +- return newNode; +-} +- +-void SurfaceHandler::dirtyMeasurableNodes(ShadowNode& root) const { +- for (const auto& child : root.getChildren()) { +- if (const auto& layoutableNode = +- std::dynamic_pointer_cast(child)) { +- const auto newChild = dirtyMeasurableNodesRecursive(layoutableNode); +- if (newChild != nullptr) { +- root.replaceChild(*child, newChild); +- } +- } +- } +-} +- + void SurfaceHandler::constraintLayout( + const LayoutConstraints& layoutConstraints, + const LayoutContext& layoutContext) const { +@@ -315,19 +242,8 @@ void SurfaceHandler::constraintLayout( + link_.shadowTree && "`link_.shadowTree` must not be null."); + link_.shadowTree->commit( + [&](const RootShadowNode& oldRootShadowNode) { +- auto newRoot = oldRootShadowNode.clone( ++ return oldRootShadowNode.clone( + propsParserContext, layoutConstraints, layoutContext); +- +- // Dirty all measurable nodes when the fontSizeMultiplier changes to +- // trigger re-measurement. +- if (ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout() && +- layoutContext.fontSizeMultiplier != +- oldRootShadowNode.getConcreteProps() +- .layoutContext.fontSizeMultiplier) { +- dirtyMeasurableNodes(*newRoot); +- } +- +- return newRoot; + }, + {/* default commit options */}); + } +diff --git a/node_modules/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.h b/node_modules/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.h +index b031a51..93cfb22 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/scheduler/SurfaceHandler.h +@@ -151,12 +151,6 @@ class SurfaceHandler { + + void applyDisplayMode(DisplayMode displayMode) const; + +- /* +- * An utility for dirtying all measurable shadow nodes present in the tree. +- */ +- void dirtyMeasurableNodes(ShadowNode &root) const; +- std::shared_ptr dirtyMeasurableNodesRecursive(std::shared_ptr node) const; +- + #pragma mark - Link & Parameters + + /*