Skip to content

Add support for accessibilityIgnoresInvertColors #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 9, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* eslint-env jest */
/**
* @fileoverview Ensure that accessibilityIgnoresInvertColors property value is a boolean.
* @author Dominic Coelho
*/

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/has-valid-accessibility-ignores-invert-colors';

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

const ruleTester = new RuleTester();

const typeError = {
message: 'accessibilityIgnoresInvertColors prop is not a boolean value',
type: 'JSXElement'
};

const missingPropError = {
message:
'Found an element which will be inverted. Add the accessibilityIgnoresInvertColors prop',
type: 'JSXElement'
};

ruleTester.run('has-valid-accessibility-ignores-invert-colors', rule, {
valid: [
{ code: '<View accessibilityIgnoresInvertColors></View>;' },
{ code: '<View accessibilityIgnoresInvertColors={true}></View>' },
{ code: '<View accessibilityIgnoresInvertColors={false}></View>' },
{
code: '<ScrollView accessibilityIgnoresInvertColors></ScrollView>'
},
{
code: '<Image accessibilityIgnoresInvertColors />'
},
{
code: '<View accessibilityIgnoresInvertColors><Image /></View>'
},
{
code:
'<View accessibilityIgnoresInvertColors><View><Image /></View></View>'
},
{
code: '<View><View /></View>'
},
{
code: '<FastImage accessibilityIgnoresInvertColors />',
options: [
{
invertableComponents: ['FastImage']
}
]
}
].map(parserOptionsMapper),
invalid: [
{
code: '<View accessibilityIgnoresInvertColors={"true"}></View>',
errors: [typeError]
},
{
code: '<View accessibilityIgnoresInvertColors={"False"}></View>',
errors: [typeError]
},
{
code: '<View accessibilityIgnoresInvertColors={0}></View>',
errors: [typeError]
},
{
code: `<View accessibilityIgnoresInvertColors={1}>
<Image
style={{width: 50, height: 50}}
source={{uri: 'https://facebook.github.io/react-native/img/tiny_logo.png'}}
/>
</View>`,
errors: [typeError, missingPropError]
},
{
code: '<View accessibilityIgnoresInvertColors={{enabled: 1}}></View>',
errors: [typeError]
},
{
code: '<View accessibilityIgnoresInvertColors={{value: true}}></View>',
errors: [typeError]
},
{
code: '<Image />',
errors: [missingPropError]
},
{
code: '<View><Image /></View>',
errors: [missingPropError]
},
{
code: '<View><View><Image /></View></View>',
errors: [missingPropError]
},
{
code: '<FastImage />',
errors: [missingPropError],
options: [
{
invertableComponents: ['FastImage']
}
]
}
].map(parserOptionsMapper)
});
100 changes: 100 additions & 0 deletions docs/rules/has-valid-accessibility-ignores-invert-colors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# has-valid-accessibility-ignores-invert-colors

The `accessibilityIgnoresInvertColors` property can be used to tell iOS whether or not to invert colors on a view (including all of its subviews) when the Invert Colors accessibility feature is enabled.

This feature can be enabled on iOS via: `Settings -> General -> Accessibility -> Display Accommodations -> Invert Colors -> [Smart Invert or Classic Invert]`. Note that the Smart Invert feature will avoid inverting the colors of images and other media without need for `accessibilityIgnoresInvertColors`, but Classic Invert *will* still invert colors on media without `accessibilityIgnoresInvertColors`.

`accessibilityIgnoresInvertColors` is usually used on elements like `<Image />` -- however in some cases it may be used on a parent wrapper.

For example, both of the following snippets are valid (and will achieve the same result in practice).

```js
<View>
<Image accessibilityIgnoresInvertColors={true} />
</View>
```

```js
<View accessibilityIgnoresInvertColors={true}>
<Image />
</View>
```

## Values may be one of the following (boolean)

- `true`: colors of everything in this view will *not* be inverted when color inversion is enabled
- `false`: the default value (unless the view is nested inside a view with `accessibilityIgnoresInvertColors={true}`). Colors in everything contained in this view may be inverted

### References

1. [React Native accessibility documentation](http://facebook.github.io/react-native/docs/accessibility#accessibilityignoresinvertcolorsios)
2. [accessibilityIgnoresInvertColors Apple developer docs](https://developer.apple.com/documentation/uikit/uiview/2865843-accessibilityignoresinvertcolors)

## Rule details

By default, the rule will only check `<Image />`.

If you would like to check additional components which might require `accessibilityIgnoresInvertColors`, you can pass an options object which contains `invertableComponents` in your ESLint config.

`invertableComponents` should be an Array of component names as strings.

```js
"react-native-a11y/has-valid-accessibility-ignores-invert-colors": [
"error",
{
"invertableComponents": [
"FastImage",
"MyCustomComponent",
...
]
}
]
```

```js
{/* invalid, rule will error */}
<FastImage />

<View>
<FastImage />
</View>

{/* valid */}
<FastImage accessibilityIgnoresInvertColors />

<View accessibilityIgnoresInvertColors>
<FastImage />
</View>
```

These extra `invertableComponents` will also be checked in addition to `<Image />`.

### Succeed
```jsx
<View accessibilityIgnoresInvertColors={true}>
<Image
style={{width: 50, height: 50}}
source={{uri: 'https://facebook.github.io/react-native/img/tiny_logo.png'}}
/>
</View>

<View accessibilityIgnoresInvertColors></View>

<View accessibilityIgnoresInvertColors={false}></View>

<ScrollView accessibilityIgnoresInvertColors={false}></ScrollView>
```

### Fail
```jsx
<View accessibilityIgnoresInvertColors="true">
<Image
style={{width: 50, height: 50}}
source={{uri: 'https://facebook.github.io/react-native/img/tiny_logo.png'}}
/>
</View>

<View accessibilityIgnoresInvertColors={{value: true}}></View>

<View accessibilityIgnoresInvertColors={0}></View>
```
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {
'accessibility-label': require('./rules/accessibility-label'),
'has-accessibility-props': require('./rules/has-accessibility-props'),
'has-valid-accessibility-component-type': require('./rules/has-valid-accessibility-component-type'),
'has-valid-accessibility-ignores-invert-colors': require('./rules/has-valid-accessibility-ignores-invert-colors'),
'has-valid-accessibility-live-region': require('./rules/has-valid-accessibility-live-region'),
'has-valid-accessibility-role': require('./rules/has-valid-accessibility-role'),
'has-valid-accessibility-state': require('./rules/has-valid-accessibility-state'),
Expand All @@ -24,6 +25,8 @@ module.exports = {
'react-native-a11y/accessibility-label': 'error',
'react-native-a11y/has-accessibility-props': 'error',
'react-native-a11y/has-valid-accessibility-component-type': 'error',
'react-native-a11y/has-valid-accessibility-ignores-invert-colors':
'error',
'react-native-a11y/has-valid-accessibility-live-region': 'error',
'react-native-a11y/has-valid-accessibility-role': 'error',
'react-native-a11y/has-valid-accessibility-states': 'error',
Expand Down
88 changes: 88 additions & 0 deletions src/rules/has-valid-accessibility-ignores-invert-colors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @fileoverview Enforce accessibilityIgnoresInvertColors property value is a boolean.
* @author Dominic Coelho
* @flow
*/

// ----------------------------------------------------------------------------
// Rule Definition
// ----------------------------------------------------------------------------

import { generateObjSchema } from '../util/schemas';
import { elementType, getProp, hasProp } from 'jsx-ast-utils';
import type { JSXElement } from 'ast-types-flow';
import type { ESLintContext } from '../../flow/eslint';
import isNodePropValueBoolean from '../util/isNodePropValueBoolean';

const propName = 'accessibilityIgnoresInvertColors';
const schema = generateObjSchema();

const defaultInvertableComponents = ['Image'];

const hasValidIgnoresInvertColorsProp = ({ attributes }) =>
hasProp(attributes, propName) &&
isNodePropValueBoolean(getProp(attributes, propName));

const checkParent = ({ openingElement, parent }) => {
if (hasValidIgnoresInvertColorsProp(openingElement)) {
return false;
} else if (parent.openingElement) {
return checkParent(parent);
}
return true;
};

module.exports = {
meta: {
docs: {},
schema: [schema]
},

create: ({ options, report }: ESLintContext) => ({
JSXElement: (node: JSXElement) => {
// $FlowFixMe
const { children, openingElement, parent } = node;

if (
hasProp(openingElement.attributes, propName) &&
!isNodePropValueBoolean(getProp(openingElement.attributes, propName))
) {
report({
node,
message:
'accessibilityIgnoresInvertColors prop is not a boolean value'
});
} else {
const elementsToCheck = defaultInvertableComponents;
if (options.length > 0) {
const { invertableComponents } = options[0];
if (invertableComponents) {
elementsToCheck.push(...invertableComponents);
}
}

const type = elementType(openingElement);

if (
elementsToCheck.indexOf(type) > -1 &&
!hasValidIgnoresInvertColorsProp(openingElement) &&
children.length === 0
) {
let shouldReport = true;

if (parent.openingElement) {
shouldReport = checkParent(parent);
}

if (shouldReport) {
report({
node,
message:
'Found an element which will be inverted. Add the accessibilityIgnoresInvertColors prop'
});
}
}
}
}
})
};
27 changes: 27 additions & 0 deletions src/util/isNodePropValueBoolean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @flow
import type { JSXAttribute } from 'ast-types-flow';

export default function isattrPropValueBoolean(attr: JSXAttribute): boolean {
/**
* Using `typeof getLiteralPropValue(attr) === 'boolean'` with getLiteralPropValue/getPropValue
* from `jsx-ast-utils` doesn't work as expected, because it "converts" strings `"true"` and `"false"`
* to booleans `true` and `false`. This function aims to correctly identify uses of the string instead
* of the boolean, so that we can correctly identify this error.
*/

if (typeof attr !== 'object' || !attr.hasOwnProperty('value')) {
// Loose check for correct data being passed in to this function
throw new Error('isattrPropValueBoolean expects a attr object as argument');
}
if (attr.value === null) {
// attr.value is null when it is declared as a prop but not equal to anything. This defaults to `true` in JSX
return true;
}
// $FlowFixMe
if (attr.value.expression.type !== 'Literal') {
// If not a literal, it cannot be a boolean
return false;
}
// $FlowFixMe
return typeof attr.value.expression.value === 'boolean';
}