Skip to content

Commit

Permalink
[Refactor] use es-iterator-helpers
Browse files Browse the repository at this point in the history
 - also, maximally avoid iterator spreads
  • Loading branch information
ljharb committed Aug 13, 2023
1 parent 0c14630 commit 0d7deeb
Show file tree
Hide file tree
Showing 15 changed files with 55 additions and 57 deletions.
4 changes: 1 addition & 3 deletions __tests__/src/util/isDOMElement-test.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"axobject-query": "^3.2.1",
"damerau-levenshtein": "^1.0.8",
"emoji-regex": "^9.2.2",
"es-iterator-helpers": "^1.0.12",
"has": "^1.0.3",
"jsx-ast-utils": "^3.3.5",
"language-tags": "=1.0.5",
Expand Down
4 changes: 1 addition & 3 deletions src/rules/aria-activedescendant-has-tabindex.js
Original file line number Diff line number Diff line change
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
5 changes: 4 additions & 1 deletion src/rules/aria-role.js
Original file line number Diff line number Diff line change
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: 1 addition & 3 deletions src/rules/click-events-have-key-events.js
Original file line number Diff line number Diff line change
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
4 changes: 2 additions & 2 deletions src/rules/interactive-supports-focus.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ import getTabIndex from '../util/getTabIndex';
// ----------------------------------------------------------------------------

const schema = generateObjSchema({
// TODO: convert to use iterFilter and iterFrom
tabbable: enumArraySchema([...roles.keys()].filter((name) => (
!roles.get(name).abstract
&& roles.get(name).superClass.some((klasses) => includes(klasses, 'widget'))
))),
});
const domElements = [...dom.keys()];

const interactiveProps = [].concat(
eventHandlersByType.mouse,
Expand Down Expand Up @@ -69,7 +69,7 @@ export default ({
const hasInteractiveProps = hasAnyProp(attributes, interactiveProps);
const hasTabindex = getTabIndex(getProp(attributes, 'tabIndex')) !== undefined;

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
4 changes: 1 addition & 3 deletions src/rules/no-interactive-element-to-noninteractive-role.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ import isPresentationRole from '../util/isPresentationRole';

const errorMessage = 'Interactive elements should not be assigned non-interactive roles.';

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

export default ({
meta: {
docs: {
Expand Down Expand Up @@ -62,7 +60,7 @@ export default ({
const type = elementType(node);
const role = getLiteralPropValue(getProp(node.attributes, 'role'));

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
3 changes: 1 addition & 2 deletions src/rules/no-noninteractive-element-interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import isPresentationRole from '../util/isPresentationRole';

const errorMessage = 'Non-interactive elements should not be assigned mouse or keyboard event listeners.';

const domElements = [...dom.keys()];
const defaultInteractiveProps = [].concat(
eventHandlersByType.focus,
eventHandlersByType.image,
Expand Down Expand Up @@ -72,7 +71,7 @@ export default ({
&& getPropValue(getProp(attributes, prop)) != null
));

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
4 changes: 1 addition & 3 deletions src/rules/no-noninteractive-element-to-interactive-role.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import isInteractiveRole from '../util/isInteractiveRole';

const errorMessage = 'Non-interactive elements should not be assigned interactive roles.';

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

export default ({
meta: {
docs: {
Expand Down Expand Up @@ -60,7 +58,7 @@ export default ({
const type = elementType(node);
const role = getExplicitRole(type, node.attributes);

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
4 changes: 1 addition & 3 deletions src/rules/no-static-element-interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
hasProp,
} from 'jsx-ast-utils';
import type { JSXOpeningElement } from 'ast-types-flow';
import includes from 'array-includes';
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
import { arraySchema, generateObjSchema } from '../util/schemas';
import getElementType from '../util/getElementType';
Expand All @@ -31,7 +30,6 @@ import isPresentationRole from '../util/isPresentationRole';

const errorMessage = 'Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.';

const domElements = [...dom.keys()];
const defaultInteractiveProps = [].concat(
eventHandlersByType.focus,
eventHandlersByType.keyboard,
Expand Down Expand Up @@ -69,7 +67,7 @@ export default ({
&& getPropValue(getProp(attributes, prop)) != null
));

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
9 changes: 5 additions & 4 deletions src/util/isAbstractRole.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import {
roles,
} from 'aria-query';
import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';

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

const DOMElements = [...dom.keys()];
const DOMElements = new Set(dom.keys());

const isAbstractRole = (tagName, attributes) => {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
if (DOMElements.indexOf(tagName) === -1) {
if (!DOMElements.has(tagName)) {
return false;
}

Expand Down
5 changes: 1 addition & 4 deletions src/util/isDOMElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@
* @flow
*/
import { dom } from 'aria-query';
import includes from 'array-includes';

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

/**
* Returns boolean indicating whether the given element is a DOM element.
*/
const isDOMElement = (
tagName: string,
): boolean => includes(domElements, tagName);
): boolean => dom.has(tagName);

export default isDOMElement;
30 changes: 18 additions & 12 deletions src/util/isInteractiveElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ import {
} from 'axobject-query';
import includes from 'array-includes';
import flatMap from 'array.prototype.flatmap';
import iterFrom from 'es-iterator-helpers/Iterator.from';
// import iterFlatMap from 'es-iterator-helpers/Iterator.prototype.flatMap';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';
import some from 'es-iterator-helpers/Iterator.prototype.some';

import attributesComparator from './attributesComparator';

const domKeys = [...dom.keys()];
const roleKeys = [...roles.keys()];
const elementRoleEntries = [...elementRoles];

Expand Down Expand Up @@ -51,22 +55,24 @@ const interactiveRoles = new Set(roleKeys
'toolbar',
));

const nonInteractiveElementRoleSchemas = flatMap(
// TODO: convert to use iterFlatMap and iterFrom
const interactiveElementRoleSchemas = flatMap(
elementRoleEntries,
([elementSchema, roleSet]) => ([...roleSet].every((role): boolean => nonInteractiveRoles.has(role)) ? [elementSchema] : []),
([elementSchema, rolesArr]) => (rolesArr.some((role): boolean => interactiveRoles.has(role)) ? [elementSchema] : []),
);

const interactiveElementRoleSchemas = flatMap(
// TODO: convert to use iterFlatMap and iterFrom
const nonInteractiveElementRoleSchemas = flatMap(
elementRoleEntries,
([elementSchema, roleSet]) => ([...roleSet].some((role): boolean => interactiveRoles.has(role)) ? [elementSchema] : []),
([elementSchema, rolesArr]) => (rolesArr.every((role): boolean => nonInteractiveRoles.has(role)) ? [elementSchema] : []),
);

const interactiveAXObjects = new Set([...AXObjects.keys()]
.filter((name) => AXObjects.get(name).type === 'widget'));
const interactiveAXObjects = new Set(filter(iterFrom(AXObjects.keys()), (name) => AXObjects.get(name).type === 'widget'));

// TODO: convert to use iterFlatMap and iterFrom
const interactiveElementAXObjectSchemas = flatMap(
[...elementAXObjects],
([elementSchema, AXObjectSet]) => ([...AXObjectSet].every((role): boolean => interactiveAXObjects.has(role)) ? [elementSchema] : []),
([elementSchema, AXObjectsArr]) => (AXObjectsArr.every((role): boolean => interactiveAXObjects.has(role)) ? [elementSchema] : []),
);

function checkIsInteractiveElement(tagName, attributes): boolean {
Expand All @@ -78,18 +84,18 @@ function checkIsInteractiveElement(tagName, attributes): boolean {
}
// Check in elementRoles for inherent interactive role associations for
// this element.
const isInherentInteractiveElement = interactiveElementRoleSchemas.some(elementSchemaMatcher);
const isInherentInteractiveElement = some(iterFrom(interactiveElementRoleSchemas), elementSchemaMatcher);
if (isInherentInteractiveElement) {
return true;
}
// Check in elementRoles for inherent non-interactive role associations for
// this element.
const isInherentNonInteractiveElement = nonInteractiveElementRoleSchemas.some(elementSchemaMatcher);
const isInherentNonInteractiveElement = some(iterFrom(nonInteractiveElementRoleSchemas), elementSchemaMatcher);
if (isInherentNonInteractiveElement) {
return false;
}
// Check in elementAXObjects for AX Tree associations for this element.
const isInteractiveAXElement = interactiveElementAXObjectSchemas.some(elementSchemaMatcher);
const isInteractiveAXElement = some(iterFrom(interactiveElementAXObjectSchemas), elementSchemaMatcher);
if (isInteractiveAXElement) {
return true;
}
Expand All @@ -109,7 +115,7 @@ const isInteractiveElement = (
): boolean => {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
if (!includes(domKeys, tagName)) {
if (!dom.has(tagName)) {
return false;
}

Expand Down
29 changes: 16 additions & 13 deletions src/util/isNonInteractiveElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
import type { Node } from 'ast-types-flow';
import includes from 'array-includes';
import flatMap from 'array.prototype.flatmap';
import iterFrom from 'es-iterator-helpers/Iterator.from';
// import iterFlatMap from 'es-iterator-helpers/Iterator.prototype.flatMap';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';
import some from 'es-iterator-helpers/Iterator.prototype.some';

import attributesComparator from './attributesComparator';

Expand Down Expand Up @@ -62,22 +66,24 @@ const interactiveRoles = new Set(roleKeys
'toolbar',
));

const nonInteractiveElementRoleSchemas = flatMap(
// TODO: convert to use iterFlatMap and iterFrom
const interactiveElementRoleSchemas = flatMap(
elementRoleEntries,
([elementSchema, roleSet]) => ([...roleSet].every((role): boolean => nonInteractiveRoles.has(role)) ? [elementSchema] : []),
([elementSchema, rolesArr]) => (rolesArr.some((role): boolean => interactiveRoles.has(role)) ? [elementSchema] : []),
);

const interactiveElementRoleSchemas = flatMap(
// TODO: convert to use iterFlatMap and iterFrom
const nonInteractiveElementRoleSchemas = flatMap(
elementRoleEntries,
([elementSchema, roleSet]) => ([...roleSet].some((role): boolean => interactiveRoles.has(role)) ? [elementSchema] : []),
([elementSchema, rolesArr]) => (rolesArr.every((role): boolean => nonInteractiveRoles.has(role)) ? [elementSchema] : []),
);

const nonInteractiveAXObjects = new Set([...AXObjects.keys()]
.filter((name) => includes(['window', 'structure'], AXObjects.get(name).type)));
const nonInteractiveAXObjects = new Set(filter(iterFrom(AXObjects.keys()), (name) => includes(['window', 'structure'], AXObjects.get(name).type)));

// TODO: convert to use iterFlatMap and iterFrom
const nonInteractiveElementAXObjectSchemas = flatMap(
[...elementAXObjects],
([elementSchema, AXObjectSet]) => ([...AXObjectSet].every((role): boolean => nonInteractiveAXObjects.has(role)) ? [elementSchema] : []),
([elementSchema, AXObjectsArr]) => (AXObjectsArr.every((role): boolean => nonInteractiveAXObjects.has(role)) ? [elementSchema] : []),
);

function checkIsNonInteractiveElement(tagName, attributes): boolean {
Expand All @@ -89,21 +95,18 @@ function checkIsNonInteractiveElement(tagName, attributes): boolean {
}
// Check in elementRoles for inherent non-interactive role associations for
// this element.
const isInherentNonInteractiveElement = nonInteractiveElementRoleSchemas
.some(elementSchemaMatcher);
const isInherentNonInteractiveElement = some(iterFrom(nonInteractiveElementRoleSchemas), elementSchemaMatcher);
if (isInherentNonInteractiveElement) {
return true;
}
// Check in elementRoles for inherent interactive role associations for
// this element.
const isInherentInteractiveElement = interactiveElementRoleSchemas
.some(elementSchemaMatcher);
const isInherentInteractiveElement = some(iterFrom(interactiveElementRoleSchemas), elementSchemaMatcher);
if (isInherentInteractiveElement) {
return false;
}
// Check in elementAXObjects for AX Tree associations for this element.
const isNonInteractiveAXElement = nonInteractiveElementAXObjectSchemas
.some(elementSchemaMatcher);
const isNonInteractiveAXElement = some(iterFrom(nonInteractiveElementAXObjectSchemas), elementSchemaMatcher);
if (isNonInteractiveAXElement) {
return true;
}
Expand Down

0 comments on commit 0d7deeb

Please sign in to comment.