From ac8bb63440e469379b914d0039531921cb95bf83 Mon Sep 17 00:00:00 2001 From: Genevieve Bulger Date: Mon, 21 Jun 2021 10:27:09 -0600 Subject: [PATCH] Removing eslint jsx autocomplete valid rule till we can allow `nope` PR number Fixing whitespace Update packages/eslint-plugin/CHANGELOG.md Co-authored-by: Ben Scott <227292+BPScott@users.noreply.github.com> Add react-require-autocomplete lint rule --- packages/eslint-plugin/CHANGELOG.md | 7 +- .../docs/rules/react-require-autocomplete.md | 31 ++++++++ packages/eslint-plugin/index.js | 1 + packages/eslint-plugin/lib/config/react.js | 1 + .../lib/config/rules/jsx-a11y.js | 7 -- .../eslint-plugin/lib/config/rules/shopify.js | 2 + .../lib/rules/react-require-autocomplete.js | 70 ++++++++++++++++++ packages/eslint-plugin/package.json | 1 + .../rules/react-require-autocomplete.test.js | 71 +++++++++++++++++++ yarn.lock | 19 +++++ 10 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/react-require-autocomplete.md create mode 100644 packages/eslint-plugin/lib/rules/react-require-autocomplete.js create mode 100644 packages/eslint-plugin/tests/lib/rules/react-require-autocomplete.test.js diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 4ebbeac0..a515fccf 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - +## Unreleased + +### Changed + +- Removed `jsx-a11y/autocomplete-valid` rule for `eslint-plugin`([264](https://github.com/Shopify/web-configs/pull/264)) +- Add `react-require-autocomplete` lint rule to `@shopify/eslint-plugin` https://github.com/Shopify/web-configs/pull/251 ## 40.2.3 - 2021-05-10 diff --git a/packages/eslint-plugin/docs/rules/react-require-autocomplete.md b/packages/eslint-plugin/docs/rules/react-require-autocomplete.md new file mode 100644 index 00000000..ea179a53 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/react-require-autocomplete.md @@ -0,0 +1,31 @@ +# Require inputs have have autocomplete attributes when necessary + +The HTML `autocomplete` attribute helps users to complete filling in forms by using data stored in the browser. This is particularly useful for people with motor disabilities or cognitive impairment who may have difficulties filling out forms online. In React, the `autocomplete` attribute is written in camelCase as `autoComplete`. + +For example, a form input asking for someone’s email address would need to include the `autoComplete` attribute in order to send a hint to browser or other user agents to query for this data: + +```tsx + +``` + +## Rule Details + +This rule ensures that we always add an `autoComplete` attribute and value to React input elements if the type is: `color`, `date`, `datetime-local`, `email`, `month`, `number`, `password`, `range`, `search`, `tel`, `text`, `time`, `url`, or `week`. + +Even if you do not want the browser to autofill a user's information, it is recommended you still have an `autoComplete` attribute with the value `off` or `nope`. + +The following pattern is considered a warning: + +Examples of **incorrect** code for this rule: + +```tsx + + +``` + +Examples of **correct** code for this rule: + +```tsx + + +``` diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js index f227bff0..c3d1dc6a 100644 --- a/packages/eslint-plugin/index.js +++ b/packages/eslint-plugin/index.js @@ -23,6 +23,7 @@ module.exports = { 'react-initialize-state': require('./lib/rules/react-initialize-state'), 'react-no-multiple-render-methods': require('./lib/rules/react-no-multiple-render-methods'), 'react-prefer-private-members': require('./lib/rules/react-prefer-private-members'), + 'react-require-autocomplete': require('./lib/rules/react-require-autocomplete'), 'react-type-state': require('./lib/rules/react-type-state'), 'restrict-full-import': require('./lib/rules/restrict-full-import'), 'sinon-no-restricted-features': require('./lib/rules/sinon-no-restricted-features'), diff --git a/packages/eslint-plugin/lib/config/react.js b/packages/eslint-plugin/lib/config/react.js index 5a8114b3..c490970d 100644 --- a/packages/eslint-plugin/lib/config/react.js +++ b/packages/eslint-plugin/lib/config/react.js @@ -26,6 +26,7 @@ module.exports = { '@shopify/react-initialize-state': 'error', '@shopify/react-no-multiple-render-methods': 'error', '@shopify/react-prefer-private-members': 'error', + '@shopify/react-require-autocomplete': 'error', '@shopify/react-type-state': 'error', '@shopify/jsx-no-complex-expressions': 'error', '@shopify/jsx-no-hardcoded-content': 'error', diff --git a/packages/eslint-plugin/lib/config/rules/jsx-a11y.js b/packages/eslint-plugin/lib/config/rules/jsx-a11y.js index 6ea49e7c..dd059330 100644 --- a/packages/eslint-plugin/lib/config/rules/jsx-a11y.js +++ b/packages/eslint-plugin/lib/config/rules/jsx-a11y.js @@ -71,11 +71,4 @@ module.exports = { 'jsx-a11y/scope': 'error', // Enforce tabIndex value is not greater than zero. 'jsx-a11y/tabindex-no-positive': 'error', - // Ensure the autocomplete attribute is correct and suitable for the form field it is used with. - 'jsx-a11y/autocomplete-valid': [ - 'error', - { - inputComponents: ['TextField', 'Select'], - }, - ], }; diff --git a/packages/eslint-plugin/lib/config/rules/shopify.js b/packages/eslint-plugin/lib/config/rules/shopify.js index 932baaa7..ffbb8bbc 100644 --- a/packages/eslint-plugin/lib/config/rules/shopify.js +++ b/packages/eslint-plugin/lib/config/rules/shopify.js @@ -45,6 +45,8 @@ module.exports = { '@shopify/react-no-multiple-render-methods': 'off', // Prefer all non-React-specific members be marked private in React class components. '@shopify/react-prefer-private-members': 'off', + // Require input elements to have autocomplete values + '@shopify/react-require-autocomplete': 'off', // Require that React component state be typed in TypeScript. '@shopify/react-type-state': 'off', // Prevent importing the entirety of a package. diff --git a/packages/eslint-plugin/lib/rules/react-require-autocomplete.js b/packages/eslint-plugin/lib/rules/react-require-autocomplete.js new file mode 100644 index 00000000..bc09ef5e --- /dev/null +++ b/packages/eslint-plugin/lib/rules/react-require-autocomplete.js @@ -0,0 +1,70 @@ +const {hasProp, getProp, getPropValue} = require('jsx-ast-utils'); + +const autoCompleteInputTypes = [ + 'color', + 'date', + 'datetime-local', + 'email', + 'month', + 'number', + 'password', + 'range', + 'search', + 'tel', + 'text', + 'time', + 'url', + 'week', +]; + +module.exports = { + meta: { + docs: { + description: 'Require autocomplete attribute on certain input types.', + category: 'Best Practices', + recommended: true, + }, + schema: { + inputComponents: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + }, + }, + }, + create: (context) => ({ + JSXOpeningElement: (node) => { + const inputComponents = + (context.options[0] && context.options[0].inputComponents) || []; + const nodeName = node.name.name; + + const autoCompleteElements = ['input', ...inputComponents]; + + if (!autoCompleteElements.includes(nodeName)) { + return; + } + + // input elements without a type attribute are treated as text + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#input_types + const inputType = + getPropValue(getProp(node.attributes, 'type')) || 'text'; + + if (!autoCompleteInputTypes.includes(inputType)) { + return; + } + + const hasAutoCompleteProp = hasProp(node.attributes, 'autoComplete'); + + if (hasAutoCompleteProp) { + return; + } + + context.report( + node, + `'${nodeName}' elements of type '${inputType}' must have an autoComplete attribute`, + ); + }, + }), +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 4b130c91..322da11a 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -53,6 +53,7 @@ "eslint-plugin-react": "7.22.0", "eslint-plugin-react-hooks": "4.2.0", "eslint-plugin-sort-class-members": "1.9.0", + "jsx-ast-utils": "^3.2.0", "merge": "^2.1.1", "pkg-dir": "^4.2.0", "pluralize": "^8.0.0" diff --git a/packages/eslint-plugin/tests/lib/rules/react-require-autocomplete.test.js b/packages/eslint-plugin/tests/lib/rules/react-require-autocomplete.test.js new file mode 100644 index 00000000..d44cdff3 --- /dev/null +++ b/packages/eslint-plugin/tests/lib/rules/react-require-autocomplete.test.js @@ -0,0 +1,71 @@ +const {RuleTester} = require('eslint'); + +const rule = require('../../../lib/rules/react-require-autocomplete'); + +const ruleTester = new RuleTester({parser: require.resolve('babel-eslint')}); + +function errorMessage(componentName, tagName) { + return [ + { + type: 'JSXOpeningElement', + message: `'${componentName}' elements of type '${tagName}' must have an autoComplete attribute`, + }, + ]; +} + +ruleTester.run('react-require-autocomplete', rule, { + valid: [ + { + code: '', + options: [{inputComponents: ['TextField']}], + }, + { + code: '', + }, + { + code: '', + }, + { + code: '
', + }, + { + code: '

', + }, + { + code: + 'const autoComplete = "email"; const myInput = ;', + }, + { + code: ` + function MyComponent() { + const fieldProps = { + label: "Email", + autoComplete: "emaill" + } + return ; + }`, + }, + { + code: ` + function MyComponent({fieldProps}) { + return ; + }`, + }, + ], + invalid: [ + { + code: '', + options: [{inputComponents: ['TextField']}], + errors: errorMessage('TextField', 'email'), + }, + { + code: '', + errors: errorMessage('input', 'email'), + }, + { + code: + 'const props = {label: "Name"}; const myInput = ;', + errors: errorMessage('input', 'text'), + }, + ], +}); diff --git a/yarn.lock b/yarn.lock index b5b50742..de973224 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2737,6 +2737,17 @@ array-includes@^3.1.1: es-abstract "^1.17.0" is-string "^1.0.5" +array-includes@^3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" + integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + get-intrinsic "^1.1.1" + is-string "^1.0.5" + array-slice@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" @@ -6828,6 +6839,14 @@ jsprim@^1.2.2: array-includes "^3.1.1" object.assign "^4.1.1" +jsx-ast-utils@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82" + integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q== + dependencies: + array-includes "^3.1.2" + object.assign "^4.1.2" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"