Repository navigation
Commit b39053e
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: bartlomiejbloniarz1 parent b904f57 commit b39053e
11 files changed
Lines changed: 147 additions & 27 deletions
File tree
- packages/react-native
- ReactCommon/react/renderer/components
- scrollview
- platform/android/react/renderer/components/scrollview
- tests
- text
- tests
- React/Fabric/Mounting/ComponentViews/ScrollView
- scripts/cxx-api/api-snapshots
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
413 | 413 | | |
414 | 414 | | |
415 | 415 | | |
416 | | - | |
| 416 | + | |
| 417 | + | |
417 | 418 | | |
418 | 419 | | |
419 | 420 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
83 | | - | |
| 83 | + | |
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| |||
427 | 427 | | |
428 | 428 | | |
429 | 429 | | |
430 | | - | |
431 | | - | |
432 | | - | |
433 | | - | |
| 430 | + | |
434 | 431 | | |
435 | 432 | | |
436 | 433 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | | - | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
45 | 51 | | |
46 | 52 | | |
47 | 53 | | |
| |||
63 | 69 | | |
64 | 70 | | |
65 | 71 | | |
66 | | - | |
| 72 | + | |
67 | 73 | | |
68 | 74 | | |
69 | 75 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
53 | 59 | | |
54 | 60 | | |
55 | 61 | | |
| |||
69 | 75 | | |
70 | 76 | | |
71 | 77 | | |
| 78 | + | |
72 | 79 | | |
73 | 80 | | |
74 | 81 | | |
| |||
96 | 103 | | |
97 | 104 | | |
98 | 105 | | |
99 | | - | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
100 | 111 | | |
101 | 112 | | |
102 | 113 | | |
| |||
301 | 312 | | |
302 | 313 | | |
303 | 314 | | |
304 | | - | |
305 | | - | |
306 | | - | |
307 | | - | |
308 | | - | |
309 | | - | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
314 | | - | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
315 | 318 | | |
316 | 319 | | |
317 | 320 | | |
| |||
380 | 383 | | |
381 | 384 | | |
382 | 385 | | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
383 | 390 | | |
384 | 391 | | |
385 | 392 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
16 | 18 | | |
17 | 19 | | |
18 | 20 | | |
| |||
436 | 438 | | |
437 | 439 | | |
438 | 440 | | |
439 | | - | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
440 | 444 | | |
441 | 445 | | |
442 | 446 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1433 | 1433 | | |
1434 | 1434 | | |
1435 | 1435 | | |
1436 | | - | |
| 1436 | + | |
1437 | 1437 | | |
| 1438 | + | |
1438 | 1439 | | |
1439 | 1440 | | |
1440 | 1441 | | |
| |||
2044 | 2045 | | |
2045 | 2046 | | |
2046 | 2047 | | |
| 2048 | + | |
2047 | 2049 | | |
2048 | 2050 | | |
2049 | 2051 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4020 | 4020 | | |
4021 | 4021 | | |
4022 | 4022 | | |
4023 | | - | |
| 4023 | + | |
4024 | 4024 | | |
| 4025 | + | |
4025 | 4026 | | |
4026 | 4027 | | |
4027 | 4028 | | |
| |||
0 commit comments