Repository navigation
Fix View accessibility payloads that Props 2.0 sends wrong or drops (#59079) - #59079
Open
bartlomiejbloniarz wants to merge 3 commits into
Open
bartlomiejbloniarz wants to merge 3 commits into
bartlomiejbloniarz wants to merge 3 commits into
Conversation
bartlomiejbloniarz
force-pushed
the
bartlomiejbloniarz/props2-view-accessibility-payloads
branch
from
October 14, 2026 17:38
30bb044 to
82db5f8
Compare
|
@bartlomiejbloniarz has imported this pull request. If you are a Meta employee, you can view this in D125438453. |
meta-codesync Bot
pushed a commit
that referenced
this pull request
Oct 15, 2026
…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
meta-codesync Bot
pushed a commit
that referenced
this pull request
Oct 15, 2026
…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
meta-codesync
Bot
force-pushed
the
bartlomiejbloniarz/props2-view-accessibility-payloads
branch
from
October 15, 2026 12:01
82db5f8 to
c9ad068
Compare
|
@bartlomiejbloniarz has exported this pull request. If you are a Meta employee, you can view the originating Diff in D125438453. |
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
meta-codesync Bot
pushed a commit
that referenced
this pull request
Oct 16, 2026
…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
meta-codesync
Bot
force-pushed
the
bartlomiejbloniarz/props2-view-accessibility-payloads
branch
from
October 16, 2026 09:45
c9ad068 to
3c286cf
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
With Props 2.0 on Android (
enableExclusivePropsUpdateAndroidand the related flags),BaseViewManagerreceives the View payload fromgetDiffProps. Some accessibility props come out wrong:accessibilityState, such asbusy: only that member is sent, and Java treats the map as the whole state, so a selected, disabled view becomes unselected and enabled.accessibilityRoleorrole: not cleared.role="none": not sent on mount, so the view falls back toaccessibilityRole.accessibilityValue: sent as{}, so the old value stays in the content description.This PR sends
accessibilityStatein full, clears a removed role or value, and makesroleoptional 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
Test Plan:
ViewPropsDiffTestcases, 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).uiautomator dump. Settingbusykeeps the view selected and disabled. Removing the props:roleremovedandroid.view.ViewGroupandroid.widget.Buttonandroid.view.ViewGroupaccessibilityRoleremovedandroid.view.ViewGroupandroid.widget.Buttonandroid.view.ViewGroupaccessibilityValueremoved"value target, 50%"role="none"+accessibilityRole="button"android.view.Viewandroid.widget.Buttonandroid.view.ViewRNTesterPlayground.js: paste intopackages/rn-tester/js/examples/Playground/RNTesterPlayground.jsand open Playground.Differential Revision: D125438453
Pulled By: bartlomiejbloniarz