diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index d9e3b901824e..6f84e8e24a41 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -413,7 +413,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & auto *scrollView = (RCTEnhancedScrollView *)_scrollView; if (oldScrollViewProps.snapToAlignment != newScrollViewProps.snapToAlignment) { - scrollView.snapToAlignment = RCTNSStringFromString(toString(newScrollViewProps.snapToAlignment)); + scrollView.snapToAlignment = + RCTNSStringFromString(toString(newScrollViewProps.snapToAlignment.value_or(ScrollViewSnapToAlignment::Start))); } scrollView.snapToStart = newScrollViewProps.snapToStart; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 582557336d61..71b38291fdf5 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -67,7 +67,7 @@ static BOOL RCTViewIsInteractiveAccessibilityElement(UIView *view, const ViewPro // `role` wins over the legacy `accessibilityRole` when both are set, matching // how the traits themselves are resolved. - if (props.role != Role::None) { + if (props.role.value_or(Role::None) != Role::None) { static const std::unordered_set interactiveRoles{ Role::Button, Role::Checkbox, @@ -82,7 +82,7 @@ static BOOL RCTViewIsInteractiveAccessibilityElement(UIView *view, const ViewPro Role::Switch, Role::Tab, Role::Treeitem}; - return interactiveRoles.contains(props.role); + return interactiveRoles.contains(*props.role); } static const std::unordered_set interactiveAccessibilityRoles{ @@ -1678,8 +1678,9 @@ - (NSString *)accessibilityValue } NSMutableArray *valueComponents = [NSMutableArray new]; - NSString *roleString = (props.role != Role::None) ? [NSString stringWithUTF8String:toString(props.role).c_str()] - : [NSString stringWithUTF8String:props.accessibilityRole.c_str()]; + NSString *roleString = (props.role.value_or(Role::None) != Role::None) + ? [NSString stringWithUTF8String:toString(*props.role).c_str()] + : [NSString stringWithUTF8String:props.accessibilityRole.c_str()]; // In iOS, checkbox and radio buttons aren't recognized as traits. However, // because our apps use checkbox and radio buttons often, we should announce diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 3f413faaf7c0..b894ac8129f6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -369,8 +369,12 @@ public class ReactImageView( // no worth marking as dirty if it already rendered. } + /** Sets the request headers. A change rebuilds the image request on the next update. */ public fun setHeaders(headers: ReadableMap?) { - this.headers = headers + if (this.headers != headers) { + this.headers = headers + isDirty = true + } } // Disable rasterizing to offscreen layer in order to preserve background effects like box-shadow diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt index c3b78c3f3834..b69f015595e0 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt @@ -15,7 +15,9 @@ import android.graphics.Color import android.util.DisplayMetrics import com.facebook.common.logging.FLog import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder import com.facebook.drawee.drawable.ScalingUtils +import com.facebook.imagepipeline.request.ImageRequest import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.BridgeReactContext import com.facebook.react.bridge.CatalystInstance @@ -23,6 +25,7 @@ import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance import com.facebook.react.common.ReactConstants import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.modules.fresco.ReactNetworkImageRequest import com.facebook.react.uimanager.BackgroundStyleApplicator import com.facebook.react.uimanager.DisplayMetricsHolder import com.facebook.react.uimanager.LengthPercentage @@ -40,12 +43,15 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockedStatic +import org.mockito.Mockito.RETURNS_SELF import org.mockito.Mockito.anyString import org.mockito.Mockito.mockStatic import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.reset +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @@ -281,4 +287,27 @@ class ReactImagePropertyTest { viewManager.setHeaders(mockView, headers) verify(mockView).setHeaders(headers) } + + @Test + fun testHeadersChangeRebuildsImageRequest() { + val controllerBuilder = mock(defaultAnswer = RETURNS_SELF) + val viewManager = ReactImageManager(controllerBuilder) + val view = viewManager.createViewInstance(themeContext) + val sources = Arguments.createArray() + sources.pushMap(JavaOnlyMap.of("uri", "https://example.com/image.png")) + viewManager.setSource(view, sources) + viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-1")) + view.maybeUpdateView() + + viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-2")) + view.maybeUpdateView() + viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-2")) + view.maybeUpdateView() + + val requests = argumentCaptor() + verify(controllerBuilder, times(2)).setImageRequest(requests.capture()) + assertThat(requests.firstValue.sourceUri).isEqualTo(requests.secondValue.sourceUri) + assertThat((requests.secondValue as ReactNetworkImageRequest).headers) + .isEqualTo(JavaOnlyMap.of("Authorization", "token-2")) + } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp index 8cb669ebf127..2785fa720679 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp @@ -12,6 +12,9 @@ #include #include +#include +#include + namespace facebook::react { ImageProps::ImageProps( @@ -145,6 +148,30 @@ static folly::dynamic convertEdgeInsets(const EdgeInsets& edgeInsets) { return edgeInsetsResult; } +// ImageSource::operator== compares only the request identity: type and URI. +static bool hasSamePayload(const ImageSource& lhs, const ImageSource& rhs) { + return std::tie( + lhs.type, + lhs.uri, + lhs.bundle, + lhs.scale, + lhs.size, + lhs.body, + lhs.method, + lhs.cache, + lhs.headers) == + std::tie( + rhs.type, + rhs.uri, + rhs.bundle, + rhs.scale, + rhs.size, + rhs.body, + rhs.method, + rhs.cache, + rhs.headers); +} + ComponentName ImageProps::getDiffPropsImplementationTarget() const { return "Image"; } @@ -158,11 +185,14 @@ folly::dynamic ImageProps::getDiffProps(const Props* prevProps) const { folly::dynamic result = ViewProps::getDiffProps(oldProps); - if (sources != oldProps->sources) { + if (!std::ranges::equal(sources, oldProps->sources, hasSamePayload)) { auto sourcesArray = folly::dynamic::array(); for (const auto& source : sources) { sourcesArray.push_back(toDynamic(source)); } + if (!sources.empty()) { + result["headers"] = sourcesArray[0]["headers"]; + } result["source"] = sourcesArray; } @@ -221,7 +251,8 @@ folly::dynamic ImageProps::getDiffProps(const Props* prevProps) const { SharedColor prevTintColorValue = oldProps->tintColor.value_or(SharedColor{}); if (tintColorValue != prevTintColorValue) { - result["tintColor"] = *tintColorValue; + result["tintColor"] = tintColorValue ? folly::dynamic(*tintColorValue) + : folly::dynamic(nullptr); } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/image/tests/ImagePropsDiffTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/image/tests/ImagePropsDiffTest.cpp new file mode 100644 index 000000000000..22720793e0b3 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/image/tests/ImagePropsDiffTest.cpp @@ -0,0 +1,93 @@ +/* + * 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. + */ + +#include + +#include +#include + +#ifdef RN_SERIALIZABLE_STATE + +namespace facebook::react { + +namespace { + +using folly::dynamic; + +// Props 2.0 serializes every field of a source, JS only the ones it sets. +void expectPayloads(const std::vector& commits) { + expectPayloadsMatchRawProps(commits, {"source"}); +} + +} // namespace + +// ReactImageView.setSource reads width and height at the top level of each +// source when there are several, and ReactImageManager reads the request +// headers from the top-level headers prop, which JS derives from the first +// source. +TEST(ImagePropsDiffTest, multipleSourcesCarrySizesAndHeaders) { + dynamic headers = dynamic::object("Authorization", "token"); + expectPayloads({{dynamic::object( + "source", + dynamic::array( + dynamic::object("uri", "https://a")("width", 10)("height", 20)( + "headers", headers), + dynamic::object("uri", "https://b")("width", 30)("height", 40)))( + "headers", headers)}}); +} + +// ReactImageManager ignores a null headers prop, so only an empty map clears +// the headers of the previous source. +TEST(ImagePropsDiffTest, clearsHeadersWhenTheNewSourceHasNone) { + dynamic headers = dynamic::object("Authorization", "old"); + expectPayloads({ + {dynamic::object( + "source", + dynamic::array( + dynamic::object("uri", "https://a")("headers", headers)))( + "headers", headers)}, + {dynamic::object( + "source", dynamic::array(dynamic::object("uri", "https://b")))( + "headers", nullptr), + dynamic::object("headers", dynamic::object())}, + }); +} + +// ReactImageManager clears the tint only for null; 0 is a transparent SRC_IN +// tint that hides the image. +TEST(ImagePropsDiffTest, removingTintColorEmitsNull) { + expectPayloads({ + {dynamic::object("tintColor", static_cast(0xFFFF0000))}, + {dynamic::object("tintColor", nullptr)}, + }); +} + +// ReactImageView reads the cache policy and the sizes of each source, and +// ReactImageManager the headers; a source with the same URI can change them. +TEST(ImagePropsDiffTest, emitsASameUriSourceWhenItsOtherFieldsChange) { + dynamic headers = dynamic::object("Authorization", "old"); + dynamic source = dynamic::object("uri", "https://a")("width", 10)( + "height", 20)("scale", 1)("cache", "default")("headers", headers); + std::vector commits{ + {dynamic::object("source", dynamic::array(source))("headers", headers)}}; + dynamic changes = dynamic::object("cache", "reload")("width", 30)("scale", 2); + for (const auto& [field, value] : changes.items()) { + source[field] = value; + // Props 2.0 sends the first source's headers whenever the sources change. + commits.push_back( + {dynamic::object("source", dynamic::array(source)), + dynamic::object("headers", headers)}); + } + source["headers"] = dynamic::object("Authorization", "new"); + commits.push_back({dynamic::object("source", dynamic::array(source))( + "headers", source["headers"])}); + expectPayloads(commits); +} + +} // namespace facebook::react + +#endif diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp index 238fa494c142..9144464d5f9f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp @@ -80,7 +80,7 @@ BaseScrollViewProps::BaseScrollViewProps( rawProps, "decelerationRate", sourceProps.decelerationRate, - (Float)0.998)), + kDefaultDecelerationRate)), endDraggingSensitivityMultiplier(convertRawProp( context, rawProps, @@ -427,10 +427,7 @@ SharedDebugStringConvertibleList BaseScrollViewProps::getDebugProps() const { "snapToInterval", snapToInterval, defaultScrollViewProps.snapToInterval), - debugStringConvertibleItem( - "snapToAlignment", - snapToAlignment, - defaultScrollViewProps.snapToAlignment), + debugStringConvertibleItem("snapToAlignment", snapToAlignment), debugStringConvertibleItem( "disableIntervalMomentum", disableIntervalMomentum, diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h index 3a3ebbe84b9b..4e4b5d447852 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h @@ -41,7 +41,13 @@ class BaseScrollViewProps : public ViewProps { bool automaticallyAdjustContentInsets{}; bool automaticallyAdjustsScrollIndicatorInsets{true}; bool automaticallyAdjustKeyboardInsets{false}; - Float decelerationRate{0.998f}; +#ifdef ANDROID + // ReactScrollView's initial rate. + static constexpr Float kDefaultDecelerationRate = 0.985f; +#else + static constexpr Float kDefaultDecelerationRate = 0.998f; +#endif + Float decelerationRate{kDefaultDecelerationRate}; Float endDraggingSensitivityMultiplier{1}; bool directionalLockEnabled{}; ScrollViewIndicatorStyle indicatorStyle{}; @@ -63,7 +69,7 @@ class BaseScrollViewProps : public ViewProps { Point contentOffset{}; EdgeInsets scrollIndicatorInsets{}; Float snapToInterval{}; - ScrollViewSnapToAlignment snapToAlignment{}; + std::optional snapToAlignment{}; bool disableIntervalMomentum{false}; std::vector snapToOffsets{}; bool snapToStart{true}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp index d8812796d475..2aa02d73854c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp @@ -25,13 +25,13 @@ HostPlatformScrollViewProps::HostPlatformScrollViewProps( rawProps, "sendMomentumEvents", sourceProps.sendMomentumEvents, - true)), + false)), nestedScrollEnabled(convertRawProp( context, rawProps, "nestedScrollEnabled", sourceProps.nestedScrollEnabled, - true)), + false)), fadingEdgeLength(convertRawProp( context, rawProps, @@ -49,7 +49,13 @@ HostPlatformScrollViewProps::HostPlatformScrollViewProps( rawProps, "endFillColor", sourceProps.endFillColor, - clearColor())) {} + clearColor())), + scrollsChildToFocus(convertRawProp( + context, + rawProps, + "scrollsChildToFocus", + sourceProps.scrollsChildToFocus, + true)) {} void HostPlatformScrollViewProps::setProp( const PropsParserContext& context, @@ -69,6 +75,7 @@ void HostPlatformScrollViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(fadingEdgeLength); RAW_SET_PROP_SWITCH_CASE_BASIC(overScrollMode); RAW_SET_PROP_SWITCH_CASE_BASIC(endFillColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(scrollsChildToFocus); } } @@ -96,7 +103,11 @@ SharedDebugStringConvertibleList HostPlatformScrollViewProps::getDebugProps() debugStringConvertibleItem( "endFillColor", endFillColor, - defaultScrollViewProps.endFillColor)}; + defaultScrollViewProps.endFillColor), + debugStringConvertibleItem( + "scrollsChildToFocus", + scrollsChildToFocus, + defaultScrollViewProps.scrollsChildToFocus)}; } #endif @@ -301,17 +312,9 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps( } if (snapToAlignment != oldProps->snapToAlignment) { - switch (snapToAlignment) { - case ScrollViewSnapToAlignment::Start: - result["snapToAlignment"] = "start"; - break; - case ScrollViewSnapToAlignment::Center: - result["snapToAlignment"] = "center"; - break; - case ScrollViewSnapToAlignment::End: - result["snapToAlignment"] = "end"; - break; - } + result["snapToAlignment"] = snapToAlignment.has_value() + ? toString(*snapToAlignment) + : folly::dynamic(nullptr); } if (disableIntervalMomentum != oldProps->disableIntervalMomentum) { @@ -380,6 +383,10 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps( result["endFillColor"] = *endFillColor; } + if (scrollsChildToFocus != oldProps->scrollsChildToFocus) { + result["scrollsChildToFocus"] = scrollsChildToFocus; + } + return result; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h index 7ac321ec89fd..49fcc1cb7f7f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h @@ -38,6 +38,7 @@ class HostPlatformScrollViewProps : public BaseScrollViewProps { folly::dynamic fadingEdgeLength{}; std::string overScrollMode{"auto"}; SharedColor endFillColor{clearColor()}; + bool scrollsChildToFocus{true}; #pragma mark - DebugStringConvertible diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/tests/ScrollViewPropsDiffTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/tests/ScrollViewPropsDiffTest.cpp new file mode 100644 index 000000000000..8c46c2b2db88 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/tests/ScrollViewPropsDiffTest.cpp @@ -0,0 +1,70 @@ +/* + * 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. + */ + +#include + +#include +#include + +#ifdef RN_SERIALIZABLE_STATE + +namespace facebook::react { + +namespace { + +using folly::dynamic; + +void expectPayloads(const std::vector& commits) { + expectPayloadsMatchRawProps(commits); +} + +} // namespace + +// ReactScrollViewManager has a setter for it; with +// enableExclusivePropsUpdateAndroid there is no rawProps fallback. +TEST(ScrollViewPropsDiffTest, emitsScrollsChildToFocus) { + expectPayloads({{dynamic::object("scrollsChildToFocus", false)}}); +} + +// Android ScrollViews start with snapping alignment disabled, and +// setSnapToAlignment disables it for null. +TEST( + ScrollViewPropsDiffTest, + keepsAnExplicitSnapToAlignmentStartApartFromUnset) { + expectPayloads({ + {dynamic::object("snapToAlignment", "start")}, + {dynamic::object("snapToAlignment", "center")}, + {dynamic::object("snapToAlignment", nullptr)}, + }); +} + +// Android ScrollViews start decelerating at 0.985 (ReactScrollViewHelper). +// A removed rate restores it; Props 1.5 sends null, which the setter reads as +// 0. +TEST(ScrollViewPropsDiffTest, emitsAnExplicitDecelerationRate) { + expectPayloads({ + {dynamic::object("decelerationRate", 0.998)}, + {dynamic::object("decelerationRate", nullptr), + dynamic::object("decelerationRate", 0.985)}, + }); +} + +// The ScrollView managers turn both off for null. +TEST(ScrollViewPropsDiffTest, removingNestedScrollAndMomentumEventsEmitsFalse) { + expectPayloads({ + {dynamic::object("nestedScrollEnabled", true)( + "sendMomentumEvents", true)}, + {dynamic::object("nestedScrollEnabled", nullptr)( + "sendMomentumEvents", nullptr), + dynamic::object("nestedScrollEnabled", false)( + "sendMomentumEvents", false)}, + }); +} + +} // namespace facebook::react + +#endif diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp index 135463aa2d09..407445b5a6b2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp @@ -13,6 +13,8 @@ #include #include +#include + namespace facebook::react { static TextAttributes convertRawProp( @@ -436,7 +438,9 @@ void BaseTextProps::appendTextAttributesProps( if (!floatEquality( textAttributes.letterSpacing, oldProps->textAttributes.letterSpacing)) { - result["letterSpacing"] = textAttributes.letterSpacing; + result["letterSpacing"] = std::isnan(textAttributes.letterSpacing) + ? folly::dynamic(nullptr) + : textAttributes.letterSpacing; } if (textAttributes.textTransform != oldProps->textAttributes.textTransform) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/tests/ParagraphPropsDiffTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/tests/ParagraphPropsDiffTest.cpp new file mode 100644 index 000000000000..96ede0ca8e10 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/tests/ParagraphPropsDiffTest.cpp @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#include + +#include +#include + +#ifdef RN_SERIALIZABLE_STATE + +namespace facebook::react { + +using folly::dynamic; + +// ReactTextViewManager resets letterSpacing to 0 for null; +// ReactTextView.setLetterSpacing ignores NaN. +TEST(ParagraphPropsDiffTest, removingLetterSpacingEmitsNull) { + expectPayloadsMatchRawProps({ + {dynamic::object("letterSpacing", 4.0)}, + {dynamic::object("letterSpacing", nullptr)}, + }); +} + +} // namespace facebook::react + +#endif diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp index b204a3fd3c58..4bc58608d2a2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp @@ -18,12 +18,12 @@ namespace facebook::react { // members. This makes accessibilityTraits the single source of truth. // See: github.com/facebook/react-native/issues/57515 static AccessibilityTraits deriveAccessibilityTraits( - Role role, + std::optional role, const std::string& accessibilityRole, const std::optional& accessibilityState) { AccessibilityTraits traits = AccessibilityTraits::None; - if (role != Role::None) { - fromString(toString(role), traits); + if (role.value_or(Role::None) != Role::None) { + fromString(toString(*role), traits); } else if (!accessibilityRole.empty()) { fromString(accessibilityRole, traits); } @@ -179,16 +179,15 @@ AccessibilityProps::AccessibilityProps( auto* accessibilityRoleValue = rawProps.at("accessibilityRole"); auto* roleValue = rawProps.at("role"); - if (accessibilityRoleValue == nullptr || - !accessibilityRoleValue->hasValue()) { + if (accessibilityRoleValue == nullptr) { accessibilityRole = sourceProps.accessibilityRole; - } else { + } else if (accessibilityRoleValue->hasValue()) { fromRawValue(context, *accessibilityRoleValue, accessibilityRole); } - if (roleValue == nullptr || !roleValue->hasValue()) { + if (roleValue == nullptr) { role = sourceProps.role; - } else { + } else if (roleValue->hasValue()) { fromRawValue(context, *roleValue, role); } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h index 53524bb4a015..0b055d60f9b8 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h @@ -56,7 +56,7 @@ class AccessibilityProps { bool onAccessibilityEscape{}; bool onAccessibilityAction{}; ImportantForAccessibility importantForAccessibility{ImportantForAccessibility::Auto}; - Role role{Role::None}; + std::optional role{}; std::string testId; #pragma mark - DebugStringConvertible diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp index 5c035510fe1c..2f2820422161 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp @@ -159,7 +159,7 @@ static void updateBorderWidthPropValue( if (newValue.has_value()) { result[propName] = newValue.value(); } else { - result[propName] = NULL; + result[propName] = nullptr; } } } @@ -395,58 +395,89 @@ inline static void updateNativeDrawableProp( result[propName] = nativeDrawableResult; } -inline static void updateAccessibilityStateProp( - folly::dynamic& result, - const std::optional& newState, - const std::optional& oldState) { - folly::dynamic resultState = folly::dynamic::object(); +static folly::dynamic toDynamic(const AccessibilityState& state) { + folly::dynamic result = folly::dynamic::object(); + result["disabled"] = state.disabled; - if (!newState.has_value() && oldState.has_value()) { - result["accessibilityState"] = resultState; - return; + // Omitting the key when `selected` is unset is what tells the platform the + // component is not selectable. BaseViewManager#setViewState falls back to + // `setSelected(false)` for an absent key, so the rendered result is + // unchanged from when this was a plain `bool`. + if (state.selected.has_value()) { + result["selected"] = state.selected.value(); } - if (!oldState.has_value() || newState->disabled != oldState->disabled) { - resultState["disabled"] = newState->disabled; + result["busy"] = state.busy; + + if (state.expanded.has_value()) { + result["expanded"] = state.expanded.value(); } - if (!oldState.has_value() || newState->selected != oldState->selected) { - // Omitting the key when `selected` is unset is what tells the platform the - // component is not selectable. BaseViewManager#setViewState falls back to - // `setSelected(false)` for an absent key, so the rendered result is - // unchanged from when this was a plain `bool`. - if (newState->selected.has_value()) { - resultState["selected"] = newState->selected.value(); - } + switch (state.checked) { + case AccessibilityState::Unchecked: + result["checked"] = false; + break; + case AccessibilityState::Checked: + result["checked"] = true; + break; + case AccessibilityState::Mixed: + result["checked"] = "mixed"; + break; + case AccessibilityState::None: + result["checked"] = "none"; + break; } + return result; +} + +static folly::dynamic toDynamic( + const std::variant& lengthPercentageOrAuto) { + const auto* valueUnit = std::get_if(&lengthPercentageOrAuto); + return valueUnit != nullptr ? valueUnit->toDynamic() : "auto"; +} - if (!oldState.has_value() || newState->busy != oldState->busy) { - resultState["busy"] = newState->busy; +static folly::dynamic toDynamic(const BackgroundSize& backgroundSize) { + if (const auto* keyword = + std::get_if(&backgroundSize)) { + return *keyword == BackgroundSizeKeyword::Cover ? "cover" : "contain"; } + const auto& size = std::get(backgroundSize); + return folly::dynamic::object("x", toDynamic(size.x))("y", toDynamic(size.y)); +} - if (!oldState.has_value() || newState->expanded != oldState->expanded) { - if (newState->expanded.has_value()) { - resultState["expanded"] = newState->expanded.value(); - } +// JS sends one edge per axis, but the parser keeps the default top and left +// next to a bottom or right. +static folly::dynamic toDynamic(const BackgroundPosition& backgroundPosition) { + folly::dynamic result = folly::dynamic::object(); + if (backgroundPosition.bottom.has_value()) { + result["bottom"] = backgroundPosition.bottom->toDynamic(); + } else if (backgroundPosition.top.has_value()) { + result["top"] = backgroundPosition.top->toDynamic(); } + if (backgroundPosition.right.has_value()) { + result["right"] = backgroundPosition.right->toDynamic(); + } else if (backgroundPosition.left.has_value()) { + result["left"] = backgroundPosition.left->toDynamic(); + } + return result; +} - if (!oldState.has_value() || newState->checked != oldState->checked) { - switch (newState->checked) { - case AccessibilityState::Unchecked: - resultState["checked"] = false; - break; - case AccessibilityState::Checked: - resultState["checked"] = true; - break; - case AccessibilityState::Mixed: - resultState["checked"] = "mixed"; - break; - case AccessibilityState::None: - resultState["checked"] = "none"; - break; - } +static const char* toString(BackgroundRepeatStyle backgroundRepeatStyle) { + switch (backgroundRepeatStyle) { + case BackgroundRepeatStyle::Repeat: + return "repeat"; + case BackgroundRepeatStyle::Space: + return "space"; + case BackgroundRepeatStyle::Round: + return "round"; + case BackgroundRepeatStyle::NoRepeat: + return "no-repeat"; } - result["accessibilityState"] = resultState; +} + +static folly::dynamic toDynamic(const BackgroundRepeat& backgroundRepeat) { + return folly::dynamic::object("x", toString(backgroundRepeat.x))( + "y", toString(backgroundRepeat.y)); } ComponentName HostPlatformViewProps::getDiffPropsImplementationTarget() const { @@ -490,7 +521,8 @@ folly::dynamic HostPlatformViewProps::getDiffProps( } if (role != oldProps->role) { - result["role"] = toString(role); + result["role"] = + role.has_value() ? toString(*role) : folly::dynamic(nullptr); } if (opacity != oldProps->opacity) { @@ -528,7 +560,8 @@ folly::dynamic HostPlatformViewProps::getDiffProps( } if (shadowColor != oldProps->shadowColor) { - result["shadowColor"] = *shadowColor; + result["shadowColor"] = + shadowColor ? folly::dynamic(*shadowColor) : folly::dynamic(nullptr); } if (shadowOpacity != oldProps->shadowOpacity) { @@ -580,6 +613,18 @@ folly::dynamic HostPlatformViewProps::getDiffProps( result["backgroundImage"] = toDynamic(backgroundImage); } + if (backgroundSize != oldProps->backgroundSize) { + result["backgroundSize"] = toDynamic(backgroundSize); + } + + if (backgroundPosition != oldProps->backgroundPosition) { + result["backgroundPosition"] = toDynamic(backgroundPosition); + } + + if (backgroundRepeat != oldProps->backgroundRepeat) { + result["backgroundRepeat"] = toDynamic(backgroundRepeat); + } + if (mixBlendMode != oldProps->mixBlendMode) { result["mixBlendMode"] = toString(mixBlendMode); } @@ -844,8 +889,9 @@ folly::dynamic HostPlatformViewProps::getDiffProps( // Accessibility if (accessibilityState != oldProps->accessibilityState) { - updateAccessibilityStateProp( - result, accessibilityState, oldProps->accessibilityState); + result["accessibilityState"] = accessibilityState.has_value() + ? toDynamic(*accessibilityState) + : folly::dynamic::object(); } if (accessibilityLabel != oldProps->accessibilityLabel) { @@ -878,7 +924,9 @@ folly::dynamic HostPlatformViewProps::getDiffProps( } if (accessibilityRole != oldProps->accessibilityRole) { - result["accessibilityRole"] = accessibilityRole; + result["accessibilityRole"] = accessibilityRole.empty() + ? folly::dynamic(nullptr) + : folly::dynamic(accessibilityRole); } if (accessibilityLanguage != oldProps->accessibilityLanguage) { @@ -899,7 +947,9 @@ folly::dynamic HostPlatformViewProps::getDiffProps( if (accessibilityValue.text.has_value()) { accessibilityValueObject["text"] = accessibilityValue.text.value(); } - result["accessibilityValue"] = accessibilityValueObject; + result["accessibilityValue"] = accessibilityValueObject.empty() + ? folly::dynamic(nullptr) + : accessibilityValueObject; } if (accessibilityActions != oldProps->accessibilityActions) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewPropsDiffTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewPropsDiffTest.cpp new file mode 100644 index 000000000000..7fe5a6dfdab3 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewPropsDiffTest.cpp @@ -0,0 +1,157 @@ +/* + * 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. + */ + +#include + +#include +#include + +#ifdef RN_SERIALIZABLE_STATE + +namespace facebook::react { + +namespace { + +using folly::dynamic; + +void expectPayloads(const std::vector& commits) { + expectPayloadsMatchRawProps(commits); +} + +const auto kRed = static_cast(0xFFFF0000); + +} // namespace + +// ReactViewManager has setters for all three; with +// enableExclusivePropsUpdateAndroid there is no rawProps fallback. +TEST(ViewPropsDiffTest, emitsBackgroundSizePositionAndRepeat) { + expectPayloads({{dynamic::object( + "backgroundSize", + dynamic::array( + dynamic::object("x", "50%")("y", "auto"), + dynamic::object("x", 20)("y", "25%")))( + "backgroundPosition", + dynamic::array( + dynamic::object("top", "10%")("left", 5), + dynamic::object("bottom", 0)("right", "20%")))( + "backgroundRepeat", + dynamic::array(dynamic::object("x", "no-repeat")("y", "space")))}}); +} + +// The Android transform parser knows `perspective`, not `perspectiveX`. +TEST(ViewPropsDiffTest, emitsPerspectiveUnderItsOwnName) { + expectPayloads({{dynamic::object( + "transform", dynamic::array(dynamic::object("perspective", 850)))}}); +} + +// ReactViewManager resets a side to `borderWidth` only for null (NaN); 0 keeps +// the side at width 0. +TEST(ViewPropsDiffTest, removingASideBorderWidthEmitsNull) { + expectPayloads({ + {dynamic::object("borderWidth", 4)("borderLeftWidth", 12)}, + {dynamic::object("borderLeftWidth", nullptr)}, + }); +} + +// BoxShadow.parse defaults an absent color to black; 0 is transparent. +TEST(ViewPropsDiffTest, boxShadowWithoutColorOmitsColor) { + expectPayloads({{dynamic::object( + "boxShadow", + dynamic::array( + dynamic::object("offsetX", 0)("offsetY", 4)("blurRadius", 8)( + "spreadDistance", 0)("inset", false)))}}); +} + +// FilterHelper defaults an absent drop shadow color to black; 0 is +// transparent. +TEST(ViewPropsDiffTest, dropShadowWithoutColorOmitsColor) { + expectPayloads({{dynamic::object( + "filter", + dynamic::array( + dynamic::object( + "dropShadow", + dynamic::object("offsetX", 0)("offsetY", 4)( + "standardDeviation", 4))))}}); +} + +// BaseViewManager.setShadowColor resets to black for null; 0 makes the +// elevation shadow transparent. +TEST(ViewPropsDiffTest, removingShadowColorEmitsNull) { + expectPayloads({ + {dynamic::object("elevation", 8)("shadowColor", kRed)}, + {dynamic::object("shadowColor", nullptr)}, + }); +} + +// The Android gradients treat a stop with a null color as a transition hint; +// 0 is a transparent stop. +TEST(ViewPropsDiffTest, gradientColorHintEmitsNullColor) { + expectPayloads({{dynamic::object( + "backgroundImage", + dynamic::array( + dynamic::object("type", "linear-gradient")( + "direction", dynamic::object("type", "angle")("value", 180))( + "colorStops", + dynamic::array( + dynamic::object("color", kRed)("position", nullptr), + dynamic::object("color", nullptr)("position", "20%"), + dynamic::object("color", static_cast(0xFF0000FF))( + "position", nullptr)))))}}); +} + +// BaseViewManager.setViewState replaces the whole state: an absent `selected` +// deselects the view and an absent `disabled` re-enables it. Props 2.0 sends an +// unset `checked` as "none", which Java ignores. +TEST(ViewPropsDiffTest, emitsTheWholeAccessibilityStateWhenOneMemberChanges) { + auto state = [](bool busy) { + return dynamic::object("selected", true)("disabled", true)("busy", busy); + }; + auto sentState = [&](bool busy) { + return dynamic::object( + "accessibilityState", state(busy)("checked", "none")); + }; + expectPayloads({ + {dynamic::object("accessibilityState", state(false)), sentState(false)}, + {dynamic::object("accessibilityState", state(true)), sentState(true)}, + }); +} + +// React sends a removed prop as null on top of the previous props. +// BaseViewManager clears the role only for null, and +// AccessibilityRole.fromValue("") throws. +TEST(ViewPropsDiffTest, removingAccessibilityRoleEmitsNull) { + expectPayloads({ + {dynamic::object("accessibilityRole", "button")}, + {dynamic::object("accessibilityRole", nullptr)}, + }); +} + +// BaseViewManager falls back to accessibilityRole only for a null role. +TEST(ViewPropsDiffTest, removingRoleEmitsNull) { + expectPayloads({ + {dynamic::object("role", "button")}, + {dynamic::object("role", nullptr)}, + }); +} + +// An explicit role="none" overrides accessibilityRole. +TEST(ViewPropsDiffTest, emitsAnExplicitRoleNone) { + expectPayloads( + {{dynamic::object("role", "none")("accessibilityRole", "button")}}); +} + +// BaseViewManager clears the value and the content description only for null. +TEST(ViewPropsDiffTest, removingAccessibilityValueEmitsNull) { + expectPayloads({ + {dynamic::object("accessibilityValue", dynamic::object("text", "50%"))}, + {dynamic::object("accessibilityValue", nullptr)}, + }); +} + +} // namespace facebook::react + +#endif diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/BoxShadow.h b/packages/react-native/ReactCommon/react/renderer/graphics/BoxShadow.h index b8664bc44a37..2a3284f9c7e5 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/BoxShadow.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/BoxShadow.h @@ -32,7 +32,9 @@ struct BoxShadow { result["offsetY"] = offsetY; result["blurRadius"] = blurRadius; result["spreadDistance"] = spreadDistance; - result["color"] = *color; + if (color) { + result["color"] = *color; + } result["inset"] = inset; return result; } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.cpp index 01cf692d99dd..a57690fcaf35 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ColorStop.cpp @@ -12,7 +12,7 @@ namespace facebook::react { #ifdef RN_SERIALIZABLE_STATE folly::dynamic ColorStop::toDynamic() const { folly::dynamic result = folly::dynamic::object(); - result["color"] = *color; + result["color"] = color ? folly::dynamic(*color) : folly::dynamic(nullptr); result["position"] = position.toDynamic(); return result; } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Filter.h b/packages/react-native/ReactCommon/react/renderer/graphics/Filter.h index fc18207f93a2..bfe3b9c6a848 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Filter.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Filter.h @@ -102,7 +102,9 @@ struct DropShadowParams { result["offsetX"] = offsetX; result["offsetY"] = offsetY; result["standardDeviation"] = standardDeviation; - result["color"] = *color; + if (color) { + result["color"] = *color; + } return result; } #endif diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/TransformUtils.h b/packages/react-native/ReactCommon/react/renderer/graphics/TransformUtils.h index daefaebfeb3e..bcc366cbf69b 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/TransformUtils.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/TransformUtils.h @@ -75,7 +75,7 @@ inline void updateTransformProps( serializeTransformAxis(operation, "rotate", 0, resultTranslateArray); return; case TransformOperationType::Perspective: - serializeTransformAxis(operation, "perspective", 0, resultTranslateArray); + serializeTransformOperationValue("perspective", operation.x, resultTranslateArray); return; case TransformOperationType::Arbitrary: { folly::dynamic result = folly::dynamic::object(); diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h index 86756373d6da..17593ca52f78 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h @@ -62,10 +62,8 @@ class ImageSource { imageSourceResult["bundle"] = bundle; imageSourceResult["scale"] = scale; - folly::dynamic sizeResult = folly::dynamic::object(); - sizeResult["width"] = size.width; - sizeResult["height"] = size.height; - imageSourceResult["size"] = sizeResult; + imageSourceResult["width"] = size.width; + imageSourceResult["height"] = size.height; imageSourceResult["body"] = body; imageSourceResult["method"] = method; diff --git a/packages/react-native/ReactCommon/react/test_utils/PropsPayloads.h b/packages/react-native/ReactCommon/react/test_utils/PropsPayloads.h new file mode 100644 index 000000000000..88da44225401 --- /dev/null +++ b/packages/react-native/ReactCommon/react/test_utils/PropsPayloads.h @@ -0,0 +1,131 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef RN_SERIALIZABLE_STATE + +namespace facebook::react { + +/* + * One commit: the props JS sends (a removed prop is null), and the Props 2.0 + * values that intentionally differ from what Props 1.5 sends. + */ +struct PropsCommit { + folly::dynamic props; + folly::dynamic intendedDifferences = folly::dynamic::object(); +}; + +namespace detail { + +inline bool payloadValuesMatch(const folly::dynamic &actual, const folly::dynamic &expected) +{ + if (actual.isNumber() && expected.isNumber()) { + // Props 2.0 stores floats, so 0.998 comes back as 0.99800002574... + auto tolerance = 1e-6 * std::max(1.0, std::abs(expected.asDouble())); + return std::abs(actual.asDouble() - expected.asDouble()) <= tolerance; + } + if (actual.isArray() && expected.isArray()) { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin(), payloadValuesMatch); + } + if (actual.isObject() && expected.isObject()) { + return actual.size() == expected.size() && std::ranges::all_of(expected.items(), [&](const auto &item) { + return actual.count(item.first) != 0 && payloadValuesMatch(actual[item.first], item.second); + }); + } + return actual == expected; +} + +// Each element carries the fields of the expected element, and may add more. +inline bool elementsContainFields(const folly::dynamic &actual, const folly::dynamic &expected) +{ + if (!actual.isArray() || !expected.isArray() || actual.size() != expected.size()) { + return payloadValuesMatch(actual, expected); + } + for (size_t i = 0; i < expected.size(); i++) { + for (const auto &[field, value] : expected[i].items()) { + if (actual[i].count(field) == 0 || !payloadValuesMatch(actual[i][field], value)) { + return false; + } + } + } + return true; +} + +} // namespace detail + +/* + * Applies the commits in order and checks that each Props 2.0 payload + * (`getDiffProps`) has the keys, nulls and values of the Props 1.5 payload + * (accumulated raw props), apart from the commit's intended differences. The + * elements of the arrays under `keysWithAddedFields` may carry fields that JS + * does not send. + */ +template +void expectPayloadsMatchRawProps( + const std::vector &commits, + const std::set &keysWithAddedFields = {}) +{ + ContextContainer contextContainer{}; + PropsParserContext parserContext{-1, contextContainer}; + + auto previous = std::make_shared(); + folly::dynamic previousRawProps = folly::dynamic::object(); + for (size_t i = 0; i < commits.size(); i++) { + const auto &commit = commits[i]; + auto rawProps = RawProps(commit.props); + auto parser = RawPropsParser(); + parser.template prepare(); + rawProps.parse(parser); + auto props = std::make_shared(parserContext, *previous, rawProps); + auto accumulatedRawProps = mergeDynamicProps(previousRawProps, commit.props, NullValueStrategy::Override); + + auto actual = props->getDiffProps(i == 0 ? nullptr : previous.get()); + auto expected = i == 0 ? accumulatedRawProps : diffDynamicProps(previousRawProps, accumulatedRawProps); + for (const auto &[key, value] : commit.intendedDifferences.items()) { + expected[key] = value; + } + + std::set keys; + for (const auto &payload : {actual, expected}) { + for (const auto &key : payload.keys()) { + keys.insert(key.asString()); + } + } + for (const auto &key : keys) { + bool matches = actual.count(key) != 0 && expected.count(key) != 0 && + (keysWithAddedFields.contains(key) ? detail::elementsContainFields(actual[key], expected[key]) + : detail::payloadValuesMatch(actual[key], expected[key])); + EXPECT_TRUE(matches) << "commit " << i << " " << commit.props << ", key '" << key + << "'\n Props 2.0: " << actual.getDefault(key, "(absent)") + << "\n expected: " << expected.getDefault(key, "(absent)"); + } + + previous = props; + previousRawProps = accumulatedRawProps; + } +} + +} // namespace facebook::react + +#endif diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 0a971585b18c..3dcbe62a3682 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -700,8 +700,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index f0ce34575668..8bff967fb794 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -1010,8 +1010,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; @@ -1433,8 +1433,9 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor public facebook::react::Point contentOffset; public facebook::react::ScrollViewIndicatorStyle indicatorStyle; public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode; - public facebook::react::ScrollViewSnapToAlignment snapToAlignment; + public static constexpr facebook::react::Float kDefaultDecelerationRate; public std::optional maintainVisibleContentPosition; + public std::optional snapToAlignment; public std::vector snapToOffsets; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); } @@ -2043,6 +2044,7 @@ class facebook::react::HostPlatformScrollViewProps : public facebook::react::Bas public HostPlatformScrollViewProps() = default; public HostPlatformScrollViewProps(const facebook::react::PropsParserContext& context, const facebook::react::HostPlatformScrollViewProps& sourceProps, const facebook::react::RawProps& rawProps); public bool nestedScrollEnabled; + public bool scrollsChildToFocus; public bool sendMomentumEvents; public facebook::react::SharedColor endFillColor; public folly::dynamic fadingEdgeLength; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index a9d04bddb9d5..f92cdcbd7436 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -700,8 +700,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index a9d04bddb9d5..f92cdcbd7436 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -700,8 +700,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index bdd5a4e46f56..6192b4485801 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -3430,8 +3430,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index f1f68b19e566..a82846f907cf 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -3785,8 +3785,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; @@ -4020,8 +4020,9 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor public facebook::react::Point contentOffset; public facebook::react::ScrollViewIndicatorStyle indicatorStyle; public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode; - public facebook::react::ScrollViewSnapToAlignment snapToAlignment; + public static constexpr facebook::react::Float kDefaultDecelerationRate; public std::optional maintainVisibleContentPosition; + public std::optional snapToAlignment; public std::vector snapToOffsets; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 02c316b08ddc..4be08efb71ed 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -3422,8 +3422,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 8b82a486f934..cb0cf4a1edb0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -3430,8 +3430,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 5e394479d606..3bfeb8a6447c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -321,8 +321,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index 085f78edc124..6afe6ef4ce6b 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -566,8 +566,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; @@ -776,8 +776,9 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor public facebook::react::Point contentOffset; public facebook::react::ScrollViewIndicatorStyle indicatorStyle; public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode; - public facebook::react::ScrollViewSnapToAlignment snapToAlignment; + public static constexpr Float kDefaultDecelerationRate; public std::optional maintainVisibleContentPosition; + public std::optional snapToAlignment; public std::vector snapToOffsets; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 6ccc50a5a89a..c614fa5bbeec 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -321,8 +321,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 6ccc50a5a89a..c614fa5bbeec 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -321,8 +321,8 @@ class facebook::react::AccessibilityProps { public facebook::react::AccessibilityTraits accessibilityTraits; public facebook::react::AccessibilityValue accessibilityValue; public facebook::react::ImportantForAccessibility importantForAccessibility; - public facebook::react::Role role; public std::optional accessibilityState; + public std::optional role; public std::string accessibilityHint; public std::string accessibilityLabel; public std::string accessibilityLanguage;