Skip to content

Instantly share code, notes, and snippets.

@louy
Last active November 30, 2023 10:23
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save louy/6b66c45ae47bb4e3bac5a104dd0649ff to your computer and use it in GitHub Desktop.
Save louy/6b66c45ae47bb4e3bac5a104dd0649ff to your computer and use it in GitHub Desktop.
RN Accessibility Wrapper, a custom view that allows you to control the accessibility behaviour of a React Native component tree
/**
* @author Louay Alakkad (github.com/louy)
* @license MIT https://opensource.org/licenses/MIT
*/
import React from 'react'
import PropTypes from 'prop-types'
import {
NativeModules,
ViewProps,
ViewPropTypes,
findNodeHandle,
requireNativeComponent,
Platform,
View
} from 'react-native'
const { RNAccessibilityWrapperManager } = NativeModules
const RNAccessibilityWrapper = requireNativeComponent(
'RNAccessibilityWrapper'
) as React.ComponentClass<any>
interface AccessibilityWrapperProps extends ViewProps {
fieldsRefs?: React.RefObject<React.Component>[]
}
const AccessibilityWrapperPropTypes = {
...ViewPropTypes,
fieldsRefs: PropTypes.arrayOf(PropTypes.shape({
current: PropTypes.object
}) as PropTypes.Validator<React.RefObject<React.Component>>)
}
class AccessibilityWrapperIOS extends React.Component<
AccessibilityWrapperProps
> {
public static propTypes = AccessibilityWrapperPropTypes
private ref = React.createRef<React.Component<any>>()
public componentDidMount() {
if (this.props.fieldsRefs) {
this.setAccessibilityFields(this.props.fieldsRefs.map(ref => ref.current))
}
}
public componentDidUpdate() {
if (this.props.fieldsRefs) {
this.setAccessibilityFields(this.props.fieldsRefs.map(ref => ref.current))
}
}
private setAccessibilityFields = (
fields: (React.Component<any> | null)[]
) => {
const fieldTags =
fields && fields.map(component => component && findNodeHandle(component))
return RNAccessibilityWrapperManager.setAccessibilityFields(
findNodeHandle(this.ref.current),
fieldTags
)
}
public render() {
return <RNAccessibilityWrapper {...this.props} ref={this.ref} />
}
}
const AccessibilityWrapperAndroid: React.FunctionComponent<
AccessibilityWrapperProps
> = ({ fieldsRefs, ...props }) => <View {...props} />
AccessibilityWrapperAndroid.propTypes = AccessibilityWrapperPropTypes
/**
* The AccessibilityWrapper component allows you to adjust the behaviour of the native platform
* when it comes to accessibility. Using this component you can tell the native platform to
* group all subviews together for accessibility purposes (since it's not always done by
* default), and you can even override the focus order of subviews
*
* @example
* export default class App extends Component<{}> {
* fooRef = React.createRef<Text>();
* barRef = React.createRef<Text>();
* bazRef = React.createRef<Text>();
*
* public render() {
* return (
* <AccessibilityWrapper fieldsRefs={[
* this.barRef,
* this.fooRef,
* this.bazRef,
* ]}>
* <SafeAreaView>
* <Text ref={this.fooRef}>Foo</Text>
* <Text ref={this.barRef}>Bar</Text>
* <Text ref={this.bazRef}>Baz</Text>
* </SafeAreaView>
* </AccessibilityWrapper>
* );
* }
* }
*/
export default Platform.select<React.ComponentType<AccessibilityWrapperProps>>({
ios: AccessibilityWrapperIOS as React.ComponentType<
AccessibilityWrapperProps
>,
android: AccessibilityWrapperAndroid as React.ComponentType<
AccessibilityWrapperProps
>
})
//
// RNAccessibilityWrapper.h
//
// Created by Louay Alakkad on 10/04/2019.
// License: MIT https://opensource.org/licenses/MIT
//
#import <UIKit/UIKit.h>
#import <UIKit/UIAccessibilityContainer.h>
#import <React/RCTView.h>
@interface RNAccessibilityWrapper : RCTView
- (void) setAccessibilityFields: (NSArray *)reactTags;
@end
//
// RNAccessibilityWrapper.m
//
// Created by Louay Alakkad on 10/04/2019.
// License: MIT https://opensource.org/licenses/MIT
//
#import <Foundation/Foundation.h>
#import "RNAccessibilityWrapper.h"
#import <UIKit/UIKit.h>
@implementation RNAccessibilityWrapper
- (void) setAccessibilityFields: (NSArray *)fields
{
NSMutableArray *accessibleElements = [NSMutableArray arrayWithCapacity:[fields count]];
[fields enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop) {
UIView *field = obj;
[accessibleElements addObject:field];
}];
self.accessibilityElements = (NSArray *)accessibleElements;
}
- (bool) shouldGroupAccessibilityChildren {
return YES;
}
@end
//
// RNAccessibilityViewManager
//
// Created by Louay Alakkad on 10/04/2019.
// License: MIT https://opensource.org/licenses/MIT
//
#import <React/RCTViewManager.h>
@interface RNAccessibilityWrapperManager : RCTViewManager
@end
//
// RNAccessibilityWrapper.m
//
// Created by Louay Alakkad on 10/04/2019.
// License: MIT https://opensource.org/licenses/MIT
//
#import <Foundation/Foundation.h>
#import <React/RCTUIManager.h>
#import "RNAccessibilityWrapper.h"
#import "RNAccessibilityWrapperManager.h"
@implementation RNAccessibilityWrapperManager
RCT_EXPORT_MODULE()
- (UIView *)view {
return [[RNAccessibilityWrapper alloc] init];
}
RCT_EXPORT_METHOD(setAccessibilityFields:(nonnull NSNumber *)reactTag
fieldsReactTags: (nonnull NSArray *)fieldsReactTags) {
dispatch_async(dispatch_get_main_queue(), ^{
RNAccessibilityWrapper *component = (RNAccessibilityWrapper *)[self.bridge.uiManager viewForReactTag:reactTag];
NSMutableArray *fields = [NSMutableArray arrayWithCapacity:[fieldsReactTags count]];
[fieldsReactTags enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop) {
NSNumber *tag = (NSNumber *)obj;
UIView *field = [self.bridge.uiManager viewForReactTag:tag];
[fields addObject:field];
}];
[component setAccessibilityFields: fields];
});
}
@end
@danielrvt
Copy link

Hi @louy thanks for sharing this! I'm relatively new to react native and I'm not sure how to use these files. Do I just compy them into the project folder?

@DigitalZebra
Copy link

DigitalZebra commented Jun 12, 2022

@danielrvt the RNAccessibilityWrapperManager.m/h and RNAccessibilityWrapper.m/h files would go somewhere under <project_root>/ios/<project_name> directory, assuming you are using a "bare" RN or ejected Expo project. You'd want to boot up Xcode, opening up the <project_name>.xcworkspace file. From there you can manually add them to the project. Once added, you'll have to re-build the iOS project - either by running yarn ios (aka react-native run-ios) or building in Xcode.

The AccessibilityWrapper.tsx file would go where you prefer under <project_root>/src.

@danielrvt
Copy link

Thanks! It worked perfectly!

@AdamGerthel
Copy link

Is there an equivalent for Android as well?

@rahulpunchh
Copy link

Hello @louy
I am facing a problem with the custom order if the View renders like this in AccessbilityWrapper :
Bar
Baz
Foo

but voice over order should be like this
Bar
Foo
Baz

I am changing the order of fieldsRefs but not working as expected. It is the same as render

Please help me with this

@louy
Copy link
Author

louy commented Mar 31, 2023

Is there an equivalent for Android as well?

Android didn't have voice over controls when I did this, but things might have changed now, IDK sadly

@louy
Copy link
Author

louy commented Mar 31, 2023

@rahulpunchh I'm no longer maintaining this I'm afraid, so I can't help, but you can try asking on stackoverflow or somewhere similar

@ArturKalach
Copy link

Hi, tnx for your work man. Probably it's why I can fix bugs.
I wrote a lib, it can be usefull I guess https://www.npmjs.com/package/react-native-a11y (NewArch, Android, iOs ) supported

@DigitalZebra
Copy link

Hi, tnx for your work man. Probably it's why I can fix bugs. I wrote a lib, it can be usefull I guess https://www.npmjs.com/package/react-native-a11y (NewArch, Android, iOs ) supported

Nice @ArturKalach !! Will check this out. Focus order has long been a pain point with RN and some layouts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment