From db28187a4e4157ac143c218b6000d6fedadc0f3b Mon Sep 17 00:00:00 2001 From: ngerlem <> Date: Tue, 3 Mar 2026 01:52:13 -0800 Subject: [PATCH 1/2] Intermediate commit hash for 1772529007 Summary: https://www.internalfb.com/intern/devai/devmate/inspector/d95021048-fix_all-bc5e214e-0905-4b89-93e0-075e6a441558#e8c0a9be-c71f-4a1c-86da-6ce1e5f739d4 Differential Revision: D95021048 --- .../View/ReactNativeStyleAttributes.js | 6 +- .../renderer/attributedstring/conversions.h | 106 +++++++++++++++++- .../components/view/tests/ConversionsTest.cpp | 26 +++++ 3 files changed, 135 insertions(+), 3 deletions(-) diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index ac54183bb554..ba4e727d86c3 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -67,6 +67,10 @@ export const transformOriginAttribute: AnyAttributeType = nativeCSSParsing ? true : {process: processTransformOrigin}; +export const fontVariantAttribute: AnyAttributeType = nativeCSSParsing + ? true + : {process: processFontVariant}; + const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { /** * Layout @@ -248,7 +252,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { fontFamily: true, fontSize: true, fontStyle: true, - fontVariant: {process: processFontVariant}, + fontVariant: fontVariantAttribute, fontWeight: true, includeFontPadding: true, letterSpacing: true, diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 5635a692b17b..a509abfab571 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,8 @@ #include #include #include +#include +#include #include #ifdef RN_SERIALIZABLE_STATE @@ -317,7 +320,73 @@ inline std::string toString(const FontStyle &fontStyle) return "normal"; } -inline void fromRawValue(const PropsParserContext &context, const RawValue &value, FontVariant &result) +inline std::optional fontVariantFromCSSFontVariant(CSSFontVariant cssVariant) +{ + switch (cssVariant) { + case CSSFontVariant::SmallCaps: + return FontVariant::SmallCaps; + case CSSFontVariant::OldstyleNums: + return FontVariant::OldstyleNums; + case CSSFontVariant::LiningNums: + return FontVariant::LiningNums; + case CSSFontVariant::TabularNums: + return FontVariant::TabularNums; + case CSSFontVariant::ProportionalNums: + return FontVariant::ProportionalNums; + case CSSFontVariant::StylisticOne: + return FontVariant::StylisticOne; + case CSSFontVariant::StylisticTwo: + return FontVariant::StylisticTwo; + case CSSFontVariant::StylisticThree: + return FontVariant::StylisticThree; + case CSSFontVariant::StylisticFour: + return FontVariant::StylisticFour; + case CSSFontVariant::StylisticFive: + return FontVariant::StylisticFive; + case CSSFontVariant::StylisticSix: + return FontVariant::StylisticSix; + case CSSFontVariant::StylisticSeven: + return FontVariant::StylisticSeven; + case CSSFontVariant::StylisticEight: + return FontVariant::StylisticEight; + case CSSFontVariant::StylisticNine: + return FontVariant::StylisticNine; + case CSSFontVariant::StylisticTen: + return FontVariant::StylisticTen; + case CSSFontVariant::StylisticEleven: + return FontVariant::StylisticEleven; + case CSSFontVariant::StylisticTwelve: + return FontVariant::StylisticTwelve; + case CSSFontVariant::StylisticThirteen: + return FontVariant::StylisticThirteen; + case CSSFontVariant::StylisticFourteen: + return FontVariant::StylisticFourteen; + case CSSFontVariant::StylisticFifteen: + return FontVariant::StylisticFifteen; + case CSSFontVariant::StylisticSixteen: + return FontVariant::StylisticSixteen; + case CSSFontVariant::StylisticSeventeen: + return FontVariant::StylisticSeventeen; + case CSSFontVariant::StylisticEighteen: + return FontVariant::StylisticEighteen; + case CSSFontVariant::StylisticNineteen: + return FontVariant::StylisticNineteen; + case CSSFontVariant::StylisticTwenty: + return FontVariant::StylisticTwenty; + case CSSFontVariant::CommonLigatures: + case CSSFontVariant::NoCommonLigatures: + case CSSFontVariant::DiscretionaryLigatures: + case CSSFontVariant::NoDiscretionaryLigatures: + case CSSFontVariant::HistoricalLigatures: + case CSSFontVariant::NoHistoricalLigatures: + case CSSFontVariant::Contextual: + case CSSFontVariant::NoContextual: + return std::nullopt; + } +} + +inline void +parseProcessedFontVariant(const PropsParserContext & /*context*/, const RawValue &value, FontVariant &result) { result = FontVariant::Default; react_native_expect(value.hasType>()); @@ -376,7 +445,6 @@ inline void fromRawValue(const PropsParserContext &context, const RawValue &valu result = (FontVariant)((int)result | (int)FontVariant::StylisticTwenty); } else { LOG(ERROR) << "Unsupported FontVariant value: " << item; - react_native_expect(false); } } } else { @@ -384,6 +452,40 @@ inline void fromRawValue(const PropsParserContext &context, const RawValue &valu } } +inline void parseUnprocessedFontVariantString(const std::string &value, FontVariant &result) +{ + auto fontVariantList = parseCSSProperty(value); + if (!std::holds_alternative(fontVariantList)) { + result = FontVariant::Default; + return; + } + + result = FontVariant::Default; + for (const auto &cssVariant : std::get(fontVariantList)) { + if (auto fv = fontVariantFromCSSFontVariant(cssVariant)) { + result = (FontVariant)((int)result | (int)*fv); + } + } +} + +inline void parseUnprocessedFontVariant(const PropsParserContext &context, const RawValue &value, FontVariant &result) +{ + if (value.hasType()) { + parseUnprocessedFontVariantString((std::string)value, result); + } else { + parseProcessedFontVariant(context, value, result); + } +} + +inline void fromRawValue(const PropsParserContext &context, const RawValue &value, FontVariant &result) +{ + if (ReactNativeFeatureFlags::enableNativeCSSParsing()) { + parseUnprocessedFontVariant(context, value, result); + } else { + parseProcessedFontVariant(context, value, result); + } +} + inline std::string toString(const FontVariant &fontVariant) { auto result = std::string{}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp index c784ecfd3ed7..e3c716415c5a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp @@ -7,6 +7,7 @@ #include +#include #include #include #include @@ -454,4 +455,29 @@ TEST(ConversionsTest, unprocessed_transform_origin_rawvalue_string_with_z) { EXPECT_EQ(result.z, 15.0f); } +TEST(ConversionsTest, unprocessed_font_variant_string_single) { + FontVariant result{}; + parseUnprocessedFontVariantString("small-caps", result); + + EXPECT_EQ((int)result, (int)FontVariant::SmallCaps); +} + +TEST(ConversionsTest, unprocessed_font_variant_string_multiple) { + FontVariant result{}; + parseUnprocessedFontVariantString( + "small-caps oldstyle-nums tabular-nums", result); + + EXPECT_EQ( + (int)result, + (int)FontVariant::SmallCaps | (int)FontVariant::OldstyleNums | + (int)FontVariant::TabularNums); +} + +TEST(ConversionsTest, unprocessed_font_variant_string_invalid) { + FontVariant result{}; + parseUnprocessedFontVariantString("not-a-variant", result); + + EXPECT_EQ((int)result, (int)FontVariant::Default); +} + } // namespace facebook::react From 97405e3396e64c6b1afe85add2bca3a634b69af5 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 3 Mar 2026 06:56:48 -0800 Subject: [PATCH 2/2] Reland: Wire native CSS parsing for aspectRatio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: ## Reland `convertRawProp` for `yoga::Style` did not preserve `sourceValue.aspectRatio()` when `aspectRatio` was absent from the raw props diff. This caused `aspectRatio` to silently reset to undefined whenever any other prop was updated on a component that had `aspectRatio` set. This led to S627731. Fixed by adding an else-branch fallback to `sourceValue.aspectRatio()`: ``` { const auto *rawValue = rawProps.at("aspectRatio", nullptr, nullptr); if (rawValue != nullptr) { yogaStyle.setAspectRatio(rawValue->hasValue() ? convertAspectRatio(context, *rawValue) : yogaStyle.aspectRatio()); + } else { + yogaStyle.setAspectRatio(sourceValue.aspectRatio()); } } ``` Added `clonedPropsPreserveAspectRatio` test in ViewTest.cpp to verify aspectRatio survives a prop clone with empty RawProps. ## Original Gate `processAspectRatio` behind `enableNativeCSSParsing()`. When the flag is on, CSS ratio strings like `"16/9"` and number strings are parsed natively using the existing CSS ratio parser instead of being preprocessed in JS. The parsing is done in `fromRawValue(... FloatOptional &)` — string values are only sent for aspectRatio; other FloatOptional yoga props never receive strings from JS. Changelog: [Internal] Reviewed By: javache Differential Revision: D95032494 --- .../View/ReactNativeStyleAttributes.js | 6 +- .../Components/View/__tests__/View-itest.js | 95 +++++++++++++++++++ .../components/view/YogaStylableProps.cpp | 61 ++++++------ .../renderer/components/view/conversions.h | 17 ++++ .../components/view/propsConversions.h | 10 +- .../components/view/tests/ConversionsTest.cpp | 60 ++++++++++++ .../components/view/tests/ViewTest.cpp | 34 +++++++ 7 files changed, 252 insertions(+), 31 deletions(-) diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index ba4e727d86c3..ba345e117cae 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -71,6 +71,10 @@ export const fontVariantAttribute: AnyAttributeType = nativeCSSParsing ? true : {process: processFontVariant}; +export const aspectRatioAttribute: AnyAttributeType = nativeCSSParsing + ? true + : {process: processAspectRatio}; + const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { /** * Layout @@ -78,7 +82,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { alignContent: true, alignItems: true, alignSelf: true, - aspectRatio: {process: processAspectRatio}, + aspectRatio: aspectRatioAttribute, borderBottomWidth: true, borderEndWidth: true, borderLeftWidth: true, diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js index e860b09779f9..3a35099bbe5c 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js @@ -231,6 +231,101 @@ describe('', () => { }); }); + describe('aspectRatio', () => { + it('is preserved when updating an unrelated prop', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + // width=100, aspectRatio=2 → height = 100 / 2 = 50 + expect( + root + .getRenderedOutput({ + includeLayoutMetrics: true, + props: ['layoutMetrics-frame'], + }) + .toJSX(), + ).toEqual( + , + ); + + // Update only nativeID, not aspectRatio + Fantom.runTask(() => { + root.render( + , + ); + }); + + // aspectRatio must still be preserved → same layout + expect( + root + .getRenderedOutput({ + includeLayoutMetrics: true, + props: ['layoutMetrics-frame'], + }) + .toJSX(), + ).toEqual( + , + ); + }); + + it('can be changed to undefined after initially having a value', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + // width=100, aspectRatio=2 → height = 100 / 2 = 50 + expect( + root + .getRenderedOutput({ + includeLayoutMetrics: true, + props: ['layoutMetrics-frame'], + }) + .toJSX(), + ).toEqual( + , + ); + + // Update aspectRatio to undefined + Fantom.runTask(() => { + root.render( + , + ); + }); + + // aspectRatio is now undefined → height collapses to 0 + expect( + root + .getRenderedOutput({ + includeLayoutMetrics: true, + props: ['layoutMetrics-frame'], + }) + .toJSX(), + ).toEqual( + , + ); + }); + }); + describe('background-image', () => { it('it parses CSS and object syntax', () => { const root = Fantom.createRoot(); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp index 0e2529c1e5af..ae2da15fc0a4 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp @@ -147,35 +147,40 @@ void YogaStylableProps::setProp( REBUILD_FIELD_SWITCH_CASE_YSP(flexBasis, setFlexBasis); REBUILD_FIELD_SWITCH_CASE2(positionType, setPositionType, "position"); REBUILD_FIELD_YG_GUTTER(gap, setGap, "rowGap", "columnGap", "gap"); - REBUILD_FIELD_SWITCH_CASE_YSP(aspectRatio, setAspectRatio); - REBUILD_FIELD_SWITCH_CASE_YSP(boxSizing, setBoxSizing); - REBUILD_FIELD_YG_DIMENSION(dimension, setDimension, "width", "height"); - REBUILD_FIELD_YG_DIMENSION( - minDimension, setMinDimension, "minWidth", "minHeight"); - REBUILD_FIELD_YG_DIMENSION( - maxDimension, setMaxDimension, "maxWidth", "maxHeight"); - REBUILD_FIELD_YG_EDGES_POSITION(); - REBUILD_FIELD_YG_EDGES(margin, setMargin, "margin", ""); - REBUILD_FIELD_YG_EDGES(padding, setPadding, "padding", ""); - REBUILD_FIELD_YG_EDGES(border, setBorder, "border", "Width"); + case CONSTEXPR_RAW_PROPS_KEY_HASH("aspectRatio"): { + yogaStyle.setAspectRatio( + value.hasValue() ? convertAspectRatio(context, value) + : ygDefaults.aspectRatio()); + return; + } + REBUILD_FIELD_SWITCH_CASE_YSP(boxSizing, setBoxSizing); + REBUILD_FIELD_YG_DIMENSION(dimension, setDimension, "width", "height"); + REBUILD_FIELD_YG_DIMENSION( + minDimension, setMinDimension, "minWidth", "minHeight"); + REBUILD_FIELD_YG_DIMENSION( + maxDimension, setMaxDimension, "maxWidth", "maxHeight"); + REBUILD_FIELD_YG_EDGES_POSITION(); + REBUILD_FIELD_YG_EDGES(margin, setMargin, "margin", ""); + REBUILD_FIELD_YG_EDGES(padding, setPadding, "padding", ""); + REBUILD_FIELD_YG_EDGES(border, setBorder, "border", "Width"); - // Aliases - RAW_SET_PROP_SWITCH_CASE(insetBlockEnd, "insetBlockEnd"); - RAW_SET_PROP_SWITCH_CASE(insetBlockStart, "insetBlockStart"); - RAW_SET_PROP_SWITCH_CASE(insetInlineEnd, "insetInlineEnd"); - RAW_SET_PROP_SWITCH_CASE(insetInlineStart, "insetInlineStart"); - RAW_SET_PROP_SWITCH_CASE(marginInline, "marginInline"); - RAW_SET_PROP_SWITCH_CASE(marginInlineStart, "marginInlineStart"); - RAW_SET_PROP_SWITCH_CASE(marginInlineEnd, "marginInlineEnd"); - RAW_SET_PROP_SWITCH_CASE(marginBlock, "marginBlock"); - RAW_SET_PROP_SWITCH_CASE(marginBlockStart, "marginBlockStart"); - RAW_SET_PROP_SWITCH_CASE(marginBlockEnd, "marginBlockEnd"); - RAW_SET_PROP_SWITCH_CASE(paddingInline, "paddingInline"); - RAW_SET_PROP_SWITCH_CASE(paddingInlineStart, "paddingInlineStart"); - RAW_SET_PROP_SWITCH_CASE(paddingInlineEnd, "paddingInlineEnd"); - RAW_SET_PROP_SWITCH_CASE(paddingBlock, "paddingBlock"); - RAW_SET_PROP_SWITCH_CASE(paddingBlockStart, "paddingBlockStart"); - RAW_SET_PROP_SWITCH_CASE(paddingBlockEnd, "paddingBlockEnd"); + // Aliases + RAW_SET_PROP_SWITCH_CASE(insetBlockEnd, "insetBlockEnd"); + RAW_SET_PROP_SWITCH_CASE(insetBlockStart, "insetBlockStart"); + RAW_SET_PROP_SWITCH_CASE(insetInlineEnd, "insetInlineEnd"); + RAW_SET_PROP_SWITCH_CASE(insetInlineStart, "insetInlineStart"); + RAW_SET_PROP_SWITCH_CASE(marginInline, "marginInline"); + RAW_SET_PROP_SWITCH_CASE(marginInlineStart, "marginInlineStart"); + RAW_SET_PROP_SWITCH_CASE(marginInlineEnd, "marginInlineEnd"); + RAW_SET_PROP_SWITCH_CASE(marginBlock, "marginBlock"); + RAW_SET_PROP_SWITCH_CASE(marginBlockStart, "marginBlockStart"); + RAW_SET_PROP_SWITCH_CASE(marginBlockEnd, "marginBlockEnd"); + RAW_SET_PROP_SWITCH_CASE(paddingInline, "paddingInline"); + RAW_SET_PROP_SWITCH_CASE(paddingInlineStart, "paddingInlineStart"); + RAW_SET_PROP_SWITCH_CASE(paddingInlineEnd, "paddingInlineEnd"); + RAW_SET_PROP_SWITCH_CASE(paddingBlock, "paddingBlock"); + RAW_SET_PROP_SWITCH_CASE(paddingBlockStart, "paddingBlockStart"); + RAW_SET_PROP_SWITCH_CASE(paddingBlockEnd, "paddingBlockEnd"); } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index 929fe68cd58c..1e211befa2dd 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -498,6 +498,23 @@ inline void fromRawValue(const PropsParserContext &context, const RawValue &valu result = value.hasType() ? yoga::FloatOptional((float)value) : yoga::FloatOptional(); } +inline yoga::FloatOptional convertAspectRatio(const PropsParserContext & /*context*/, const RawValue &value) +{ + if (value.hasType()) { + return yoga::FloatOptional((float)value); + } + if (ReactNativeFeatureFlags::enableNativeCSSParsing() && value.hasType()) { + auto ratio = parseCSSProperty((std::string)value); + if (std::holds_alternative(ratio)) { + auto r = std::get(ratio); + if (!r.isDegenerate()) { + return yoga::FloatOptional(r.numerator / r.denominator); + } + } + } + return {}; +} + inline std::optional toRadians(const RawValue &value) { if (value.hasType()) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h index 410fde1b56df..985a3bbffb8c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h @@ -356,8 +356,14 @@ convertRawProp(const PropsParserContext &context, const RawProps &rawProps, cons yoga::Dimension::Height, convertRawProp(context, rawProps, "maxHeight", sourceValue.maxDimension(yoga::Dimension::Height), {})); - yogaStyle.setAspectRatio( - convertRawProp(context, rawProps, "aspectRatio", sourceValue.aspectRatio(), yogaStyle.aspectRatio())); + { + const auto *rawValue = rawProps.at("aspectRatio", nullptr, nullptr); + if (rawValue != nullptr) { + yogaStyle.setAspectRatio(rawValue->hasValue() ? convertAspectRatio(context, *rawValue) : yogaStyle.aspectRatio()); + } else { + yogaStyle.setAspectRatio(sourceValue.aspectRatio()); + } + } yogaStyle.setBoxSizing( convertRawProp(context, rawProps, "boxSizing", sourceValue.boxSizing(), yogaStyle.boxSizing())); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp index e3c716415c5a..a96a75aef756 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp @@ -480,4 +480,64 @@ TEST(ConversionsTest, unprocessed_font_variant_string_invalid) { EXPECT_EQ((int)result, (int)FontVariant::Default); } +TEST(ConversionsTest, convert_aspect_ratio_float) { + RawValue value{folly::dynamic(1.5)}; + auto result = + convertAspectRatio(PropsParserContext{-1, ContextContainer{}}, value); + + EXPECT_FALSE(result.isUndefined()); + EXPECT_EQ(result.unwrap(), 1.5f); +} + +TEST(ConversionsTest, convert_aspect_ratio_ratio_string) { + // CSSRatio parses "16/9" as {numerator: 16, denominator: 9} + auto ratio = parseCSSProperty("16/9"); + ASSERT_TRUE(std::holds_alternative(ratio)); + auto r = std::get(ratio); + EXPECT_FALSE(r.isDegenerate()); + EXPECT_NEAR(r.numerator / r.denominator, 16.0f / 9.0f, 0.001f); +} + +TEST(ConversionsTest, convert_aspect_ratio_number_string) { + // CSSRatio parses "1.5" as {numerator: 1.5, denominator: 1.0} + auto ratio = parseCSSProperty("1.5"); + ASSERT_TRUE(std::holds_alternative(ratio)); + auto r = std::get(ratio); + EXPECT_FALSE(r.isDegenerate()); + EXPECT_EQ(r.numerator / r.denominator, 1.5f); +} + +TEST(ConversionsTest, convert_aspect_ratio_degenerate) { + auto ratio = parseCSSProperty("0/0"); + ASSERT_TRUE(std::holds_alternative(ratio)); + EXPECT_TRUE(std::get(ratio).isDegenerate()); +} + +TEST(ConversionsTest, float_optional_from_rawvalue_float) { + RawValue value{folly::dynamic(1.5)}; + yoga::FloatOptional result; + fromRawValue(PropsParserContext{-1, ContextContainer{}}, value, result); + + EXPECT_FALSE(result.isUndefined()); + EXPECT_EQ(result.unwrap(), 1.5f); +} + +TEST(ConversionsTest, float_optional_undefined_for_non_float) { + RawValue value{folly::dynamic(nullptr)}; + yoga::FloatOptional result; + fromRawValue(PropsParserContext{-1, ContextContainer{}}, value, result); + + EXPECT_TRUE(result.isUndefined()); +} + +TEST(ConversionsTest, float_optional_undefined_for_string) { + // fromRawValue for FloatOptional does not parse strings — + // that is handled by convertAspectRatio specifically. + RawValue value{folly::dynamic("16/9")}; + yoga::FloatOptional result; + fromRawValue(PropsParserContext{-1, ContextContainer{}}, value, result); + + EXPECT_TRUE(result.isUndefined()); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp index c6d426ac5583..82c363b8d792 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp @@ -19,6 +19,7 @@ #include #include +#include namespace facebook::react { @@ -229,4 +230,37 @@ TEST_F(YogaDirtyFlagTest, updatingStateForScrollViewMistNotDirtyYogaNode) { static_cast(*newRootShadowNode).layoutIfNeeded()); } +TEST_F(YogaDirtyFlagTest, clonedPropsPreserveAspectRatio) { + ContextContainer contextContainer{}; + PropsParserContext parserContext{-1, contextContainer}; + + /* + * Cloning props with empty RawProps must preserve aspectRatio set on the + * source props. + */ + auto newRootShadowNode = rootShadowNode_->cloneTree( + innerShadowNode_->getFamily(), [&](const ShadowNode& oldShadowNode) { + // First clone: set aspectRatio to 1.5 + auto viewProps = std::make_shared(); + viewProps->yogaStyle.setAspectRatio(yoga::FloatOptional(1.5f)); + auto nodeWithAspectRatio = + oldShadowNode.clone(ShadowNodeFragment{.props = viewProps}); + + // Second clone: clone props with empty RawProps (simulating a prop + // update that does not touch aspectRatio) + auto& componentDescriptor = + nodeWithAspectRatio->getComponentDescriptor(); + auto clonedProps = componentDescriptor.cloneProps( + parserContext, nodeWithAspectRatio->getProps(), RawProps()); + + auto& clonedViewProps = + static_cast(*clonedProps); + EXPECT_TRUE(clonedViewProps.yogaStyle.aspectRatio().isDefined()); + EXPECT_EQ(clonedViewProps.yogaStyle.aspectRatio().unwrap(), 1.5f); + + return nodeWithAspectRatio->clone( + ShadowNodeFragment{.props = clonedProps}); + }); +} + } // namespace facebook::react