From 1ca723220d81c85d5618543eb3a16c2fb6d44477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 6 Aug 2025 09:36:35 -0700 Subject: [PATCH] fix(a11y): `TextInput` `aria-label` handling (#53051) Summary: The `aria-label` prop was ignored on `TextInput` component. Which resulted in screen reader not able to read it. This PR forwards `aria-label` to `accessibilityLabel` in a manner similar to e.g. `View` and `Text` ## Changelog: [GENERAL] [FIXED] - a11y: fix `aria-label` on `TextInput` Pull Request resolved: https://github.com/facebook/react-native/pull/53051 Test Plan: Run RNTester => TextInput => Accessibility section under Accessibility Inspector or screen reader. iOS fixed: https://github.com/user-attachments/assets/68c3a2ef-7dfe-479c-97fc-cbe72108c45c iOS baseline: https://github.com/user-attachments/assets/2e8372ba-10dc-47d2-b6b1-9f664000de7d Reviewed By: andrewdacenko Differential Revision: D79635413 Pulled By: rshest fbshipit-source-id: dd2f583d67c6c6c6393e02c5fe534308e1e2f921 --- .../Components/TextInput/TextInput.js | 7 +++++- .../TextInput/__tests__/TextInput-test.js | 1 + .../Modal/RCTModalHostViewComponentView.mm | 22 +++++++++++++++++++ .../TextInput/TextInputExample.android.js | 13 +++++++++++ .../TextInput/TextInputExample.ios.js | 15 +++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 8bd86ccc2f0..1e95eb6d44a 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -618,6 +618,9 @@ function InternalTextInput(props: TextInputProps): React.Node { // so omitting onBlur and onFocus pressability handlers here. const {onBlur, onFocus, ...eventHandlers} = usePressability(config); + const _accessibilityLabel = + props?.['aria-label'] ?? props?.accessibilityLabel; + let _accessibilityState; if ( accessibilityState != null || @@ -681,6 +684,7 @@ function InternalTextInput(props: TextInputProps): React.Node { {...otherProps} {...eventHandlers} acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes} + accessibilityLabel={_accessibilityLabel} accessibilityState={_accessibilityState} accessible={accessible} submitBehavior={submitBehavior} @@ -744,8 +748,9 @@ function InternalTextInput(props: TextInputProps): React.Node { {...otherProps} {...colorProps} {...eventHandlers} - accessibilityState={_accessibilityState} + accessibilityLabel={_accessibilityLabel} accessibilityLabelledBy={_accessibilityLabelledBy} + accessibilityState={_accessibilityState} accessible={accessible} acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes} autoCapitalize={autoCapitalize} diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js index bf63d695721..a33757e1e15 100644 --- a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-test.js @@ -432,6 +432,7 @@ jest.unmock('../TextInput'); expect(instance.toJSON()).toMatchInlineSnapshot(` +@property (nonatomic, weak) UIView *accessibilityFocusedView; + @end @implementation RCTModalHostViewComponentView { @@ -148,6 +150,7 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct( { BOOL shouldBePresented = !_isPresented && _shouldPresent && self.window; if (shouldBePresented) { + [self saveAccessibilityFocusedView]; self.viewController.presentationController.delegate = self; _isPresented = YES; @@ -179,6 +182,8 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct( if (eventEmitter) { eventEmitter->onDismiss(ModalHostViewEventEmitter::OnDismiss{}); } + + [self restoreAccessibilityFocusedView]; }]; } } @@ -207,6 +212,23 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct( [self ensurePresentedOnlyIfNeeded]; } +- (void)saveAccessibilityFocusedView +{ + id focusedElement = UIAccessibilityFocusedElement(nil); + if (focusedElement && [focusedElement isKindOfClass:[UIView class]]) { + self.accessibilityFocusedView = (UIView *)focusedElement; + } +} + +- (void)restoreAccessibilityFocusedView +{ + id viewToFocus = self.accessibilityFocusedView; + if (viewToFocus) { + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, viewToFocus); + self.accessibilityFocusedView = nil; + } +} + #pragma mark - RCTFabricModalHostViewControllerDelegate - (void)boundsDidChange:(CGRect)newBounds diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js index 930b241647b..eda21cd69be 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js @@ -451,6 +451,19 @@ const examples: Array = [ return ; }, }, + { + title: 'Accessibility', + render: function (): React.Node { + return ( + + accessibilityLabel prop + + aria-label prop + + + ); + }, + }, ]; module.exports = ({ diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js index 62f9607de9e..0994b851ed0 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -1036,6 +1036,21 @@ const textInputExamples: Array = [ ); }, }, + { + title: 'Accessibility', + render: function (): React.Node { + return ( + + + + + + + + + ); + }, + }, ]; module.exports = ({