From 0c008070adc5b1c42d3cb8e898ce5637f793b25f Mon Sep 17 00:00:00 2001
From: Dmitry Zhigulin <104712148+faciledictu@users.noreply.github.com>
Date: Thu, 22 Oct 2026 21:06:08 +0000
Subject: [PATCH] fix: map aria-* props in Switch, ScrollView and
ActivityIndicator
These components render a native component directly instead of a View,
so the aria-*, id and tabIndex props reach the native side unconverted
and are dropped. Move the conversion View does into a helper and use it
in the three components.
---
.../ActivityIndicator/ActivityIndicator.js | 3 +-
.../__tests__/ActivityIndicator-itest.js | 11 ++
.../Components/ScrollView/ScrollView.js | 3 +-
.../ScrollView/__tests__/ScrollView-itest.js | 32 ++++++
.../Libraries/Components/Switch/Switch.js | 3 +-
.../Switch/__tests__/Switch-itest.js | 42 +++++++
.../components/view/resolveAriaProps.js | 108 ++++++++++++++++++
7 files changed, 199 insertions(+), 3 deletions(-)
create mode 100644 packages/react-native/src/private/components/view/resolveAriaProps.js
diff --git a/packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js
index e73577e68c0b..acbd6fa4a845 100644
--- a/packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js
+++ b/packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js
@@ -13,6 +13,7 @@
import type {HostInstance} from '../../../src/private/types/HostInstance';
import type {ViewProps} from '../View/ViewPropTypes';
+import resolveAriaProps from '../../../src/private/components/view/resolveAriaProps';
import StyleSheet, {type ColorValue} from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import View from '../View/View';
@@ -142,7 +143,7 @@ const ActivityIndicator: component(
animating,
color,
hidesWhenStopped,
- ...restProps,
+ ...resolveAriaProps(restProps),
ref: forwardedRef,
style: sizeStyle,
size: sizeProp,
diff --git a/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js b/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js
index 51636fc49de2..7b7897545dc2 100644
--- a/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js
+++ b/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js
@@ -55,4 +55,15 @@ describe('ActivityIndicator', () => {
),
).toEqual();
});
+
+ it('maps aria-label to accessibilityLabel', () => {
+ const root = Fantom.createRoot();
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
+ ).toEqual();
+ });
});
diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js
index 55ebda75d1a9..ecea86c7dedc 100644
--- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js
+++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js
@@ -31,6 +31,7 @@ import {
VScrollContentViewNativeComponent,
VScrollViewNativeComponent,
} from '../../../src/private/components/scrollview/VScrollViewNativeComponents';
+import resolveAriaProps from '../../../src/private/components/view/resolveAriaProps';
import AnimatedImplementation from '../../Animated/AnimatedImplementation';
import FrameRateLogger from '../../Interaction/FrameRateLogger';
import {findNodeHandle} from '../../ReactNative/RendererProxy';
@@ -1861,7 +1862,7 @@ class ScrollView extends React.Component {
const {experimental_endDraggingSensitivityMultiplier, ...otherProps} =
this.props;
const props = {
- ...otherProps,
+ ...resolveAriaProps(otherProps),
alwaysBounceHorizontal,
alwaysBounceVertical,
style: StyleSheet.compose(baseStyle, this.props.style),
diff --git a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js
index 04135f43326f..e85a81561a29 100644
--- a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js
+++ b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js
@@ -71,6 +71,38 @@ describe('', () => {
});
});
+ describe('web-style accessibility props', () => {
+ it('maps aria-label to accessibilityLabel', () => {
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
+ ).toEqual(
+
+
+ ,
+ );
+ });
+
+ it('maps id to nativeID', () => {
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(root.getRenderedOutput({props: ['nativeID']}).toJSX()).toEqual(
+
+
+ ,
+ );
+ });
+ });
+
describe('ref', () => {
it('receives an instance or null', () => {
const root = Fantom.createRoot();
diff --git a/packages/react-native/Libraries/Components/Switch/Switch.js b/packages/react-native/Libraries/Components/Switch/Switch.js
index bf284ba57789..5469ce0e9d1d 100644
--- a/packages/react-native/Libraries/Components/Switch/Switch.js
+++ b/packages/react-native/Libraries/Components/Switch/Switch.js
@@ -14,6 +14,7 @@ import type {NativeSyntheticEvent} from '../../Types/CoreEventTypes';
import type {AccessibilityState} from '../View/ViewAccessibility';
import type {ViewProps} from '../View/ViewPropTypes';
+import resolveAriaProps from '../../../src/private/components/view/resolveAriaProps';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import useMergeRefs from '../../Utilities/useMergeRefs';
@@ -186,7 +187,7 @@ const Switch: component(
trackColor,
value,
...restProps
- } = props;
+ } = resolveAriaProps(props);
const trackColorForFalse = trackColor?.false;
const trackColorForTrue = trackColor?.true;
diff --git a/packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js b/packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js
index a97753e4b268..cd1c5fce3242 100644
--- a/packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js
+++ b/packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js
@@ -87,4 +87,46 @@ describe('Switch', () => {
expect(onValueChange).toHaveBeenCalledWith(true);
expect(onChange).toHaveBeenCalled();
});
+
+ describe('web-style accessibility props', () => {
+ it('maps aria-label to accessibilityLabel', () => {
+ const root = render();
+
+ expect(
+ root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
+ ).toEqual();
+ });
+
+ it('gives aria-label precedence over accessibilityLabel', () => {
+ const root = render(
+ ,
+ );
+
+ expect(
+ root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
+ ).toEqual();
+ });
+
+ it('maps aria-disabled to accessibilityState', () => {
+ const root = render();
+
+ expect(
+ root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
+ ).toEqual(
+ ,
+ );
+ });
+
+ it('maps id to nativeID', () => {
+ const root = render();
+
+ expect(root.getRenderedOutput({props: ['nativeID']}).toJSX()).toEqual(
+ ,
+ );
+ });
+ });
});
diff --git a/packages/react-native/src/private/components/view/resolveAriaProps.js b/packages/react-native/src/private/components/view/resolveAriaProps.js
new file mode 100644
index 000000000000..69295416cda2
--- /dev/null
+++ b/packages/react-native/src/private/components/view/resolveAriaProps.js
@@ -0,0 +1,108 @@
+/**
+ * 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 {ViewProps} from '../../../../Libraries/Components/View/ViewPropTypes';
+
+/**
+ * Converts the web-style accessibility props (`aria-*`, `id`, `tabIndex`) to
+ * the props the native side reads, the same way `View` does. For components
+ * that render a native component directly instead of a `View`: without it,
+ * the native side drops these props.
+ */
+export default function resolveAriaProps(
+ props: T,
+): T {
+ const {
+ accessibilityState,
+ accessibilityValue,
+ 'aria-busy': ariaBusy,
+ 'aria-checked': ariaChecked,
+ 'aria-disabled': ariaDisabled,
+ 'aria-expanded': ariaExpanded,
+ 'aria-hidden': ariaHidden,
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledBy,
+ 'aria-live': ariaLive,
+ 'aria-selected': ariaSelected,
+ 'aria-valuemax': ariaValueMax,
+ 'aria-valuemin': ariaValueMin,
+ 'aria-valuenow': ariaValueNow,
+ 'aria-valuetext': ariaValueText,
+ id,
+ tabIndex,
+ ...otherProps
+ } = props;
+
+ const resolvedProps = otherProps as {...ViewProps, ...};
+
+ const parsedAriaLabelledBy = ariaLabelledBy?.split(/\s*,\s*/g);
+ if (parsedAriaLabelledBy !== undefined) {
+ resolvedProps.accessibilityLabelledBy = parsedAriaLabelledBy;
+ }
+
+ if (ariaLabel !== undefined) {
+ resolvedProps.accessibilityLabel = ariaLabel;
+ }
+
+ if (ariaLive !== undefined) {
+ resolvedProps.accessibilityLiveRegion =
+ ariaLive === 'off' ? 'none' : ariaLive;
+ }
+
+ if (ariaHidden !== undefined) {
+ resolvedProps.accessibilityElementsHidden = ariaHidden;
+ if (ariaHidden === true) {
+ resolvedProps.importantForAccessibility = 'no-hide-descendants';
+ }
+ }
+
+ if (id !== undefined) {
+ resolvedProps.nativeID = id;
+ }
+
+ if (tabIndex !== undefined) {
+ resolvedProps.focusable = !tabIndex;
+ }
+
+ if (
+ accessibilityState != null ||
+ ariaBusy != null ||
+ ariaChecked != null ||
+ ariaDisabled != null ||
+ ariaExpanded != null ||
+ ariaSelected != null
+ ) {
+ resolvedProps.accessibilityState = {
+ busy: ariaBusy ?? accessibilityState?.busy,
+ checked: ariaChecked ?? accessibilityState?.checked,
+ disabled: ariaDisabled ?? accessibilityState?.disabled,
+ expanded: ariaExpanded ?? accessibilityState?.expanded,
+ selected: ariaSelected ?? accessibilityState?.selected,
+ };
+ }
+
+ if (
+ accessibilityValue != null ||
+ ariaValueMax != null ||
+ ariaValueMin != null ||
+ ariaValueNow != null ||
+ ariaValueText != null
+ ) {
+ resolvedProps.accessibilityValue = {
+ max: ariaValueMax ?? accessibilityValue?.max,
+ min: ariaValueMin ?? accessibilityValue?.min,
+ now: ariaValueNow ?? accessibilityValue?.now,
+ text: ariaValueText ?? accessibilityValue?.text,
+ };
+ }
+
+ // $FlowFixMe[incompatible-type] The aria-* props are replaced, the rest of T is kept
+ return resolvedProps;
+}