├── src
└── utils
│ ├── nodeParsers
│ ├── noop.js
│ ├── JSXOpeningElement.js
│ ├── JSXElement.js
│ └── JSXAttribute.js
│ ├── constants.js
│ ├── getAsProp.js
│ ├── mapChainExpressions.js
│ ├── mergeStyledAttrsWithNodeAttrs.js
│ ├── keyValuePairToProps.js
│ ├── makeRule.js
│ └── ruleNameToTypeDict.js
├── .babelrc
├── public
├── handshake.png
└── linting-styled-components.png
├── .gitignore
├── .prettierrc.js
├── __tests__
├── jsx-a11y-rules
│ ├── util
│ │ ├── implicitRoles
│ │ │ ├── dl.js
│ │ │ ├── ol.js
│ │ │ ├── ul.js
│ │ │ ├── form.js
│ │ │ ├── h1.js
│ │ │ ├── h2.js
│ │ │ ├── h3.js
│ │ │ ├── h4.js
│ │ │ ├── h5.js
│ │ │ ├── h6.js
│ │ │ ├── hr.js
│ │ │ ├── li.js
│ │ │ ├── body.js
│ │ │ ├── nav.js
│ │ │ ├── button.js
│ │ │ ├── details.js
│ │ │ ├── dialog.js
│ │ │ ├── meter.js
│ │ │ ├── option.js
│ │ │ ├── output.js
│ │ │ ├── select.js
│ │ │ ├── tbody.js
│ │ │ ├── tfoot.js
│ │ │ ├── thead.js
│ │ │ ├── article.js
│ │ │ ├── aside.js
│ │ │ ├── datalist.js
│ │ │ ├── section.js
│ │ │ ├── textarea.js
│ │ │ ├── progress.js
│ │ │ ├── a.js
│ │ │ ├── area.js
│ │ │ ├── link.js
│ │ │ ├── img.js
│ │ │ ├── menu.js
│ │ │ ├── menuitem.js
│ │ │ ├── input.js
│ │ │ └── index.js
│ │ ├── isPresentationRole.js
│ │ ├── isDOMElement.js
│ │ ├── getComputedRole.js
│ │ ├── getImplicitRole.js
│ │ ├── isAbstractRole.js
│ │ ├── getExplicitRole.js
│ │ ├── isHiddenFromScreenReader.js
│ │ ├── getSuggestion.js
│ │ ├── schemas.js
│ │ ├── isDisabledElement.js
│ │ ├── hasAccessibleChild.js
│ │ ├── getTabIndex.js
│ │ ├── isNonLiteralProperty.js
│ │ ├── attributesComparator.js
│ │ ├── mayContainChildComponent.js
│ │ ├── isSemanticRoleElement.js
│ │ ├── isInteractiveRole.js
│ │ ├── isNonInteractiveRole.js
│ │ └── mayHaveAccessibleLabel.js
│ ├── rules
│ │ ├── html-has-lang.js
│ │ ├── no-access-key.js
│ │ ├── iframe-has-title.js
│ │ ├── tabindex-no-positive.js
│ │ ├── scope.js
│ │ ├── no-onchange.js
│ │ ├── no-distracting-elements.js
│ │ ├── anchor-has-content.js
│ │ ├── aria-props.js
│ │ ├── no-autofocus.js
│ │ ├── heading-has-content.js
│ │ ├── aria-unsupported-elements.js
│ │ ├── autocomplete-valid.js
│ │ ├── lang.js
│ │ ├── accessible-emoji.js
│ │ ├── mouse-events-have-key-events.js
│ │ ├── aria-role.js
│ │ ├── aria-activedescendant-has-tabindex.js
│ │ ├── click-events-have-key-events.js
│ │ ├── img-redundant-alt.js
│ │ ├── no-redundant-roles.js
│ │ ├── no-noninteractive-element-to-interactive-role.js
│ │ ├── role-has-required-aria-props.js
│ │ ├── no-noninteractive-tabindex.js
│ │ ├── role-supports-aria-props.js
│ │ ├── no-interactive-element-to-noninteractive-role.js
│ │ └── media-has-caption.js
│ └── tests
│ │ ├── scope-test.js
│ │ ├── html-has-lang-test.js
│ │ ├── lang-test.js
│ │ ├── anchor-has-content-test.js
│ │ ├── no-distracting-elements-test.js
│ │ ├── iframe-has-title-test.js
│ │ ├── no-onchange-test.js
│ │ ├── no-autofocus-test.js
│ │ ├── no-access-key-test.js
│ │ ├── tabindex-no-positive-test.js
│ │ ├── index.test.js
│ │ ├── no-redundant-roles-test.js
│ │ ├── accessible-emoji-test.js
│ │ ├── aria-props-test.js
│ │ ├── aria-unsupported-elements-test.js
│ │ ├── aria-activedescendant-has-tabindex-test.js
│ │ ├── mouse-events-have-key-events-test.js
│ │ ├── heading-has-content-test.js
│ │ ├── no-noninteractive-tabindex-test.js
│ │ └── autocomplete-valid-test.js
├── utils
│ ├── parserOptionsMapper.js
│ └── getSuggestion.js
├── rules
│ ├── template.js
│ ├── autocomplete-valid.test.js
│ ├── no-access-key.test.js
│ ├── no-distracting-elements.test.js
│ ├── no-interactive-element-to-noninteractive-role.test.js
│ ├── html-has-lang.test.js
│ ├── aria-unsupported-elements.test.js
│ ├── control-has-associated-label.test.js
│ ├── scope.test.js
│ ├── no-onchange.test.js
│ ├── aria-role.test.js
│ ├── media-has-caption.test.js
│ ├── heading-has-content.test.js
│ ├── role-has-required-aria-props.test.js
│ ├── no-static-element-interactions.test.js
│ ├── no-redundant-roles.test.js
│ ├── interactive-supports-focus.test.js
│ ├── label-has-associated-control.test.js
│ ├── accessible-emoji.test.js
│ ├── aria-proptypes.test.js
│ ├── label-has-for.test.js
│ ├── no-noninteractive-element-to-interactive-role.test.js
│ ├── role-supports-aria-props.test.js
│ ├── anchor-has-content.test.js
│ ├── no-noninteractive-element-interactions.test.js
│ ├── img-redundant-alt.test.js
│ ├── mouse-events-have-key-events.test.js
│ ├── no-noninteractive-tabindex.test.js
│ └── iframe-has-title.test.js
└── index.test.js
├── playground
├── .eslintrc.js
└── package.json
└── package.json
/src/utils/nodeParsers/noop.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {};
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["@babel/plugin-proposal-optional-chaining"]
4 | }
5 |
--------------------------------------------------------------------------------
/public/handshake.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brendanmorrell/eslint-plugin-styled-components-a11y/HEAD/public/handshake.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | .idea
3 | .iml
4 | .DS_Store
5 |
6 | node_modules
7 | lib
8 | playground/**.js
9 | playground/**.jsx
10 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | const __UNKNOWN_IDENTIFER__ = '__UNKNOWN_IDENTIFER__';
2 |
3 | module.exports = { __UNKNOWN_IDENTIFER__ };
4 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'all',
3 | singleQuote: true,
4 | endOfLine: 'lf',
5 | printWidth: 120,
6 | };
7 |
--------------------------------------------------------------------------------
/public/linting-styled-components.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brendanmorrell/eslint-plugin-styled-components-a11y/HEAD/public/linting-styled-components.png
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/dl.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a dl tag.
3 | */
4 | export default function getImplicitRoleForDl() {
5 | return 'list';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/ol.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an ol tag.
3 | */
4 | export default function getImplicitRoleForOl() {
5 | return 'list';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/ul.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a ul tag.
3 | */
4 | export default function getImplicitRoleForUl() {
5 | return 'list';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/form.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a form tag.
3 | */
4 | export default function getImplicitRoleForForm() {
5 | return 'form';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/h1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an h1 tag.
3 | */
4 | export default function getImplicitRoleForH1() {
5 | return 'heading';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/h2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an h2 tag.
3 | */
4 | export default function getImplicitRoleForH2() {
5 | return 'heading';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/h3.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an h3 tag.
3 | */
4 | export default function getImplicitRoleForH3() {
5 | return 'heading';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/h4.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an h4 tag.
3 | */
4 | export default function getImplicitRoleForH4() {
5 | return 'heading';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/h5.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an h5 tag.
3 | */
4 | export default function getImplicitRoleForH5() {
5 | return 'heading';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/h6.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an h6tag.
3 | */
4 | export default function getImplicitRoleForH6() {
5 | return 'heading';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/hr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an hr tag.
3 | */
4 | export default function getImplicitRoleForHr() {
5 | return 'separator';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/li.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an li tag.
3 | */
4 | export default function getImplicitRoleForLi() {
5 | return 'listitem';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/body.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a body tag.
3 | */
4 | export default function getImplicitRoleForBody() {
5 | return 'document';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/nav.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a nav tag.
3 | */
4 | export default function getImplicitRoleForNav() {
5 | return 'navigation';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/button.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a button tag.
3 | */
4 | export default function getImplicitRoleForButton() {
5 | return 'button';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/details.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a details tag.
3 | */
4 | export default function getImplicitRoleForDetails() {
5 | return 'group';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/dialog.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a dialog tag.
3 | */
4 | export default function getImplicitRoleForDialog() {
5 | return 'dialog';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/meter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a meter tag.
3 | */
4 | export default function getImplicitRoleForMeter() {
5 | return 'progressbar';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/option.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an option tag.
3 | */
4 | export default function getImplicitRoleForOption() {
5 | return 'option';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/output.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an output tag.
3 | */
4 | export default function getImplicitRoleForOutput() {
5 | return 'status';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/select.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a select tag.
3 | */
4 | export default function getImplicitRoleForSelect() {
5 | return 'listbox';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/tbody.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a tbody tag.
3 | */
4 | export default function getImplicitRoleForTbody() {
5 | return 'rowgroup';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/tfoot.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a tfoot tag.
3 | */
4 | export default function getImplicitRoleForTfoot() {
5 | return 'rowgroup';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/thead.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a thead tag.
3 | */
4 | export default function getImplicitRoleForThead() {
5 | return 'rowgroup';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/article.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an article tag.
3 | */
4 | export default function getImplicitRoleForArticle() {
5 | return 'article';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/aside.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for an aside tag.
3 | */
4 | export default function getImplicitRoleForAside() {
5 | return 'complementary';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/datalist.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a datalist tag.
3 | */
4 | export default function getImplicitRoleForDatalist() {
5 | return 'listbox';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/section.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a section tag.
3 | */
4 | export default function getImplicitRoleForSection() {
5 | return 'region';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/textarea.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a textarea tag.
3 | */
4 | export default function getImplicitRoleForTextarea() {
5 | return 'textbox';
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/progress.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the implicit role for a progress tag.
3 | */
4 | export default function getImplicitRoleForProgress() {
5 | return 'progressbar';
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/getAsProp.js:
--------------------------------------------------------------------------------
1 | module.exports = (attributes) => {
2 | const [asProp] = attributes
3 | .filter((x) => x && x.name && x.name.name === 'as')
4 | .map((x) => x && x.value && x.value.value);
5 | return asProp;
6 | };
7 |
--------------------------------------------------------------------------------
/src/utils/mapChainExpressions.js:
--------------------------------------------------------------------------------
1 | module.exports = (attributes) =>
2 | attributes.map((attribute) => {
3 | if (attribute.value?.expression?.type === 'ChainExpression') {
4 | attribute.value.expression = attribute.value.expression.expression;
5 | }
6 |
7 | return attribute;
8 | });
9 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/a.js:
--------------------------------------------------------------------------------
1 | import { getProp } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns the implicit role for an anchor tag.
5 | */
6 | export default function getImplicitRoleForAnchor(attributes) {
7 | if (getProp(attributes, 'href')) {
8 | return 'link';
9 | }
10 |
11 | return '';
12 | }
13 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/area.js:
--------------------------------------------------------------------------------
1 | import { getProp } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns the implicit role for an area tag.
5 | */
6 | export default function getImplicitRoleForArea(attributes) {
7 | if (getProp(attributes, 'href')) {
8 | return 'link';
9 | }
10 |
11 | return '';
12 | }
13 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/link.js:
--------------------------------------------------------------------------------
1 | import { getProp } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns the implicit role for a link tag.
5 | */
6 | export default function getImplicitRoleForLink(attributes) {
7 | if (getProp(attributes, 'href')) {
8 | return 'link';
9 | }
10 |
11 | return '';
12 | }
13 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/isPresentationRole.js:
--------------------------------------------------------------------------------
1 | import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
2 |
3 | const presentationRoles = new Set([
4 | 'presentation',
5 | 'none',
6 | ]);
7 |
8 | const isPresentationRole = (tagName, attributes) => presentationRoles.has(getLiteralPropValue(getProp(attributes, 'role')));
9 |
10 | export default isPresentationRole;
11 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/img.js:
--------------------------------------------------------------------------------
1 | import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns the implicit role for an img tag.
5 | */
6 | export default function getImplicitRoleForImg(attributes) {
7 | const alt = getProp(attributes, 'alt');
8 |
9 | if (alt && getLiteralPropValue(alt) === '') {
10 | return '';
11 | }
12 |
13 | return 'img';
14 | }
15 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/isDOMElement.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 | import { dom } from 'aria-query';
5 | import includes from 'array-includes';
6 |
7 | const domElements = [...dom.keys()];
8 |
9 | /**
10 | * Returns boolean indicating whether the given element is a DOM element.
11 | */
12 | const isDOMElement = (
13 | tagName: string,
14 | ): boolean => includes(domElements, tagName);
15 |
16 | export default isDOMElement;
17 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/menu.js:
--------------------------------------------------------------------------------
1 | import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns the implicit role for a menu tag.
5 | */
6 | export default function getImplicitRoleForMenu(attributes) {
7 | const type = getProp(attributes, 'type');
8 |
9 | if (type) {
10 | const value = getLiteralPropValue(type);
11 |
12 | return (value && value.toUpperCase() === 'TOOLBAR') ? 'toolbar' : '';
13 | }
14 |
15 | return '';
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/mergeStyledAttrsWithNodeAttrs.js:
--------------------------------------------------------------------------------
1 | const { parse } = require('@babel/parser');
2 | const keyValuePairToProps = require('./keyValuePairToProps');
3 |
4 | module.exports = (styledAttrs, nodeAttrs) => {
5 | const jsxString = `
`;
6 | const ast = parse(jsxString, { plugins: ['jsx', 'estree'] });
7 | const astAttributes = ast.program.body[0].expression.openingElement.attributes;
8 | return nodeAttrs.concat(astAttributes);
9 | };
10 |
--------------------------------------------------------------------------------
/__tests__/utils/parserOptionsMapper.js:
--------------------------------------------------------------------------------
1 | const defaultParserOptions = {
2 | ecmaVersion: 2018,
3 | ecmaFeatures: {
4 | experimentalObjectRestSpread: true,
5 | jsx: true,
6 | },
7 | };
8 |
9 | module.exports = function parserOptionsMapper({ code, errors, options = [], parserOptions = {}, settings = {} }) {
10 | return {
11 | code,
12 | errors,
13 | options,
14 | parserOptions: {
15 | ...defaultParserOptions,
16 | ...parserOptions,
17 | },
18 | settings,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/utils/keyValuePairToProps.js:
--------------------------------------------------------------------------------
1 | const { __UNKNOWN_IDENTIFER__ } = require('./constants');
2 |
3 | module.exports = ({ key, value }) =>
4 | // we set all attributes which are variable identifiers as '__UNKNOWN_IDENTIFER__' just so there can be some value for the kv pair since we have no way of knowing the real value.
5 | // thus, here we want to just plug that string in without any quotes so the linter treats it just like a variable would have been handled normally
6 | `${key}=${typeof value !== 'string' || value === __UNKNOWN_IDENTIFER__ ? `{${value}}` : `"${value}"`} `;
7 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/getComputedRole.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import type { Node } from 'ast-types-flow';
3 | import getExplicitRole from './getExplicitRole';
4 | import getImplicitRole from './getImplicitRole';
5 | /**
6 | * Returns an element's computed role, which is
7 | *
8 | * 1. The valid value of its explicit role attribute; or
9 | * 2. The implicit value of its tag.
10 | */
11 | export default function getComputedRole(
12 | tag: string,
13 | attributes: Array,
14 | ): ?string {
15 | return getExplicitRole(tag, attributes) || getImplicitRole(tag, attributes);
16 | }
17 |
--------------------------------------------------------------------------------
/playground/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['react', 'styled-components-a11y'],
3 | extends: ['eslint-config-airbnb', 'prettier', 'prettier/react', 'plugin:styled-components-a11y/recommended'],
4 | parserOptions: {
5 | "ecmaVersion": 2020,
6 | parser: 'babel-eslint',
7 | sourceType: 'module',
8 | },
9 | env: {
10 | es6: true,
11 | browser: true,
12 | },
13 | settings: {
14 | "jsx-a11y": {
15 | components: {
16 | ImgComponent: 'img',
17 | },
18 | },
19 | },
20 | rules: {
21 | 'react/jsx-filename-extension': 0,
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/__tests__/rules/template.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require('eslint');
2 | const makeRule = require('../../src/utils/makeRule');
3 | const parserOptionsMapper = require('../utils/parserOptionsMapper');
4 | const getSuggestion = require('../utils/getSuggestion.js');
5 |
6 | const ruleTester = new RuleTester();
7 | const makeStyledTestCases = require('../utils/makeStyledTestCases');
8 |
9 | const ruleName = '';
10 | const rule = makeRule(ruleName);
11 |
12 | const expectedError = {};
13 |
14 | // ## VALID
15 |
16 | // ## INVALID
17 |
18 | ruleTester.run(ruleName, rule, {
19 | valid: [],
20 | invalid: [],
21 | });
22 |
--------------------------------------------------------------------------------
/__tests__/rules/autocomplete-valid.test.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require('eslint');
2 | const makeRule = require('../../src/utils/makeRule');
3 | const parserOptionsMapper = require('../utils/parserOptionsMapper');
4 | const getSuggestion = require('../utils/getSuggestion.js');
5 |
6 | const ruleTester = new RuleTester();
7 | const makeStyledTestCases = require('../utils/makeStyledTestCases');
8 |
9 | const ruleName = 'autocomplete-valid';
10 | const rule = makeRule(ruleName);
11 |
12 | const expectedError = {};
13 |
14 | // ## VALID
15 |
16 | // ## INVALID
17 |
18 | ruleTester.run(ruleName, rule, {
19 | valid: [],
20 | invalid: [],
21 | });
22 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/getImplicitRole.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { roles as rolesMap } from 'aria-query';
3 | import type { Node } from 'ast-types-flow';
4 | import implicitRoles from './implicitRoles';
5 |
6 | /**
7 | * Returns an element's implicit role given its attributes and type.
8 | * Some elements only have an implicit role when certain props are defined.
9 | */
10 | export default function getImplicitRole(
11 | type: string,
12 | attributes: Array,
13 | ): ?string {
14 | let implicitRole;
15 | if (implicitRoles[type]) {
16 | implicitRole = implicitRoles[type](attributes);
17 | }
18 | if (rolesMap.has(implicitRole)) {
19 | return implicitRole;
20 | }
21 | return null;
22 | }
23 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/menuitem.js:
--------------------------------------------------------------------------------
1 | import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns the implicit role for a menuitem tag.
5 | */
6 | export default function getImplicitRoleForMenuitem(attributes) {
7 | const type = getProp(attributes, 'type');
8 |
9 | if (type) {
10 | const value = getLiteralPropValue(type) || '';
11 |
12 | switch (typeof value === 'string' && value.toUpperCase()) {
13 | case 'COMMAND':
14 | return 'menuitem';
15 | case 'CHECKBOX':
16 | return 'menuitemcheckbox';
17 | case 'RADIO':
18 | return 'menuitemradio';
19 | default:
20 | return '';
21 | }
22 | }
23 |
24 | return '';
25 | }
26 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "dependencies": {
7 | "react": "^16.13.1",
8 | "react-dom": "^16.13.1",
9 | "styled-components": "^5.1.1"
10 | },
11 | "scripts": {
12 | "test": "echo \"Error: no test specified\" && exit 1",
13 | "reset": "rm -rf node_modules; yarn",
14 | "postinstall": "echo \"need to figure out how to use cli to reload vscode\""
15 | },
16 | "author": "",
17 | "license": "ISC",
18 | "devDependencies": {
19 | "eslint": "^7.1.0",
20 | "eslint-config-airbnb": "17.1.0",
21 | "eslint-plugin-react": "^7.20.0",
22 | "eslint-plugin-styled-components-a11y": "file:../"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/isAbstractRole.js:
--------------------------------------------------------------------------------
1 | import {
2 | dom,
3 | roles,
4 | } from 'aria-query';
5 | import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
6 |
7 | const abstractRoles = new Set([...roles.keys()]
8 | .filter((role) => roles.get(role).abstract));
9 |
10 | const DOMElements = [...dom.keys()];
11 |
12 | const isAbstractRole = (tagName, attributes) => {
13 | // Do not test higher level JSX components, as we do not know what
14 | // low-level DOM element this maps to.
15 | if (DOMElements.indexOf(tagName) === -1) {
16 | return false;
17 | }
18 |
19 | const role = getLiteralPropValue(getProp(attributes, 'role'));
20 |
21 | return abstractRoles.has(role);
22 | };
23 |
24 | export default isAbstractRole;
25 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/getExplicitRole.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { roles as rolesMap } from 'aria-query';
3 | import {
4 | getProp,
5 | getLiteralPropValue,
6 | } from 'jsx-ast-utils';
7 | import type { Node } from 'ast-types-flow';
8 | /**
9 | * Returns an element's computed role, which is
10 | *
11 | * 1. The valid value of its explicit role attribute; or
12 | * 2. The implicit value of its tag.
13 | */
14 | export default function getExplicitRole(
15 | tag: string,
16 | attributes: Array,
17 | ): ?string {
18 | const explicitRole = (function toLowerCase(role) {
19 | if (typeof role === 'string') {
20 | return role.toLowerCase();
21 | }
22 | return null;
23 | }(getLiteralPropValue(getProp(attributes, 'role'))));
24 |
25 | if (rolesMap.has(explicitRole)) {
26 | return explicitRole;
27 | }
28 | return null;
29 | }
30 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/isHiddenFromScreenReader.js:
--------------------------------------------------------------------------------
1 | import { getProp, getPropValue, getLiteralPropValue } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns boolean indicating that the aria-hidden prop
5 | * is present or the value is true. Will also return true if
6 | * there is an input with type='hidden'.
7 | *
8 | * is equivalent to the DOM as .
9 | */
10 | const isHiddenFromScreenReader = (type, attributes) => {
11 | if (type.toUpperCase() === 'INPUT') {
12 | const hidden = getLiteralPropValue(getProp(attributes, 'type'));
13 |
14 | if (hidden && hidden.toUpperCase() === 'HIDDEN') {
15 | return true;
16 | }
17 | }
18 |
19 | const ariaHidden = getPropValue(getProp(attributes, 'aria-hidden'));
20 | return ariaHidden === true;
21 | };
22 |
23 | export default isHiddenFromScreenReader;
24 |
--------------------------------------------------------------------------------
/__tests__/utils/getSuggestion.js:
--------------------------------------------------------------------------------
1 | const editDistance = require('damerau-levenshtein');
2 |
3 | // Minimum edit distance to be considered a good suggestion.
4 | const THRESHOLD = 2;
5 |
6 | /**
7 | * Returns an array of suggestions given a word and a dictionary and limit of suggestions
8 | * to return.
9 | */
10 | module.exports = function getSuggestion(word, dictionary = [], limit = 2) {
11 | const distances = dictionary.reduce((suggestions, dictionaryWord) => {
12 | const distance = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase());
13 | const { steps } = distance;
14 | suggestions[dictionaryWord] = steps; // eslint-disable-line
15 | return suggestions;
16 | }, {});
17 |
18 | return Object.keys(distances)
19 | .filter(suggestion => distances[suggestion] <= THRESHOLD)
20 | .sort((a, b) => distances[a] - distances[b]) // Sort by distance
21 | .slice(0, limit);
22 | };
23 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/getSuggestion.js:
--------------------------------------------------------------------------------
1 | import editDistance from 'damerau-levenshtein';
2 |
3 | // Minimum edit distance to be considered a good suggestion.
4 | const THRESHOLD = 2;
5 |
6 | /**
7 | * Returns an array of suggestions given a word and a dictionary and limit of suggestions
8 | * to return.
9 | */
10 | export default function getSuggestion(word, dictionary = [], limit = 2) {
11 | const distances = dictionary.reduce((suggestions, dictionaryWord) => {
12 | const distance = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase());
13 | const { steps } = distance;
14 | suggestions[dictionaryWord] = steps; // eslint-disable-line
15 | return suggestions;
16 | }, {});
17 |
18 | return Object.keys(distances)
19 | .filter((suggestion) => distances[suggestion] <= THRESHOLD)
20 | .sort((a, b) => distances[a] - distances[b]) // Sort by distance
21 | .slice(0, limit);
22 | }
23 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/schemas.js:
--------------------------------------------------------------------------------
1 | /**
2 | * JSON schema to accept an array of unique strings
3 | */
4 | 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 | const enumArraySchema = (enumeratedList = [], minItems = 0) => ({
17 | ...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 | const generateObjSchema = (properties = {}, required) => ({
30 | type: 'object',
31 | properties,
32 | required,
33 | });
34 |
35 | module.exports = {
36 | generateObjSchema,
37 | enumArraySchema,
38 | arraySchema,
39 | };
40 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/isDisabledElement.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | import { getProp, getLiteralPropValue, getPropValue } from 'jsx-ast-utils';
6 | import type { Node } from 'ast-types-flow';
7 |
8 | const isDisabledElement = (attributes: Array): boolean => {
9 | const disabledAttr = getProp(attributes, 'disabled');
10 | const disabledAttrValue = getPropValue(disabledAttr);
11 | const isHTML5Disabled = disabledAttr && disabledAttrValue !== undefined;
12 | if (isHTML5Disabled) {
13 | return true;
14 | }
15 | const ariaDisabledAttr = getProp(attributes, 'aria-disabled');
16 | const ariaDisabledAttrValue = getLiteralPropValue(ariaDisabledAttr);
17 |
18 | if (
19 | ariaDisabledAttr
20 | && ariaDisabledAttrValue !== undefined
21 | && ariaDisabledAttrValue === true
22 | ) {
23 | return true;
24 | }
25 | return false;
26 | };
27 |
28 | export default isDisabledElement;
29 |
--------------------------------------------------------------------------------
/__tests__/rules/no-access-key.test.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require('eslint');
2 | const makeRule = require('../../src/utils/makeRule');
3 | const parserOptionsMapper = require('../utils/parserOptionsMapper');
4 | const getSuggestion = require('../utils/getSuggestion.js');
5 |
6 | const ruleTester = new RuleTester();
7 | const makeStyledTestCases = require('../utils/makeStyledTestCases');
8 |
9 | const ruleName = 'no-access-key';
10 | const rule = makeRule(ruleName);
11 |
12 | const errorMessage =
13 | 'No access key attribute allowed. Inconsistencies between keyboard shortcuts and keyboard commands used by screenreaders and keyboard-only users create a11y complications.';
14 |
15 | // ## VALID
16 | const noAccessKey = makeStyledTestCases();
17 | // ## INVALID
18 | const accessKey = makeStyledTestCases({
19 | attrs: `{ accessKey: 'h' }`,
20 | props: 'accessKey="h"',
21 | errors: [errorMessage],
22 | });
23 |
24 | ruleTester.run(ruleName, rule, {
25 | valid: [...noAccessKey],
26 | invalid: [...accessKey],
27 | });
28 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/hasAccessibleChild.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { elementType, hasAnyProp } from 'jsx-ast-utils';
4 | import type { JSXElement } from 'ast-types-flow';
5 | import isHiddenFromScreenReader from './isHiddenFromScreenReader';
6 |
7 | export default function hasAccessibleChild(node: JSXElement): boolean {
8 | return node.children.some((child) => {
9 | switch (child.type) {
10 | case 'Literal':
11 | case 'JSXText':
12 | return Boolean(child.value);
13 | case 'JSXElement':
14 | return !isHiddenFromScreenReader(
15 | elementType(child.openingElement),
16 | child.openingElement.attributes,
17 | );
18 | case 'JSXExpressionContainer':
19 | if (child.expression.type === 'Identifier') {
20 | return child.expression.name !== 'undefined';
21 | }
22 | return true;
23 | default:
24 | return false;
25 | }
26 | }) || hasAnyProp(node.openingElement.attributes, ['dangerouslySetInnerHTML', 'children']);
27 | }
28 |
--------------------------------------------------------------------------------
/__tests__/rules/no-distracting-elements.test.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require('eslint');
2 | const makeRule = require('../../src/utils/makeRule');
3 | const parserOptionsMapper = require('../utils/parserOptionsMapper');
4 | const getSuggestion = require('../utils/getSuggestion.js');
5 |
6 | const ruleTester = new RuleTester();
7 | const makeStyledTestCases = require('../utils/makeStyledTestCases');
8 |
9 | const errorMessage = element =>
10 | `Do not use <${element}> elements as they can create visual accessibility issues and are deprecated.`;
11 |
12 | const ruleName = 'no-distracting-elements';
13 | const rule = makeRule(ruleName);
14 |
15 | const expectedError = {};
16 |
17 | // ## VALID
18 | const div = makeStyledTestCases();
19 | // ## INVALID
20 | const marquee = makeStyledTestCases({ tag: 'marquee', errors: [errorMessage('marquee')] });
21 | const blink = makeStyledTestCases({ tag: 'blink', errors: [errorMessage('blink')] });
22 |
23 | ruleTester.run(ruleName, rule, {
24 | valid: div,
25 | invalid: [...marquee, ...blink],
26 | });
27 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/getTabIndex.js:
--------------------------------------------------------------------------------
1 | import { getPropValue, getLiteralPropValue } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns the tabIndex value.
5 | */
6 | export default function getTabIndex(tabIndex) {
7 | const literalValue = getLiteralPropValue(tabIndex);
8 |
9 | // String and number values.
10 | if (['string', 'number'].indexOf(typeof literalValue) > -1) {
11 | // Empty string will convert to zero, so check for it explicity.
12 | if (
13 | typeof literalValue === 'string'
14 | && literalValue.length === 0
15 | ) {
16 | return undefined;
17 | }
18 | const value = Number(literalValue);
19 | if (Number.isNaN(value)) {
20 | return undefined;
21 | }
22 |
23 | return Number.isInteger(value)
24 | ? value
25 | : undefined;
26 | }
27 |
28 | // Booleans are not valid values, return undefined.
29 | if (
30 | literalValue === true
31 | || literalValue === false
32 | ) {
33 | return undefined;
34 | }
35 |
36 | return getPropValue(tabIndex);
37 | }
38 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/implicitRoles/input.js:
--------------------------------------------------------------------------------
1 | import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
2 |
3 | /**
4 | * Returns the implicit role for an input tag.
5 | */
6 | export default function getImplicitRoleForInput(attributes) {
7 | const type = getProp(attributes, 'type');
8 |
9 | if (type) {
10 | const value = getLiteralPropValue(type) || '';
11 |
12 | switch (typeof value === 'string' && value.toUpperCase()) {
13 | case 'BUTTON':
14 | case 'IMAGE':
15 | case 'RESET':
16 | case 'SUBMIT':
17 | return 'button';
18 | case 'CHECKBOX':
19 | return 'checkbox';
20 | case 'RADIO':
21 | return 'radio';
22 | case 'RANGE':
23 | return 'slider';
24 | case 'EMAIL':
25 | case 'PASSWORD':
26 | case 'SEARCH': // with [list] selector it's combobox
27 | case 'TEL': // with [list] selector it's combobox
28 | case 'URL': // with [list] selector it's combobox
29 | default:
30 | return 'textbox';
31 | }
32 | }
33 |
34 | return 'textbox';
35 | }
36 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/isNonLiteralProperty.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | import type { Node } from 'ast-types-flow';
6 | import { getProp } from 'jsx-ast-utils';
7 |
8 | /**
9 | * Returns boolean indicating whether the given element has been specified with
10 | * an AST node with a non-literal type.
11 | *
12 | * Returns true if the elements has a role and its value is not of a type Literal.
13 | * Otherwise returns false.
14 | */
15 |
16 | const isNonLiteralProperty = (
17 | attributes: Array,
18 | propName: string,
19 | ): boolean => {
20 | const prop = getProp(attributes, propName);
21 | if (!prop) return false;
22 |
23 | const propValue = prop.value;
24 | if (!propValue) return false;
25 |
26 | if (propValue.type === 'Literal') return false;
27 |
28 | if (propValue.type === 'JSXExpressionContainer') {
29 | const { expression } = propValue;
30 | if (expression.type === 'Identifier' && expression.name === 'undefined') return false;
31 | if (expression.type === 'JSXText') return false;
32 | }
33 |
34 | return true;
35 | };
36 |
37 | export default isNonLiteralProperty;
38 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/util/attributesComparator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | import {
6 | getLiteralPropValue,
7 | propName,
8 | } from 'jsx-ast-utils';
9 | import type { Node } from 'ast-types-flow';
10 |
11 | /**
12 | * Returns true if all items in baseAttributes are found in attributes. Always
13 | * returns true if baseAttributes is empty.
14 | */
15 | function attributesComparator(
16 | baseAttributes: Array<{[key: string]: mixed}> = [],
17 | attributes: Array = [],
18 | ): boolean {
19 | return baseAttributes.every((baseAttr): boolean => attributes.some((attribute): boolean => {
20 | // Guard against non-JSXAttribute nodes like JSXSpreadAttribute
21 | if (attribute.type !== 'JSXAttribute') {
22 | return false;
23 | }
24 | // Attribute matches.
25 | if (baseAttr.name !== propName(attribute)) {
26 | return false;
27 | }
28 | // Value exists and does not match.
29 | if (
30 | baseAttr.value
31 | && baseAttr.value !== getLiteralPropValue(attribute)
32 | ) {
33 | return false;
34 | }
35 | return true;
36 | }));
37 | }
38 |
39 | export default attributesComparator;
40 |
--------------------------------------------------------------------------------
/__tests__/rules/no-interactive-element-to-noninteractive-role.test.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require('eslint');
2 | const makeRule = require('../../src/utils/makeRule');
3 | const parserOptionsMapper = require('../utils/parserOptionsMapper');
4 | const getSuggestion = require('../utils/getSuggestion.js');
5 |
6 | const ruleTester = new RuleTester();
7 | const makeStyledTestCases = require('../utils/makeStyledTestCases');
8 |
9 | const ruleName = 'no-interactive-element-to-noninteractive-role';
10 | const rule = makeRule(ruleName);
11 |
12 | const errorMessage = 'Interactive elements should not be assigned non-interactive roles.';
13 |
14 | // ## VALID
15 | //
16 | // ;
17 | //
;
18 | const divArticleRoleWrapperButton = makeStyledTestCases({
19 | attrs: `{ role: 'article' }`,
20 | props: 'role="article"',
21 | children: '',
22 | });
23 |
24 | // ## INVALID
25 | //
26 | const buttonArticleRole = makeStyledTestCases({
27 | tag: 'button',
28 | attrs: `{ role: 'article' }`,
29 | props: 'role="article"',
30 | errors: [errorMessage],
31 | });
32 |
33 | ruleTester.run(ruleName, rule, {
34 | valid: divArticleRoleWrapperButton,
35 | invalid: buttonArticleRole,
36 | });
37 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/rules/html-has-lang.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Enforce html element has lang prop.
3 | * @author Ethan Cohen
4 | */
5 |
6 | // ----------------------------------------------------------------------------
7 | // Rule Definition
8 | // ----------------------------------------------------------------------------
9 |
10 | import { elementType, getProp, getPropValue } from 'jsx-ast-utils';
11 | import { generateObjSchema } from '../util/schemas';
12 |
13 | const errorMessage = ' elements must have the lang prop.';
14 |
15 | const schema = generateObjSchema();
16 |
17 | module.exports = {
18 | meta: {
19 | docs: {
20 | url: 'https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules/html-has-lang.md',
21 | },
22 | schema: [schema],
23 | },
24 |
25 | create: (context) => ({
26 | JSXOpeningElement: (node) => {
27 | const type = elementType(node);
28 |
29 | if (type && type !== 'html') {
30 | return;
31 | }
32 |
33 | const lang = getPropValue(getProp(node.attributes, 'lang'));
34 |
35 | if (lang) {
36 | return;
37 | }
38 |
39 | context.report({
40 | node,
41 | message: errorMessage,
42 | });
43 | },
44 | }),
45 | };
46 |
--------------------------------------------------------------------------------
/src/utils/makeRule.js:
--------------------------------------------------------------------------------
1 | const { rules } = require('eslint-plugin-jsx-a11y');
2 | const path = require('path');
3 |
4 | const collectStyledComponentData = require(process.env.NODE_ENV === 'test'
5 | ? '../../lib/utils/collectStyledComponentData.js'
6 | : './collectStyledComponentData');
7 |
8 | const ruleNameToTypeDict = require('./ruleNameToTypeDict');
9 |
10 | module.exports = (name) => ({
11 | meta: rules[name]?.meta,
12 | create(context) {
13 | const nodeParserPath = path.join(__dirname, 'nodeParsers', ruleNameToTypeDict[name]);
14 | const rule = rules[name];
15 | const styledComponents = {};
16 | const nodesArray = [];
17 | const parserMapping = {
18 | JSXOpeningElement: 'JSXOpeningElement',
19 | JSXElement: 'JSXElement',
20 | JSXAttribute: 'JSXOpeningElement',
21 | };
22 | const parsedElement = parserMapping[ruleNameToTypeDict[name]];
23 | return {
24 | ...collectStyledComponentData(styledComponents, context, name),
25 | [parsedElement]: (node) => nodesArray.push(node),
26 | 'Program:exit': () => {
27 | const parser = require(nodeParserPath)(context, styledComponents, rule, name);
28 | nodesArray.forEach((node) => parser[parsedElement](node));
29 | },
30 | };
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/__tests__/rules/html-has-lang.test.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require('eslint');
2 | const makeRule = require('../../src/utils/makeRule');
3 | const parserOptionsMapper = require('../utils/parserOptionsMapper');
4 | const getSuggestion = require('../utils/getSuggestion.js');
5 |
6 | const ruleTester = new RuleTester();
7 | const makeStyledTestCases = require('../utils/makeStyledTestCases');
8 |
9 | const ruleName = 'html-has-lang';
10 | const rule = makeRule(ruleName);
11 |
12 | const expectedError = {
13 | message: ' elements must have the lang prop.',
14 | type: 'JSXOpeningElement',
15 | };
16 |
17 | // ## VALID
18 | //
19 | const htmlEn = makeStyledTestCases({ tag: 'html', attrs: `{ lang:'en' }`, props: 'lang="en"' });
20 | //
21 | const htmlEnUs = makeStyledTestCases({ tag: 'html', attrs: `{ lang:'en-US' }`, props: 'lang="en-US"' });
22 | //
23 | const htmlVarLang = makeStyledTestCases({ tag: 'html', attrs: `{ lang: language }`, props: 'lang={language}' });
24 |
25 | // ## INVALID
26 | //
27 | const emptyHtml = makeStyledTestCases({ tag: 'html', errors: [expectedError] });
28 |
29 | ruleTester.run(ruleName, rule, {
30 | valid: [...htmlEn, ...htmlEnUs, ...htmlVarLang],
31 | invalid: [...emptyHtml],
32 | });
33 |
--------------------------------------------------------------------------------
/__tests__/rules/aria-unsupported-elements.test.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require('eslint');
2 | const makeRule = require('../../src/utils/makeRule');
3 | const parserOptionsMapper = require('../utils/parserOptionsMapper');
4 | const getSuggestion = require('../utils/getSuggestion.js');
5 |
6 | const ruleTester = new RuleTester();
7 | const makeStyledTestCases = require('../utils/makeStyledTestCases');
8 |
9 | const ruleName = 'aria-unsupported-elements';
10 | const rule = makeRule(ruleName);
11 |
12 | const errorMessage = invalidProp => ({
13 | message: `This element does not support ARIA roles, states and properties. \
14 | Try removing the prop '${invalidProp}'.`,
15 | type: 'JSXOpeningElement',
16 | });
17 |
18 | // ##VALID
19 | //
20 | const metaCharset = makeStyledTestCases({
21 | attrs: "{ 'charset': 'UTF-8' }",
22 | props: 'charset="UTF-8"',
23 | tag: 'meta',
24 | });
25 | // ##INVALID
26 | //
27 | const metaCharsetAriaHidden = makeStyledTestCases({
28 | attrs: "{ 'aria-hidden': false }",
29 | props: 'aria-hidden="false"',
30 | tag: 'meta',
31 | errors: [errorMessage('aria-hidden')],
32 | });
33 |
34 | ruleTester.run(ruleName, rule, {
35 | valid: metaCharset,
36 | invalid: metaCharsetAriaHidden,
37 | });
38 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/rules/no-access-key.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Enforce no accesskey attribute on element.
3 | * @author Ethan Cohen
4 | */
5 |
6 | // ----------------------------------------------------------------------------
7 | // Rule Definition
8 | // ----------------------------------------------------------------------------
9 |
10 | import { getProp, getPropValue } from 'jsx-ast-utils';
11 | import { generateObjSchema } from '../util/schemas';
12 |
13 | const errorMessage = 'No access key attribute allowed. Inconsistencies between keyboard shortcuts and keyboard comments used by screenreader and keyboard only users create a11y complications.';
14 |
15 | const schema = generateObjSchema();
16 |
17 | module.exports = {
18 | meta: {
19 | docs: {
20 | url: 'https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules/no-access-key.md',
21 | },
22 | schema: [schema],
23 | },
24 |
25 | create: (context) => ({
26 | JSXOpeningElement: (node) => {
27 | const accessKey = getProp(node.attributes, 'accesskey');
28 | const accessKeyValue = getPropValue(accessKey);
29 |
30 | if (accessKey && accessKeyValue) {
31 | context.report({
32 | node,
33 | message: errorMessage,
34 | });
35 | }
36 | },
37 | }),
38 | };
39 |
--------------------------------------------------------------------------------
/__tests__/jsx-a11y-rules/rules/iframe-has-title.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Enforce iframe elements have a title attribute.
3 | * @author Ethan Cohen
4 | */
5 |
6 | // ----------------------------------------------------------------------------
7 | // Rule Definition
8 | // ----------------------------------------------------------------------------
9 |
10 | import { elementType, getProp, getPropValue } from 'jsx-ast-utils';
11 | import { generateObjSchema } from '../util/schemas';
12 |
13 | const errorMessage = '' },
30 | { code: '' },
31 | { code: '' },
32 | { code: '' },
33 | { code: '' },
34 | ].map(parserOptionsMapper),
35 | invalid: [
36 | { code: '', errors: [expectedError] },
37 | { code: '', errors: [expectedError] },
38 | { code: '