Skip to content

Fix Text and ScrollView payloads that Props 2.0 sends wrong or drops (#59080) - #59080

Open
bartlomiejbloniarz wants to merge 4 commits into
mainfrom
bartlomiejbloniarz/props2-text-scrollview-payloads
Open

bartlomiejbloniarz wants to merge 4 commits into
mainfrom
bartlomiejbloniarz/props2-text-scrollview-payloads

Conversation

@bartlomiejbloniarz

@bartlomiejbloniarz bartlomiejbloniarz commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

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

Test Plan:

  • New ParagraphPropsDiffTest and ScrollViewPropsDiffTest, using the payload helper from Fix Image payloads that Props 2.0 sends wrong or drops (#59077) #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

BeforeAfter
letterspacing-before-props2.mp4
letterspacing-after-props2.mp4

Focus an input below the fold with scrollsChildToFocus={false}

BeforeAfter
c10-before-props2.mp4
c10-after-props2.mp4

Paging ScrollView with snapToAlignment="start"

BeforeAfter
snapstart-before-props2.mp4
snapstart-after-props2.mp4

Remove snapToAlignment="center"

BeforeAfter
snapcenter-before-props2.mp4
snapcenter-after-props2.mp4

One fling with decelerationRate={0.998}

BeforeAfter
decel-before-props2.mp4
decel-after-props2.mp4

Remove nestedScrollEnabled from the inner ScrollView

BeforeAfter
nested-before-props2.mp4
nested-after-props2.mp4

Remove sendMomentumEvents

BeforeAfter
momentum-before-props2.mp4
momentum-after-props2.mp4
RNTesterPlayground.js: paste into packages/rn-tester/js/examples/Playground/RNTesterPlayground.js and open Playground.
/**
 * 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;

Differential Revision: D125438443

Pulled By: bartlomiejbloniarz

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 14, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner p: Facebook Partner: Facebook labels Oct 14, 2026
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the bartlomiejbloniarz/props2-text-scrollview-payloads branch from 75c9c5e to 0acf49a Compare October 14, 2026 17:38
@meta-codesync

meta-codesync Bot commented Oct 15, 2026

Copy link
Copy Markdown

@bartlomiejbloniarz has imported this pull request. If you are a Meta employee, you can view this in D125438443.

Summary:
With Props 2.0 on Android (`enableExclusivePropsUpdateAndroid` and the related flags), `ReactImageManager` receives the payload from `ImageProps::getDiffProps`. Some Image props come out wrong:

- Several sources or `srcSet`: crash with `NoSuchKeyException: width`, because each source's size is nested under `size`.
- `headers`: never sent, and not cleared when the new source has none.
- A source with the same URI but different headers, cache, size or scale: not sent.
- A removed `tintColor`: sent as `0`, so the image disappears.

This PR sends each of them the way the Java side reads them. Nothing changes with the flags off.

## Changelog:

[ANDROID] [FIXED] - Fix Image sources, headers and a removed tintColor with Props 2.0

Pull Request resolved: #59077

Test Plan:
- New `ImagePropsDiffTest`. It uses a small helper, `PropsPayloads.h`, that checks each Props 2.0 payload against what Props 1.5 sends for the same prop updates. 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.

**Image example: `srcSet` and several sources**

<table>
<tr><th>Before</th><th>After</th></tr>
<tr>
<td>

https://github.com/user-attachments/assets/483306f2-f308-401e-8f4c-809337ec2292

</td>
<td>

https://github.com/user-attachments/assets/cf978447-071e-4537-8d3f-31e663f42bc0

</td>
</tr>
</table>

**Remove `tintColor` (Playground screen below)**

<table>
<tr><th>Before</th><th>After</th></tr>
<tr>
<td>

https://github.com/user-attachments/assets/349be881-8de6-4731-b01b-362fabd0278f

</td>
<td>

https://github.com/user-attachments/assets/04a7e35a-a64a-4317-8d91-cc30a7534b0d

</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 {useState} from 'react';
import {Button, Image, StyleSheet, Text, View} from 'react-native';

function Tint() {
  const [on, setOn] = useState(true);
  return (
    <View>
      <Button
        testID="toggle"
        title={on ? 'Remove tintColor' : 'Set tintColor'}
        onPress={() => setOn(v => !v)}
      />
      <Text testID="state">tintColor: {on ? 'purple' : 'unset'}</Text>
      <View style={styles.row}>
        <Image
          source={require('../../assets/bunny.png')}
          style={[styles.image, on && {tintColor: 'purple'}]}
        />
      </View>
    </View>
  );
}

function Playground() {
  return (
    <View style={styles.container}>
      <Tint />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {padding: 10},
  image: {backgroundColor: '#e0e0e0', height: 160, width: 160},
  row: {
    alignItems: 'center',
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'center',
    marginTop: 24,
  },
});

export default {
  title: 'Playground',
  name: 'playground',
  description: 'Test out new features and ideas.',
  render: (): React.Node => <Playground />,
} as RNTesterModuleExample;
```

</details>

Differential Revision: D125438476

Pulled By: bartlomiejbloniarz
Summary:
With Props 2.0 on Android (`enableExclusivePropsUpdateAndroid` and the related flags), `ReactViewManager` receives the payload from `HostPlatformViewProps::getDiffProps`. Some style props come out wrong:

- `backgroundSize`, `backgroundPosition`, `backgroundRepeat`: never sent, so a tiled gradient fills the whole view.
- A `perspective` transform: sent as `perspectiveX`, which Android ignores.
- A removed side border width: sent as `0` instead of falling back to `borderWidth`.
- `boxShadow` or `drop-shadow` without a color: transparent instead of black.
- A removed `shadowColor`: sent as `0`, so the elevation shadow disappears.
- A gradient color hint, as in `linear-gradient(red, 20%, blue)`: drawn as a transparent stop.

This PR sends each of them the way the Java side reads them. Nothing changes with the flags off, except that the animation backend shares the transform code and now also sends `perspective`.

## Changelog:

[ANDROID] [FIXED] - Fix View background, perspective, border width, shadow and gradient props with Props 2.0

Pull Request resolved: #59078

Test Plan:
- New `ViewPropsDiffTest`, 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 BackgroundImage example and the Playground screen below.

**Gradient with Background Repeat**

<table>
<tr><th>Before</th><th>After</th></tr>
<tr>
<td>

https://github.com/user-attachments/assets/6e337636-608d-4c06-996f-914ff005528d

</td>
<td>

https://github.com/user-attachments/assets/6d346094-eff6-4b20-a1aa-693988c47048

</td>
</tr>
</table>

**Flip a card with `perspective: 400`**

<table>
<tr><th>Before</th><th>After</th></tr>
<tr>
<td>

https://github.com/user-attachments/assets/9fcdcbc0-ec94-43be-aa95-31e8d5b813c3

</td>
<td>

https://github.com/user-attachments/assets/7942a58c-f7c1-4292-aa02-edadb31924d3

</td>
</tr>
</table>

**Remove `borderLeftWidth` (`borderWidth: 8`)**

<table>
<tr><th>Before</th><th>After</th></tr>
<tr>
<td>

https://github.com/user-attachments/assets/5098d21a-2686-4d01-9120-07247832579e

</td>
<td>

https://github.com/user-attachments/assets/758dc39e-230e-45ec-81d5-098f601183d6

</td>
</tr>
</table>

**`boxShadow` and `drop-shadow` without a color**

<table>
<tr><th>Before</th><th>After</th></tr>
<tr>
<td>

https://github.com/user-attachments/assets/20fa2a78-240f-4e62-afa9-b0f687b1493c

</td>
<td>

https://github.com/user-attachments/assets/2c6257f5-e978-4710-b0b9-b1fdb34f1052

</td>
</tr>
</table>

**Remove `shadowColor` (`elevation: 24`)**

<table>
<tr><th>Before</th><th>After</th></tr>
<tr>
<td>

https://github.com/user-attachments/assets/e74d27b5-6c1c-45f2-be88-a27b6a5fb0cd

</td>
<td>

https://github.com/user-attachments/assets/55593913-96ea-46e5-91b8-c1fd3cf53c43

</td>
</tr>
</table>

**Add a 20% color hint**

<table>
<tr><th>Before</th><th>After</th></tr>
<tr>
<td>

https://github.com/user-attachments/assets/9f2c2b2a-8b39-4e0d-9ca6-5c972c5abf75

</td>
<td>

https://github.com/user-attachments/assets/b4ab4fa6-7d92-4705-af76-bae13b091bc4

</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 {useState} from 'react';
import {Button, StyleSheet, Text, View} from 'react-native';

function Perspective() {
  const [on, setOn] = useState(false);
  return (
    <View>
      <Button
        testID="toggle"
        title={on ? 'Flip back' : 'Flip card'}
        onPress={() => setOn(v => !v)}
      />
      <Text testID="state">
        transform: perspective 400, rotateY {on ? '60deg' : '0deg'}
      </Text>
      <View style={styles.row}>
        <View
          style={[
            styles.card,
            {transform: [{perspective: 400}, {rotateY: on ? '60deg' : '0deg'}]},
          ]}>
          <Text style={styles.cardText}>CARD</Text>
        </View>
      </View>
    </View>
  );
}

function Border() {
  const [on, setOn] = useState(true);
  return (
    <View>
      <Button
        testID="toggle"
        title={on ? 'Remove borderLeftWidth' : 'Set borderLeftWidth'}
        onPress={() => setOn(v => !v)}
      />
      <Text testID="state">
        borderWidth 8{on ? ', borderLeftWidth 32' : ''}
      </Text>
      <View style={styles.row}>
        <View style={[styles.bordered, on && {borderLeftWidth: 32}]} />
      </View>
    </View>
  );
}

function Shadows() {
  const [on, setOn] = useState(true);
  return (
    <View>
      <Button
        testID="toggle"
        title={on ? 'Remove shadows' : 'Set shadows'}
        onPress={() => setOn(v => !v)}
      />
      <Text testID="state">
        {on ? 'boxShadow, drop-shadow: no color' : 'no shadows'}
      </Text>
      <View style={styles.row}>
        <View style={[styles.box, on && {boxShadow: '0 12px 16px'}]}>
          <Text>boxShadow</Text>
        </View>
        <View style={[styles.box, on && {filter: 'drop-shadow(0 12px 6px)'}]}>
          <Text>drop-shadow</Text>
        </View>
      </View>
    </View>
  );
}

function ShadowColor() {
  const [on, setOn] = useState(true);
  return (
    <View>
      <Button
        testID="toggle"
        title={on ? 'Remove shadowColor' : 'Set shadowColor'}
        onPress={() => setOn(v => !v)}
      />
      <Text testID="state">
        elevation 24, shadowColor {on ? 'red' : 'unset'}
      </Text>
      <View style={styles.row}>
        <View style={[styles.box, styles.elevated, on && {shadowColor: 'red'}]}>
          <Text>elevation</Text>
        </View>
      </View>
    </View>
  );
}

function Gradient() {
  const [hint, setHint] = useState(false);
  return (
    <View>
      <Button
        testID="toggle"
        title={hint ? 'Remove the 20% hint' : 'Add a 20% hint'}
        onPress={() => setHint(v => !v)}
      />
      <Text testID="state">
        linear-gradient(red, {hint ? '20%, ' : ''}blue)
      </Text>
      <View style={styles.row}>
        <View
          style={[
            styles.gradient,
            {
              backgroundImage: hint
                ? 'linear-gradient(red, 20%, blue)'
                : 'linear-gradient(red, blue)',
            },
          ]}
        />
      </View>
    </View>
  );
}

const demos = {
  perspective: Perspective,
  border: Border,
  shadows: Shadows,
  shadowColor: ShadowColor,
  gradient: Gradient,
};

function Playground() {
  const [demo, setDemo] = useState<$Keys<typeof demos>>('perspective');
  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({
  bordered: {
    backgroundColor: '#ffd54f',
    borderColor: '#1a237e',
    borderWidth: 8,
    height: 120,
    width: 200,
  },
  box: {
    alignItems: 'center',
    backgroundColor: 'white',
    borderRadius: 12,
    height: 120,
    justifyContent: 'center',
    margin: 16,
    width: 140,
  },
  card: {
    alignItems: 'center',
    backgroundColor: '#1e88e5',
    height: 140,
    justifyContent: 'center',
    width: 200,
  },
  cardText: {color: 'white', fontSize: 28, fontWeight: 'bold'},
  container: {padding: 10},
  elevated: {elevation: 24},
  gradient: {height: 160, width: 240},
  row: {
    alignItems: 'center',
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'center',
    marginTop: 24,
  },
  tabs: {flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginBottom: 12},
});

export default {
  title: 'Playground',
  name: 'playground',
  description: 'Test out new features and ideas.',
  render: (): React.Node => <Playground />,
} as RNTesterModuleExample;
```

</details>

Differential Revision: D125438461

Pulled By: bartlomiejbloniarz
…59079)

Summary:
With Props 2.0 on Android (`enableExclusivePropsUpdateAndroid` and the related flags), `BaseViewManager` receives the View payload from `getDiffProps`. Some accessibility props come out wrong:

- Changing one member of `accessibilityState`, such as `busy`: only that member is sent, and Java treats the map as the whole state, so a selected, disabled view becomes unselected and enabled.
- A removed `accessibilityRole` or `role`: not cleared.
- An explicit `role="none"`: not sent on mount, so the view falls back to `accessibilityRole`.
- A removed `accessibilityValue`: sent as `{}`, so the old value stays in the content description.

This PR sends `accessibilityState` in full, clears a removed role or value, and makes `role` optional so that unset and `"none"` differ. Nothing changes on Android with the flags off. iOS shares the props constructor, so a removed role is now also cleared in its C++ props. I didn't test iOS.

## Changelog:

[ANDROID] [FIXED] - Fix accessibilityState, a removed accessibilityRole, role or accessibilityValue, and role="none" with Props 2.0

Pull Request resolved: #59079

Test Plan:
- New `ViewPropsDiffTest` cases, 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, checked with `uiautomator dump`. Setting `busy` keeps the view selected and disabled. Removing the props:

| | Flags off | Main, Props 2.0 | This PR, Props 2.0 |
|---|---|---|---|
| `role` removed | `android.view.ViewGroup` | `android.widget.Button` | `android.view.ViewGroup` |
| `accessibilityRole` removed | `android.view.ViewGroup` | `android.widget.Button` | `android.view.ViewGroup` |
| `accessibilityValue` removed | content description cleared | `"value target, 50%"` | content description cleared |
| `role="none"` + `accessibilityRole="button"` | `android.view.View` | `android.widget.Button` | `android.view.View` |

<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 {useState} from 'react';
import {Button, StyleSheet, Text, View} from 'react-native';

function A11yState() {
  const [busy, setBusy] = useState(false);
  return (
    <View>
      <Button
        testID="toggle"
        title={busy ? 'Set busy: false' : 'Set busy: true'}
        onPress={() => setBusy(v => !v)}
      />
      <Text testID="state">
        accessibilityState: selected true, disabled true, busy {String(busy)}
      </Text>
      <View style={styles.row}>
        <View
          accessible={true}
          accessibilityLabel="a11y target"
          testID="a11yTarget"
          accessibilityState={{selected: true, disabled: true, busy}}
          style={styles.card}
        />
      </View>
    </View>
  );
}

function A11y() {
  const [on, setOn] = useState(true);
  return (
    <View>
      <Button
        testID="toggle"
        title={on ? 'Remove role, accessibilityRole, value' : 'Set them'}
        onPress={() => setOn(v => !v)}
      />
      <Text testID="state">
        {on ? 'role/accessibilityRole button, value 50%' : 'all unset'}
      </Text>
      <View style={styles.row}>
        <View
          accessible={true}
          accessibilityLabel="role target"
          role={on ? 'button' : undefined}
          style={styles.chip}
        />
        <View
          accessible={true}
          accessibilityLabel="accessibilityRole target"
          accessibilityRole={on ? 'button' : undefined}
          style={styles.chip}
        />
        <View
          accessible={true}
          accessibilityLabel="value target"
          accessibilityValue={on ? {text: '50%'} : undefined}
          style={styles.chip}
        />
        <View
          accessible={true}
          accessibilityLabel="none target"
          role="none"
          accessibilityRole="button"
          style={styles.chip}
        />
      </View>
    </View>
  );
}

const demos = {
  a11yState: A11yState,
  a11y: A11y,
};

function Playground() {
  const [demo, setDemo] = useState<$Keys<typeof demos>>('a11yState');
  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({
  card: {
    alignItems: 'center',
    backgroundColor: '#1e88e5',
    height: 140,
    justifyContent: 'center',
    width: 200,
  },
  chip: {backgroundColor: '#90caf9', height: 48, margin: 8, width: 48},
  container: {padding: 10},
  row: {
    alignItems: 'center',
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'center',
    marginTop: 24,
  },
  tabs: {flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginBottom: 12},
});

export default {
  title: 'Playground',
  name: 'playground',
  description: 'Test out new features and ideas.',
  render: (): React.Node => <Playground />,
} as RNTesterModuleExample;
```

</details>

Differential Revision: D125438453

Pulled By: bartlomiejbloniarz
…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
@meta-codesync meta-codesync Bot changed the title [Android] Fix Text and ScrollView payloads that Props 2.0 sends wrong or drops Fix Text and ScrollView payloads that Props 2.0 sends wrong or drops (#59080) Oct 15, 2026
@meta-codesync
meta-codesync Bot force-pushed the bartlomiejbloniarz/props2-text-scrollview-payloads branch from 0acf49a to b39053e Compare October 15, 2026 12:01
@meta-codesync

meta-codesync Bot commented Oct 15, 2026

Copy link
Copy Markdown

@bartlomiejbloniarz has exported this pull request. If you are a Meta employee, you can view the originating Diff in D125438443.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported p: Facebook Partner: Facebook p: Software Mansion Partner: Software Mansion Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant