├── .eslintignore ├── .prettierrc ├── .vscode └── settings.json ├── .gitignore ├── .flowconfig ├── .eslintrc ├── .npmignore ├── flow ├── eslint-jsx.js └── eslint.js ├── .babelrc ├── .changeset └── config.json ├── src ├── util │ ├── isOneOf.js │ ├── isNodePropExpression.js │ ├── schemas.js │ ├── findChild.js │ ├── isTouchable.js │ └── isNodePropValueBoolean.js ├── rules │ ├── has-valid-accessibility-live-region.js │ ├── has-valid-accessibility-states.js │ ├── has-valid-important-for-accessibility.js │ ├── has-valid-accessibility-component-type.js │ ├── has-valid-accessibility-traits.js │ ├── has-valid-accessibility-role.js │ ├── has-accessibility-hint.js │ ├── no-nested-touchables.js │ ├── has-accessibility-props.js │ ├── has-valid-accessibility-descriptors.js │ ├── has-valid-accessibility-state.js │ ├── has-valid-accessibility-value.js │ ├── has-valid-accessibility-actions.js │ └── has-valid-accessibility-ignores-invert-colors.js ├── factory │ └── valid-prop.js └── index.js ├── scripts ├── boilerplate │ ├── doc.js │ ├── rule.js │ └── test.js ├── create-rule.md ├── addRuleToIndex.js └── create-rule.js ├── __tests__ ├── __util__ │ ├── parserOptionsMapper.js │ └── ruleOptionsMapperFactory.js ├── index-test.js └── src │ └── rules │ ├── has-accessibility-hint-test.js │ ├── has-valid-important-for-accessibility-test.js │ ├── has-valid-accessibility-live-region-test.js │ ├── has-valid-accessibility-component-type-test.js │ ├── has-valid-accessibility-states-test.js │ ├── has-valid-accessibility-traits-test.js │ ├── no-nested-touchables-test.js │ ├── has-valid-accessibility-descriptors-test.js │ ├── has-valid-accessibility-value-test.js │ ├── has-valid-accessibility-role-test.js │ ├── has-valid-accessibility-state-test.js │ ├── has-accessibility-props-test.js │ ├── has-valid-accessibility-actions-test.js │ └── has-valid-accessibility-ignores-invert-colors-test.js ├── docs └── rules │ ├── has-accessibility-hint.md │ ├── has-valid-accessibility-descriptors.md │ ├── has-valid-accessibility-component-type.md │ ├── has-valid-accessibility-live-region.md │ ├── no-nested-touchables.md │ ├── has-valid-accessibility-traits.md │ ├── has-valid-accessibility-value.md │ ├── has-valid-accessibility-states.md │ ├── has-valid-important-for-accessibility.md │ ├── has-accessibility-props.md │ ├── has-valid-accessibility-state.md │ ├── has-valid-accessibility-role.md │ ├── has-valid-accessibility-ignores-invert-colors.md │ └── has-valid-accessibility-actions.md ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── LICENSE.md ├── package.json ├── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | reports/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage 3 | lib 4 | node_modules 5 | npm-debug.log 6 | package-lock.json 7 | reports 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /lib/.* 3 | /docs/.* 4 | /reports/.* 5 | 6 | [options] 7 | esproposal.optional_chaining=enable 8 | types_first=false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:flowtype/recommended", "plugin:prettier/recommended"], 3 | "parser": "@babel/eslint-parser", 4 | "plugins": ["flowtype"], 5 | "rules": { 6 | "no-unused-vars": 1 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .flowconfig 3 | .travis.yml 4 | /coverage 5 | /flow 6 | /node_modules 7 | npm-debug.log 8 | /src 9 | /reports 10 | yarn-error.log 11 | /.vscode 12 | /docs 13 | /.github 14 | /.changeset 15 | /__tests__ 16 | -------------------------------------------------------------------------------- /flow/eslint-jsx.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @flow 3 | */ 4 | import type { 5 | JSXAttribute, 6 | JSXOpeningElement, 7 | } from 'ast-types-flow'; 8 | 9 | export type ESLintJSXAttribute = { 10 | parent: JSXOpeningElement 11 | } & JSXAttribute; 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "4" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": ["@babel/transform-flow-strip-types"] 13 | } 14 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { 6 | "repo": "FormidableLabs/eslint-plugin-react-native-a11y" 7 | } 8 | ], 9 | "access": "public", 10 | "baseBranch": "master" 11 | } 12 | -------------------------------------------------------------------------------- /flow/eslint.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @flow 3 | */ 4 | export type ESLintReport = { 5 | node: any, 6 | message: string, 7 | }; 8 | 9 | export type ESLintContext = { 10 | id: string, 11 | options: Array, 12 | report: (ESLintReport) => void, 13 | getSourceCode: () => { 14 | text: string, 15 | }, 16 | sourceCode: { 17 | text: string, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/util/isOneOf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns boolean indicating whether a value to check 3 | * is one of a given set of values. 4 | * @flow 5 | */ 6 | 7 | // should be expanded to work with more than just strings 8 | // as and when it's needed 9 | export default function isOneOf(toCheck: string = '', values: string[] = []) { 10 | return values.includes(toCheck); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/boilerplate/doc.js: -------------------------------------------------------------------------------- 1 | const docBoilerplateGenerator = (name) => `# ${name} 2 | 3 | Write a useful explanation here! 4 | 5 | ### References 6 | 7 | 1. 8 | 9 | ## Rule details 10 | 11 | This rule takes no arguments. 12 | 13 | ### Succeed 14 | \`\`\`jsx 15 | 16 | \`\`\` 17 | 18 | ### Fail 19 | \`\`\`jsx 20 | 21 | \`\`\` 22 | `; 23 | 24 | module.exports = docBoilerplateGenerator; 25 | -------------------------------------------------------------------------------- /src/util/isNodePropExpression.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { JSXAttribute } from 'ast-types-flow'; 3 | 4 | const ALLOWED_TYPES = [ 5 | 'Identifier', 6 | 'CallExpression', 7 | 'ConditionalExpression', 8 | 'MemberExpression', 9 | ]; 10 | 11 | export default function isattrPropExpression(attr: JSXAttribute): boolean { 12 | // $FlowFixMe 13 | const expression = attr.value?.expression; 14 | // $FlowFixMe 15 | return expression && ALLOWED_TYPES.includes(expression.type); 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/__util__/parserOptionsMapper.js: -------------------------------------------------------------------------------- 1 | const defaultParserOptions = { 2 | ecmaVersion: 6, 3 | ecmaFeatures: { 4 | jsx: true, 5 | }, 6 | }; 7 | 8 | export default function parserOptionsMapper({ 9 | code, 10 | errors, 11 | options = [], 12 | output = null, 13 | parserOptions = {}, 14 | }) { 15 | return { 16 | code, 17 | errors, 18 | options, 19 | output, 20 | parserOptions: { 21 | ...defaultParserOptions, 22 | ...parserOptions, 23 | }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/rules/has-valid-accessibility-live-region.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce `accessibilityLiveRegion` property value is valid 3 | 4 | * @author Alex Saunders 5 | * @flow 6 | */ 7 | 8 | // ---------------------------------------------------------------------------- 9 | // Rule Definition 10 | // ---------------------------------------------------------------------------- 11 | 12 | import createValidPropRule from '../factory/valid-prop'; 13 | 14 | const errorMessage = 'accessibilityLiveRegion must be one of defined values'; 15 | 16 | const validValues = ['none', 'polite', 'assertive']; 17 | 18 | module.exports = createValidPropRule( 19 | 'accessibilityLiveRegion', 20 | validValues, 21 | errorMessage 22 | ); 23 | -------------------------------------------------------------------------------- /src/rules/has-valid-accessibility-states.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Used to tell Talkback or Voiceover the state a UI Element is in 3 | * @author Jen Luker 4 | * @flow 5 | */ 6 | 7 | import createValidPropRule from '../factory/valid-prop'; 8 | 9 | // ---------------------------------------------------------------------------- 10 | // Rule Definition 11 | // ---------------------------------------------------------------------------- 12 | 13 | const errorMessage = 14 | 'accessibilityStates must be one, both or neither of the defined values'; 15 | 16 | const validValues = ['selected', 'disabled', '']; 17 | 18 | module.exports = createValidPropRule( 19 | 'accessibilityStates', 20 | validValues, 21 | errorMessage 22 | ); 23 | -------------------------------------------------------------------------------- /src/rules/has-valid-important-for-accessibility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce importantForAccessibility property value is valid 3 | * @author Alex Saunders 4 | * @flow 5 | */ 6 | 7 | // ---------------------------------------------------------------------------- 8 | // Rule Definition 9 | // ---------------------------------------------------------------------------- 10 | 11 | import createValidPropRule from '../factory/valid-prop'; 12 | 13 | const errorMessage = 'importantForAccessibility must be one of defined values'; 14 | 15 | const validValues = ['auto', 'yes', 'no', 'no-hide-descendants']; 16 | 17 | module.exports = createValidPropRule( 18 | 'importantForAccessibility', 19 | validValues, 20 | errorMessage 21 | ); 22 | -------------------------------------------------------------------------------- /src/rules/has-valid-accessibility-component-type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce accessibilityComponentType property value is valid 3 | * @author Alex Saunders 4 | * @flow 5 | */ 6 | 7 | // ---------------------------------------------------------------------------- 8 | // Rule Definition 9 | // ---------------------------------------------------------------------------- 10 | 11 | import createValidPropRule from '../factory/valid-prop'; 12 | 13 | const errorMessage = 'accessibilityComponentType must be one of defined values'; 14 | 15 | const validValues = [ 16 | 'none', 17 | 'button', 18 | 'radiobutton_checked', 19 | 'radiobutton_unchecked', 20 | ]; 21 | 22 | module.exports = createValidPropRule( 23 | 'accessibilityComponentType', 24 | validValues, 25 | errorMessage 26 | ); 27 | -------------------------------------------------------------------------------- /docs/rules/has-accessibility-hint.md: -------------------------------------------------------------------------------- 1 | # has-accessibility-hint 2 | 3 | An accessibility hint helps users understand what will happen when they perform an action on the accessibility element when that result is not apparent from the accessibility label. 4 | 5 | ### References 6 | 7 | 1. [React Native Docs - accessibilityHint (iOS, Android)](https://facebook.github.io/react-native/docs/accessibility#accessibilityhint-ios-android) 8 | 9 | ## Rule details 10 | 11 | This rule takes no arguments. 12 | 13 | ### Succeed 14 | ```jsx 15 | 16 | 17 | 18 | ``` 19 | 20 | ### Fail 21 | ```jsx 22 | 23 | ``` 24 | -------------------------------------------------------------------------------- /src/util/schemas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON schema to accept an array of unique strings 3 | */ 4 | export const arraySchema = { 5 | type: 'array', 6 | items: { 7 | type: 'string', 8 | }, 9 | uniqueItems: true, 10 | additionalItems: false, 11 | }; 12 | 13 | /** 14 | * JSON schema to accept an array of unique strings from an enumerated list. 15 | */ 16 | export const enumArraySchema = (enumeratedList = [], minItems = 0) => 17 | Object.assign({}, arraySchema, { 18 | items: { 19 | type: 'string', 20 | enum: enumeratedList, 21 | }, 22 | minItems, 23 | }); 24 | 25 | /** 26 | * Factory function to generate an object schema 27 | * with specified properties object 28 | */ 29 | export const generateObjSchema = (properties = {}, required) => ({ 30 | type: 'object', 31 | properties, 32 | required, 33 | }); 34 | -------------------------------------------------------------------------------- /docs/rules/has-valid-accessibility-descriptors.md: -------------------------------------------------------------------------------- 1 | # has-valid-accessibility-descriptors 2 | 3 | Ensures that Touchable* components have appropriate props to communicate with assistive technologies. 4 | 5 | The rule will trigger when a Touchable* component does not have **any** of the following props:- 6 | 7 | - `accessibiltyRole` 8 | - `accessibilityLabel` 9 | - `accessibilityActions` 10 | 11 | In some cases, fixing this may then trigger other rules for related props (e.g. if you add `accessibilityActions` to fix this but are missing `onAccessibilityAction`) 12 | 13 | ## Rule details 14 | 15 | This rule takes no arguments. 16 | 17 | ### Succeed 18 | ```jsx 19 | 20 | Back 21 | 22 | ``` 23 | 24 | ### Fail 25 | ```jsx 26 | 27 | Back 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /__tests__/__util__/ruleOptionsMapperFactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | type ESLintTestRunnerTestCase = { 6 | code: string, 7 | errors: ?Array<{ message: string, type: string }>, 8 | options: ?Array, 9 | parserOptions: ?Array, 10 | }; 11 | 12 | export default function ruleOptionsMapperFactory( 13 | ruleOptions: Array = [] 14 | ) { 15 | // eslint-disable-next-line 16 | return ({ code, errors, options, parserOptions }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => { 17 | return { 18 | code, 19 | errors, 20 | // Flatten the array of objects in an array of one object. 21 | options: (options || []).concat(ruleOptions).reduce( 22 | (acc, item) => [ 23 | { 24 | ...acc[0], 25 | ...item, 26 | }, 27 | ], 28 | [{}] 29 | ), 30 | parserOptions, 31 | }; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /scripts/create-rule.md: -------------------------------------------------------------------------------- 1 | # Rule Generator 2 | 3 | ```bash 4 | $ node scripts/create-rule.js rule-name --author="Your name" --description="Description of the rule" 5 | # OR with npm script alias 6 | $ npm run create -- rule-name --author="Your name" --description="Description of rule" 7 | ``` 8 | 9 | This script will generate three files with basic boilerplate for the given rule: 10 | 1. src/rules/${rule-name}.js 11 | 2. \__tests__/src/rules/${rule-name}-test.js 12 | 3. docs/rules/${rule-name}.md 13 | 14 | If the rule already exists or is not specified in the correct format, an error will be thrown. 15 | 16 | If we wanted to scaffold a rule for `no-marquee`, we could run: 17 | ```bash 18 | $ node scripts/create-rule.js no-marquee --author="Ethan Cohen <@evcohen>" --description="Enforce elements are not used." 19 | # OR 20 | $ npm run create -- no-marquee --author="Ethan Cohen <@evcohen>" --description="Enforce elements are not used." 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/rules/has-valid-accessibility-component-type.md: -------------------------------------------------------------------------------- 1 | # has-valid-accessibility-component-type 2 | 3 | The accessibilityComponentType property is essentially the android version of [accessibilityTraits](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/blob/master/docs/rules/has-valid-accessibility-traits.md) and is used to alert a user, using TalkBack, what kind of element they have selected. 4 | 5 | Values may be one of the following: 6 | 7 | - `"none"` 8 | - `"button"` 9 | - `"radiobutton_checked"` 10 | - `"radiobutton_unchecked"` 11 | 12 | ### References 13 | 14 | 1. https://facebook.github.io/react-native/docs/accessibility.html#accessibilitycomponenttype-android 15 | 16 | ## Rule details 17 | 18 | This rule takes no arguments. 19 | 20 | ### Succeed 21 | 22 | ```jsx 23 | 24 | ``` 25 | 26 | ### Fail 27 | 28 | ```jsx 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /src/util/findChild.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { JSXOpeningElement, JSXElement } from 'ast-types-flow'; 3 | 4 | /** 5 | * Recursively searches for an child element within a 6 | * JSXOpeningElement that matches the condition specificed in 7 | * `callback` 8 | */ 9 | export default function findChild( 10 | node: JSXElement, 11 | callback: (child: JSXOpeningElement) => boolean 12 | ): ?JSXOpeningElement { 13 | const { children } = node; 14 | if (children && children.length > 0) { 15 | for (let i = 0; i < children.length; i += 1) { 16 | // $FlowFixMe 17 | const child: JSXElement = children[i]; 18 | if (child.openingElement && child.openingElement.name) { 19 | if (callback(child.openingElement)) { 20 | return child.openingElement; 21 | } 22 | } 23 | const foundChild = findChild(child, callback); 24 | if (foundChild) { 25 | return foundChild; 26 | } 27 | } 28 | } 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /docs/rules/has-valid-accessibility-live-region.md: -------------------------------------------------------------------------------- 1 | # has-valid-accessibility-live-region 2 | 3 | On android devices, when components dynamically change, we want TalkBack to alert the end user. This is made possible by the `accessibilityLiveRegion` property. It can be set to the following values: 4 | 5 | - `"none"`: Accessibility services should not announce changes to this view. 6 | - `"polite"`: Accessibility services should announce changes to this view. 7 | - `"assertive"`: Accessibility services should interrupt ongoing speech to immediately announce changes to this view. 8 | 9 | ### References 10 | 11 | 1. https://facebook.github.io/react-native/docs/accessibility.html#accessibilityliveregion-android 12 | 13 | ## Rule details 14 | 15 | This rule takes no arguments. 16 | 17 | ### Succeed 18 | 19 | ```jsx 20 | Click Me 21 | ``` 22 | 23 | ### Fail 24 | 25 | ```jsx 26 | Click Me 27 | ``` 28 | -------------------------------------------------------------------------------- /src/rules/has-valid-accessibility-traits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce accessibilityTraits property value is valid 3 | * @author Alex Saunders 4 | * @flow 5 | */ 6 | 7 | import createValidPropRule from '../factory/valid-prop'; 8 | 9 | // ---------------------------------------------------------------------------- 10 | // Rule Definition 11 | // ---------------------------------------------------------------------------- 12 | 13 | const errorMessage = 'accessibilityTraits must be one of defined values'; 14 | 15 | const validValues = [ 16 | 'none', 17 | 'button', 18 | 'link', 19 | 'header', 20 | 'search', 21 | 'image', 22 | 'selected', 23 | 'plays', 24 | 'key', 25 | 'text', 26 | 'summary', 27 | 'disabled', 28 | 'frequentUpdates', 29 | 'startsMedia', 30 | 'adjustable', 31 | 'allowsDirectInteraction', 32 | 'pageTurn', 33 | ]; 34 | 35 | module.exports = createValidPropRule( 36 | 'accessibilityTraits', 37 | validValues, 38 | errorMessage 39 | ); 40 | -------------------------------------------------------------------------------- /scripts/boilerplate/rule.js: -------------------------------------------------------------------------------- 1 | const ruleBoilerplate = (author, description) => `/** 2 | * @fileoverview ${description} 3 | * @author ${author} 4 | * @flow 5 | */ 6 | 7 | // ---------------------------------------------------------------------------- 8 | // Rule Definition 9 | // ---------------------------------------------------------------------------- 10 | 11 | import type { JSXOpeningElement } from 'ast-types-flow'; 12 | import type { ESLintContext } from '../../flow/eslint'; 13 | import { generateObjSchema } from '../util/schemas'; 14 | 15 | const errorMessage = ''; 16 | 17 | const schema = generateObjSchema(); 18 | 19 | module.exports = { 20 | meta: { 21 | docs: {}, 22 | schema: [schema], 23 | }, 24 | 25 | create: (context: ESLintContext) => ({ 26 | JSXOpeningElement: (node: JSXOpeningElement) => { 27 | context.report({ 28 | node, 29 | message: errorMessage, 30 | }); 31 | }, 32 | }), 33 | }; 34 | `; 35 | 36 | module.exports = ruleBoilerplate; 37 | -------------------------------------------------------------------------------- /docs/rules/no-nested-touchables.md: -------------------------------------------------------------------------------- 1 | # no-nested-touchables 2 | 3 | and or `, 65 | errors: [expectedError], 66 | }, 67 | { 68 | code: `Nested`, 74 | errors: [expectedError], 75 | }, 76 | { 77 | code: `Nested`, 83 | errors: [expectedError], 84 | }, 85 | ].map(parserOptionsMapper), 86 | }); 87 | -------------------------------------------------------------------------------- /src/rules/has-valid-accessibility-state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Describes the current state of a component to the user of an assistive technology. 3 | * @author JP Driver 4 | * @flow 5 | */ 6 | 7 | import type { JSXOpeningElement } from 'ast-types-flow'; 8 | import { hasProp } from 'jsx-ast-utils'; 9 | import { generateObjSchema } from '../util/schemas'; 10 | import type { ESLintContext } from '../../flow/eslint'; 11 | import getPropValue from 'jsx-ast-utils/lib/getPropValue'; 12 | 13 | // ---------------------------------------------------------------------------- 14 | // Rule Definition 15 | // ---------------------------------------------------------------------------- 16 | 17 | const PROP_NAME = 'accessibilityState'; 18 | 19 | const validKeys = ['disabled', 'selected', 'checked', 'busy', 'expanded']; 20 | 21 | module.exports = { 22 | meta: { 23 | docs: {}, 24 | schema: [generateObjSchema()], 25 | }, 26 | 27 | create: (context: ESLintContext) => ({ 28 | JSXOpeningElement: (node: JSXOpeningElement) => { 29 | if (hasProp(node.attributes, PROP_NAME)) { 30 | const stateProp = node.attributes.find( 31 | // $FlowFixMe 32 | (f) => f.name?.name === PROP_NAME 33 | ); 34 | const statePropType = 35 | // $FlowFixMe 36 | stateProp.value.expression?.type || stateProp.value.type; 37 | 38 | const error = (message) => 39 | context.report({ 40 | node, 41 | message, 42 | }); 43 | 44 | if ( 45 | statePropType === 'Literal' || 46 | statePropType === 'ArrayExpression' 47 | ) { 48 | error('accessibilityState must be an object'); 49 | } else if (statePropType === 'ObjectExpression') { 50 | const stateValue = getPropValue(stateProp); 51 | Object.entries(stateValue).map(([key, value]) => { 52 | if (!validKeys.includes(key)) { 53 | error(`accessibilityState object: "${key}" is not a valid key`); 54 | } else if ( 55 | // we can't determine the associated value type of non-Literal expressions 56 | // treat these cases as though they are valid 57 | // $FlowFixMe 58 | stateProp.value.expression.properties.every( 59 | // $FlowFixMe 60 | (p) => p.value.type === 'Literal' 61 | ) 62 | ) { 63 | if ( 64 | key === 'checked' && 65 | !(typeof value === 'boolean' || value === 'mixed') 66 | ) { 67 | error( 68 | `accessibilityState object: "checked" value is not either a boolean or 'mixed'` 69 | ); 70 | } else if (key !== 'checked' && typeof value !== 'boolean') { 71 | error( 72 | `accessibilityState object: "${key}" value is not a boolean` 73 | ); 74 | } 75 | } 76 | }); 77 | } 78 | } 79 | }, 80 | }), 81 | }; 82 | -------------------------------------------------------------------------------- /docs/rules/has-valid-accessibility-role.md: -------------------------------------------------------------------------------- 1 | # has-valid-accessibility-role 2 | 3 | *Note*: `accessibilityRole` and `accessibilityStates` are meant to be a cross-platform solution to replace `accessibilityTraits` and `accessibilityComponentType`, which will soon be deprecated. When possible, use `accessibilityRole` and `accessibilityStates` instead of `accessibilityTraits` and `accessibilityComponentType`. 4 | 5 | The accessibilityRole property is used to tell Talkback or Voiceover the role of a UI Element. 6 | 7 | ## Values may be one of the following 8 | 9 | - `"adjustable"`: Used when an element can be "adjusted" (e.g. a slider). 10 | - `"alert"`: Used when an element contains important text to be presented to the user. 11 | - `"button"`: Used when the element should be treated as a button. 12 | - `"checkbox"`: Used when an element represents a checkbox which can be checked, unchecked, or have mixed checked state. 13 | - `"combobox"`: Used when an element represents a combo box, which allows the user to select among several choices. 14 | - `"header"`: Used when an element acts as a header for a content section (e.g. the title of a navigation bar). 15 | - `"image"`: Used when the element should be treated as an image. Can be combined with button or link, for example. 16 | - `"imagebutton"`: Used when the element should be treated as a button and is also an image. 17 | - `"keyboardkey"`: Used when the element acts as a keyboard key. 18 | - `"link"`: Used when the element should be treated as a link. 19 | - `"menu"`: Used when the component is a menu of choices. 20 | - `"menubar"`: Used when a component is a container of multiple menus. 21 | - `"menuitem"`: Used to represent an item within a menu. 22 | - `"none"`: Used when the element has no role. 23 | - `"progressbar"`: Used to represent a component which indicates progress of a task. 24 | - `"radio"`: Used to represent a radio button. 25 | - `"radiogroup"`: Used to represent a group of radio buttons. 26 | - `"scrollbar"`: Used to represent a scroll bar. 27 | - `"search"`: Used when the text field element should also be treated as a search field. 28 | - `"spinbutton"`: Used to represent a button which opens a list of choices. 29 | - `"summary"`: Used when an element can be used to provide a quick summary of current conditions in the app when the app first launches. 30 | - `"switch"`: Used to represent a switch which can be turned on and off. 31 | - `"tab"`: Used to represent a tab. 32 | - `"tablist"`: Used to represent a list of tabs. 33 | - `"text"`: Used when the element should be treated as static text that cannot change. 34 | - `"timer"`: Used to represent a timer. 35 | - `"toolbar"`: Used to represent a tool bar (a container of action buttons or components). 36 | 37 | ### References 38 | 39 | 1. [React Native Docs - accessibilityRole](https://facebook.github.io/react-native/docs/accessibility.html#accessibilityrole-ios-android) 40 | 41 | ## Rule details 42 | 43 | This rule takes no arguments. 44 | 45 | ### Succeed 46 | 47 | ```jsx 48 | 49 | ``` 50 | 51 | ### Fail 52 | 53 | ```jsx 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /src/rules/has-valid-accessibility-value.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Represents the current value of a component. 3 | * @author JP Driver 4 | * @flow 5 | */ 6 | 7 | import type { JSXOpeningElement } from 'ast-types-flow'; 8 | import { getPropValue, hasProp } from 'jsx-ast-utils'; 9 | import { generateObjSchema } from '../util/schemas'; 10 | import type { ESLintContext } from '../../flow/eslint'; 11 | 12 | // ---------------------------------------------------------------------------- 13 | // Rule Definition 14 | // ---------------------------------------------------------------------------- 15 | 16 | const PROP_NAME = 'accessibilityValue'; 17 | 18 | module.exports = { 19 | meta: { 20 | docs: {}, 21 | schema: [generateObjSchema()], 22 | }, 23 | 24 | create: (context: ESLintContext) => ({ 25 | JSXOpeningElement: (node: JSXOpeningElement) => { 26 | if (hasProp(node.attributes, PROP_NAME)) { 27 | const valueProp = node.attributes.find( 28 | // $FlowFixMe 29 | (f) => f.name?.name === PROP_NAME 30 | ); 31 | const valuePropType = 32 | // $FlowFixMe 33 | valueProp.value.expression?.type || valueProp.value.type; 34 | 35 | const error = (message) => 36 | context.report({ 37 | node, 38 | message, 39 | }); 40 | 41 | if (valuePropType === 'Literal') { 42 | error('accessibilityValue must be an object'); 43 | } else if (valuePropType === 'ObjectExpression') { 44 | const attrValue = getPropValue(valueProp); 45 | const keys = Object.keys(attrValue); 46 | 47 | // $FlowFixMe 48 | const properties = valueProp.value.expression?.properties || []; 49 | 50 | if (keys.includes('text')) { 51 | if (keys.length > 1) { 52 | error( 53 | 'accessibilityValue object must only contain either min, now, max *or* text' 54 | ); 55 | } 56 | // $FlowFixMe 57 | properties.forEach(({ key, value }) => { 58 | if ( 59 | key.name === 'text' && 60 | // $FlowFixMe 61 | value.type === 'Literal' && 62 | // $FlowFixMe 63 | typeof value.value !== 'string' 64 | ) { 65 | error('accessibilityValue text value must be a string'); 66 | } 67 | }); 68 | } else { 69 | ['min', 'max', 'now'].forEach((key) => { 70 | if (!keys.includes(key)) { 71 | error(`accessibilityValue object is missing ${key} value`); 72 | } 73 | }); 74 | 75 | // $FlowFixMe 76 | properties.forEach(({ key, value }) => { 77 | // $FlowFixMe 78 | if (value.type === 'Literal' && typeof value.value !== 'number') { 79 | error( 80 | // $FlowFixMe 81 | `accessibilityValue ${key.name} value must be an integer` 82 | ); 83 | } 84 | }); 85 | } 86 | } 87 | } 88 | }, 89 | }), 90 | }; 91 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | const defaultConfig = { 4 | parserOptions: { 5 | ecmaFeatures: { 6 | jsx: true, 7 | }, 8 | }, 9 | plugins: ['react-native-a11y'], 10 | }; 11 | 12 | const basicRules = { 13 | 'react-native-a11y/has-accessibility-hint': 'error', 14 | 'react-native-a11y/has-accessibility-props': 'error', 15 | 'react-native-a11y/has-valid-accessibility-actions': 'error', 16 | 'react-native-a11y/has-valid-accessibility-component-type': 'error', 17 | 'react-native-a11y/has-valid-accessibility-descriptors': 'error', 18 | 'react-native-a11y/has-valid-accessibility-role': 'error', 19 | 'react-native-a11y/has-valid-accessibility-state': 'error', 20 | 'react-native-a11y/has-valid-accessibility-states': 'error', 21 | 'react-native-a11y/has-valid-accessibility-traits': 'error', 22 | 'react-native-a11y/has-valid-accessibility-value': 'error', 23 | 'react-native-a11y/no-nested-touchables': 'error', 24 | }; 25 | 26 | const iOSRules = { 27 | 'react-native-a11y/has-valid-accessibility-ignores-invert-colors': 'error', 28 | }; 29 | 30 | const AndroidRules = { 31 | 'react-native-a11y/has-valid-accessibility-live-region': 'error', 32 | 'react-native-a11y/has-valid-important-for-accessibility': 'error', 33 | }; 34 | 35 | module.exports = { 36 | rules: { 37 | 'has-accessibility-hint': require('./rules/has-accessibility-hint'), 38 | 'has-accessibility-props': require('./rules/has-accessibility-props'), 39 | 'has-valid-accessibility-actions': require('./rules/has-valid-accessibility-actions'), 40 | 'has-valid-accessibility-component-type': require('./rules/has-valid-accessibility-component-type'), 41 | 'has-valid-accessibility-descriptors': require('./rules/has-valid-accessibility-descriptors'), 42 | 'has-valid-accessibility-ignores-invert-colors': require('./rules/has-valid-accessibility-ignores-invert-colors'), 43 | 'has-valid-accessibility-live-region': require('./rules/has-valid-accessibility-live-region'), 44 | 'has-valid-accessibility-role': require('./rules/has-valid-accessibility-role'), 45 | 'has-valid-accessibility-state': require('./rules/has-valid-accessibility-state'), 46 | 'has-valid-accessibility-states': require('./rules/has-valid-accessibility-states'), 47 | 'has-valid-accessibility-traits': require('./rules/has-valid-accessibility-traits'), 48 | 'has-valid-accessibility-value': require('./rules/has-valid-accessibility-value'), 49 | 'has-valid-important-for-accessibility': require('./rules/has-valid-important-for-accessibility'), 50 | 'no-nested-touchables': require('./rules/no-nested-touchables'), 51 | }, 52 | configs: { 53 | basic: { 54 | ...defaultConfig, 55 | rules: basicRules, 56 | }, 57 | ios: { 58 | ...defaultConfig, 59 | rules: { 60 | ...basicRules, 61 | ...iOSRules, 62 | }, 63 | }, 64 | android: { 65 | ...defaultConfig, 66 | rules: { 67 | ...basicRules, 68 | ...AndroidRules, 69 | }, 70 | }, 71 | all: { 72 | ...defaultConfig, 73 | rules: { 74 | ...basicRules, 75 | ...iOSRules, 76 | ...AndroidRules, 77 | }, 78 | }, 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /__tests__/src/rules/has-valid-accessibility-descriptors-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /** 3 | * @fileoverview Ensures that Touchable* components have appropriate props to communicate with assistive technologies 4 | * @author JP Driver 5 | */ 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Requirements 9 | // ----------------------------------------------------------------------------- 10 | 11 | import { RuleTester } from 'eslint'; 12 | import parserOptionsMapper from '../../__util__/parserOptionsMapper'; 13 | import rule from '../../../src/rules/has-valid-accessibility-descriptors'; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | 21 | const expectedError = { 22 | message: 23 | 'Missing a11y props. Expected one of: accessibilityRole OR role OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction', 24 | type: 'JSXOpeningElement', 25 | }; 26 | 27 | ruleTester.run('has-valid-accessibility-descriptors', rule, { 28 | valid: [ 29 | { 30 | code: ';', 31 | }, 32 | { 33 | code: ` 34 | Back 35 | `, 36 | }, 37 | { 38 | code: ` 39 | Back 40 | `, 41 | }, 42 | { 43 | code: ` 46 | Back 47 | `, 48 | }, 49 | { 50 | code: ` { 57 | switch (event.nativeEvent.actionName) { 58 | case 'cut': 59 | Alert.alert('Alert', 'cut action success'); 60 | break; 61 | case 'copy': 62 | Alert.alert('Alert', 'copy action success'); 63 | break; 64 | case 'paste': 65 | Alert.alert('Alert', 'paste action success'); 66 | break; 67 | } 68 | }} 69 | />`, 70 | }, 71 | { 72 | code: ``, 73 | }, 74 | { 75 | code: ` 76 | Back 77 | `, 78 | }, 79 | { 80 | code: ``, 81 | }, 82 | ].map(parserOptionsMapper), 83 | invalid: [ 84 | { 85 | code: ` 86 | Back 87 | `, 88 | errors: [expectedError], 89 | output: ` 90 | Back 91 | `, 92 | }, 93 | { 94 | code: ``, 95 | errors: [expectedError], 96 | output: ``, 97 | }, 98 | ].map(parserOptionsMapper), 99 | }); 100 | -------------------------------------------------------------------------------- /__tests__/src/rules/has-valid-accessibility-value-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /** 3 | * @fileoverview Represents the current value of a component. 4 | * @author JP Driver 5 | */ 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Requirements 9 | // ----------------------------------------------------------------------------- 10 | 11 | import { RuleTester } from 'eslint'; 12 | import parserOptionsMapper from '../../__util__/parserOptionsMapper'; 13 | import rule from '../../../src/rules/has-valid-accessibility-value'; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | 21 | ruleTester.run('has-valid-accessibility-value', rule, { 22 | valid: [ 23 | { 24 | code: '', 25 | }, 26 | { code: '' }, 27 | { 28 | code: ``, 31 | }, 32 | { 33 | code: ``, 39 | }, 40 | { 41 | code: ``, 45 | }, 46 | ].map(parserOptionsMapper), 47 | invalid: [ 48 | { 49 | code: '', 50 | errors: [ 51 | { 52 | message: 53 | 'accessibilityValue object must only contain either min, now, max *or* text', 54 | type: 'JSXOpeningElement', 55 | }, 56 | ], 57 | }, 58 | { 59 | code: '', 60 | errors: [ 61 | { 62 | message: 'accessibilityValue object is missing min value', 63 | type: 'JSXOpeningElement', 64 | }, 65 | { 66 | message: 'accessibilityValue object is missing max value', 67 | type: 'JSXOpeningElement', 68 | }, 69 | ], 70 | }, 71 | { 72 | code: '', 73 | errors: [ 74 | { 75 | message: 'accessibilityValue must be an object', 76 | type: 'JSXOpeningElement', 77 | }, 78 | ], 79 | }, 80 | { 81 | code: '', 82 | errors: [ 83 | { 84 | message: 'accessibilityValue min value must be an integer', 85 | type: 'JSXOpeningElement', 86 | }, 87 | { 88 | message: 'accessibilityValue now value must be an integer', 89 | type: 'JSXOpeningElement', 90 | }, 91 | { 92 | message: 'accessibilityValue max value must be an integer', 93 | type: 'JSXOpeningElement', 94 | }, 95 | ], 96 | }, 97 | { 98 | code: '', 99 | errors: [ 100 | { 101 | message: 'accessibilityValue text value must be a string', 102 | type: 'JSXOpeningElement', 103 | }, 104 | ], 105 | }, 106 | ].map(parserOptionsMapper), 107 | }); 108 | -------------------------------------------------------------------------------- /__tests__/src/rules/has-valid-accessibility-role-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /** 3 | * @fileoverview Used to tell Talkback or Voiceover the role of a UI Element 4 | * @author Jen Luker 5 | */ 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Requirements 9 | // ----------------------------------------------------------------------------- 10 | 11 | import { RuleTester } from 'eslint'; 12 | import parserOptionsMapper from '../../__util__/parserOptionsMapper'; 13 | import rule from '../../../src/rules/has-valid-accessibility-role'; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | 21 | const expectedError = { 22 | message: 'accessibilityRole must be one of defined values', 23 | type: 'JSXAttribute', 24 | }; 25 | 26 | ruleTester.run('has-valid-accessibility-role', rule, { 27 | valid: [ 28 | { code: ';' }, 29 | { code: ';' }, 30 | { code: ';' }, 31 | { code: ';' }, 32 | { code: ';' }, 33 | { code: ';' }, 34 | { code: ';' }, 35 | { code: ';' }, 36 | { code: ';' }, 37 | { code: ';' }, 38 | { code: ';' }, 39 | { code: ';' }, 40 | { code: ';' }, 41 | { code: ';' }, 42 | { code: ';' }, 43 | { code: ';' }, 44 | { code: ';' }, 45 | { code: ';' }, 46 | { code: ';' }, 47 | { code: ';' }, 48 | { code: ';' }, 49 | { code: ';' }, 50 | { code: ';' }, 51 | { code: ';' }, 52 | { code: ';' }, 53 | { code: ';' }, 54 | { code: ';' }, 55 | { 56 | code: ``, 61 | }, 62 | ].map(parserOptionsMapper), 63 | invalid: [ 64 | { 65 | code: '', 66 | errors: [expectedError], 67 | }, 68 | { 69 | code: ';', 70 | errors: [expectedError], 71 | }, 72 | { 73 | code: ';', 74 | errors: [expectedError], 75 | }, 76 | { 77 | code: '', 78 | errors: [expectedError], 79 | }, 80 | ].map(parserOptionsMapper), 81 | }); 82 | -------------------------------------------------------------------------------- /docs/rules/has-valid-accessibility-ignores-invert-colors.md: -------------------------------------------------------------------------------- 1 | # has-valid-accessibility-ignores-invert-colors 2 | 3 | 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. 4 | 5 | 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`. 6 | 7 | `accessibilityIgnoresInvertColors` is usually used on elements like `` -- however in some cases it may be used on a parent wrapper. 8 | 9 | For example, both of the following snippets are valid (and will achieve the same result in practice). 10 | 11 | ```js 12 | 13 | 14 | 15 | ``` 16 | 17 | ```js 18 | 19 | 20 | 21 | ``` 22 | 23 | ## Values may be one of the following (boolean) 24 | 25 | - `true`: colors of everything in this view will *not* be inverted when color inversion is enabled 26 | - `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 27 | 28 | ### References 29 | 30 | 1. [React Native accessibility documentation](http://facebook.github.io/react-native/docs/accessibility#accessibilityignoresinvertcolorsios) 31 | 2. [accessibilityIgnoresInvertColors Apple developer docs](https://developer.apple.com/documentation/uikit/uiview/2865843-accessibilityignoresinvertcolors) 32 | 33 | ## Rule details 34 | 35 | By default, the rule will only check ``. 36 | 37 | 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. 38 | 39 | `invertableComponents` should be an Array of component names as strings. 40 | 41 | ```js 42 | "react-native-a11y/has-valid-accessibility-ignores-invert-colors": [ 43 | "error", 44 | { 45 | "invertableComponents": [ 46 | "FastImage", 47 | "MyCustomComponent", 48 | ... 49 | ] 50 | } 51 | ] 52 | ``` 53 | 54 | ```js 55 | {/* invalid, rule will error */} 56 | 57 | 58 | 59 | 60 | 61 | 62 | {/* valid */} 63 | 64 | 65 | 66 | 67 | 68 | ``` 69 | 70 | These extra `invertableComponents` will also be checked in addition to ``. 71 | 72 | ### Succeed 73 | ```jsx 74 | 75 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ``` 87 | 88 | ### Fail 89 | ```jsx 90 | 91 | 95 | 96 | 97 | 98 | 99 | 100 | ``` 101 | -------------------------------------------------------------------------------- /src/rules/has-valid-accessibility-actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Allow an assistive technology to programmatically invoke the actions of a component. 3 | * @author JP Driver 4 | * @flow 5 | */ 6 | 7 | import type { JSXOpeningElement } from 'ast-types-flow'; 8 | import { getProp, getPropValue, hasEveryProp, hasProp } from 'jsx-ast-utils'; 9 | import isNodePropExpression from '../util/isNodePropExpression'; 10 | import { generateObjSchema } from '../util/schemas'; 11 | import type { ESLintContext } from '../../flow/eslint'; 12 | 13 | // ---------------------------------------------------------------------------- 14 | // Rule Definition 15 | // ---------------------------------------------------------------------------- 16 | 17 | const standardActions = [ 18 | 'magicTap', // iOS only 19 | 'escape', // iOS only 20 | 'activate', 21 | 'increment', 22 | 'decrement', 23 | 'longpress', // Android only 24 | ]; 25 | 26 | module.exports = { 27 | meta: { 28 | docs: {}, 29 | schema: [generateObjSchema()], 30 | }, 31 | 32 | create: (context: ESLintContext) => ({ 33 | JSXOpeningElement: (node: JSXOpeningElement) => { 34 | const error = (message) => 35 | context.report({ 36 | node, 37 | message, 38 | }); 39 | 40 | if ( 41 | hasEveryProp(node.attributes, [ 42 | 'accessibilityActions', 43 | 'onAccessibilityAction', 44 | ]) 45 | ) { 46 | const handlerProp = getProp(node.attributes, 'onAccessibilityAction'); 47 | const isHandlerExpression = isNodePropExpression(handlerProp); 48 | if (!isHandlerExpression) { 49 | const handlerPropValue = getPropValue(handlerProp); 50 | if (typeof handlerPropValue !== 'function') { 51 | error( 52 | 'accessibilityActions: has accessibilityActions but onAccessibilityAction is not a function' 53 | ); 54 | } 55 | } 56 | 57 | const actionsProp = getProp(node.attributes, 'accessibilityActions'); 58 | const isActionsExpression = isNodePropExpression(actionsProp); 59 | if (!isActionsExpression) { 60 | const attrValue = getPropValue(actionsProp); 61 | 62 | if (!Array.isArray(attrValue)) { 63 | error('accessibilityActions: value must be an Array'); 64 | } else if (attrValue.length === 0) { 65 | error('accessibilityActions: Array cannot be empty'); 66 | } else { 67 | attrValue.forEach((action) => { 68 | if (!action.name) { 69 | error('accessibilityActions: action missing name'); 70 | } else if ( 71 | standardActions.indexOf(action.name) < 0 && 72 | !action.label 73 | ) { 74 | error( 75 | `accessibilityActions: custom action "${action.name}" missing label` 76 | ); 77 | } 78 | if ( 79 | Object.keys(action).filter((f) => f !== 'name' && f !== 'label') 80 | .length > 0 81 | ) { 82 | error( 83 | `accessibilityActions: action "${action.name}" contains unrecognised keys` 84 | ); 85 | } 86 | }); 87 | } 88 | } 89 | } else { 90 | if (hasProp(node.attributes, 'accessibilityActions')) { 91 | error( 92 | 'accessibilityActions: has accessibilityActions but onAccessibilityAction is not a function' 93 | ); 94 | } else if (hasProp(node.attributes, 'onAccessibilityAction')) { 95 | error( 96 | 'accessibilityActions: has onAccessibilityAction function but no accessibilityActions Array' 97 | ); 98 | } 99 | } 100 | }, 101 | }), 102 | }; 103 | -------------------------------------------------------------------------------- /docs/rules/has-valid-accessibility-actions.md: -------------------------------------------------------------------------------- 1 | # has-valid-accessibility-actions 2 | 3 | Accessibility actions allow an assistive technology to programmatically invoke the actions of a component. In order to support accessibility actions, a component must do two things: 4 | 5 | - Define the list of actions it supports via the `accessibilityActions` property. 6 | - Implement an `onAccessibilityAction` function to handle action requests. 7 | 8 | ## `acccessibilityActions` is an Array containing a list of action objects. 9 | 10 | There are two types of actions: Standard Actions, and Custom Actions. 11 | 12 | Depending on the action type, the action should contain the following fields:- 13 | 14 | NAME|TYPE|REQUIRED 15 | -|-|- 16 | `name`|string|**Yes** 17 | `label`|string|Only required for Custom actions 18 | 19 | ### Standard Actions 20 | 21 | Standard Actions must have a `name` field matching one of:- 22 | 23 | NAME|PLATFORM SUPPORT 24 | -|- 25 | `"magicTap"`|iOS only 26 | `"escape"`|iOS only 27 | `"activate"`|both iOS & Android 28 | `"increment"`|both iOS & Android 29 | `"decrement"`|both iOS & Android 30 | `"longpress"`|Android only 31 | 32 | Providing a `label` for a Standard Action is optional. 33 | 34 | ```js 35 | accessibilityActions={[ 36 | {name:'magicTap'} 37 | ]} 38 | ``` 39 | 40 | ### Custom Actions 41 | 42 | Custom Actions can have any `name`, but must also include a `label`. 43 | 44 | ```js 45 | accessibilityActions={[ 46 | {name: 'cut', label: 'cut'} 47 | ]} 48 | ``` 49 | 50 | ## `onAccessibilityAction` is a function. 51 | 52 | The only argument to this function is an event containing the name of the action to perform. 53 | 54 | e.g. 55 | ```js 56 | { 64 | switch (event.nativeEvent.actionName) { 65 | case 'cut': 66 | Alert.alert('Alert', 'cut action success'); 67 | break; 68 | case 'copy': 69 | Alert.alert('Alert', 'copy action success'); 70 | break; 71 | case 'paste': 72 | Alert.alert('Alert', 'paste action success'); 73 | break; 74 | } 75 | }} 76 | /> 77 | ``` 78 | 79 | ### References 80 | 81 | 1. [React Native Docs - Accessibility Actions](https://facebook.github.io/react-native/docs/accessibility#accessibility-actions) 82 | 83 | ## Rule details 84 | 85 | This rule takes no arguments. 86 | 87 | ### Succeed 88 | 89 | ```js 90 | { 96 | switch (event.nativeEvent.actionName) { 97 | case 'cut': 98 | Alert.alert('Alert', 'cut action success'); 99 | break; 100 | case 'copy': 101 | Alert.alert('Alert', 'copy action success'); 102 | break; 103 | } 104 | }} 105 | /> 106 | ``` 107 | 108 | ```js 109 | { 114 | switch (event.nativeEvent.actionName) { 115 | case 'magicTap': 116 | Alert.alert('Alert', 'magicTap action success'); 117 | break; 118 | } 119 | }} 120 | /> 121 | ``` 122 | 123 | ### Fail 124 | 125 | ```js 126 | 131 | 132 | // no onAccessibilityAction prop 133 | ``` 134 | 135 | ```js 136 | { 141 | switch (event.nativeEvent.actionName) { 142 | case 'cut': 143 | Alert.alert('Alert', 'cut action success'); 144 | break; 145 | } 146 | }} 147 | /> 148 | 149 | // custom action "cut" missing label 150 | ``` 151 | -------------------------------------------------------------------------------- /__tests__/src/rules/has-valid-accessibility-state-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /** 3 | * @fileoverview Describes the current state of a component to the user of an assistive technology. 4 | * @author JP Driver 5 | */ 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Requirements 9 | // ----------------------------------------------------------------------------- 10 | 11 | import { RuleTester } from 'eslint'; 12 | import parserOptionsMapper from '../../__util__/parserOptionsMapper'; 13 | import rule from '../../../src/rules/has-valid-accessibility-state'; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | 21 | const propMustBeAnObject = { 22 | message: 'accessibilityState must be an object', 23 | type: 'JSXOpeningElement', 24 | }; 25 | 26 | const invalidObjectKey = (key) => ({ 27 | message: `accessibilityState object: "${key}" is not a valid key`, 28 | type: 'JSXOpeningElement', 29 | }); 30 | 31 | const valueMustBeBoolean = (key) => ({ 32 | message: `accessibilityState object: "${key}" value is not a boolean`, 33 | type: 'JSXOpeningElement', 34 | }); 35 | 36 | const checkedMustBeBooleanOrMixed = { 37 | message: `accessibilityState object: "checked" value is not either a boolean or 'mixed'`, 38 | type: 'JSXOpeningElement', 39 | }; 40 | 41 | ruleTester.run('has-valid-accessibility-state', rule, { 42 | valid: [ 43 | { code: ';' }, 44 | { code: ';' }, 45 | { code: ';' }, 46 | { 47 | code: ';', 48 | }, 49 | { 50 | code: `const active = true; 51 | 52 | const Component = () => ( 53 | 54 | );`, 55 | }, 56 | { 57 | code: `const itemChecked = true; 58 | 59 | <> 60 | 61 | 62 | `, 63 | }, 64 | { 65 | code: `const isFirst = () => { 66 | return something === "example"; 67 | } 68 | 69 | `, 74 | }, 75 | { 76 | code: `const myObj = { 77 | myBool: true 78 | }; 79 | 80 | `, 84 | }, 85 | { 86 | code: `const accessibilityState = disabled 87 | ? { disabled: true } 88 | : { disabled: false }; 89 | 90 | `, 93 | }, 94 | { 95 | code: ``, 99 | }, 100 | ].map(parserOptionsMapper), 101 | invalid: [ 102 | { 103 | code: '', 104 | errors: [propMustBeAnObject], 105 | }, 106 | { 107 | code: '', 108 | errors: [propMustBeAnObject], 109 | }, 110 | { 111 | code: '', 112 | errors: [valueMustBeBoolean('disabled')], 113 | }, 114 | { 115 | code: '', 116 | errors: [checkedMustBeBooleanOrMixed], 117 | }, 118 | { 119 | code: '', 120 | errors: [invalidObjectKey('foo')], 121 | }, 122 | { 123 | code: '', 124 | errors: [invalidObjectKey('foo')], 125 | }, 126 | { 127 | code: `const active = true; 128 | 129 | const Component = () => ( 130 | 131 | );`, 132 | errors: [valueMustBeBoolean('selected')], 133 | }, 134 | ].map(parserOptionsMapper), 135 | }); 136 | -------------------------------------------------------------------------------- /__tests__/src/rules/has-accessibility-props-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /** 3 | * @fileoverview Enforce that components only have either the accessibilityRole prop or both accessibilityTraits and accessibilityComponentType props set. 4 | * @author Alex Saunders 5 | */ 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Requirements 9 | // ----------------------------------------------------------------------------- 10 | 11 | import { RuleTester } from 'eslint'; 12 | import parserOptionsMapper from '../../__util__/parserOptionsMapper'; 13 | import rule from '../../../src/rules/has-accessibility-props'; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | 21 | const expectedError = (touchable) => ({ 22 | message: `<${touchable}> must only have either the accessibilityRole prop or both accessibilityTraits and accessibilityComponentType props set`, 23 | type: 'JSXOpeningElement', 24 | }); 25 | 26 | ruleTester.run('has-accessibility-props', rule, { 27 | valid: [ 28 | { code: '
;' }, 29 | { code: ';' }, 30 | { code: ';' }, 31 | { 32 | code: ';', 33 | }, 34 | { 35 | code: ';', 36 | }, 37 | { 38 | code: ';', 39 | }, 40 | { 41 | code: ';', 42 | }, 43 | { 44 | code: ';', 45 | }, 46 | { 47 | code: '
;', 48 | }, 49 | { 50 | code: '
;', 51 | }, 52 | { 53 | code: '
;', 54 | }, 55 | { 56 | code: '
;', 57 | }, 58 | { 59 | code: ';', 60 | }, 61 | { 62 | code: ';', 63 | }, 64 | { 65 | code: ';', 66 | }, 67 | { 68 | code: ';', 69 | }, 70 | { 71 | code: ';', 72 | }, 73 | { 74 | code: '
;', 75 | }, 76 | { 77 | code: '
;', 78 | }, 79 | { 80 | code: '
;', 81 | options: [ 82 | { 83 | touchables: ['TouchableFoo'], 84 | }, 85 | ], 86 | }, 87 | { 88 | code: '
;', 89 | options: [ 90 | { 91 | touchables: ['FooTouchable'], 92 | }, 93 | ], 94 | }, 95 | ].map(parserOptionsMapper), 96 | invalid: [ 97 | { 98 | code: ';', 99 | errors: [expectedError('TouchableOpacity')], 100 | }, 101 | { 102 | code: ';', 103 | errors: [expectedError('TouchableOpacity')], 104 | }, 105 | { 106 | code: ';', 107 | errors: [expectedError('TouchableHighlight')], 108 | }, 109 | { 110 | code: ';', 111 | errors: [expectedError('TouchableHighlight')], 112 | }, 113 | { 114 | code: ';', 115 | errors: [expectedError('TouchableWithoutFeedback')], 116 | }, 117 | { 118 | code: ';', 119 | errors: [expectedError('TouchableWithoutFeedback')], 120 | }, 121 | { 122 | code: ';', 123 | errors: [expectedError('TouchableNativeFeedback')], 124 | }, 125 | { 126 | code: ';', 127 | errors: [expectedError('TouchableNativeFeedback')], 128 | }, 129 | { 130 | code: ';', 131 | errors: [expectedError('TouchableOpacity')], 132 | }, 133 | { 134 | code: ';', 135 | errors: [expectedError('TouchableOpacity')], 136 | }, 137 | { 138 | code: ';', 139 | errors: [expectedError('TouchableOpacity')], 140 | }, 141 | ].map(parserOptionsMapper), 142 | }); 143 | -------------------------------------------------------------------------------- /src/rules/has-valid-accessibility-ignores-invert-colors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce accessibilityIgnoresInvertColors property value is a boolean. 3 | * @author Dominic Coelho 4 | * @flow 5 | */ 6 | 7 | // ---------------------------------------------------------------------------- 8 | // Rule Definition 9 | // ---------------------------------------------------------------------------- 10 | 11 | import { generateObjSchema } from '../util/schemas'; 12 | import { elementType, getProp, hasProp } from 'jsx-ast-utils'; 13 | import type { JSXElement } from 'ast-types-flow'; 14 | import type { ESLintContext } from '../../flow/eslint'; 15 | import isNodePropValueBoolean from '../util/isNodePropValueBoolean'; 16 | 17 | const propName = 'accessibilityIgnoresInvertColors'; 18 | const schema = generateObjSchema(); 19 | 20 | const defaultInvertableComponents = ['Image']; 21 | 22 | const hasValidIgnoresInvertColorsProp = ({ attributes }) => 23 | hasProp(attributes, propName) && 24 | isNodePropValueBoolean(getProp(attributes, propName)); 25 | 26 | const checkParent = ({ openingElement, parent }) => { 27 | if (hasValidIgnoresInvertColorsProp(openingElement)) { 28 | return false; 29 | } else if (parent.openingElement) { 30 | return checkParent(parent); 31 | } 32 | return true; 33 | }; 34 | 35 | type VerifyRNImageRes = { 36 | enableLinting: boolean, 37 | elementsToCheck: string[], 38 | }; 39 | 40 | /** 41 | * @description varifies that the Image asset is imported from 'react-native' otherwise exits linting 42 | */ 43 | const verifyReactNativeImage = (text: string): VerifyRNImageRes => { 44 | const res: VerifyRNImageRes = { 45 | enableLinting: true, 46 | elementsToCheck: defaultInvertableComponents, 47 | }; 48 | 49 | // Escape hatch for tests 50 | if (!text.match(new RegExp(/import/, 'g'))) { 51 | return res; 52 | } 53 | 54 | // Flow has issues with String.raw 55 | // $FlowFixMe 56 | const namedSelector = String.raw`(import\s{)(.*)(\bImage\b)(.*)(}\sfrom\s'react-native')`; 57 | // $FlowFixMe 58 | const es6moduleSelector = String.raw`(?<=Image as )(.*?)(?=} from 'react-native')`; 59 | 60 | const imageSourceReactNativeRegExp = new RegExp(`${namedSelector}`, 'gs'); 61 | const imageSourceReactNativeAliasRegExp = new RegExp( 62 | `${es6moduleSelector}`, 63 | 'gs' 64 | ); 65 | 66 | const matchedImage = text.match(imageSourceReactNativeRegExp) || []; 67 | const matchedAliasImage = text.match(imageSourceReactNativeAliasRegExp) || []; 68 | 69 | res.enableLinting = 70 | matchedImage.length === 1 || matchedAliasImage.length === 1; 71 | 72 | if (matchedAliasImage.length === 1) { 73 | res.elementsToCheck = [matchedAliasImage[0].trim()]; 74 | } 75 | 76 | return res; 77 | }; 78 | 79 | module.exports = { 80 | meta: { 81 | docs: {}, 82 | schema: [schema], 83 | }, 84 | functions: { 85 | verifyReactNativeImage, 86 | }, 87 | 88 | create: ({ options, report, getSourceCode, sourceCode }: ESLintContext) => { 89 | /** 90 | * Checks to see if there are valid imports and if so verifies that those imports related to 'react-native' or if a custom module exists 91 | * */ 92 | const { text } = sourceCode || getSourceCode(); 93 | const { enableLinting, elementsToCheck } = verifyReactNativeImage(text); 94 | 95 | // Add in any other invertible components to check for 96 | if (options.length > 0) { 97 | const { invertableComponents } = options[0]; 98 | if (invertableComponents) { 99 | elementsToCheck.push(...invertableComponents); 100 | } 101 | } 102 | 103 | // Exit process if there is nothing to check 104 | if (!enableLinting && options.length === 0) { 105 | return {}; 106 | } 107 | 108 | return { 109 | JSXElement: (node: JSXElement) => { 110 | // $FlowFixMe 111 | const { children, openingElement, parent } = node; 112 | 113 | if ( 114 | hasProp(openingElement.attributes, propName) && 115 | !isNodePropValueBoolean(getProp(openingElement.attributes, propName)) 116 | ) { 117 | report({ 118 | node, 119 | message: 120 | 'accessibilityIgnoresInvertColors prop is not a boolean value', 121 | }); 122 | } else { 123 | if (options.length > 0) { 124 | const { invertableComponents } = options[0]; 125 | if (invertableComponents) { 126 | elementsToCheck.push(...invertableComponents); 127 | } 128 | } 129 | 130 | const type = elementType(openingElement); 131 | 132 | if ( 133 | elementsToCheck.indexOf(type) > -1 && 134 | !hasValidIgnoresInvertColorsProp(openingElement) && 135 | children.length === 0 136 | ) { 137 | let shouldReport = true; 138 | 139 | if (parent.openingElement) { 140 | shouldReport = checkParent(parent); 141 | } 142 | 143 | if (shouldReport) { 144 | report({ 145 | node, 146 | message: 147 | 'Found an element which will be inverted. Add the accessibilityIgnoresInvertColors prop', 148 | }); 149 | } 150 | } 151 | } 152 | }, 153 | }; 154 | }, 155 | }; 156 | -------------------------------------------------------------------------------- /__tests__/src/rules/has-valid-accessibility-actions-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /** 3 | * @fileoverview Allow an assistive technology to programmatically invoke the actions of a component. 4 | * @author JP Driver 5 | */ 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Requirements 9 | // ----------------------------------------------------------------------------- 10 | 11 | import { RuleTester } from 'eslint'; 12 | import parserOptionsMapper from '../../__util__/parserOptionsMapper'; 13 | import rule from '../../../src/rules/has-valid-accessibility-actions'; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | 21 | ruleTester.run('has-valid-accessibility-actions', rule, { 22 | valid: [ 23 | { 24 | code: ` { 31 | switch (event.nativeEvent.actionName) { 32 | case 'cut': 33 | Alert.alert('Alert', 'cut action success'); 34 | break; 35 | case 'copy': 36 | Alert.alert('Alert', 'copy action success'); 37 | break; 38 | case 'paste': 39 | Alert.alert('Alert', 'paste action success'); 40 | break; 41 | } 42 | }} 43 | />`, 44 | }, 45 | { 46 | code: ` { 51 | switch (event.nativeEvent.actionName) { 52 | case 'magicTap': 53 | Alert.alert('Alert', 'magicTap action success'); 54 | break; 55 | } 56 | }} 57 | />`, 58 | }, 59 | { 60 | code: ``, 64 | }, 65 | { 66 | code: `const onAccessibilityAction = (event) => { 67 | switch (event.nativeEvent.actionName) { 68 | case "delete": 69 | deleteAction(); 70 | break; 71 | default: 72 | Alert.alert("Some text"); 73 | } 74 | } 75 | 76 | const accessibilityActionsList = [{ name: "delete", label: "Delete" }]; 77 | 78 | `, 82 | }, 83 | { 84 | code: ``, 88 | }, 89 | ].map(parserOptionsMapper), 90 | invalid: [ 91 | { 92 | code: ``, 97 | errors: [ 98 | { 99 | message: 100 | 'accessibilityActions: has accessibilityActions but onAccessibilityAction is not a function', 101 | type: 'JSXOpeningElement', 102 | }, 103 | ], 104 | }, 105 | { 106 | code: ` { 108 | switch (event.nativeEvent.actionName) { 109 | case 'cut': 110 | Alert.alert('Alert', 'cut action success'); 111 | break; 112 | } 113 | }} 114 | />`, 115 | errors: [ 116 | { 117 | message: 118 | 'accessibilityActions: has onAccessibilityAction function but no accessibilityActions Array', 119 | type: 'JSXOpeningElement', 120 | }, 121 | ], 122 | }, 123 | { 124 | code: ` { 130 | switch (event.nativeEvent.actionName) { 131 | case 'cut': 132 | Alert.alert('Alert', 'cut action success'); 133 | break; 134 | } 135 | }} 136 | />`, 137 | errors: [ 138 | { 139 | message: 'accessibilityActions: value must be an Array', 140 | type: 'JSXOpeningElement', 141 | }, 142 | ], 143 | }, 144 | { 145 | code: ` { 148 | switch (event.nativeEvent.actionName) { 149 | case 'cut': 150 | Alert.alert('Alert', 'cut action success'); 151 | break; 152 | } 153 | }} 154 | />`, 155 | errors: [ 156 | { 157 | message: 'accessibilityActions: Array cannot be empty', 158 | type: 'JSXOpeningElement', 159 | }, 160 | ], 161 | }, 162 | { 163 | code: ` { 168 | switch (event.nativeEvent.actionName) { 169 | case 'cut': 170 | Alert.alert('Alert', 'cut action success'); 171 | break; 172 | } 173 | }} 174 | />`, 175 | errors: [ 176 | { 177 | message: 'accessibilityActions: custom action "cut" missing label', 178 | type: 'JSXOpeningElement', 179 | }, 180 | ], 181 | }, 182 | { 183 | code: ` { 188 | switch (event.nativeEvent.actionName) { 189 | case 'cut': 190 | Alert.alert('Alert', 'cut action success'); 191 | break; 192 | } 193 | }} 194 | />`, 195 | errors: [ 196 | { 197 | message: 'accessibilityActions: action missing name', 198 | type: 'JSXOpeningElement', 199 | }, 200 | ], 201 | }, 202 | { 203 | code: ` { 208 | switch (event.nativeEvent.actionName) { 209 | case 'cut': 210 | Alert.alert('Alert', 'cut action success'); 211 | break; 212 | } 213 | }} 214 | />`, 215 | errors: [ 216 | { 217 | message: 218 | 'accessibilityActions: action "cut" contains unrecognised keys', 219 | type: 'JSXOpeningElement', 220 | }, 221 | ], 222 | }, 223 | ].map(parserOptionsMapper), 224 | }); 225 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 3.5.1 4 | 5 | ### Patch Changes 6 | 7 | - Fix for has-valid-accessibility-ignores-invert-colors with eslint 9 ([#162](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/162)) 8 | 9 | ## 3.5.0 10 | 11 | ### Minor Changes 12 | 13 | - Allow aliasing Images ([#93](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/93)) 14 | 15 | ## 3.4.1 16 | 17 | ### Patch Changes 18 | 19 | - Remove changeset from deps ([#156](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/156)) 20 | 21 | ## 3.4.0 22 | 23 | ### Minor Changes 24 | 25 | - 0f2e813: Additional support for role and accessibility props 26 | 27 | ## V3.3.0 28 | 29 | ### ✨ New Features ✨ 30 | 31 | - Allow Eslint 8 as a peer dependency [(#145)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/145) 32 | 33 | ## V3.2.1 34 | 35 | ### 🐛 Bugfixes 🐛 36 | 37 | - update fixer for has-valid-accessibility-descriptors [(#136)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/136) 38 | 39 | ## V3.2.0 40 | 41 | ### ✨ New Features ✨ 42 | 43 | - make has-valid-accessibility-descriptors fixable [(#131)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/131) 44 | 45 | ### 🐛 Bugfixes 🐛 46 | 47 | - allow Identifiers in accessibilityState [(#129)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/129) 48 | - support spread props in accessibilityState [(#132)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/132) 49 | - allow Touchables with accessible={false} [(#130)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/130) 50 | - assume MemberExpressions are valid [(#133)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/133) 51 | - update has-accessibility-value to only typecheck Literals [(#134)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/134) 52 | - update has-accessibility-role typechecking [(#135)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/135) 53 | 54 | ## V3.1.0 55 | 56 | ### ✨ New Features ✨ 57 | 58 | - Checks Touchable\* components have accessibility props [(#128)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/128) 59 | 60 | ## V3.0.0 61 | 62 | ### 🚨 Breaking 🚨 63 | 64 | - This release removes support for Node 10 [(#126)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/126) 65 | 66 | ### ✨ New Features ✨ 67 | 68 | - Allow Eslint 7 as a peer dependency [(#111)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/111) 69 | 70 | ### 🐛 Bugfixes 🐛 71 | 72 | - only validate Literals in accessibilityState [(#112)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/112) 73 | - allow Identifiers in accessibilityActions [(#113)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/113) 74 | 75 | ## V2.0.4 76 | 77 | - include Pressable when checking `no-nested-touchables` [(#103](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/103) 78 | - Dependency upgrades [(#106)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/106) 79 | 80 | ## V2.0.3 81 | 82 | - allow CallExpressions in accessibilityActions [(#101)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/101) 83 | - Dependency upgrades [(#102)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/102) 84 | 85 | ## V2.0.2 86 | 87 | - Update accessibilityState to allow Identifiers for `checked` value [(#98)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/98) 88 | - Dev Dependency upgrades [(#99)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/99) 89 | 90 | ## V2.0.1 91 | 92 | - Allow Expressions in prop validators [(#96)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/96) 93 | - Dependency upgrades [(#95)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/95) 94 | 95 | ## V2.0.0 96 | 97 | - Minor doc improvements [(#78)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/78) 98 | - Dev Dependency upgrades [(#89)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/89) 99 | 100 | ## V2.0.0-rc2 101 | 102 | - Dev Dependency upgrades [(#88)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/88) 103 | 104 | ## V2.0.0-rc1 105 | 106 | - Ignore `Identifier` expressions in bool typechecks [(#85)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/85) 107 | - Enabled `no-unused-vars` [(#86)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/86) 108 | 109 | ## V2.0.0-rc0 110 | 111 | ### 🚨 Breaking 🚨 112 | 113 | - This release removes support for Node 8 [(#80)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/80) 114 | - The `has-valid-accessibility-state` rule has been re-written to cover the new `accessibilityState` implementation [(#60)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/60) 115 | - Deprecates the `recommended` config and introduces new platform-specific configs [(#83)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/83) 116 | 117 | ### ✨ New Features ✨ 118 | 119 | - Adds `has-valid-accessibility-value` rule for `accessibilityValue` prop [(#68)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/68) 120 | - Adds `has-valid-accessibility-actions` rule for `accessibilityActions` and `onAccessibilityAction` props [(#69)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/69) 121 | - Adds `has-valid-accessibility-ignores-invert-colors` rule for `accessibilityIgnoresInvertColors` [(#73)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/73) 122 | - Adds `has-accessibility-hint` for `accessibilityHint` [(#74)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/74) 123 | 124 | ### 🐛 Bugfixes 🐛 125 | 126 | - Removes `Touchable~` as a requirement for custom Touchable names [(#70)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/70) 127 | - Allows `Touchable`s without either `accessibilityRole` or both `accessibilityTraits` and `accessibilityComponentType` [(#81)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/81) 128 | - Removes `has-accessibility-label` rule [(#82)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/82) 129 | 130 | ## V1.3.1 131 | 132 | - Migrate to Babel v7 (to fix security issue) [(#67)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/67) 133 | 134 | ## V1.3.0 135 | 136 | - Adds support for modern `accessibilityRole`s [(#54)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/54) 137 | - Allow empty `accessibilityState` prop for `View` [(#48)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/48) 138 | - Allow empty `accessibilityState` prop for `Touchable*` [(#44)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/44) 139 | - `accessibilityRole` no longer required on Components with `accessible={false}` [(#43)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/43) 140 | - Support for ESLint version ^6 [(#57)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/57) 141 | - Adopted Prettier [(#51)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/51) 142 | - Dev Dependency upgrades [(#58)](https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/58) 143 | 144 | ## V1.2.0 145 | 146 | - Updated `accessibilityState` to `accessibilityStates` 147 | 148 | ## V1.1.0 149 | 150 | - Added support for `accessibilityRole` and `accessibilityState` 151 | - Added support for validating an array being passed by a prop. 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Eslint-plugin-react-native-a11y — Formidable, We build the modern web](https://oss.nearform.com/api/banner?text=eslint-plugin-native-a11y&bg=c99f46)](https://commerce.nearform.com/open-source/) 2 | 3 | 4 | [![Maintenance Status][maintenance-image]](#maintenance-status) 5 | 6 | Eslint-plugin-react-native-a11y is a collection of React Native specific ESLint rules for identifying accessibility issues. Building upon the foundation set down by eslint-plugin-jsx-a11y, eslint-plugin-react-native-a11y detects a few of the most commonly made accessibility issues found in react native apps. These rules make it easier for your apps to be navigable by users with screen readers. 7 | 8 | ## Setup 9 | 10 | ### Pre-Requisites 11 | 12 | Before starting, check you already have ESLint as a `devDependency` of your project. 13 | 14 | > Projects created using `react-native init` will already have this, but for Expo depending on your template you may need to follow ESLint's [installation instructions](https://eslint.org/docs/user-guide/getting-started#installation-and-usage). 15 | 16 | ### Installation 17 | 18 | Next, install `eslint-plugin-react-native-a11y`: 19 | 20 | ```sh 21 | npm install eslint-plugin-react-native-a11y --save-dev 22 | 23 | # or 24 | 25 | yarn add eslint-plugin-react-native-a11y --dev 26 | ``` 27 | 28 | **Note:** If you installed ESLint globally (using the `-g` flag in npm, or the `global` prefix in yarn) then you must also install `eslint-plugin-react-native-a11y` globally. 29 | 30 | ## Configuration 31 | 32 | This plugin exposes four recommended configs. 33 | 34 | | Name | Description | 35 | | ------- | ---------------------------------------------------------------------------------- | 36 | | basic | Only use basic validation rules common to both iOS & Android | 37 | | ios | Use all rules from "basic", plus iOS-specific extras | 38 | | android | Use all rules from "basic", plus Android-specific extras | 39 | | all | Use all rules from "basic", plus iOS-specific extras, plus Android-specific extras | 40 | 41 | If your project only supports a single platform, you may get the best experience using a platform-specific config. This will both avoid reporting issues which do not affect your platform and also results in slightly faster linting for larger projects. 42 | 43 | > If you are unsure which one to use, in most cases `all` can be safely used. 44 | 45 | Add the config you want to use to the `extends` section of your ESLint config using the pattern `plugin:react-native-a11y/` followed by your config name, as shown below: 46 | 47 | ```js 48 | // .eslintrc.js 49 | 50 | module.exports = { 51 | root: true, 52 | extends: ['@react-native-community', 'plugin:react-native-a11y/ios'], 53 | }; 54 | ``` 55 | 56 | Alternatively if you do not want to use one of the pre-defined configs — or want to override the behaviour of a specific rule — you can always include a list rules and configurations in the `rules` section of your ESLint config. 57 | 58 | ```js 59 | // .eslintrc.js 60 | 61 | module.exports = { 62 | root: true, 63 | extends: ['@react-native-community'], 64 | rules: { 65 | 'react-native-a11y/rule-name': 2, 66 | }, 67 | }; 68 | ``` 69 | 70 | For more information on configuring behaviour of an individual rule, please refer to the [ESLint docs](react-native-a11y/rule-name) 71 | 72 | ## Supported Rules 73 | 74 | ### Basic 75 | 76 | - [has-accessibility-hint](docs/rules/has-accessibility-hint.md): Enforce `accessibilityHint` is used in conjunction with `accessibilityLabel` 77 | - [has-accessibility-props](docs/rules/has-accessibility-props.md): Enforce that `` components only have either the `accessibilityRole` prop or both `accessibilityTraits` and `accessibilityComponentType` props set 78 | - [has-valid-accessibility-actions](docs/rules/has-valid-accessibility-actions.md): Enforce both `accessibilityActions` and `onAccessibilityAction` props are valid 79 | - [has-valid-accessibility-role](docs/rules/has-valid-accessibility-role.md): Enforce `accessibilityRole` property value is valid 80 | - [has-valid-accessibility-state](docs/rules/has-valid-accessibility-state.md): Enforce `accessibilityState` property value is valid 81 | - [has-valid-accessibility-states](docs/rules/has-valid-accessibility-states.md): Enforce `accessibilityStates` property value is valid 82 | - [has-valid-accessibility-component-type](docs/rules/has-valid-accessibility-component-type.md): Enforce `accessibilityComponentType` property value is valid 83 | - [has-valid-accessibility-traits](docs/rules/has-valid-accessibility-traits.md): Enforce `accessibilityTraits` and `accessibilityComponentType` prop values must be valid 84 | - [has-valid-accessibility-value](docs/rules/has-valid-accessibility-value.md): Enforce `accessibilityValue` property value is valid 85 | - [no-nested-touchables](docs/rules/no-nested-touchables.md): Enforce if a view has `accessible={true}`, that there are no touchable elements inside 86 | - [has-valid-accessibility-descriptors](docs/rules/has-valid-accessibility-descriptors.md): Ensures that Touchable* components have appropriate props to communicate with assistive technologies 87 | 88 | ### iOS 89 | 90 | - [has-valid-accessibility-ignores-invert-colors](docs/rules/has-valid-accessibility-ignores-invert-colors.md): Enforce that certain elements use `accessibilityIgnoresInvertColors` to avoid being inverted by device color settings. 91 | 92 | ### Android 93 | 94 | - [has-valid-accessibility-live-region](docs/rules/has-valid-accessibility-live-region.md): Enforce `accessibilityLiveRegion` prop values must be valid 95 | - [has-valid-important-for-accessibility](docs/rules/has-valid-important-for-accessibility.md): Enforce `importantForAccessibility` property value is valid 96 | 97 | ### Rule Options 98 | 99 | The following options are available to customize the recommended rule set. 100 | 101 | #### Custom Touchables 102 | 103 | `react-native-a11y/has-accessibility-props` and `react-native-a11y/no-nested-touchables` allow you to define an array of names for custom components that you may have that conform to the same accessibility interfaces as Touchables. 104 | 105 | ```js 106 | "react-native-a11y/has-accessibility-props": [ 107 | "error", 108 | { 109 | "touchables": ["TouchableCustom"] 110 | } 111 | ] 112 | ``` 113 | 114 | #### Custom Invertable Components (iOS) 115 | 116 | `react-native-a11y/has-valid-accessibility-ignores-invert-colors` allows you to optionally define an Array of component names to check in addition to ``. 117 | 118 | For more information, see the [rule docs](docs/has-valid-accessibility-ignores-invert-colors.md#rule-details). 119 | 120 | ```js 121 | "react-native-a11y/has-valid-accessibility-ignores-invert-colors": [ 122 | "error", 123 | { 124 | "invertableComponents": [ 125 | "FastImage", 126 | ] 127 | } 128 | ] 129 | ``` 130 | 131 | ## Creating a new rule 132 | 133 | If you are developing new rules for this project, you can use the `create-rule` 134 | script to scaffold the new files. 135 | 136 | ``` 137 | $ ./scripts/create-rule.js my-new-rule 138 | ``` 139 | 140 | ## Attribution 141 | 142 | This project started as a fork of [eslint-plugin-jsx-a11y](https://github.com/evcohen/eslint-plugin-jsx-a11y) and a lot of the work was carried out by its [contributors](https://github.com/evcohen/eslint-plugin-jsx-a11y/graphs/contributors), to whom we owe a lot! 143 | 144 | ## License 145 | 146 | eslint-plugin-react-native-a11y is licensed under the [MIT License](LICENSE.md). 147 | 148 | ### Maintenance Status 149 | 150 | **Active:** Nearform is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome. 151 | 152 | [maintenance-image]: https://img.shields.io/badge/maintenance-active-green.svg 153 | -------------------------------------------------------------------------------- /__tests__/src/rules/has-valid-accessibility-ignores-invert-colors-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /** 3 | * @fileoverview Ensure that accessibilityIgnoresInvertColors property value is a boolean. 4 | * @author Dominic Coelho 5 | */ 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Requirements 9 | // ----------------------------------------------------------------------------- 10 | 11 | import { RuleTester } from 'eslint'; 12 | import parserOptionsMapper from '../../__util__/parserOptionsMapper'; 13 | import rule from '../../../src/rules/has-valid-accessibility-ignores-invert-colors'; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | const ruleTester = new RuleTester(); 19 | 20 | const typeError = { 21 | message: 'accessibilityIgnoresInvertColors prop is not a boolean value', 22 | type: 'JSXElement', 23 | }; 24 | 25 | const missingPropError = { 26 | message: 27 | 'Found an element which will be inverted. Add the accessibilityIgnoresInvertColors prop', 28 | type: 'JSXElement', 29 | }; 30 | 31 | describe('verifyReactNativeImage', () => { 32 | it('returns true when importing a named export of Image from react-native', () => { 33 | const output = rule.functions 34 | .verifyReactNativeImage(`import { Text, Image, View } from 'react-native'; 35 | const Component = () => ( 36 | 37 | 38 | 39 | );`); 40 | 41 | expect(output.enableLinting).toBe(true); 42 | }); 43 | 44 | it('returns false when importing a named export of a Image from any other library', () => { 45 | const output = rule.functions 46 | .verifyReactNativeImage(`import { Text, Image, View } from './custom-image-component/Image'; 47 | const Component = () => ( 48 | 49 | 50 | 51 | );`); 52 | expect(output.enableLinting).toBe(false); 53 | }); 54 | 55 | /** 56 | * Super edge case if someone wants to alias ReactNative.Image as another component like RNImage and imports an Image from './any-library' 57 | */ 58 | it('returns true when provided a named export of Image that is aliased as something from react-native', () => { 59 | const output = rule.functions 60 | .verifyReactNativeImage(`import React from 'react' 61 | import {Image as RNImage} from 'react-native' 62 | 63 | const CustomImage = () => { 64 | return 65 | } 66 | 67 | export default CustomImage`); 68 | 69 | expect(output.enableLinting).toBe(true); 70 | }); 71 | }); 72 | 73 | const validCustomImportTests = [ 74 | { 75 | title: 'does not throw an error with custom Image components', 76 | code: `import { Text, Image, View } from './custom-image-component/Image'; 77 | const Component = () => ( 78 | 79 | 80 | 81 | );`, 82 | parserOptions: { 83 | sourceType: 'module', 84 | }, 85 | }, 86 | ]; 87 | 88 | const validCases = [ 89 | ...validCustomImportTests, 90 | { code: ';' }, 91 | { code: '' }, 92 | { code: '' }, 93 | { 94 | code: '', 95 | }, 96 | { 97 | code: '', 98 | }, 99 | { 100 | code: '', 101 | }, 102 | { 103 | code: '', 104 | }, 105 | { 106 | code: '', 107 | }, 108 | { 109 | code: '', 110 | options: [ 111 | { 112 | invertableComponents: ['FastImage'], 113 | }, 114 | ], 115 | }, 116 | { 117 | code: `const invertColors = true; 118 | 119 | const Component = () => ( 120 | 121 | );`, 122 | }, 123 | { 124 | code: ``, 125 | }, 126 | ]; 127 | 128 | const invalidCustomImport = [ 129 | { 130 | title: 131 | 'throws a missing prop error for custom components alongside passing Image that is imported from react-native', 132 | code: `import { 133 | Image, 134 | Button, 135 | FlatList, 136 | Platform, 137 | ScrollView, 138 | View, 139 | } from 'react-native'; 140 | import FastImage from './components/FastImage' 141 | const Component = () => ( 142 | 143 | 144 | 145 | 146 | );`, 147 | errors: [missingPropError], 148 | options: [ 149 | { 150 | invertableComponents: ['FastImage'], 151 | }, 152 | ], 153 | parserOptions: { 154 | sourceType: 'module', 155 | }, 156 | }, 157 | { 158 | title: 159 | 'throws a missingPropError for invertibleComponents and type error for Image when it is imported from react-native', 160 | code: `import { 161 | Image, 162 | Button, 163 | FlatList, 164 | Platform, 165 | ScrollView, 166 | View, 167 | } from 'react-native'; 168 | import FastImage from './components/FastImage' 169 | const Component = () => ( 170 | 171 | 172 | 173 | 174 | );`, 175 | errors: [missingPropError, typeError], 176 | options: [ 177 | { 178 | invertableComponents: ['FastImage'], 179 | }, 180 | ], 181 | parserOptions: { 182 | sourceType: 'module', 183 | }, 184 | }, 185 | { 186 | title: 'can throw multiple errors for custom and normal Image components', 187 | code: `import { 188 | Image, 189 | Button, 190 | FlatList, 191 | Platform, 192 | ScrollView, 193 | View, 194 | } from 'react-native'; 195 | import FastImage from './components/FastImage' 196 | const Component = () => ( 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | );`, 206 | errors: [ 207 | missingPropError, 208 | typeError, 209 | typeError, 210 | missingPropError, 211 | typeError, 212 | typeError, 213 | ], 214 | options: [ 215 | { 216 | invertableComponents: ['FastImage'], 217 | }, 218 | ], 219 | parserOptions: { 220 | sourceType: 'module', 221 | }, 222 | }, 223 | { 224 | title: 'should fail', 225 | code: `import React from 'react' 226 | import { Image } from 'react-native'; 227 | 228 | export const RNImage = (props) => 229 | `, 230 | errors: [missingPropError], 231 | parserOptions: { 232 | sourceType: 'module', 233 | }, 234 | }, 235 | { 236 | title: 237 | 'supports linting on Custom Invertable ImageComponents without react-native imported', 238 | code: `import { FastImage } from './fast-image' 239 | 240 | const Component = (props) => ( 241 | <> 242 | 243 | 244 | 245 | ); 246 | `, 247 | errors: [missingPropError, typeError], 248 | parserOptions: { 249 | sourceType: 'module', 250 | }, 251 | options: [ 252 | { 253 | invertableComponents: ['FastImage'], 254 | }, 255 | ], 256 | }, 257 | ]; 258 | 259 | const invalidCases = [ 260 | { 261 | code: '', 262 | errors: [typeError], 263 | }, 264 | { 265 | code: '', 266 | errors: [typeError], 267 | }, 268 | { 269 | code: '', 270 | errors: [typeError], 271 | }, 272 | { 273 | code: '', 274 | errors: [typeError], 275 | }, 276 | { 277 | code: ` 278 | 282 | `, 283 | errors: [typeError, missingPropError], 284 | }, 285 | { 286 | code: '', 287 | errors: [typeError], 288 | }, 289 | { 290 | code: '', 291 | errors: [typeError], 292 | }, 293 | { 294 | code: '', 295 | errors: [missingPropError], 296 | }, 297 | { 298 | code: '', 299 | errors: [missingPropError], 300 | }, 301 | { 302 | code: '', 303 | errors: [missingPropError], 304 | }, 305 | { 306 | code: '', 307 | errors: [missingPropError], 308 | options: [ 309 | { 310 | invertableComponents: ['FastImage'], 311 | }, 312 | ], 313 | }, 314 | ...invalidCustomImport, 315 | ]; 316 | 317 | /** 318 | * Solution to rule tester's dynamic title issue 319 | */ 320 | RuleTester.describe = function (text, method) { 321 | RuleTester.testId = 0; 322 | RuleTester.it.title = text; 323 | 324 | method.bind({ testId: 0 }); 325 | 326 | describe(`${RuleTester.it.title}`, method); 327 | }; 328 | 329 | RuleTester.it = function (text, method) { 330 | const computedTitle = eval(`${RuleTester.it.title}Cases`)[RuleTester.testId] 331 | .title; 332 | 333 | if (computedTitle) { 334 | describe(computedTitle, () => { 335 | it(text, method); 336 | }); 337 | } else { 338 | it(text, method); 339 | } 340 | 341 | RuleTester.testId += 1; 342 | }; 343 | 344 | ruleTester.run('has-valid-accessibility-ignores-invert-colors', rule, { 345 | valid: validCases.map(parserOptionsMapper), 346 | invalid: invalidCases.map(parserOptionsMapper), 347 | }); 348 | --------------------------------------------------------------------------------