Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
458846d
chore: creating a11y eventEmitter
OtavioStasiak Jan 14, 2025
4726adb
chore: trying to create first module
OtavioStasiak Jan 15, 2025
3614da5
chore: created module and test change focus by function
OtavioStasiak Jan 15, 2025
945792c
fix: lint
OtavioStasiak Jan 15, 2025
237639b
chore: changeOrder by rcTag working on iOS
OtavioStasiak Jan 16, 2025
04d6d22
chore: created android module
OtavioStasiak Jan 18, 2025
0eb5186
chore: code organization
OtavioStasiak Jan 20, 2025
62006e3
chore: added accessible on element and container
OtavioStasiak Jan 21, 2025
c497b93
chore: updated code organization
OtavioStasiak Jan 21, 2025
5fb2165
chore: updated moduleName
OtavioStasiak Jan 21, 2025
894427b
fix: accessibility tag update flow
OtavioStasiak Jan 21, 2025
02f5f07
fix: accessibility Element focus
OtavioStasiak Jan 21, 2025
e4233b8
fix: lint and improved organization
OtavioStasiak Jan 21, 2025
b121b2b
fix: remove empty space
OtavioStasiak Jan 21, 2025
813973b
fix: remove empty space
OtavioStasiak Jan 21, 2025
9b9e4cc
fix: OTHER_LDFLAGS
OtavioStasiak Jan 21, 2025
2fae040
fix: remove empty space
OtavioStasiak Jan 21, 2025
b417c26
fix: revert empty space of appDelegate
OtavioStasiak Jan 21, 2025
fbaed77
fix: android accessibilityTextInput changes
OtavioStasiak Jan 21, 2025
230167b
Merge branch 'develop' into chore-a11y-focus-react-tag-poc
OtavioStasiak Jan 23, 2025
926f8e1
chore: changed folder
OtavioStasiak Jan 24, 2025
5b573bb
fix: android a11yElement using view
OtavioStasiak Feb 3, 2025
17dbf57
chore: improved code
OtavioStasiak Feb 12, 2025
1120e96
Merge branch 'develop' into chore-a11y-focus-react-tag-poc
diegolmello Mar 12, 2025
02aff4b
Merge branch 'develop' into chore-a11y-focus-react-tag-poc
diegolmello Mar 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/containers/A11yFlow/A11yContainer/A11yContainer.ios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { useRef } from 'react';
import { View, ViewProps } from 'react-native';

import { AccessibilityOrderProvider } from '../contexts/useAccessibilityOrder';

interface IA11yContainer extends ViewProps {}

const A11yContainer = ({ children, ...rest }: IA11yContainer) => {
const containerRef = useRef<View>(null);

return (
<AccessibilityOrderProvider containerRef={containerRef}>
<View {...rest} ref={containerRef}>
{children}
</View>
</AccessibilityOrderProvider>
);
};

export default A11yContainer;
7 changes: 7 additions & 0 deletions app/containers/A11yFlow/A11yContainer/A11yContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ViewProps } from 'react-native';

interface IA11yContainer extends ViewProps {}

const A11yContainer = ({ children }: IA11yContainer) => children;

export default A11yContainer;
32 changes: 32 additions & 0 deletions app/containers/A11yFlow/A11yElement/A11yElement.ios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { useEffect, useRef } from 'react';
import { View, ViewProps, findNodeHandle } from 'react-native';

import { useAccessibilityOrder } from '../contexts/useAccessibilityOrder';

interface IA11yElementProps extends ViewProps {
order?: number;
}

const A11yElement = ({ order, children, ...rest }: IA11yElementProps) => {
const elementRef = useRef<View>(null);
const { updateElementsList } = useAccessibilityOrder();

const handleUpdateOrder = () => {
const tag = findNodeHandle(elementRef.current);
if (!tag) return;

updateElementsList({ tag, order });
};

useEffect(() => {
handleUpdateOrder();
}, []);

return (
<View {...rest} ref={elementRef}>
{children}
</View>
);
};

export default A11yElement;
9 changes: 9 additions & 0 deletions app/containers/A11yFlow/A11yElement/A11yElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { View, ViewProps } from 'react-native';

interface IA11yElementProps extends ViewProps {
order?: number;
}

const A11yElement = ({ children, ...rest }: IA11yElementProps) => <View {...rest}>{children}</View>;

export default A11yElement;
14 changes: 14 additions & 0 deletions app/containers/A11yFlow/A11yFlowModule/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NativeModules, Platform } from 'react-native';

const { A11yFlow } = NativeModules;

const A11yFlowModule = Platform.select({
ios: {
setA11yOrder: (elements: number[], node: number) => {
if (!node || !elements) return;
A11yFlow.setA11yOrder(elements, node);
}
}
});

export default A11yFlowModule;
53 changes: 53 additions & 0 deletions app/containers/A11yFlow/contexts/useAccessibilityOrder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { findNodeHandle, View } from 'react-native';

import A11yFlowModule from '../A11yFlowModule';

interface IElement {
tag: number;
order?: number;
}
interface IAccessibilityContextData {
updateElementsList: (element: IElement) => void;
}

interface IAccessibilityOrderProviderProps {
children: React.ReactNode;
containerRef: React.RefObject<View>;
}

export const AccessibilityOrderContext = createContext({} as IAccessibilityContextData);

function AccessibilityOrderProvider({ children, containerRef }: IAccessibilityOrderProviderProps) {
const [elements, setElements] = useState<IElement[]>([]);

const sortElements = (elementsList: IElement[]) => elementsList.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));

const extractTags = (elements: IElement[]) => elements.map(item => item.tag);

const updateElementsList = (element: IElement) => {
setElements(prevState => sortElements([...prevState, element]));
};

const updateAccessibilityOrder = () => {
const parentTag = findNodeHandle(containerRef.current);

if (!parentTag) return;

A11yFlowModule?.setA11yOrder(extractTags(elements), parentTag);
};

useEffect(() => {
updateAccessibilityOrder();
}, [elements]);

return <AccessibilityOrderContext.Provider value={{ updateElementsList }}>{children}</AccessibilityOrderContext.Provider>;
}

function useAccessibilityOrder() {
const context = useContext(AccessibilityOrderContext);

return context;
}

export { AccessibilityOrderProvider, useAccessibilityOrder };
4 changes: 4 additions & 0 deletions app/containers/A11yFlow/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import A11yElement from './A11yElement/A11yElement';
import A11yContainer from './A11yContainer/A11yContainer';

export { A11yContainer, A11yElement };
186 changes: 97 additions & 89 deletions app/containers/TextInput/FormTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ActivityIndicator from '../ActivityIndicator';
import { CustomIcon, TIconsName } from '../CustomIcon';
import { TextInput } from './TextInput';
import { isIOS } from '../../lib/methods/helpers';
import { A11yContainer, A11yElement } from '../A11yFlow';

const styles = StyleSheet.create({
error: {
Expand Down Expand Up @@ -100,101 +101,108 @@ export const FormTextInput = ({
const Input = bottomSheet ? BottomSheetTextInput : TextInput;

const accessibilityLabelRequired = required ? `, ${i18n.t('Required')}` : '';
const accessibilityInputValue = (!secureTextEntry && value && isIOS) || showPassword ? `, ${value}` : '';
const accessibilityInputValue = (!secureTextEntry && value && isIOS) || showPassword ? `, ${value ?? ''}` : '';
return (
<View
accessible
accessibilityLabel={`${label}${accessibilityLabelRequired}${accessibilityInputValue}`}
style={[styles.inputContainer, containerStyle]}>
{label ? (
<Text style={[styles.label, { color: colors.fontTitlesLabels }, error?.error && { color: colors.fontDanger }]}>
{label}{' '}
{required && <Text style={[styles.required, { color: colors.fontSecondaryInfo }]}>{`(${i18n.t('Required')})`}</Text>}
</Text>
) : null}
<A11yContainer>
<A11yElement order={1}>
<View
accessible
accessibilityLabel={`${label}${accessibilityLabelRequired}${accessibilityInputValue}`}
style={[styles.inputContainer, containerStyle]}>
{label ? (
<Text style={[styles.label, { color: colors.fontTitlesLabels }, error?.error && { color: colors.fontDanger }]}>
{label}{' '}
{required && (
<Text style={[styles.required, { color: colors.fontSecondaryInfo }]}>{`(${i18n.t('Required')})`}</Text>
)}
</Text>
) : null}

<View accessible style={styles.wrap}>
<Input
style={[
styles.input,
iconLeft && styles.inputIconLeft,
(secureTextEntry || iconRight || showClearInput) && styles.inputIconRight,
{
backgroundColor: colors.surfaceRoom,
borderColor: colors.strokeMedium,
color: colors.fontTitlesLabels
},
error?.error && {
color: colors.buttonBackgroundDangerDefault,
borderColor: colors.buttonBackgroundDangerDefault
},
inputStyle
]}
// @ts-ignore ref error
ref={inputRef}
autoCorrect={false}
autoCapitalize='none'
underlineColorAndroid='transparent'
secureTextEntry={secureTextEntry && !showPassword}
testID={testID}
placeholder={placeholder}
value={value}
placeholderTextColor={colors.fontAnnotation}
{...inputProps}
/>
<View accessible style={styles.wrap}>
<Input
style={[
styles.input,
iconLeft && styles.inputIconLeft,
(secureTextEntry || iconRight || showClearInput) && styles.inputIconRight,
{
backgroundColor: colors.surfaceRoom,
borderColor: colors.strokeMedium,
color: colors.fontTitlesLabels
},
error?.error && {
color: colors.buttonBackgroundDangerDefault,
borderColor: colors.buttonBackgroundDangerDefault
},
inputStyle
]}
// @ts-ignore ref error
ref={inputRef}
autoCorrect={false}
autoCapitalize='none'
underlineColorAndroid='transparent'
secureTextEntry={secureTextEntry && !showPassword}
testID={testID}
placeholder={placeholder}
value={value}
placeholderTextColor={colors.fontAnnotation}
{...inputProps}
/>

{iconLeft ? (
<CustomIcon
name={iconLeft}
testID={testID ? `${testID}-icon-left` : undefined}
size={20}
color={colors.fontSecondaryInfo}
style={[styles.iconContainer, styles.iconLeft]}
/>
) : null}
{iconLeft ? (
<CustomIcon
name={iconLeft}
testID={testID ? `${testID}-icon-left` : undefined}
size={20}
color={colors.fontSecondaryInfo}
style={[styles.iconContainer, styles.iconLeft]}
/>
) : null}

{showClearInput ? (
<Touchable onPress={onClearInput} style={[styles.iconContainer, styles.iconRight]} testID='clear-text-input'>
<CustomIcon name='input-clear' size={20} color={colors.fontDefault} />
</Touchable>
) : null}
{showClearInput ? (
<Touchable onPress={onClearInput} style={[styles.iconContainer, styles.iconRight]} testID='clear-text-input'>
<CustomIcon name='input-clear' size={20} color={colors.fontDefault} />
</Touchable>
) : null}

{iconRight && !showClearInput ? (
<CustomIcon
name={iconRight}
testID={testID ? `${testID}-icon-right` : undefined}
size={20}
color={colors.fontDefault}
style={[styles.iconContainer, styles.iconRight]}
accessible={false}
/>
) : null}
{iconRight && !showClearInput ? (
<CustomIcon
name={iconRight}
testID={testID ? `${testID}-icon-right` : undefined}
size={20}
color={colors.fontDefault}
style={[styles.iconContainer, styles.iconRight]}
accessible={false}
/>
) : null}

{secureTextEntry ? (
<Touchable
style={[styles.iconContainer, styles.iconRight]}
accessible
accessibilityLabel={showPassword ? i18n.t('Hide_Password') : i18n.t('Show_Password')}
onPress={() => setShowPassword(!showPassword)}>
<CustomIcon
name={showPassword ? 'unread-on-top' : 'unread-on-top-disabled'}
testID={testID ? `${testID}-icon-password` : undefined}
size={20}
color={colors.fontDefault}
/>
</Touchable>
) : null}
{secureTextEntry ? (
<A11yElement order={2} style={[styles.iconContainer, styles.iconRight]}>
<Touchable
accessible
accessibilityLabel={showPassword ? i18n.t('Hide_Password') : i18n.t('Show_Password')}
onPress={() => setShowPassword(!showPassword)}>
<CustomIcon
name={showPassword ? 'unread-on-top' : 'unread-on-top-disabled'}
testID={testID ? `${testID}-icon-password` : undefined}
size={20}
color={colors.fontDefault}
/>
</Touchable>
</A11yElement>
) : null}

{loading ? (
<ActivityIndicator
style={[styles.iconContainer, styles.iconRight]}
color={colors.fontDefault}
testID={testID ? `${testID}-loading` : undefined}
/>
) : null}
{left}
</View>
{error && error.reason ? <Text style={[styles.error, { color: colors.fontDanger }]}>{error.reason}</Text> : null}
</View>
{loading ? (
<ActivityIndicator
style={[styles.iconContainer, styles.iconRight]}
color={colors.fontDefault}
testID={testID ? `${testID}-loading` : undefined}
/>
) : null}
{left}
</View>
{error && error.reason ? <Text style={[styles.error, { color: colors.fontDanger }]}>{error.reason}</Text> : null}
</View>
</A11yElement>
</A11yContainer>
);
};
4 changes: 4 additions & 0 deletions ios/A11yFlowModule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#import <React/RCTBridgeModule.h>
@interface A11yFlowModule : NSObject <RCTBridgeModule>
@end

40 changes: 40 additions & 0 deletions ios/A11yFlowModule.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#import "A11yFlowModule.h"
#import <React/RCTLog.h>
#import <React/RCTUIManager.h>
#import <UIKit/UIKit.h>

@implementation A11yFlowModule
@synthesize bridge = _bridge;

RCT_EXPORT_MODULE(A11yFlow);

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isVoiceOverEnabled)
{
return @(UIAccessibilityIsVoiceOverRunning());
}

RCT_EXPORT_METHOD(
setA11yOrder: (nonnull NSArray<NSNumber *> *)elements
node:(nonnull NSNumber *)node
) {
dispatch_async(dispatch_get_main_queue(), ^{
UIView *parentView = [self.bridge.uiManager viewForReactTag:node];
if (parentView != nil) {
NSMutableArray *orderedElements = [NSMutableArray arrayWithCapacity:[elements count]];

for (NSNumber *tag in elements) {
UIView *childView = [self.bridge.uiManager viewForReactTag:tag];
if (childView != nil) {
[orderedElements addObject:childView];
}
}

parentView.accessibilityElements = orderedElements;

UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, parentView);
}
});
}


@end
Loading
Loading