Skip to content

Commit ba82eaf

Browse files
javachemeta-codesync[bot]
authored andcommitted
Delegate touchable accessibility and forward View props
Summary: `Pressable`, `TouchableHighlight`, `TouchableOpacity` and `TouchableNativeFeedback` delegate accessibility aliases to `View` instead of reconstructing optional accessibility props. Forward unconsumed Touchable props through `restProps`, preserving touch-specific defaults and existing child props. Use the resolved Pressability disabled state so accessibility and press handling agree, including inherited `aria-disabled` and `accessibilityState.disabled` values. Keep compatibility mappings for non-View native-feedback children. Changelog: [General][Fixed] - Forward accessibility aliases and View props consistently through Pressable and Touchable components. Differential Revision: D126907218
1 parent 2cdc498 commit ba82eaf

8 files changed

Lines changed: 524 additions & 263 deletions

File tree

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

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -203,16 +203,8 @@ function Pressable({
203203
}): React.Node {
204204
const {
205205
accessible,
206-
accessibilityState,
207-
'aria-live': ariaLive,
208206
android_disableSound,
209207
android_ripple,
210-
'aria-busy': ariaBusy,
211-
'aria-checked': ariaChecked,
212-
'aria-disabled': ariaDisabled,
213-
'aria-expanded': ariaExpanded,
214-
'aria-label': ariaLabel,
215-
'aria-selected': ariaSelected,
216208
blockNativeResponder,
217209
cancelable,
218210
children,
@@ -248,46 +240,13 @@ function Pressable({
248240
const shouldUpdatePressed =
249241
typeof children === 'function' || typeof style === 'function';
250242

251-
let _accessibilityState = {
252-
busy: ariaBusy ?? accessibilityState?.busy,
253-
checked: ariaChecked ?? accessibilityState?.checked,
254-
disabled: ariaDisabled ?? accessibilityState?.disabled,
255-
expanded: ariaExpanded ?? accessibilityState?.expanded,
256-
selected: ariaSelected ?? accessibilityState?.selected,
257-
};
258-
259-
_accessibilityState =
260-
disabled != null ? {..._accessibilityState, disabled} : _accessibilityState;
261-
262-
const accessibilityValue = {
263-
max: props['aria-valuemax'] ?? props.accessibilityValue?.max,
264-
min: props['aria-valuemin'] ?? props.accessibilityValue?.min,
265-
now: props['aria-valuenow'] ?? props.accessibilityValue?.now,
266-
text: props['aria-valuetext'] ?? props.accessibilityValue?.text,
267-
};
268-
269-
const accessibilityLiveRegion =
270-
ariaLive === 'off' ? 'none' : (ariaLive ?? props.accessibilityLiveRegion);
243+
const accessibilityStateDisabled =
244+
restProps['aria-disabled'] ?? restProps.accessibilityState?.disabled;
271245

272-
const accessibilityLabel = ariaLabel ?? props.accessibilityLabel;
273-
const restPropsWithDefaults: React.ComponentProps<typeof View> = {
274-
...restProps,
275-
...android_rippleConfig?.viewProps,
276-
accessible: accessible !== false,
277-
accessibilityViewIsModal:
278-
restProps['aria-modal'] ?? restProps.accessibilityViewIsModal,
279-
accessibilityLiveRegion,
280-
accessibilityLabel,
281-
accessibilityState: _accessibilityState,
282-
focusable: focusable !== false,
283-
accessibilityValue,
284-
hitSlop,
285-
};
286-
287-
const config = useMemo(
246+
const pressabilityConfig = useMemo(
288247
() => ({
289248
cancelable,
290-
disabled,
249+
disabled: disabled ?? accessibilityStateDisabled,
291250
hitSlop,
292251
pressRectOffset: pressRetentionOffset,
293252
android_disableSound,
@@ -328,6 +287,7 @@ function Pressable({
328287
blockNativeResponder,
329288
}),
330289
[
290+
accessibilityStateDisabled,
331291
android_disableSound,
332292
android_rippleConfig,
333293
blockNativeResponder,
@@ -353,7 +313,20 @@ function Pressable({
353313
],
354314
);
355315

356-
const eventHandlers = usePressability(config);
316+
const eventHandlers = usePressability(pressabilityConfig);
317+
318+
const restPropsWithDefaults: React.ComponentProps<typeof View> = {
319+
...restProps,
320+
...android_rippleConfig?.viewProps,
321+
accessible: accessible !== false,
322+
focusable: focusable !== false,
323+
hitSlop,
324+
};
325+
326+
if (pressabilityConfig.disabled != null) {
327+
// Expose Pressability's resolved disabled state to accessibility through View.
328+
restPropsWithDefaults['aria-disabled'] = pressabilityConfig.disabled;
329+
}
357330

358331
return (
359332
<View

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

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,41 @@ import {PlatformColor, Text} from 'react-native';
2424

2525
describe('<Pressable>', () => {
2626
describe('props', () => {
27+
it('lets View resolve accessibility aliases and their precedence', () => {
28+
const root = Fantom.createRoot();
29+
30+
Fantom.runTask(() => {
31+
root.render(
32+
<Pressable
33+
aria-label="ARIA label"
34+
accessibilityLabel="Legacy label"
35+
aria-busy={false}
36+
aria-checked={false}
37+
aria-expanded={false}
38+
aria-selected={false}
39+
accessibilityState={{
40+
busy: true,
41+
checked: true,
42+
expanded: true,
43+
selected: true,
44+
}}
45+
/>,
46+
);
47+
});
48+
49+
expect(
50+
root
51+
.getRenderedOutput({
52+
props: ['accessibilityLabel', 'accessibilityState'],
53+
})
54+
.toJSONObject().props,
55+
).toEqual({
56+
accessibilityLabel: 'ARIA label',
57+
accessibilityState:
58+
'{disabled:false,selected:false,checked:Unchecked,busy:false,expanded:false}',
59+
});
60+
});
61+
2762
describe('style', () => {
2863
it('can be set with ViewStyle', () => {
2964
const root = Fantom.createRoot();
@@ -46,7 +81,6 @@ describe('<Pressable>', () => {
4681
expect(root.getRenderedOutput().toJSX()).toEqual(
4782
<rn-view
4883
accessible="true"
49-
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
5084
backgroundColor="rgba(0, 0, 255, 1)"
5185
borderWidth="3"
5286
height="50"
@@ -70,7 +104,6 @@ describe('<Pressable>', () => {
70104
expect(root.getRenderedOutput().toJSX()).toEqual(
71105
<rn-view
72106
accessible="true"
73-
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
74107
backgroundColor="rgba(128, 128, 128, 1)"
75108
/>,
76109
);
@@ -101,6 +134,43 @@ describe('<Pressable>', () => {
101134
});
102135
});
103136

137+
it('uses inherited disabled state for presses and lets disabled override it', () => {
138+
const root = Fantom.createRoot();
139+
const elementRef = createRef<HostInstance>();
140+
const onPress = jest.fn();
141+
142+
for (const props of [
143+
{'aria-disabled': true},
144+
{accessibilityState: {disabled: true}},
145+
]) {
146+
Fantom.runTask(() => {
147+
root.render(
148+
<Pressable {...props} ref={elementRef} onPress={onPress} />,
149+
);
150+
});
151+
Fantom.dispatchNativeEvent(nullthrows(elementRef.current), 'click');
152+
expect(onPress).not.toHaveBeenCalled();
153+
}
154+
155+
Fantom.runTask(() => {
156+
root.render(
157+
<Pressable
158+
ref={elementRef}
159+
onPress={onPress}
160+
disabled={false}
161+
aria-disabled={true}
162+
/>,
163+
);
164+
});
165+
Fantom.dispatchNativeEvent(nullthrows(elementRef.current), 'click');
166+
expect(onPress).toHaveBeenCalledTimes(1);
167+
expect(
168+
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
169+
).toEqual(
170+
<rn-view accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}" />,
171+
);
172+
});
173+
104174
describe('disabled', () => {
105175
it('cannot be pressed', () => {
106176
const elementRef = createRef<HostInstance>();
@@ -199,10 +269,7 @@ describe('<Pressable>', () => {
199269
});
200270

201271
expect(root.getRenderedOutput().toJSX()).toEqual(
202-
<rn-view
203-
accessible="true"
204-
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
205-
/>,
272+
<rn-view accessible="true" />,
206273
);
207274
});
208275

@@ -221,10 +288,7 @@ describe('<Pressable>', () => {
221288
});
222289

223290
expect(root.getRenderedOutput().toJSX()).toEqual(
224-
<rn-view
225-
accessible="true"
226-
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
227-
/>,
291+
<rn-view accessible="true" />,
228292
);
229293
});
230294

@@ -243,10 +307,7 @@ describe('<Pressable>', () => {
243307
});
244308

245309
expect(root.getRenderedOutput().toJSX()).toEqual(
246-
<rn-view
247-
accessible="true"
248-
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
249-
/>,
310+
<rn-view accessible="true" />,
250311
);
251312
});
252313
});
@@ -268,9 +329,7 @@ describe('<Pressable>', () => {
268329
expect(element.childNodes.length).toBe(1);
269330

270331
expect(root.getRenderedOutput().toJSX()).toEqual(
271-
<rn-view
272-
accessible="true"
273-
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}">
332+
<rn-view accessible="true">
274333
<rn-paragraph
275334
allowFontScaling="true"
276335
ellipsizeMode="tail"

‎packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js‎

Lines changed: 42 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import type {HostInstance} from '../../../src/private/types/HostInstance';
1212
import type {ColorValue} from '../../StyleSheet/StyleSheet';
13-
import type {AccessibilityState} from '../View/ViewAccessibility';
1413
import type {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
1514

1615
import View from '../../Components/View/View';
@@ -98,9 +97,9 @@ class TouchableHighlightImpl extends React.Component<
9897
return {
9998
cancelable: !this.props.rejectResponderTermination,
10099
disabled:
101-
this.props.disabled != null
102-
? this.props.disabled
103-
: this.props.accessibilityState?.disabled,
100+
this.props.disabled ??
101+
this.props['aria-disabled'] ??
102+
this.props.accessibilityState?.disabled,
104103
hitSlop: this.props.hitSlop,
105104
delayLongPress: this.props.delayLongPress,
106105
delayPressIn: this.props.delayPressIn,
@@ -222,70 +221,50 @@ class TouchableHighlightImpl extends React.Component<
222221
const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} =
223222
this.state.pressability.getEventHandlers();
224223

225-
const accessibilityState: ?AccessibilityState =
226-
this.props.disabled != null
227-
? {
228-
...this.props.accessibilityState,
229-
disabled: this.props.disabled,
230-
}
231-
: this.props.accessibilityState;
232-
233-
const accessibilityValue = {
234-
max: this.props['aria-valuemax'] ?? this.props.accessibilityValue?.max,
235-
min: this.props['aria-valuemin'] ?? this.props.accessibilityValue?.min,
236-
now: this.props['aria-valuenow'] ?? this.props.accessibilityValue?.now,
237-
text: this.props['aria-valuetext'] ?? this.props.accessibilityValue?.text,
238-
};
239-
240-
const accessibilityLiveRegion =
241-
this.props['aria-live'] === 'off'
242-
? 'none'
243-
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
224+
const {
225+
accessible,
226+
activeOpacity,
227+
children,
228+
delayLongPress,
229+
delayPressIn,
230+
delayPressOut,
231+
disabled,
232+
focusable,
233+
hostRef,
234+
onBlur: _onBlur,
235+
onFocus: _onFocus,
236+
onHideUnderlay,
237+
onLongPress,
238+
onPress,
239+
onPressIn,
240+
onPressOut,
241+
onShowUnderlay,
242+
pressRetentionOffset,
243+
rejectResponderTermination,
244+
style,
245+
testOnly_pressed,
246+
touchSoundDisabled,
247+
underlayColor,
248+
...restProps
249+
} = this.props;
250+
251+
const pressabilityConfig = this._createPressabilityConfig();
252+
if (pressabilityConfig.disabled != null) {
253+
// Expose Pressability's resolved disabled state to accessibility through View.
254+
restProps['aria-disabled'] = pressabilityConfig.disabled;
255+
}
244256

245-
const accessibilityLabel =
246-
this.props['aria-label'] ?? this.props.accessibilityLabel;
247257
return (
248258
<View
249-
accessible={this.props.accessible !== false}
250-
accessibilityLabel={accessibilityLabel}
251-
accessibilityHint={this.props.accessibilityHint}
252-
accessibilityLanguage={this.props.accessibilityLanguage}
253-
accessibilityRole={this.props.accessibilityRole}
254-
accessibilityState={accessibilityState}
255-
accessibilityValue={accessibilityValue}
256-
accessibilityActions={this.props.accessibilityActions}
257-
onAccessibilityAction={this.props.onAccessibilityAction}
258-
importantForAccessibility={
259-
this.props['aria-hidden'] === true
260-
? 'no-hide-descendants'
261-
: this.props.importantForAccessibility
262-
}
263-
accessibilityViewIsModal={
264-
this.props['aria-modal'] ?? this.props.accessibilityViewIsModal
265-
}
266-
accessibilityLiveRegion={accessibilityLiveRegion}
267-
accessibilityElementsHidden={
268-
this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden
269-
}
270-
style={StyleSheet.compose(
271-
this.props.style,
272-
this.state.extraStyles?.underlay,
273-
)}
274-
onLayout={this.props.onLayout}
275-
hitSlop={this.props.hitSlop}
276-
nextFocusDown={this.props.nextFocusDown}
277-
nextFocusForward={this.props.nextFocusForward}
278-
nextFocusLeft={this.props.nextFocusLeft}
279-
nextFocusRight={this.props.nextFocusRight}
280-
nextFocusUp={this.props.nextFocusUp}
259+
{...restProps}
260+
accessible={accessible !== false}
261+
style={StyleSheet.compose(style, this.state.extraStyles?.underlay)}
281262
focusable={
282-
this.props.focusable !== false &&
283-
this.props.onPress !== undefined &&
284-
!this.props.disabled
263+
focusable !== false &&
264+
onPress !== undefined &&
265+
!pressabilityConfig.disabled
285266
}
286-
nativeID={this.props.id ?? this.props.nativeID}
287-
testID={this.props.testID}
288-
ref={this.props.hostRef}
267+
ref={hostRef}
289268
{...eventHandlersWithoutBlurAndFocus}>
290269
{child.type === React.Fragment
291270
? child

0 commit comments

Comments
 (0)