Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
2,
{ "ignore": ["^@theme", "^@docusaurus", "^@generated"] }
],
"react-native-a11y/has-valid-accessibility-ignores-invert-colors": 0,
"react-native/no-color-literals": "off",
"react-native/no-inline-styles": "off",
"react-native-a11y/has-valid-accessibility-descriptors": "off",
"react-native-a11y/has-valid-accessibility-ignores-invert-colors": 0,
"react-native-a11y/has-valid-accessibility-value": "off"
}
}
74 changes: 74 additions & 0 deletions src/helpers/__tests__/accessiblity.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import { View, Text, TextInput } from 'react-native';
import { render, isInaccessible } from '../..';

test('returns false for accessible elements', () => {
expect(
isInaccessible(render(<View testID="subject" />).getByTestId('subject'))
).toBe(false);

expect(
isInaccessible(
render(<Text testID="subject">Hello</Text>).getByTestId('subject')
)
).toBe(false);

expect(
isInaccessible(
render(<TextInput testID="subject" />).getByTestId('subject')
)
).toBe(false);
});

test('detects elements with display=none', () => {
const view = render(<View testID="subject" style={{ display: 'none' }} />);
expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
});

test('detects nested elements with display=none', () => {
const view = render(
<View style={{ display: 'none' }}>
<View testID="subject" />
</View>
);
expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
});

test('detects elements with display=none with complex style', () => {
const view = render(
<View
testID="subject"
style={[{ display: 'flex' }, [{ display: 'flex' }], { display: 'none' }]}
/>
);
expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
});

test('detects elements with opacity=0', () => {
const view = render(<View testID="subject" style={{ opacity: 0 }} />);
expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
});

test('detects nested elements with opacity=0', () => {
const view = render(
<View style={{ opacity: 0 }}>
<View testID="subject" />
</View>
);
expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
});

test('detects elements with opacity=0 with complex styles', () => {
const view = render(
<View
testID="subject"
style={[[{ opacity: 1 }], { opacity: 1 }, [{ opacity: 0 }]]}
/>
);
expect(isInaccessible(view.getByTestId('subject'))).toBe(true);
});

test('is not trigged by opacity > 0', () => {
const view = render(<View testID="subject" style={{ opacity: 0.0001 }} />);
expect(isInaccessible(view.getByTestId('subject'))).toBe(false);
});
52 changes: 52 additions & 0 deletions src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { StyleSheet } from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';

export function isInaccessible(instance: ReactTestInstance | null): boolean {
if (!instance) {
return true;
}

// Android: importantForAccessibility
// See: https://reactnative.dev/docs/accessibility#importantforaccessibility-android
if (instance.props.importantForAccessibility === 'no') return true;

let current: ReactTestInstance | null = instance;
while (current) {
if (isSubtreeInaccessible(current)) {
return true;
}

current = current.parent;
}

return false;
}

function isSubtreeInaccessible(
instance: ReactTestInstance | null | undefined
): boolean {
if (!instance) {
return true;
}

// TODO implement iOS: accessibilityViewIsModal
// The hard part is to implement this to look only for host views
// See: https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios
// if (instance.parent?.children.some((child) => child.accessibilityViewIsModal))
// return true;

// iOS: accessibilityElementsHidden
// See: https://reactnative.dev/docs/accessibility#accessibilityelementshidden-ios
if (instance.props.accessibilityElementsHidden) return true;

// Android: importantForAccessibility
// See: https://reactnative.dev/docs/accessibility#importantforaccessibility-android
if (instance.props.importantForAccessibility === 'no-hide-descendants')
return true;

const flatStyle = StyleSheet.flatten(instance.props.style) ?? {};
if (flatStyle.display === 'none') return true;
if (flatStyle.opacity === 0) return true;

return false;
}
2 changes: 2 additions & 0 deletions src/pure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { within, getQueriesForElement } from './within';
import { getDefaultNormalizer } from './matches';
import { renderHook } from './renderHook';
import { screen } from './screen';
import { isInaccessible } from './helpers/accessiblity';

export type {
RenderOptions,
Expand All @@ -26,3 +27,4 @@ export { within, getQueriesForElement };
export { getDefaultNormalizer };
export { renderHook };
export { screen };
export { isInaccessible };