Skip to content

Commit 0f39fc3

Browse files
mdjastrzebskifacebook-github-bot
authored andcommitted
fix(a11y): aria-hidden support for Text, non-editable TextInput and Image (#53364)
Summary: Fixes #53350 This PR adds support for missing `aria-hidden` prop handling on: - `Text` - non-editable `TextInput` - `Image` The changes are pretty simple and analogous to `View` logic: - iOS: setting `accessibilityElementsHidden`, `accessible` (for `Image`) - Android: setting `importantForAccessibility="no-hide-descendents" Note: [according to MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden) `aria-hidden` should not be used on focusable elements, which excludes editable `TextInput` ## Changelog: [GENERAL] [FIXED] `aria-hidden` support for `Text`, non-editable `TextInput` and `Image` <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests Pull Request resolved: #53364 Test Plan: Added new section to RN Tester (APIs => Accessibility => aria-hidden ### After (iOS/Android) https://github.com/user-attachments/assets/c62f8beb-7cb1-4919-833d-3fb906309cac https://github.com/user-attachments/assets/78ca5e28-a858-4fd6-ac1c-5ec87872f3fc ### Before (iOS/Android) https://github.com/user-attachments/assets/84560373-4b31-4793-8997-ee14daa77990 https://github.com/user-attachments/assets/b20074c9-f021-4a90-bce5-75e440a4bbc3 Reviewed By: rshest Differential Revision: D81043503 Pulled By: javache fbshipit-source-id: 26b2660a75afcdedba07bee980d8c7f154087ae2
1 parent 2246e2b commit 0f39fc3

10 files changed

Lines changed: 180 additions & 3 deletions

File tree

packages/react-native/Libraries/Components/TextInput/TextInput.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,9 @@ function InternalTextInput(props: TextInputProps): React.Node {
677677
flattenedStyle.paddingVertical == null &&
678678
flattenedStyle.paddingTop == null));
679679

680+
const _accessibilityElementsHidden =
681+
props['aria-hidden'] ?? props.accessibilityElementsHidden;
682+
680683
textInput = (
681684
<RCTTextInputView
682685
// Figure out imperative + forward refs.
@@ -686,6 +689,7 @@ function InternalTextInput(props: TextInputProps): React.Node {
686689
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
687690
accessibilityLabel={_accessibilityLabel}
688691
accessibilityState={_accessibilityState}
692+
accessibilityElementsHidden={_accessibilityElementsHidden}
689693
accessible={accessible}
690694
submitBehavior={submitBehavior}
691695
caretHidden={caretHidden}
@@ -714,6 +718,10 @@ function InternalTextInput(props: TextInputProps): React.Node {
714718
const autoCapitalize = props.autoCapitalize || 'sentences';
715719
const _accessibilityLabelledBy =
716720
props?.['aria-labelledby'] ?? props?.accessibilityLabelledBy;
721+
const _importantForAccessibility =
722+
props['aria-hidden'] === true
723+
? ('no-hide-descendants' as const)
724+
: undefined;
717725
const placeholder = props.placeholder ?? '';
718726
let children = props.children;
719727
const childCount = React.Children.count(children);
@@ -759,6 +767,7 @@ function InternalTextInput(props: TextInputProps): React.Node {
759767
children={children}
760768
disableFullscreenUI={props.disableFullscreenUI}
761769
focusable={tabIndex !== undefined ? !tabIndex : focusable}
770+
importantForAccessibility={_importantForAccessibility}
762771
mostRecentEventCount={mostRecentEventCount}
763772
nativeID={id ?? props.nativeID}
764773
numberOfLines={props.rows ?? props.numberOfLines}

packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ jest.unmock('../TextInput');
432432

433433
expect(instance.toJSON()).toMatchInlineSnapshot(`
434434
<RCTSinglelineTextInputView
435+
accessibilityElementsHidden={true}
435436
accessibilityLabel="label"
436437
accessibilityState={
437438
Object {

packages/react-native/Libraries/Image/Image.android.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInImage()) {
148148
'aria-checked': ariaChecked,
149149
'aria-disabled': ariaDisabled,
150150
'aria-expanded': ariaExpanded,
151+
'aria-hidden': ariaHidden,
151152
'aria-label': ariaLabel,
152153
'aria-selected': ariaSelected,
153154
accessibilityLabel,
@@ -302,6 +303,10 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInImage()) {
302303
};
303304
}
304305

306+
if (ariaHidden === true) {
307+
nativeProps.importantForAccessibility = 'no-hide-descendants';
308+
}
309+
305310
const flattenedStyle_ = flattenStyle<ImageStyleProp>(style);
306311
const objectFit_ = convertObjectFitToResizeMode(flattenedStyle_?.objectFit);
307312
const resizeMode_ =
@@ -406,6 +411,10 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInImage()) {
406411
expanded: props['aria-expanded'] ?? props.accessibilityState?.expanded,
407412
selected: props['aria-selected'] ?? props.accessibilityState?.selected,
408413
},
414+
importantForAccessibility:
415+
props['aria-hidden'] === true
416+
? ('no-hide-descendants' as const)
417+
: props.importantForAccessibility,
409418
};
410419

411420
const flattenedStyle = flattenStyle<ImageStyleProp>(style);

packages/react-native/Libraries/Image/Image.ios.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ let BaseImage: AbstractImageIOS = ({
153153
'aria-disabled': ariaDisabled,
154154
'aria-expanded': ariaExpanded,
155155
'aria-selected': ariaSelected,
156+
'aria-hidden': ariaHidden,
156157
src,
157158
...restProps
158159
} = props;
@@ -164,6 +165,10 @@ let BaseImage: AbstractImageIOS = ({
164165
expanded: ariaExpanded ?? props.accessibilityState?.expanded,
165166
selected: ariaSelected ?? props.accessibilityState?.selected,
166167
};
168+
169+
// In order for `aria-hidden` to work on iOS we must set `accessible` to false (`accessibilityElementsHidden` is not enough).
170+
const accessible =
171+
ariaHidden !== true && (props.alt !== undefined ? true : props.accessible);
167172
const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel;
168173

169174
const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef);
@@ -175,7 +180,7 @@ let BaseImage: AbstractImageIOS = ({
175180
<ImageViewNativeComponent
176181
accessibilityState={_accessibilityState}
177182
{...restProps}
178-
accessible={props.alt !== undefined ? true : props.accessible}
183+
accessible={accessible}
179184
accessibilityLabel={accessibilityLabel ?? props.alt}
180185
ref={actualRef}
181186
style={style}

packages/react-native/Libraries/Image/__tests__/Image-itest.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,20 @@ describe('<Image>', () => {
597597
});
598598
});
599599

600+
describe('aria-hidden', () => {
601+
it('is is passed as importantForAccessibility', () => {
602+
const root = Fantom.createRoot();
603+
Fantom.runTask(() => {
604+
root.render(<Image aria-hidden={true} />);
605+
});
606+
expect(
607+
root
608+
.getRenderedOutput({props: ['importantForAccessibility']})
609+
.toJSX(),
610+
).toEqual(<rn-image importantForAccessibility="no-hide-descendants" />);
611+
});
612+
});
613+
600614
component TestComponent(testID?: ?string, ...props: AccessibilityProps) {
601615
return <Image {...props} testID={testID} source={LOGO_SOURCE} />;
602616
}

packages/react-native/Libraries/Text/Text.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
5151
'aria-checked': ariaChecked,
5252
'aria-disabled': ariaDisabled,
5353
'aria-expanded': ariaExpanded,
54+
'aria-hidden': ariaHidden,
5455
'aria-label': ariaLabel,
5556
'aria-selected': ariaSelected,
5657
children,
@@ -126,6 +127,13 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
126127
_accessibilityState.disabled = _disabled;
127128
}
128129

130+
if (ariaHidden !== undefined) {
131+
processedProps.accessibilityElementsHidden = ariaHidden;
132+
if (ariaHidden === true) {
133+
processedProps.importantForAccessibility = 'no-hide-descendants';
134+
}
135+
}
136+
129137
const _accessible = Platform.select({
130138
ios: accessible !== false,
131139
android:
@@ -306,13 +314,16 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
306314
) = ({
307315
ref: forwardedRef,
308316
accessible,
317+
accessibilityElementsHidden,
318+
importantForAccessibility,
309319
accessibilityLabel,
310320
accessibilityState,
311321
allowFontScaling,
312322
'aria-busy': ariaBusy,
313323
'aria-checked': ariaChecked,
314324
'aria-disabled': ariaDisabled,
315325
'aria-expanded': ariaExpanded,
326+
'aria-hidden': ariaHidden,
316327
'aria-label': ariaLabel,
317328
'aria-selected': ariaSelected,
318329
children,
@@ -374,6 +385,13 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
374385
const _accessibilityStateDisabled = _accessibilityState?.disabled;
375386
const _disabled = disabled ?? _accessibilityStateDisabled;
376387

388+
let _accessibilityElementsHidden =
389+
ariaHidden ?? accessibilityElementsHidden;
390+
let _importantForAccessibility = importantForAccessibility;
391+
if (ariaHidden === true) {
392+
_importantForAccessibility = 'no-hide-descendants';
393+
}
394+
377395
const isPressable =
378396
(onPress != null ||
379397
onLongPress != null ||
@@ -442,8 +460,10 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
442460
ref={forwardedRef}
443461
textProps={{
444462
...restProps,
463+
accessibilityElementsHidden: _accessibilityElementsHidden,
445464
accessibilityLabel: _accessibilityLabel,
446465
accessibilityState: _accessibilityState,
466+
importantForAccessibility: _importantForAccessibility,
447467
nativeID: _nativeID,
448468
numberOfLines: _numberOfLines,
449469
selectable: _selectable,
@@ -473,8 +493,10 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
473493
return (
474494
<NativeVirtualText
475495
{...restProps}
496+
accessibilityElementsHidden={_accessibilityElementsHidden}
476497
accessibilityLabel={_accessibilityLabel}
477498
accessibilityState={_accessibilityState}
499+
importantForAccessibility={_importantForAccessibility}
478500
nativeID={_nativeID}
479501
numberOfLines={_numberOfLines}
480502
ref={forwardedRef}
@@ -514,12 +536,14 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
514536
ref={forwardedRef}
515537
textProps={{
516538
...restProps,
539+
accessibilityElementsHidden: _accessibilityElementsHidden,
517540
accessibilityLabel: _accessibilityLabel,
518541
accessibilityState: _accessibilityState,
519542
accessible: _accessible,
520543
allowFontScaling: allowFontScaling !== false,
521544
disabled: _disabled,
522545
ellipsizeMode: ellipsizeMode ?? 'tail',
546+
importantForAccessibility: _importantForAccessibility,
523547
nativeID: _nativeID,
524548
numberOfLines: _numberOfLines,
525549
selectable: _selectable,
@@ -547,12 +571,14 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
547571
nativeText = (
548572
<NativeText
549573
{...restProps}
574+
accessibilityElementsHidden={_accessibilityElementsHidden}
550575
accessibilityLabel={_accessibilityLabel}
551576
accessibilityState={_accessibilityState}
552577
accessible={_accessible}
553578
allowFontScaling={allowFontScaling !== false}
554579
disabled={_disabled}
555580
ellipsizeMode={ellipsizeMode ?? 'tail'}
581+
importantForAccessibility={_importantForAccessibility}
556582
nativeID={_nativeID}
557583
numberOfLines={_numberOfLines}
558584
ref={forwardedRef}

packages/react-native/Libraries/Text/__tests__/Text-itest.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,22 @@ describe('<Text>', () => {
353353
});
354354
});
355355
});
356+
357+
describe('aria-hidden', () => {
358+
it('is is passed as importantForAccessibility', () => {
359+
const root = Fantom.createRoot();
360+
Fantom.runTask(() => {
361+
root.render(<Text aria-hidden={true} />);
362+
});
363+
expect(
364+
root
365+
.getRenderedOutput({props: ['importantForAccessibility']})
366+
.toJSX(),
367+
).toEqual(
368+
<rn-paragraph importantForAccessibility="no-hide-descendants" />,
369+
);
370+
});
371+
});
356372
});
357373

358374
describe('ref', () => {

packages/react-native/Libraries/Text/__tests__/Text-test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ describe('Text compat with web', () => {
127127

128128
expect(omitRefAndFlattenStyle(instance)).toMatchInlineSnapshot(`
129129
<RCTText
130+
accessibilityElementsHidden={true}
130131
accessibilityLabel="label"
131132
accessibilityState={
132133
Object {
@@ -152,7 +153,6 @@ describe('Text compat with web', () => {
152153
aria-errormessage="errormessage"
153154
aria-flowto="flowto"
154155
aria-haspopup={true}
155-
aria-hidden={true}
156156
aria-invalid={true}
157157
aria-keyshortcuts="Cmd+S"
158158
aria-labelledby="labelledby"
@@ -180,6 +180,7 @@ describe('Text compat with web', () => {
180180
aria-valuetext="3"
181181
disabled={true}
182182
ellipsizeMode="tail"
183+
importantForAccessibility="no-hide-descendants"
183184
role="main"
184185
/>
185186
`);

packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,6 @@ void AccessibilityProps::setProp(
309309
SharedDebugStringConvertibleList AccessibilityProps::getDebugProps() const {
310310
const auto& defaultProps = AccessibilityProps();
311311
return SharedDebugStringConvertibleList{
312-
debugStringConvertibleItem("testID", testId, defaultProps.testId),
313312
debugStringConvertibleItem(
314313
"accessibilityRole",
315314
accessibilityRole,
@@ -340,6 +339,11 @@ SharedDebugStringConvertibleList AccessibilityProps::getDebugProps() const {
340339
"accessibilityLiveRegion",
341340
accessibilityLiveRegion,
342341
defaultProps.accessibilityLiveRegion),
342+
debugStringConvertibleItem(
343+
"importantForAccessibility",
344+
importantForAccessibility,
345+
defaultProps.importantForAccessibility),
346+
debugStringConvertibleItem("testID", testId, defaultProps.testId),
343347
};
344348
}
345349
#endif // RN_DEBUG_STRING_CONVERTIBLE

0 commit comments

Comments
 (0)