Skip to content

Commit

Permalink
Merge branch 'main' into patch-2
Browse files Browse the repository at this point in the history
  • Loading branch information
khiga8 committed May 6, 2024
2 parents ec87ead + 0d5321a commit 5e88649
Show file tree
Hide file tree
Showing 27 changed files with 210 additions and 146 deletions.
111 changes: 73 additions & 38 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions __tests__/src/rules/aria-unsupported-elements-test.js
Expand Up @@ -69,7 +69,7 @@ const invalidAriaValidityTests = domElements
}));

ruleTester.run('aria-unsupported-elements', rule, {
valid: parsers.all([].concat(roleValidityTests.concat(ariaValidityTests))).map(parserOptionsMapper),
invalid: parsers.all([].concat(invalidRoleValidityTests.concat(invalidAriaValidityTests)))
valid: parsers.all([].concat(roleValidityTests, ariaValidityTests)).map(parserOptionsMapper),
invalid: parsers.all([].concat(invalidRoleValidityTests, invalidAriaValidityTests))
.map(parserOptionsMapper),
});
4 changes: 4 additions & 0 deletions __tests__/src/rules/img-redundant-alt-test.js
Expand Up @@ -74,6 +74,7 @@ ruleTester.run('img-redundant-alt', rule, {
{ code: '<img alt="ImageMagick" />;' },
{ code: '<Image alt="Photo of a friend" />' },
{ code: '<Image alt="Foo" />', settings: componentsSettings },
{ code: '<img alt="画像" />', options: [{ words: ['イメージ'] }] },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<img alt="Photo of friend." />;', errors: [expectedError] },
Expand Down Expand Up @@ -129,5 +130,8 @@ ruleTester.run('img-redundant-alt', rule, {
{ code: '<img alt="Word2" />;', options: array, errors: [expectedError] },
{ code: '<Image alt="Word1" />;', options: array, errors: [expectedError] },
{ code: '<Image alt="Word2" />;', options: array, errors: [expectedError] },

{ code: '<img alt="イメージ" />', options: [{ words: ['イメージ'] }], errors: [expectedError] },
{ code: '<img alt="イメージです" />', options: [{ words: ['イメージ'] }], errors: [expectedError] },
)).map(parserOptionsMapper),
});
8 changes: 6 additions & 2 deletions __tests__/src/rules/role-has-required-aria-props-test.js
Expand Up @@ -10,6 +10,10 @@

import { roles } from 'aria-query';
import { RuleTester } from 'eslint';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import map from 'es-iterator-helpers/Iterator.prototype.map';
import toArray from 'es-iterator-helpers/Iterator.prototype.toArray';

import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/role-has-required-aria-props';
Expand Down Expand Up @@ -38,7 +42,7 @@ const componentsSettings = {
};

// Create basic test cases using all valid role types.
const basicValidityTests = [...roles.keys()].map((role) => {
const basicValidityTests = toArray(map(iterFrom(roles.keys()), (role) => {
const {
requiredProps: requiredPropKeyValues,
} = roles.get(role);
Expand All @@ -48,7 +52,7 @@ const basicValidityTests = [...roles.keys()].map((role) => {
return {
code: `<div role="${role.toLowerCase()}" ${propChain} />`,
};
});
}));

ruleTester.run('role-has-required-aria-props', rule, {
valid: parsers.all([].concat(
Expand Down
38 changes: 20 additions & 18 deletions __tests__/src/rules/role-supports-aria-props-test.js
Expand Up @@ -14,6 +14,11 @@ import {
import { RuleTester } from 'eslint';
import { version as eslintVersion } from 'eslint/package.json';
import semver from 'semver';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';
import map from 'es-iterator-helpers/Iterator.prototype.map';
import toArray from 'es-iterator-helpers/Iterator.prototype.toArray';

import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/role-supports-aria-props';
Expand All @@ -26,8 +31,7 @@ const ruleTester = new RuleTester();

const generateErrorMessage = (attr, role, tag, isImplicit) => {
if (isImplicit) {
return `The attribute ${attr} is not supported by the role ${role}. \
This role is implicit on the element ${tag}.`;
return `The attribute ${attr} is not supported by the role ${role}. This role is implicit on the element ${tag}.`;
}

return `The attribute ${attr} is not supported by the role ${role}.`;
Expand All @@ -46,30 +50,28 @@ const componentsSettings = {
},
};

const nonAbstractRoles = [...roles.keys()].filter((role) => roles.get(role).abstract === false);
const nonAbstractRoles = toArray(filter(iterFrom(roles.keys()), (role) => roles.get(role).abstract === false));

const createTests = (rolesNames) => rolesNames.reduce((tests, role) => {
const {
props: propKeyValues,
} = roles.get(role);
const validPropsForRole = Object.keys(propKeyValues);
const invalidPropsForRole = [...aria.keys()]
.map((attribute) => attribute.toLowerCase())
.filter((attribute) => validPropsForRole.indexOf(attribute) === -1);
const invalidPropsForRole = filter(
map(iterFrom(aria.keys()), (attribute) => attribute.toLowerCase()),
(attribute) => validPropsForRole.indexOf(attribute) === -1,
);
const normalRole = role.toLowerCase();

const allTests = [];

allTests[0] = tests[0].concat(validPropsForRole.map((prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
})));

allTests[1] = tests[1].concat(invalidPropsForRole.map((prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
errors: [errorMessage(prop.toLowerCase(), normalRole, 'div', false)],
})));

return allTests;
return [
tests[0].concat(validPropsForRole.map((prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
}))),
tests[1].concat(toArray(map(invalidPropsForRole, (prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
errors: [errorMessage(prop.toLowerCase(), normalRole, 'div', false)],
})))),
];
}, [[], []]);

const [validTests, invalidTests] = createTests(nonAbstractRoles);
Expand Down
4 changes: 1 addition & 3 deletions __tests__/src/util/isDOMElement-test.js
Expand Up @@ -4,11 +4,9 @@ import { elementType } from 'jsx-ast-utils';
import isDOMElement from '../../../src/util/isDOMElement';
import JSXElementMock from '../../../__mocks__/JSXElementMock';

const domElements = [...dom.keys()];

describe('isDOMElement', () => {
describe('DOM elements', () => {
domElements.forEach((el) => {
dom.forEach((_, el) => {
it(`should identify ${el} as a DOM element`, () => {
const element = JSXElementMock(el);
expect(isDOMElement(elementType(element.openingElement)))
Expand Down
2 changes: 1 addition & 1 deletion __tests__/src/util/isFocusable-test.js
Expand Up @@ -9,7 +9,7 @@ import {
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';

function mergeTabIndex(index, attributes) {
return [...attributes, JSXAttributeMock('tabIndex', index)];
return [].concat(attributes, JSXAttributeMock('tabIndex', index));
}

describe('isFocusable', () => {
Expand Down
32 changes: 17 additions & 15 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-jsx-a11y",
"version": "6.7.1",
"version": "6.8.0",
"description": "Static AST checker for accessibility rules on JSX elements.",
"keywords": [
"eslint",
Expand Down Expand Up @@ -37,23 +37,22 @@
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
"devDependencies": {
"@babel/cli": "^7.23.0",
"@babel/core": "^7.23.2",
"@babel/eslint-parser": "^7.22.15",
"@babel/plugin-transform-flow-strip-types": "^7.22.5",
"@babel/register": "^7.22.15",
"ast-types-flow": "^0.0.8",
"aud": "^2.0.3",
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.7",
"@babel/eslint-parser": "^7.23.3",
"@babel/plugin-transform-flow-strip-types": "^7.23.3",
"@babel/register": "^7.23.7",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"babel-jest": "^24.9.0",
"babel-plugin-add-module-exports": "^1.0.4",
"babel-preset-airbnb": "^5.0.0",
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-doc-generator": "^1.5.2",
"eslint-doc-generator": "^1.6.1",
"eslint-plugin-eslint-plugin": "^4.3.0",
"eslint-plugin-flowtype": "^5.8.0 || ^8.0.3",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-import": "^2.29.1",
"estraverse": "^5.3.0",
"expect": "^24.9.0",
"flow-bin": "^0.147.0",
Expand All @@ -62,8 +61,8 @@
"jest": "^24.9.0",
"jscodeshift": "^0.7.1",
"minimist": "^1.2.8",
"npmignore": "^0.3.0",
"object.assign": "^4.1.4",
"npmignore": "^0.3.1",
"object.assign": "^4.1.5",
"rimraf": "^3.0.2",
"safe-publish-latest": "^2.0.0",
"semver": "^6.3.1",
Expand All @@ -74,21 +73,24 @@
},
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2",
"@babel/runtime": "^7.23.9",
"aria-query": "^5.3.0",
"array-includes": "^3.1.7",
"array.prototype.flatmap": "^1.3.2",
"ast-types-flow": "^0.0.7",
"ast-types-flow": "^0.0.8",
"axe-core": "=4.7.0",
"axobject-query": "^3.2.1",
"damerau-levenshtein": "^1.0.8",
"emoji-regex": "^9.2.2",
"es-iterator-helpers": "^1.0.15",
"hasown": "^2.0.0",
"jsx-ast-utils": "^3.3.5",
"language-tags": "^1.0.9",
"minimatch": "^3.1.2",
"object.entries": "^1.1.7",
"object.fromentries": "^2.0.7"
"object.fromentries": "^2.0.7",
"safe-regex-test": "^1.0.2",
"string.prototype.includes": "^2.0.0"
},
"peerDependencies": {
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
Expand Down
5 changes: 4 additions & 1 deletion src/rules/accessible-emoji.js
Expand Up @@ -9,6 +9,7 @@

import emojiRegex from 'emoji-regex';
import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
import safeRegexTest from 'safe-regex-test';
import { generateObjSchema } from '../util/schemas';
import getElementType from '../util/getElementType';
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
Expand All @@ -29,11 +30,13 @@ export default {

create: (context) => {
const elementType = getElementType(context);

const testEmoji = safeRegexTest(emojiRegex());
return {
JSXOpeningElement: (node) => {
const literalChildValue = node.parent.children.find((child) => child.type === 'Literal' || child.type === 'JSXText');

if (literalChildValue && emojiRegex().test(literalChildValue.value)) {
if (literalChildValue && testEmoji(literalChildValue.value)) {
const elementIsHidden = isHiddenFromScreenReader(elementType(node), node.attributes);
if (elementIsHidden) {
return; // emoji is decorative
Expand Down
5 changes: 4 additions & 1 deletion src/rules/anchor-is-valid.js
Expand Up @@ -10,6 +10,7 @@

import { getProp, getPropValue } from 'jsx-ast-utils';
import type { JSXOpeningElement } from 'ast-types-flow';
import safeRegexTest from 'safe-regex-test';
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
import { generateObjSchema, arraySchema, enumArraySchema } from '../util/schemas';
import getElementType from '../util/getElementType';
Expand Down Expand Up @@ -39,6 +40,8 @@ export default ({

create: (context: ESLintContext): ESLintVisitorSelectorConfig => {
const elementType = getElementType(context);
const testJShref = safeRegexTest(/^\W*?javascript:/);

return {
JSXOpeningElement: (node: JSXOpeningElement): void => {
const { attributes } = node;
Expand Down Expand Up @@ -98,7 +101,7 @@ export default ({
.filter((value) => (
value != null
&& (typeof value === 'string' && (
!value.length || value === '#' || /^\W*?javascript:/.test(value)
!value.length || value === '#' || testJShref(value)
))
));
if (invalidHrefValues.length !== 0) {
Expand Down
4 changes: 1 addition & 3 deletions src/rules/aria-activedescendant-has-tabindex.js
Expand Up @@ -18,8 +18,6 @@ const errorMessage = 'An element that manages focus with `aria-activedescendant`

const schema = generateObjSchema();

const domElements = [...dom.keys()];

export default {
meta: {
docs: {
Expand All @@ -42,7 +40,7 @@ export default {
const type = elementType(node);
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
if (domElements.indexOf(type) === -1) {
if (!dom.has(type)) {
return;
}
const tabIndex = getTabIndex(getProp(attributes, 'tabIndex'));
Expand Down
2 changes: 1 addition & 1 deletion src/rules/aria-props.js
Expand Up @@ -45,7 +45,7 @@ export default {
return;
}

const isValid = ariaAttributes.indexOf(name) > -1;
const isValid = aria.has(name);

if (isValid === false) {
context.report({
Expand Down
5 changes: 4 additions & 1 deletion src/rules/aria-role.js
Expand Up @@ -9,6 +9,9 @@

import { dom, roles } from 'aria-query';
import { getLiteralPropValue, propName } from 'jsx-ast-utils';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';

import getElementType from '../util/getElementType';
import { generateObjSchema } from '../util/schemas';

Expand All @@ -28,7 +31,7 @@ const schema = generateObjSchema({
},
});

const validRoles = new Set([...roles.keys()].filter((role) => roles.get(role).abstract === false));
const validRoles = new Set(filter(iterFrom(roles.keys()), (role) => roles.get(role).abstract === false));

export default {
meta: {
Expand Down
4 changes: 2 additions & 2 deletions src/rules/aria-unsupported-elements.js
Expand Up @@ -47,7 +47,7 @@ export default {
return;
}

const invalidAttributes = [...aria.keys(), 'role'];
const invalidAttributes = new Set([...aria.keys(), 'role']);

node.attributes.forEach((prop) => {
if (prop.type === 'JSXSpreadAttribute') {
Expand All @@ -56,7 +56,7 @@ export default {

const name = propName(prop).toLowerCase();

if (invalidAttributes.indexOf(name) > -1) {
if (invalidAttributes.has(name)) {
context.report({
node,
message: errorMessage(name),
Expand Down
4 changes: 1 addition & 3 deletions src/rules/click-events-have-key-events.js
Expand Up @@ -9,7 +9,6 @@

import { dom } from 'aria-query';
import { getProp, hasAnyProp } from 'jsx-ast-utils';
import includes from 'array-includes';
import { generateObjSchema } from '../util/schemas';
import getElementType from '../util/getElementType';
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
Expand All @@ -19,7 +18,6 @@ import isPresentationRole from '../util/isPresentationRole';
const errorMessage = 'Visible, non-interactive elements with click handlers must have at least one keyboard listener.';

const schema = generateObjSchema();
const domElements = [...dom.keys()];

export default {
meta: {
Expand All @@ -42,7 +40,7 @@ export default {
const type = elementType(node);
const requiredProps = ['onkeydown', 'onkeyup', 'onkeypress'];

if (!includes(domElements, type)) {
if (!dom.has(type)) {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
return;
Expand Down
16 changes: 15 additions & 1 deletion src/rules/img-redundant-alt.js
Expand Up @@ -8,6 +8,9 @@
// ----------------------------------------------------------------------------

import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
import includes from 'array-includes';
import stringIncludes from 'string.prototype.includes';
import safeRegexTest from 'safe-regex-test';
import { generateObjSchema, arraySchema } from '../util/schemas';
import getElementType from '../util/getElementType';
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
Expand All @@ -25,6 +28,17 @@ const schema = generateObjSchema({
words: arraySchema,
});

const isASCII = safeRegexTest(/[\x20-\x7F]+/);

function containsRedundantWord(value, redundantWords) {
const lowercaseRedundantWords = redundantWords.map((redundantWord) => redundantWord.toLowerCase());

if (isASCII(value)) {
return value.split(/\s+/).some((valueWord) => includes(lowercaseRedundantWords, valueWord.toLowerCase()));
}
return lowercaseRedundantWords.some((redundantWord) => stringIncludes(value.toLowerCase(), redundantWord));
}

export default {
meta: {
docs: {
Expand Down Expand Up @@ -63,7 +77,7 @@ export default {
const redundantWords = REDUNDANT_WORDS.concat(words);

if (typeof value === 'string' && isVisible) {
const hasRedundancy = new RegExp(`(?!{)\\b(${redundantWords.join('|')})\\b(?!})`, 'i').test(value);
const hasRedundancy = containsRedundantWord(value, redundantWords);

if (hasRedundancy === true) {
context.report({
Expand Down

0 comments on commit 5e88649

Please sign in to comment.