Skip to content

Commit 3d77c84

Browse files
committed
[Refactor] use fromEntries, flatMap, etc; better use iteration methods
1 parent 89f766c commit 3d77c84

14 files changed

+110
-164
lines changed

__mocks__/genInteractives.js

+17-18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import { dom, roles } from 'aria-query';
66
import includes from 'array-includes';
7+
import fromEntries from 'object.fromentries';
8+
79
import JSXAttributeMock from './JSXAttributeMock';
810
import JSXElementMock from './JSXElementMock';
911

@@ -115,13 +117,7 @@ const nonInteractiveElementsMap: {[string]: Array<{[string]: string}>} = {
115117
ul: [],
116118
};
117119

118-
const indeterminantInteractiveElementsMap = domElements.reduce(
119-
(accumulator: { [key: string]: Array<any> }, name: string): { [key: string]: Array<any> } => ({
120-
...accumulator,
121-
[name]: [],
122-
}),
123-
{},
124-
);
120+
const indeterminantInteractiveElementsMap: { [key: string]: Array<any> } = fromEntries(domElements.map((name: string) => [name, []]));
125121

126122
Object.keys(interactiveElementsMap)
127123
.concat(Object.keys(nonInteractiveElementsMap))
@@ -138,22 +134,25 @@ const interactiveRoles = []
138134
// aria-activedescendant, thus in practice we treat it as a widget.
139135
'toolbar',
140136
)
141-
.filter((role) => !roles.get(role).abstract)
142-
.filter((role) => roles.get(role).superClass.some((klasses) => includes(klasses, 'widget')));
137+
.filter((role) => (
138+
!roles.get(role).abstract
139+
&& roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
140+
));
143141

144142
const nonInteractiveRoles = roleNames
145-
.filter((role) => !roles.get(role).abstract)
146-
.filter((role) => !roles.get(role).superClass.some((klasses) => includes(klasses, 'widget')))
147-
// 'toolbar' does not descend from widget, but it does support
148-
// aria-activedescendant, thus in practice we treat it as a widget.
149-
.filter((role) => !includes(['toolbar'], role));
143+
.filter((role) => (
144+
!roles.get(role).abstract
145+
&& !roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
146+
147+
// 'toolbar' does not descend from widget, but it does support
148+
// aria-activedescendant, thus in practice we treat it as a widget.
149+
&& !includes(['toolbar'], role)
150+
));
150151

151152
export function genElementSymbol(openingElement: Object): string {
152153
return (
153154
openingElement.name.name + (openingElement.attributes.length > 0
154-
? `${openingElement.attributes
155-
.map((attr) => `[${attr.name.name}="${attr.value.value}"]`)
156-
.join('')}`
155+
? `${openingElement.attributes.map((attr) => `[${attr.name.name}="${attr.value.value}"]`).join('')}`
157156
: ''
158157
)
159158
);
@@ -172,7 +171,7 @@ export function genInteractiveElements(): Array<JSXElementMockType> {
172171
}
173172

174173
export function genInteractiveRoleElements(): Array<JSXElementMockType> {
175-
return [...interactiveRoles, 'button article', 'fakerole button article'].map((value): JSXElementMockType => JSXElementMock(
174+
return interactiveRoles.concat('button article', 'fakerole button article').map((value): JSXElementMockType => JSXElementMock(
176175
'div',
177176
[JSXAttributeMock('role', value)],
178177
));

__tests__/__util__/ruleOptionsMapperFactory.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
* @flow
33
*/
44

5+
import entries from 'object.entries';
6+
import flatMap from 'array.prototype.flatmap';
7+
import fromEntries from 'object.fromentries';
8+
59
type ESLintTestRunnerTestCase = {
610
code: string,
711
errors: ?Array<{ message: string, type: string }>,
@@ -21,10 +25,7 @@ export default function ruleOptionsMapperFactory(ruleOptions: Array<mixed> = [])
2125
code,
2226
errors,
2327
// Flatten the array of objects in an array of one object.
24-
options: (options || []).concat(ruleOptions).reduce((acc, item) => [{
25-
...acc[0],
26-
...item,
27-
}], [{}]),
28+
options: [fromEntries(flatMap((options || []).concat(ruleOptions), (item) => entries(item)))],
2829
parserOptions,
2930
settings,
3031
};

__tests__/src/rules/aria-unsupported-elements-test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const ariaValidityTests = domElements.map((element) => {
5050

5151
// Generate invalid test cases.
5252
const invalidRoleValidityTests = domElements
53-
.filter((element) => Boolean(dom.get(element).reserved))
53+
.filter((element) => dom.get(element).reserved)
5454
.map((reservedElem) => ({
5555
code: `<${reservedElem} role {...props} />`,
5656
errors: [errorMessage('role')],
@@ -61,7 +61,7 @@ const invalidRoleValidityTests = domElements
6161
});
6262

6363
const invalidAriaValidityTests = domElements
64-
.filter((element) => Boolean(dom.get(element).reserved))
64+
.filter((element) => dom.get(element).reserved)
6565
.map((reservedElem) => ({
6666
code: `<${reservedElem} aria-hidden aria-role="none" {...props} />`,
6767
errors: [errorMessage('aria-hidden')],

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"@babel/runtime": "^7.20.7",
7373
"aria-query": "^5.1.3",
7474
"array-includes": "^3.1.6",
75+
"array.prototype.flatmap": "^1.3.1",
7576
"ast-types-flow": "^0.0.7",
7677
"axe-core": "^4.6.2",
7778
"axobject-query": "^3.1.1",
@@ -81,6 +82,8 @@
8182
"jsx-ast-utils": "^3.3.3",
8283
"language-tags": "=1.0.5",
8384
"minimatch": "^3.1.2",
85+
"object.entries": "^1.1.6",
86+
"object.fromentries": "^2.0.6",
8487
"semver": "^6.3.0"
8588
},
8689
"peerDependencies": {

src/rules/alt-text.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
getPropValue,
1313
getLiteralPropValue,
1414
} from 'jsx-ast-utils';
15+
import flatMap from 'array.prototype.flatmap';
16+
1517
import { generateObjSchema, arraySchema } from '../util/schemas';
1618
import getElementType from '../util/getElementType';
1719
import hasAccessibleChild from '../util/hasAccessibleChild';
@@ -204,12 +206,10 @@ export default {
204206
// Elements to validate for alt text.
205207
const elementOptions = options.elements || DEFAULT_ELEMENTS;
206208
// Get custom components for just the elements that will be tested.
207-
const customComponents = elementOptions
208-
.map((element) => options[element])
209-
.reduce(
210-
(components, customComponentsForElement) => components.concat(customComponentsForElement || []),
211-
[],
212-
);
209+
const customComponents = flatMap(
210+
elementOptions,
211+
(element) => options[element],
212+
);
213213
const typesToValidate = new Set(
214214
[].concat(
215215
customComponents,

src/rules/anchor-is-valid.js

+9-11
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,12 @@ export default ({
6464

6565
const propOptions = options.specialLink || [];
6666
const propsToValidate = ['href'].concat(propOptions);
67-
const values = propsToValidate
68-
.map((prop) => getProp(node.attributes, prop))
69-
.map((prop) => getPropValue(prop));
67+
const values = propsToValidate.map((prop) => getPropValue(getProp(node.attributes, prop)));
7068
// Checks if any actual or custom href prop is provided.
71-
const hasAnyHref = values
72-
.filter((value) => value === undefined || value === null).length !== values.length;
69+
const hasAnyHref = values.some((value) => value != null);
7370
// Need to check for spread operator as props can be spread onto the element
7471
// leading to an incorrect validation error.
75-
const hasSpreadOperator = attributes
76-
.filter((prop) => prop.type === 'JSXSpreadAttribute').length > 0;
72+
const hasSpreadOperator = attributes.some((prop) => prop.type === 'JSXSpreadAttribute');
7773
const onClick = getProp(attributes, 'onClick');
7874

7975
// When there is no href at all, specific scenarios apply:
@@ -99,10 +95,12 @@ export default ({
9995

10096
// Hrefs have been found, now check for validity.
10197
const invalidHrefValues = values
102-
.filter((value) => value !== undefined && value !== null)
103-
.filter((value) => (typeof value === 'string' && (
104-
!value.length || value === '#' || /^\W*?javascript:/.test(value)
105-
)));
98+
.filter((value) => (
99+
value != null
100+
&& (typeof value === 'string' && (
101+
!value.length || value === '#' || /^\W*?javascript:/.test(value)
102+
))
103+
));
106104
if (invalidHrefValues.length !== 0) {
107105
// If an onClick is found it should be a button, otherwise it is an invalid link.
108106
if (onClick && activeAspects.preferButton) {

src/rules/interactive-supports-focus.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ import getTabIndex from '../util/getTabIndex';
3636
// ----------------------------------------------------------------------------
3737

3838
const schema = generateObjSchema({
39-
tabbable: enumArraySchema([...roles.keys()]
40-
.filter((name) => !roles.get(name).abstract)
41-
.filter((name) => roles.get(name).superClass.some((klasses) => includes(klasses, 'widget')))),
39+
tabbable: enumArraySchema([...roles.keys()].filter((name) => (
40+
!roles.get(name).abstract
41+
&& roles.get(name).superClass.some((klasses) => includes(klasses, 'widget'))
42+
))),
4243
});
4344
const domElements = [...dom.keys()];
4445

src/rules/media-has-caption.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import type { JSXElement, JSXOpeningElement, Node } from 'ast-types-flow';
1212
import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
13+
import flatMap from 'array.prototype.flatmap';
14+
1315
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
1416
import { generateObjSchema, arraySchema } from '../util/schemas';
1517
import getElementType from '../util/getElementType';
@@ -26,8 +28,8 @@ const schema = generateObjSchema({
2628

2729
const isMediaType = (context, type) => {
2830
const options = context.options[0] || {};
29-
return MEDIA_TYPES.map((mediaType) => options[mediaType])
30-
.reduce((types, customComponent) => types.concat(customComponent), MEDIA_TYPES)
31+
return MEDIA_TYPES
32+
.concat(flatMap(MEDIA_TYPES, (mediaType) => options[mediaType]))
3133
.some((typeToCheck) => typeToCheck === type);
3234
};
3335

src/util/getSuggestion.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import editDistance from 'damerau-levenshtein';
2+
import fromEntries from 'object.fromentries';
23

34
// Minimum edit distance to be considered a good suggestion.
45
const THRESHOLD = 2;
@@ -8,12 +9,13 @@ const THRESHOLD = 2;
89
* to return.
910
*/
1011
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-
}, {});
12+
const distances = fromEntries(
13+
dictionary.map((dictionaryWord) => {
14+
const distance = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase());
15+
const { steps } = distance;
16+
return [dictionaryWord, steps];
17+
}),
18+
);
1719

1820
return Object.keys(distances)
1921
.filter((suggestion) => distances[suggestion] <= THRESHOLD)

src/util/hasAccessibleChild.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ export default function hasAccessibleChild(node: JSXElement, elementType: (JSXOp
88
return node.children.some((child: Node) => {
99
switch (child.type) {
1010
case 'Literal':
11-
return Boolean(child.value);
11+
return !!child.value;
1212
// $FlowFixMe JSXText is missing in ast-types-flow
1313
case 'JSXText':
14-
return Boolean(child.value);
14+
return !!child.value;
1515
case 'JSXElement':
1616
return !isHiddenFromScreenReader(
1717
elementType(child.openingElement),

src/util/isInteractiveElement.js

+18-47
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
elementAXObjects,
1313
} from 'axobject-query';
1414
import includes from 'array-includes';
15+
import flatMap from 'array.prototype.flatmap';
1516
import attributesComparator from './attributesComparator';
1617

1718
const domKeys = [...dom.keys()];
@@ -39,8 +40,8 @@ const interactiveRoles = new Set(roleKeys
3940
const role = roles.get(name);
4041
return (
4142
!role.abstract
42-
// The `progressbar` is descended from `widget`, but in practice, its
43-
// value is always `readonly`, so we treat it as a non-interactive role.
43+
// The `progressbar` is descended from `widget`, but in practice, its
44+
// value is always `readonly`, so we treat it as a non-interactive role.
4445
&& name !== 'progressbar'
4546
&& role.superClass.some((classes) => includes(classes, 'widget'))
4647
);
@@ -50,50 +51,23 @@ const interactiveRoles = new Set(roleKeys
5051
'toolbar',
5152
));
5253

53-
const nonInteractiveElementRoleSchemas = elementRoleEntries
54-
.reduce((
55-
accumulator,
56-
[
57-
elementSchema,
58-
roleSet,
59-
],
60-
) => {
61-
if ([...roleSet].every((role): boolean => nonInteractiveRoles.has(role))) {
62-
accumulator.push(elementSchema);
63-
}
64-
return accumulator;
65-
}, []);
54+
const nonInteractiveElementRoleSchemas = flatMap(
55+
elementRoleEntries,
56+
([elementSchema, roleSet]) => ([...roleSet].every((role): boolean => nonInteractiveRoles.has(role)) ? [elementSchema] : []),
57+
);
6658

67-
const interactiveElementRoleSchemas = elementRoleEntries
68-
.reduce((
69-
accumulator,
70-
[
71-
elementSchema,
72-
roleSet,
73-
],
74-
) => {
75-
if ([...roleSet].some((role): boolean => interactiveRoles.has(role))) {
76-
accumulator.push(elementSchema);
77-
}
78-
return accumulator;
79-
}, []);
59+
const interactiveElementRoleSchemas = flatMap(
60+
elementRoleEntries,
61+
([elementSchema, roleSet]) => ([...roleSet].some((role): boolean => interactiveRoles.has(role)) ? [elementSchema] : []),
62+
);
8063

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

84-
const interactiveElementAXObjectSchemas = [...elementAXObjects]
85-
.reduce((
86-
accumulator,
87-
[
88-
elementSchema,
89-
AXObjectSet,
90-
],
91-
) => {
92-
if ([...AXObjectSet].every((role): boolean => interactiveAXObjects.has(role))) {
93-
accumulator.push(elementSchema);
94-
}
95-
return accumulator;
96-
}, []);
67+
const interactiveElementAXObjectSchemas = flatMap(
68+
[...elementAXObjects],
69+
([elementSchema, AXObjectSet]) => ([...AXObjectSet].every((role): boolean => interactiveAXObjects.has(role)) ? [elementSchema] : []),
70+
);
9771

9872
function checkIsInteractiveElement(tagName, attributes): boolean {
9973
function elementSchemaMatcher(elementSchema) {
@@ -104,21 +78,18 @@ function checkIsInteractiveElement(tagName, attributes): boolean {
10478
}
10579
// Check in elementRoles for inherent interactive role associations for
10680
// this element.
107-
const isInherentInteractiveElement = interactiveElementRoleSchemas
108-
.some(elementSchemaMatcher);
81+
const isInherentInteractiveElement = interactiveElementRoleSchemas.some(elementSchemaMatcher);
10982
if (isInherentInteractiveElement) {
11083
return true;
11184
}
11285
// Check in elementRoles for inherent non-interactive role associations for
11386
// this element.
114-
const isInherentNonInteractiveElement = nonInteractiveElementRoleSchemas
115-
.some(elementSchemaMatcher);
87+
const isInherentNonInteractiveElement = nonInteractiveElementRoleSchemas.some(elementSchemaMatcher);
11688
if (isInherentNonInteractiveElement) {
11789
return false;
11890
}
11991
// Check in elementAXObjects for AX Tree associations for this element.
120-
const isInteractiveAXElement = interactiveElementAXObjectSchemas
121-
.some(elementSchemaMatcher);
92+
const isInteractiveAXElement = interactiveElementAXObjectSchemas.some(elementSchemaMatcher);
12293
if (isInteractiveAXElement) {
12394
return true;
12495
}

0 commit comments

Comments
 (0)