Skip to content

Commit ed90278

Browse files
faciledictuclaude
andcommitted
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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzPpDcGNgvkkyMFtCXhLDi
1 parent 05a2e4c commit ed90278

7 files changed

Lines changed: 199 additions & 3 deletions

File tree

‎packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import type {HostInstance} from '../../../src/private/types/HostInstance';
1414
import type {ViewProps} from '../View/ViewPropTypes';
1515

16+
import resolveAriaProps from '../../../src/private/components/view/resolveAriaProps';
1617
import StyleSheet, {type ColorValue} from '../../StyleSheet/StyleSheet';
1718
import Platform from '../../Utilities/Platform';
1819
import View from '../View/View';
@@ -142,7 +143,7 @@ const ActivityIndicator: component(
142143
animating,
143144
color,
144145
hidesWhenStopped,
145-
...restProps,
146+
...resolveAriaProps(restProps),
146147
ref: forwardedRef,
147148
style: sizeStyle,
148149
size: sizeProp,

‎packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,15 @@ describe('ActivityIndicator', () => {
5555
),
5656
).toEqual(<rn-androidProgressBar width="20" height="20" />);
5757
});
58+
59+
it('maps aria-label to accessibilityLabel', () => {
60+
const root = Fantom.createRoot();
61+
Fantom.runTask(() => {
62+
root.render(<ActivityIndicator aria-label="Loading orders" />);
63+
});
64+
65+
expect(
66+
root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
67+
).toEqual(<rn-androidProgressBar accessibilityLabel="Loading orders" />);
68+
});
5869
});

‎packages/react-native/Libraries/Components/ScrollView/ScrollView.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
VScrollContentViewNativeComponent,
3232
VScrollViewNativeComponent,
3333
} from '../../../src/private/components/scrollview/VScrollViewNativeComponents';
34+
import resolveAriaProps from '../../../src/private/components/view/resolveAriaProps';
3435
import AnimatedImplementation from '../../Animated/AnimatedImplementation';
3536
import FrameRateLogger from '../../Interaction/FrameRateLogger';
3637
import {findNodeHandle} from '../../ReactNative/RendererProxy';
@@ -1861,7 +1862,7 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
18611862
const {experimental_endDraggingSensitivityMultiplier, ...otherProps} =
18621863
this.props;
18631864
const props = {
1864-
...otherProps,
1865+
...resolveAriaProps(otherProps),
18651866
alwaysBounceHorizontal,
18661867
alwaysBounceVertical,
18671868
style: StyleSheet.compose(baseStyle, this.props.style),

‎packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,38 @@ describe('<ScrollView>', () => {
7171
});
7272
});
7373

74+
describe('web-style accessibility props', () => {
75+
it('maps aria-label to accessibilityLabel', () => {
76+
const root = Fantom.createRoot();
77+
78+
Fantom.runTask(() => {
79+
root.render(<ScrollView aria-label="Photos" />);
80+
});
81+
82+
expect(
83+
root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
84+
).toEqual(
85+
<rn-scrollView accessibilityLabel="Photos">
86+
<rn-view />
87+
</rn-scrollView>,
88+
);
89+
});
90+
91+
it('maps id to nativeID', () => {
92+
const root = Fantom.createRoot();
93+
94+
Fantom.runTask(() => {
95+
root.render(<ScrollView id="feed" />);
96+
});
97+
98+
expect(root.getRenderedOutput({props: ['nativeID']}).toJSX()).toEqual(
99+
<rn-scrollView nativeID="feed">
100+
<rn-view />
101+
</rn-scrollView>,
102+
);
103+
});
104+
});
105+
74106
describe('ref', () => {
75107
it('receives an instance or null', () => {
76108
const root = Fantom.createRoot();

‎packages/react-native/Libraries/Components/Switch/Switch.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {NativeSyntheticEvent} from '../../Types/CoreEventTypes';
1414
import type {AccessibilityState} from '../View/ViewAccessibility';
1515
import type {ViewProps} from '../View/ViewPropTypes';
1616

17+
import resolveAriaProps from '../../../src/private/components/view/resolveAriaProps';
1718
import StyleSheet from '../../StyleSheet/StyleSheet';
1819
import Platform from '../../Utilities/Platform';
1920
import useMergeRefs from '../../Utilities/useMergeRefs';
@@ -186,7 +187,7 @@ const Switch: component(
186187
trackColor,
187188
value,
188189
...restProps
189-
} = props;
190+
} = resolveAriaProps(props);
190191
const trackColorForFalse = trackColor?.false;
191192
const trackColorForTrue = trackColor?.true;
192193

‎packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,46 @@ describe('Switch', () => {
8787
expect(onValueChange).toHaveBeenCalledWith(true);
8888
expect(onChange).toHaveBeenCalled();
8989
});
90+
91+
describe('web-style accessibility props', () => {
92+
it('maps aria-label to accessibilityLabel', () => {
93+
const root = render(<Switch value={false} aria-label="Wi-Fi" />);
94+
95+
expect(
96+
root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
97+
).toEqual(<rn-androidSwitch accessibilityLabel="Wi-Fi" />);
98+
});
99+
100+
it('gives aria-label precedence over accessibilityLabel', () => {
101+
const root = render(
102+
<Switch
103+
value={false}
104+
aria-label="aria value"
105+
accessibilityLabel="native value"
106+
/>,
107+
);
108+
109+
expect(
110+
root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
111+
).toEqual(<rn-androidSwitch accessibilityLabel="aria value" />);
112+
});
113+
114+
it('maps aria-disabled to accessibilityState', () => {
115+
const root = render(<Switch value={false} aria-disabled={true} />);
116+
117+
expect(
118+
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
119+
).toEqual(
120+
<rn-androidSwitch accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
121+
);
122+
});
123+
124+
it('maps id to nativeID', () => {
125+
const root = render(<Switch value={false} id="wifi" />);
126+
127+
expect(root.getRenderedOutput({props: ['nativeID']}).toJSX()).toEqual(
128+
<rn-androidSwitch nativeID="wifi" />,
129+
);
130+
});
131+
});
90132
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {ViewProps} from '../../../../Libraries/Components/View/ViewPropTypes';
12+
13+
/**
14+
* Converts the web-style accessibility props (`aria-*`, `id`, `tabIndex`) to
15+
* the props the native side reads, the same way `View` does. For components
16+
* that render a native component directly instead of a `View`: without it,
17+
* the native side drops these props.
18+
*/
19+
export default function resolveAriaProps<T extends {...ViewProps, ...}>(
20+
props: T,
21+
): T {
22+
const {
23+
accessibilityState,
24+
accessibilityValue,
25+
'aria-busy': ariaBusy,
26+
'aria-checked': ariaChecked,
27+
'aria-disabled': ariaDisabled,
28+
'aria-expanded': ariaExpanded,
29+
'aria-hidden': ariaHidden,
30+
'aria-label': ariaLabel,
31+
'aria-labelledby': ariaLabelledBy,
32+
'aria-live': ariaLive,
33+
'aria-selected': ariaSelected,
34+
'aria-valuemax': ariaValueMax,
35+
'aria-valuemin': ariaValueMin,
36+
'aria-valuenow': ariaValueNow,
37+
'aria-valuetext': ariaValueText,
38+
id,
39+
tabIndex,
40+
...otherProps
41+
} = props;
42+
43+
const resolvedProps = otherProps as {...ViewProps, ...};
44+
45+
const parsedAriaLabelledBy = ariaLabelledBy?.split(/\s*,\s*/g);
46+
if (parsedAriaLabelledBy !== undefined) {
47+
resolvedProps.accessibilityLabelledBy = parsedAriaLabelledBy;
48+
}
49+
50+
if (ariaLabel !== undefined) {
51+
resolvedProps.accessibilityLabel = ariaLabel;
52+
}
53+
54+
if (ariaLive !== undefined) {
55+
resolvedProps.accessibilityLiveRegion =
56+
ariaLive === 'off' ? 'none' : ariaLive;
57+
}
58+
59+
if (ariaHidden !== undefined) {
60+
resolvedProps.accessibilityElementsHidden = ariaHidden;
61+
if (ariaHidden === true) {
62+
resolvedProps.importantForAccessibility = 'no-hide-descendants';
63+
}
64+
}
65+
66+
if (id !== undefined) {
67+
resolvedProps.nativeID = id;
68+
}
69+
70+
if (tabIndex !== undefined) {
71+
resolvedProps.focusable = !tabIndex;
72+
}
73+
74+
if (
75+
accessibilityState != null ||
76+
ariaBusy != null ||
77+
ariaChecked != null ||
78+
ariaDisabled != null ||
79+
ariaExpanded != null ||
80+
ariaSelected != null
81+
) {
82+
resolvedProps.accessibilityState = {
83+
busy: ariaBusy ?? accessibilityState?.busy,
84+
checked: ariaChecked ?? accessibilityState?.checked,
85+
disabled: ariaDisabled ?? accessibilityState?.disabled,
86+
expanded: ariaExpanded ?? accessibilityState?.expanded,
87+
selected: ariaSelected ?? accessibilityState?.selected,
88+
};
89+
}
90+
91+
if (
92+
accessibilityValue != null ||
93+
ariaValueMax != null ||
94+
ariaValueMin != null ||
95+
ariaValueNow != null ||
96+
ariaValueText != null
97+
) {
98+
resolvedProps.accessibilityValue = {
99+
max: ariaValueMax ?? accessibilityValue?.max,
100+
min: ariaValueMin ?? accessibilityValue?.min,
101+
now: ariaValueNow ?? accessibilityValue?.now,
102+
text: ariaValueText ?? accessibilityValue?.text,
103+
};
104+
}
105+
106+
// $FlowFixMe[incompatible-type] The aria-* props are replaced, the rest of T is kept
107+
return resolvedProps;
108+
}

0 commit comments

Comments
 (0)