Skip to content

Commit b39053e

Browse files
Fix Text and ScrollView payloads that Props 2.0 sends wrong or drops (#59080)
Summary: With Props 2.0 on Android (`enableExclusivePropsUpdateAndroid` and the related flags), the Text and ScrollView payloads come from `getDiffProps`. Some props come out wrong: - A removed Text `letterSpacing`: sent as `NaN`, which Java ignores, so the old spacing stays. - `scrollsChildToFocus`: never sent. - `snapToAlignment`: an explicit `"start"` is not sent on mount, and removing another alignment sends `"start"`. - `decelerationRate={0.998}`: not sent, because the C++ default is the iOS rate, 0.998. Android ScrollViews start at 0.985. - A removed `nestedScrollEnabled` or `sendMomentumEvents`: stays on. This PR sends `null` for a removed `letterSpacing`, adds `scrollsChildToFocus` on Android, makes `snapToAlignment` optional, defaults `decelerationRate` to 0.985 on Android, and resets the two booleans to `false`. iOS keeps its defaults. Nothing changes on Android with the flags off. One difference from the flags-off path is intended. A removed `decelerationRate` now restores 0.985. With the flags off it reaches Java as `null`, which the setter reads as 0, so a fling stops at once. ## Changelog: [ANDROID] [FIXED] - Fix Text letterSpacing and ScrollView scrollsChildToFocus, snapToAlignment, decelerationRate, nestedScrollEnabled and sendMomentumEvents with Props 2.0 Pull Request resolved: #59080 Test Plan: - New `ParagraphPropsDiffTest` and `ScrollViewPropsDiffTest`, using the payload helper from #59077. Each test fails on main. Built with the NDK and run on an Android emulator (there is no open-source gtest target). - RNTester on an Android emulator with the Props 2.0 flags forced on: the Playground screen below. **Remove `letterSpacing: 8`** <table> <tr><th>Before</th><th>After</th></tr> <tr> <td> https://github.com/user-attachments/assets/235c115d-f73d-49e5-8eb0-367d3bbabb33 </td> <td> https://github.com/user-attachments/assets/285709c7-3d62-423d-acb6-81387fbae7cc </td> </tr> </table> **Focus an input below the fold with `scrollsChildToFocus={false}`** <table> <tr><th>Before</th><th>After</th></tr> <tr> <td> https://github.com/user-attachments/assets/89002966-e9f1-45bb-81e9-1c2ee296fea9 </td> <td> https://github.com/user-attachments/assets/e13ad50f-da40-44bd-9997-405fa2c84575 </td> </tr> </table> **Paging ScrollView with `snapToAlignment="start"`** <table> <tr><th>Before</th><th>After</th></tr> <tr> <td> https://github.com/user-attachments/assets/154fcd54-bb89-44da-ab3d-efa69cad5ec1 </td> <td> https://github.com/user-attachments/assets/97d42c24-4713-4e2d-9dd4-2cba7305d0cd </td> </tr> </table> **Remove `snapToAlignment="center"`** <table> <tr><th>Before</th><th>After</th></tr> <tr> <td> https://github.com/user-attachments/assets/267b1100-c9dd-4f0d-a698-0ebbf634117b </td> <td> https://github.com/user-attachments/assets/838ea3f5-1fd2-409c-8a05-9af0feb60468 </td> </tr> </table> **One fling with `decelerationRate={0.998}`** <table> <tr><th>Before</th><th>After</th></tr> <tr> <td> https://github.com/user-attachments/assets/bb0ddc8c-129f-4c66-b813-09e08bb81d68 </td> <td> https://github.com/user-attachments/assets/5ccf4582-d0ef-41d0-a8cb-272572d96739 </td> </tr> </table> **Remove `nestedScrollEnabled` from the inner ScrollView** <table> <tr><th>Before</th><th>After</th></tr> <tr> <td> https://github.com/user-attachments/assets/068e48b9-943a-4370-9c0f-79160b5b5a02 </td> <td> https://github.com/user-attachments/assets/d1c9721c-2cd1-44b0-9178-0a117c714358 </td> </tr> </table> **Remove `sendMomentumEvents`** <table> <tr><th>Before</th><th>After</th></tr> <tr> <td> https://github.com/user-attachments/assets/1aaea9ef-2865-46ee-8f87-80aef21cd28c </td> <td> https://github.com/user-attachments/assets/3c623dca-15d5-49ac-86b6-e3ed4ff04864 </td> </tr> </table> <details><summary><code>RNTesterPlayground.js</code>: paste into <code>packages/rn-tester/js/examples/Playground/RNTesterPlayground.js</code> and open Playground.</summary> ```js /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * flow strict-local * format */ import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import * as React from 'react'; import {useRef, useState} from 'react'; import { Button, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import ScrollViewNativeComponent from 'react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent'; function log(demo: string, data: {...}) { console.log('P49 ' + JSON.stringify({demo, ...data})); } const COLORS = ['#ffcdd2', '#c8e6c9', '#bbdefb', '#fff9c4', '#e1bee7']; function Items({count, height}: {count: number, height: number}) { return Array.from({length: count}, (_, i) => ( <View key={i} style={{height, backgroundColor: COLORS[i % COLORS.length]}} collapsable={false}> <Text style={styles.itemText}> item {i}, top {i * height} </Text> </View> )); } // pagingEnabled, items 180 tall in a 400 tall ScrollView. A slow drag of // ~290 then release snaps to the nearest: item top (start: 360), item centred // (center: 250) or a viewport page (disabled: 400). function Snap({initial, next}: {initial: ?string, next: ?string}) { const ref = useRef<?React.ElementRef<typeof ScrollView>>(null); const [alignment, setAlignment] = useState<?string>(initial); const [y, setY] = useState(0); const [endY, setEndY] = useState<?number>(null); const name = 'snap-' + String(initial); return ( <View> <Button testID="toggle" title={ initial === next ? 'Reset to top' : `snapToAlignment: ${String(alignment)} -> ${String( alignment === initial ? next : initial, )}, reset` } onPress={() => { const value = initial === next || alignment !== initial ? initial : next; setAlignment(value); setEndY(null); ref.current?.scrollTo({y: 0, animated: false}); log(name, {event: 'toggle', snapToAlignment: String(value)}); }} /> <Text testID="state" onPress={() => ref.current?.scrollTo({y: 0, animated: false})}> snapToAlignment={String(alignment)} y={y} momentumEnd= {String(endY)} </Text> <ScrollView ref={ref} style={styles.snapScroll} pagingEnabled={true} // $FlowFixMe[incompatible-type] snapToAlignment={alignment ?? undefined} scrollEventThrottle={16} onScroll={e => setY(Math.round(e.nativeEvent.contentOffset.y))} onScrollEndDrag={e => log(name, { event: 'endDrag', y: Math.round(e.nativeEvent.contentOffset.y), }) } onMomentumScrollEnd={e => { const v = Math.round(e.nativeEvent.contentOffset.y); setEndY(v); log(name, { event: 'momentumEnd', snapToAlignment: String(alignment), y: v, }); }}> <Items count={20} height={180} /> </ScrollView> </View> ); } function Deceleration({initial}: {initial: ?number}) { const ref = useRef<?React.ElementRef<typeof ScrollView>>(null); const [rate, setRate] = useState<?number>(initial); const [y, setY] = useState(0); const [endY, setEndY] = useState<?number>(null); const name = 'decel-' + String(initial); return ( <View> <Button testID="toggle" title={ initial == null ? 'Reset to top' : `decelerationRate: ${String(rate)} -> ${String( rate == null ? initial : undefined, )}, reset` } onPress={() => { const value = initial == null || rate == null ? initial : undefined; setRate(value); setEndY(null); ref.current?.scrollTo({y: 0, animated: false}); log(name, {event: 'toggle', decelerationRate: String(value)}); }} /> <Text testID="state" onPress={() => ref.current?.scrollTo({y: 0, animated: false})}> decelerationRate={String(rate)} y={y} momentumEnd={String(endY)} </Text> <ScrollView ref={ref} style={styles.decelScroll} decelerationRate={rate ?? undefined} scrollEventThrottle={16} onScroll={e => setY(Math.round(e.nativeEvent.contentOffset.y))} onMomentumScrollEnd={e => { const v = Math.round(e.nativeEvent.contentOffset.y); setEndY(v); log(name, { event: 'momentumEnd', decelerationRate: String(rate), y: v, }); }}> <Items count={400} height={100} /> </ScrollView> </View> ); } function Nested() { const outer = useRef<?React.ElementRef<typeof ScrollView>>(null); const inner = useRef<?React.ElementRef<typeof ScrollView>>(null); const [nested, setNested] = useState(true); const [outerY, setOuterY] = useState(0); const [innerY, setInnerY] = useState(0); return ( <View> <Button testID="toggle" title={ nested ? 'Remove nestedScrollEnabled, reset' : 'Set nestedScrollEnabled, reset' } onPress={() => { setNested(v => !v); outer.current?.scrollTo({y: 0, animated: false}); inner.current?.scrollTo({y: 0, animated: false}); setOuterY(0); setInnerY(0); log('nested', {event: 'toggle', nestedScrollEnabled: !nested}); }} /> <Text testID="state"> nestedScrollEnabled={nested ? 'true' : 'unset'} outerY={outerY} innerY= {innerY} </Text> <ScrollView ref={outer} style={styles.outerScroll} scrollEventThrottle={16} onScroll={e => { const v = Math.round(e.nativeEvent.contentOffset.y); setOuterY(v); log('nested', {event: 'scroll', which: 'outer', y: v}); }}> <View style={styles.outerHeader}> <Text>Outer ScrollView</Text> </View> <ScrollView ref={inner} style={styles.innerScroll} // $FlowFixMe[incompatible-type] nestedScrollEnabled={nested ? true : undefined} scrollEventThrottle={16} onScroll={e => { const v = Math.round(e.nativeEvent.contentOffset.y); setInnerY(v); log('nested', {event: 'scroll', which: 'inner', y: v}); }}> <Items count={15} height={100} /> </ScrollView> <View style={styles.outerFooter}> <Text>Outer content below the inner ScrollView</Text> </View> </ScrollView> </View> ); } // The public ScrollView always passes sendMomentumEvents as a boolean, so the // prop is removed on the native component directly. function Momentum() { const [send, setSend] = useState(true); const [counts, setCounts] = useState({begin: 0, end: 0}); return ( <View> <Button testID="toggle" title={ send ? 'Remove sendMomentumEvents, reset counts' : 'Set sendMomentumEvents, reset counts' } onPress={() => { setSend(v => !v); setCounts({begin: 0, end: 0}); log('momentum', {event: 'toggle', sendMomentumEvents: !send}); }} /> <Text testID="state"> sendMomentumEvents={send ? 'true' : 'unset'} begin={counts.begin} end= {counts.end} </Text> <ScrollViewNativeComponent style={[styles.decelScroll, styles.clip]} // $FlowFixMe[incompatible-type] sendMomentumEvents={send ? true : undefined} onMomentumScrollBegin={() => { setCounts(c => ({...c, begin: c.begin + 1})); log('momentum', {event: 'momentumBegin'}); }} onMomentumScrollEnd={() => { setCounts(c => ({...c, end: c.end + 1})); log('momentum', {event: 'momentumEnd'}); }}> <View collapsable={false}> <Items count={100} height={100} /> </View> </ScrollViewNativeComponent> </View> ); } function TextProps() { const [on, setOn] = useState(true); return ( <View> <Button testID="toggle" title={ on ? 'Remove opacity, letterSpacing' : 'Set opacity, letterSpacing' } onPress={() => setOn(v => !v)} /> <Text testID="state"> {on ? 'opacity 0.3, letterSpacing 8' : 'opacity, letterSpacing unset'} </Text> <View style={styles.row}> <Text testID="text" style={[styles.bigText, on && {opacity: 0.3, letterSpacing: 8}]}> Hello text </Text> </View> </View> ); } function ScrollFocus() { const input = useRef<?React.ElementRef<typeof TextInput>>(null); const [y, setY] = useState(0); return ( <View> <Button testID="toggle" title="Focus the input below the fold" onPress={() => input.current?.focus()} /> <Text testID="state">scrollsChildToFocus=false, scrollY: {y}</Text> <ScrollView style={styles.scroll} scrollsChildToFocus={false} scrollEventThrottle={16} onScroll={e => { const v = Math.round(e.nativeEvent.contentOffset.y); setY(v); log('focus', {event: 'scroll', y: v}); }}> <View style={styles.spacer}> <Text>Top of the ScrollView</Text> </View> <TextInput ref={input} showSoftInputOnFocus={false} style={styles.input} placeholder="input" /> </ScrollView> </View> ); } const demos = { snapStart: () => <Snap initial="start" next="start" />, snapCenter: () => <Snap initial="center" next={null} />, decel: () => <Deceleration initial={0.998} />, decelUnset: () => <Deceleration initial={null} />, nested: Nested, momentum: Momentum, text: TextProps, focus: ScrollFocus, }; function Playground() { const [demo, setDemo] = useState<$Keys<typeof demos>>('snapStart'); const Demo = demos[demo]; return ( <View style={styles.container}> <View style={styles.tabs}> {Object.keys(demos).map(name => ( <Button key={name} testID={'tab-' + name} title={name} onPress={() => setDemo(name)} /> ))} </View> <Demo key={demo} /> </View> ); } const styles = StyleSheet.create({ bigText: {fontSize: 40}, clip: {overflow: 'hidden'}, container: {padding: 10}, decelScroll: {borderColor: 'gray', borderWidth: 1, height: 400, marginTop: 8}, innerScroll: {borderColor: 'blue', borderWidth: 2, height: 250}, input: {borderColor: 'gray', borderWidth: 1, height: 48}, itemText: {fontSize: 18, padding: 6}, outerFooter: {backgroundColor: '#eeeeee', height: 1500}, outerHeader: {backgroundColor: '#eeeeee', height: 80}, outerScroll: {borderColor: 'gray', borderWidth: 1, height: 450, marginTop: 8}, row: { alignItems: 'center', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', marginTop: 24, }, scroll: {borderColor: 'gray', borderWidth: 1, height: 200, marginTop: 12}, snapScroll: {borderColor: 'gray', borderWidth: 1, height: 400, marginTop: 8}, spacer: {backgroundColor: '#e3f2fd', height: 600}, tabs: {flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginBottom: 8}, }); export default { title: 'Playground', name: 'playground', description: 'Test out new features and ideas.', render: (): React.Node => <Playground />, } as RNTesterModuleExample; ``` </details> Differential Revision: D125438443 Pulled By: bartlomiejbloniarz
1 parent b904f57 commit b39053e

11 files changed

Lines changed: 147 additions & 27 deletions

File tree

‎packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
413413

414414
auto *scrollView = (RCTEnhancedScrollView *)_scrollView;
415415
if (oldScrollViewProps.snapToAlignment != newScrollViewProps.snapToAlignment) {
416-
scrollView.snapToAlignment = RCTNSStringFromString(toString(newScrollViewProps.snapToAlignment));
416+
scrollView.snapToAlignment =
417+
RCTNSStringFromString(toString(newScrollViewProps.snapToAlignment.value_or(ScrollViewSnapToAlignment::Start)));
417418
}
418419

419420
scrollView.snapToStart = newScrollViewProps.snapToStart;

‎packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ BaseScrollViewProps::BaseScrollViewProps(
8080
rawProps,
8181
"decelerationRate",
8282
sourceProps.decelerationRate,
83-
(Float)0.998)),
83+
kDefaultDecelerationRate)),
8484
endDraggingSensitivityMultiplier(convertRawProp(
8585
context,
8686
rawProps,
@@ -427,10 +427,7 @@ SharedDebugStringConvertibleList BaseScrollViewProps::getDebugProps() const {
427427
"snapToInterval",
428428
snapToInterval,
429429
defaultScrollViewProps.snapToInterval),
430-
debugStringConvertibleItem(
431-
"snapToAlignment",
432-
snapToAlignment,
433-
defaultScrollViewProps.snapToAlignment),
430+
debugStringConvertibleItem("snapToAlignment", snapToAlignment),
434431
debugStringConvertibleItem(
435432
"disableIntervalMomentum",
436433
disableIntervalMomentum,

‎packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ class BaseScrollViewProps : public ViewProps {
4141
bool automaticallyAdjustContentInsets{};
4242
bool automaticallyAdjustsScrollIndicatorInsets{true};
4343
bool automaticallyAdjustKeyboardInsets{false};
44-
Float decelerationRate{0.998f};
44+
#ifdef ANDROID
45+
// ReactScrollView's initial rate.
46+
static constexpr Float kDefaultDecelerationRate = 0.985f;
47+
#else
48+
static constexpr Float kDefaultDecelerationRate = 0.998f;
49+
#endif
50+
Float decelerationRate{kDefaultDecelerationRate};
4551
Float endDraggingSensitivityMultiplier{1};
4652
bool directionalLockEnabled{};
4753
ScrollViewIndicatorStyle indicatorStyle{};
@@ -63,7 +69,7 @@ class BaseScrollViewProps : public ViewProps {
6369
Point contentOffset{};
6470
EdgeInsets scrollIndicatorInsets{};
6571
Float snapToInterval{};
66-
ScrollViewSnapToAlignment snapToAlignment{};
72+
std::optional<ScrollViewSnapToAlignment> snapToAlignment{};
6773
bool disableIntervalMomentum{false};
6874
std::vector<Float> snapToOffsets{};
6975
bool snapToStart{true};

‎packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp‎

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ HostPlatformScrollViewProps::HostPlatformScrollViewProps(
2525
rawProps,
2626
"sendMomentumEvents",
2727
sourceProps.sendMomentumEvents,
28-
true)),
28+
false)),
2929
nestedScrollEnabled(convertRawProp(
3030
context,
3131
rawProps,
3232
"nestedScrollEnabled",
3333
sourceProps.nestedScrollEnabled,
34-
true)),
34+
false)),
3535
fadingEdgeLength(convertRawProp(
3636
context,
3737
rawProps,
@@ -49,7 +49,13 @@ HostPlatformScrollViewProps::HostPlatformScrollViewProps(
4949
rawProps,
5050
"endFillColor",
5151
sourceProps.endFillColor,
52-
clearColor())) {}
52+
clearColor())),
53+
scrollsChildToFocus(convertRawProp(
54+
context,
55+
rawProps,
56+
"scrollsChildToFocus",
57+
sourceProps.scrollsChildToFocus,
58+
true)) {}
5359

5460
void HostPlatformScrollViewProps::setProp(
5561
const PropsParserContext& context,
@@ -69,6 +75,7 @@ void HostPlatformScrollViewProps::setProp(
6975
RAW_SET_PROP_SWITCH_CASE_BASIC(fadingEdgeLength);
7076
RAW_SET_PROP_SWITCH_CASE_BASIC(overScrollMode);
7177
RAW_SET_PROP_SWITCH_CASE_BASIC(endFillColor);
78+
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollsChildToFocus);
7279
}
7380
}
7481

@@ -96,7 +103,11 @@ SharedDebugStringConvertibleList HostPlatformScrollViewProps::getDebugProps()
96103
debugStringConvertibleItem(
97104
"endFillColor",
98105
endFillColor,
99-
defaultScrollViewProps.endFillColor)};
106+
defaultScrollViewProps.endFillColor),
107+
debugStringConvertibleItem(
108+
"scrollsChildToFocus",
109+
scrollsChildToFocus,
110+
defaultScrollViewProps.scrollsChildToFocus)};
100111
}
101112
#endif
102113

@@ -301,17 +312,9 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps(
301312
}
302313

303314
if (snapToAlignment != oldProps->snapToAlignment) {
304-
switch (snapToAlignment) {
305-
case ScrollViewSnapToAlignment::Start:
306-
result["snapToAlignment"] = "start";
307-
break;
308-
case ScrollViewSnapToAlignment::Center:
309-
result["snapToAlignment"] = "center";
310-
break;
311-
case ScrollViewSnapToAlignment::End:
312-
result["snapToAlignment"] = "end";
313-
break;
314-
}
315+
result["snapToAlignment"] = snapToAlignment.has_value()
316+
? toString(*snapToAlignment)
317+
: folly::dynamic(nullptr);
315318
}
316319

317320
if (disableIntervalMomentum != oldProps->disableIntervalMomentum) {
@@ -380,6 +383,10 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps(
380383
result["endFillColor"] = *endFillColor;
381384
}
382385

386+
if (scrollsChildToFocus != oldProps->scrollsChildToFocus) {
387+
result["scrollsChildToFocus"] = scrollsChildToFocus;
388+
}
389+
383390
return result;
384391
}
385392

‎packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class HostPlatformScrollViewProps : public BaseScrollViewProps {
3838
folly::dynamic fadingEdgeLength{};
3939
std::string overScrollMode{"auto"};
4040
SharedColor endFillColor{clearColor()};
41+
bool scrollsChildToFocus{true};
4142

4243
#pragma mark - DebugStringConvertible
4344

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <gtest/gtest.h>
9+
10+
#include <react/renderer/components/scrollview/ScrollViewProps.h>
11+
#include <react/test_utils/PropsPayloads.h>
12+
13+
#ifdef RN_SERIALIZABLE_STATE
14+
15+
namespace facebook::react {
16+
17+
namespace {
18+
19+
using folly::dynamic;
20+
21+
void expectPayloads(const std::vector<PropsCommit>& commits) {
22+
expectPayloadsMatchRawProps<ScrollViewProps>(commits);
23+
}
24+
25+
} // namespace
26+
27+
// ReactScrollViewManager has a setter for it; with
28+
// enableExclusivePropsUpdateAndroid there is no rawProps fallback.
29+
TEST(ScrollViewPropsDiffTest, emitsScrollsChildToFocus) {
30+
expectPayloads({{dynamic::object("scrollsChildToFocus", false)}});
31+
}
32+
33+
// Android ScrollViews start with snapping alignment disabled, and
34+
// setSnapToAlignment disables it for null.
35+
TEST(
36+
ScrollViewPropsDiffTest,
37+
keepsAnExplicitSnapToAlignmentStartApartFromUnset) {
38+
expectPayloads({
39+
{dynamic::object("snapToAlignment", "start")},
40+
{dynamic::object("snapToAlignment", "center")},
41+
{dynamic::object("snapToAlignment", nullptr)},
42+
});
43+
}
44+
45+
// Android ScrollViews start decelerating at 0.985 (ReactScrollViewHelper).
46+
// A removed rate restores it; Props 1.5 sends null, which the setter reads as
47+
// 0.
48+
TEST(ScrollViewPropsDiffTest, emitsAnExplicitDecelerationRate) {
49+
expectPayloads({
50+
{dynamic::object("decelerationRate", 0.998)},
51+
{dynamic::object("decelerationRate", nullptr),
52+
dynamic::object("decelerationRate", 0.985)},
53+
});
54+
}
55+
56+
// The ScrollView managers turn both off for null.
57+
TEST(ScrollViewPropsDiffTest, removingNestedScrollAndMomentumEventsEmitsFalse) {
58+
expectPayloads({
59+
{dynamic::object("nestedScrollEnabled", true)(
60+
"sendMomentumEvents", true)},
61+
{dynamic::object("nestedScrollEnabled", nullptr)(
62+
"sendMomentumEvents", nullptr),
63+
dynamic::object("nestedScrollEnabled", false)(
64+
"sendMomentumEvents", false)},
65+
});
66+
}
67+
68+
} // namespace facebook::react
69+
70+
#endif

‎packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#include <react/renderer/debug/DebugStringConvertibleItem.h>
1414
#include <react/utils/FloatComparison.h>
1515

16+
#include <cmath>
17+
1618
namespace facebook::react {
1719

1820
static TextAttributes convertRawProp(
@@ -436,7 +438,9 @@ void BaseTextProps::appendTextAttributesProps(
436438
if (!floatEquality(
437439
textAttributes.letterSpacing,
438440
oldProps->textAttributes.letterSpacing)) {
439-
result["letterSpacing"] = textAttributes.letterSpacing;
441+
result["letterSpacing"] = std::isnan(textAttributes.letterSpacing)
442+
? folly::dynamic(nullptr)
443+
: textAttributes.letterSpacing;
440444
}
441445

442446
if (textAttributes.textTransform != oldProps->textAttributes.textTransform) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <gtest/gtest.h>
9+
10+
#include <react/renderer/components/text/ParagraphProps.h>
11+
#include <react/test_utils/PropsPayloads.h>
12+
13+
#ifdef RN_SERIALIZABLE_STATE
14+
15+
namespace facebook::react {
16+
17+
using folly::dynamic;
18+
19+
// ReactTextViewManager resets letterSpacing to 0 for null;
20+
// ReactTextView.setLetterSpacing ignores NaN.
21+
TEST(ParagraphPropsDiffTest, removingLetterSpacingEmitsNull) {
22+
expectPayloadsMatchRawProps<ParagraphProps>({
23+
{dynamic::object("letterSpacing", 4.0)},
24+
{dynamic::object("letterSpacing", nullptr)},
25+
});
26+
}
27+
28+
} // namespace facebook::react
29+
30+
#endif

‎scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1433,8 +1433,9 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
14331433
public facebook::react::Point contentOffset;
14341434
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
14351435
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
1436-
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
1436+
public static constexpr facebook::react::Float kDefaultDecelerationRate;
14371437
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
1438+
public std::optional<facebook::react::ScrollViewSnapToAlignment> snapToAlignment;
14381439
public std::vector<facebook::react::Float> snapToOffsets;
14391440
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
14401441
}
@@ -2044,6 +2045,7 @@ class facebook::react::HostPlatformScrollViewProps : public facebook::react::Bas
20442045
public HostPlatformScrollViewProps() = default;
20452046
public HostPlatformScrollViewProps(const facebook::react::PropsParserContext& context, const facebook::react::HostPlatformScrollViewProps& sourceProps, const facebook::react::RawProps& rawProps);
20462047
public bool nestedScrollEnabled;
2048+
public bool scrollsChildToFocus;
20472049
public bool sendMomentumEvents;
20482050
public facebook::react::SharedColor endFillColor;
20492051
public folly::dynamic fadingEdgeLength;

‎scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4020,8 +4020,9 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
40204020
public facebook::react::Point contentOffset;
40214021
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
40224022
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
4023-
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
4023+
public static constexpr facebook::react::Float kDefaultDecelerationRate;
40244024
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
4025+
public std::optional<facebook::react::ScrollViewSnapToAlignment> snapToAlignment;
40254026
public std::vector<facebook::react::Float> snapToOffsets;
40264027
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
40274028
}

0 commit comments

Comments
 (0)