@hypersolid-dev/eslint-config 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +12 -1
- package/dist/plugins/react-native-a11y/constants/accessibility-props.d.ts +32 -0
- package/dist/plugins/react-native-a11y/constants/accessibility-props.js +50 -0
- package/dist/plugins/react-native-a11y/constants/accessibility-state.d.ts +2 -0
- package/dist/plugins/react-native-a11y/constants/accessibility-state.js +8 -0
- package/dist/plugins/react-native-a11y/constants/interactive.d.ts +6 -0
- package/dist/plugins/react-native-a11y/constants/interactive.js +33 -0
- package/dist/plugins/react-native-a11y/constants/react-native-components.d.ts +7 -0
- package/dist/plugins/react-native-a11y/constants/react-native-components.js +13 -0
- package/dist/plugins/react-native-a11y/constants/schema-properties.d.ts +4 -0
- package/dist/plugins/react-native-a11y/constants/schema-properties.js +10 -0
- package/dist/plugins/react-native-a11y/index.d.ts +3 -0
- package/dist/plugins/react-native-a11y/index.js +48 -0
- package/dist/plugins/react-native-a11y/models/optional-options.d.ts +3 -0
- package/dist/plugins/react-native-a11y/models/optional-options.js +1 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +82 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-hint.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +74 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-label.d.ts +6 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-label.js +103 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-role.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-role.js +72 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +49 -0
- package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.js +40 -0
- package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +77 -0
- package/dist/plugins/react-native-a11y/rules/input-purpose.d.ts +6 -0
- package/dist/plugins/react-native-a11y/rules/input-purpose.js +84 -0
- package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.js +57 -0
- package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.js +31 -0
- package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.js +66 -0
- package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +49 -0
- package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.d.ts +9 -0
- package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.js +76 -0
- package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.js +82 -0
- package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.js +70 -0
- package/dist/plugins/react-native-a11y/rules/no-press-in-activation.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-press-in-activation.js +59 -0
- package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +75 -0
- package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +62 -0
- package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +55 -0
- package/dist/plugins/react-native-a11y/utils/create-rule.d.ts +5 -0
- package/dist/plugins/react-native-a11y/utils/create-rule.js +3 -0
- package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.d.ts +8 -0
- package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +67 -0
- package/dist/plugins/react-native-a11y/utils/get-components.d.ts +7 -0
- package/dist/plugins/react-native-a11y/utils/get-components.js +11 -0
- package/dist/plugins/react-native-a11y/utils/get-element-name.d.ts +10 -0
- package/dist/plugins/react-native-a11y/utils/get-element-name.js +66 -0
- package/dist/plugins/react-native-a11y/utils/get-jsx-descendants.d.ts +4 -0
- package/dist/plugins/react-native-a11y/utils/get-jsx-descendants.js +49 -0
- package/dist/plugins/react-native-a11y/utils/get-literal-property-value.d.ts +7 -0
- package/dist/plugins/react-native-a11y/utils/get-literal-property-value.js +21 -0
- package/dist/plugins/react-native-a11y/utils/get-literal-text-child.d.ts +8 -0
- package/dist/plugins/react-native-a11y/utils/get-literal-text-child.js +40 -0
- package/dist/plugins/react-native-a11y/utils/get-property.d.ts +4 -0
- package/dist/plugins/react-native-a11y/utils/get-property.js +9 -0
- package/dist/plugins/react-native-a11y/utils/has-accessible-name.d.ts +6 -0
- package/dist/plugins/react-native-a11y/utils/has-accessible-name.js +27 -0
- package/dist/plugins/react-native-a11y/utils/has-property.d.ts +3 -0
- package/dist/plugins/react-native-a11y/utils/has-property.js +3 -0
- package/dist/plugins/react-native-a11y/utils/has-spread-props.d.ts +8 -0
- package/dist/plugins/react-native-a11y/utils/has-spread-props.js +8 -0
- package/dist/plugins/react-native-a11y/utils/is-definitely-grouped.d.ts +7 -0
- package/dist/plugins/react-native-a11y/utils/is-definitely-grouped.js +20 -0
- package/dist/plugins/react-native-a11y/utils/is-hidden-from-accessibility.d.ts +19 -0
- package/dist/plugins/react-native-a11y/utils/is-hidden-from-accessibility.js +51 -0
- package/dist/plugins/react-native-a11y/utils/is-inside-focus-stop.d.ts +9 -0
- package/dist/plugins/react-native-a11y/utils/is-inside-focus-stop.js +36 -0
- package/dist/plugins/react-native-a11y/utils/is-interactive.d.ts +16 -0
- package/dist/plugins/react-native-a11y/utils/is-interactive.js +38 -0
- package/dist/plugins/react-native-a11y/utils/is-not-a-focus-stop.d.ts +7 -0
- package/dist/plugins/react-native-a11y/utils/is-not-a-focus-stop.js +19 -0
- package/dist/react-native.js +49 -0
- package/package.json +4 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import createRule from "../utils/create-rule.js";
|
|
3
|
+
import getProperty from "../utils/get-property.js";
|
|
4
|
+
const keysOf = (expression) => expression.properties.flatMap((property) => property.type === AST_NODE_TYPES.Property &&
|
|
5
|
+
property.key.type === AST_NODE_TYPES.Identifier
|
|
6
|
+
? [property.key.name]
|
|
7
|
+
: []);
|
|
8
|
+
const accessibilityValueNeedsRange = createRule({
|
|
9
|
+
name: "accessibility-value-needs-range",
|
|
10
|
+
meta: {
|
|
11
|
+
defaultOptions: [],
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Enforce accessibilityValue carries a range, so a current value means something.",
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
valueWithoutRange: "'accessibilityValue' sets a current value with no scale to read it against, so assistive tech announces a bare number. Add 'min' and 'max', or a 'text' that says the value in words.",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
JSXOpeningElement(node) {
|
|
24
|
+
const attribute = getProperty(node, "accessibilityValue");
|
|
25
|
+
if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const { expression } = attribute.value;
|
|
29
|
+
// A value built elsewhere cannot be read here
|
|
30
|
+
if (expression.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// A spread may carry the range, and cannot be read from here
|
|
34
|
+
if (expression.properties.some((property) => property.type === AST_NODE_TYPES.SpreadElement)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const keys = keysOf(expression);
|
|
38
|
+
if (!keys.includes("now") || keys.includes("text")) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (keys.includes("min") && keys.includes("max")) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
context.report({ node: attribute, messageId: "valueWithoutRange" });
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
export default accessibilityValueNeedsRange;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { VALUE_ALIAS_KEYS } from "../constants/accessibility-props.js";
|
|
2
|
+
import createRule from "../utils/create-rule.js";
|
|
3
|
+
import getLiteralPropertyValue from "../utils/get-literal-property-value.js";
|
|
4
|
+
import hasProperty from "../utils/has-property.js";
|
|
5
|
+
import hasSpreadProps from "../utils/has-spread-props.js";
|
|
6
|
+
const ROLES_NEEDING_VALUE = new Set(["adjustable", "progressbar"]);
|
|
7
|
+
const VALUE_PROPS = ["accessibilityValue", ...Object.keys(VALUE_ALIAS_KEYS)];
|
|
8
|
+
const adjustableNeedsValue = createRule({
|
|
9
|
+
name: "adjustable-needs-value",
|
|
10
|
+
meta: {
|
|
11
|
+
defaultOptions: [],
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Enforce accessibilityValue on components announced as adjustable or a progress bar.",
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
missingAccessibilityValue: "A component with role '{{ role }}' must have an 'accessibilityValue', otherwise assistive tech announces the control without its value.",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
JSXOpeningElement(node) {
|
|
24
|
+
const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
|
|
25
|
+
if (typeof role !== "string" || !ROLES_NEEDING_VALUE.has(role)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (hasSpreadProps(node) || hasProperty(node, VALUE_PROPS)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
context.report({
|
|
32
|
+
node,
|
|
33
|
+
messageId: "missingAccessibilityValue",
|
|
34
|
+
data: { role },
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
export default adjustableNeedsValue;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import createRule from "../utils/create-rule.js";
|
|
3
|
+
import getProperty from "../utils/get-property.js";
|
|
4
|
+
const LIVE_REGION_PROPS = ["accessibilityLiveRegion", "aria-live"];
|
|
5
|
+
const FUNCTION_TYPES = new Set([
|
|
6
|
+
AST_NODE_TYPES.FunctionDeclaration,
|
|
7
|
+
AST_NODE_TYPES.FunctionExpression,
|
|
8
|
+
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
9
|
+
]);
|
|
10
|
+
/** The function a node sits in, or the program when it sits at the top level. */
|
|
11
|
+
const enclosingScope = (node) => {
|
|
12
|
+
let current = node;
|
|
13
|
+
while (current.parent && !FUNCTION_TYPES.has(current.type)) {
|
|
14
|
+
current = current.parent;
|
|
15
|
+
}
|
|
16
|
+
return current;
|
|
17
|
+
};
|
|
18
|
+
const gateAnnounceForAccessibility = createRule({
|
|
19
|
+
name: "gate-announce-for-accessibility",
|
|
20
|
+
meta: {
|
|
21
|
+
defaultOptions: [],
|
|
22
|
+
type: "problem",
|
|
23
|
+
docs: {
|
|
24
|
+
description: "Enforce announceForAccessibility is gated on Platform.OS where a live region already announces on Android.",
|
|
25
|
+
},
|
|
26
|
+
messages: {
|
|
27
|
+
ungatedAnnounce: "This file announces through a live region, which is Android-only, and calls 'announceForAccessibility', which dispatches on Android too, so TalkBack says the message twice. Gate the call on 'Platform.OS === \"ios\"'.",
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
// A bare announce is the legitimate cross-platform pattern. The double
|
|
33
|
+
// announcement only happens where a live region covers Android already, so
|
|
34
|
+
// that pairing is the trigger rather than the call on its own.
|
|
35
|
+
let hasLiveRegion = false;
|
|
36
|
+
const announces = [];
|
|
37
|
+
const platformChecks = [];
|
|
38
|
+
return {
|
|
39
|
+
JSXOpeningElement(node) {
|
|
40
|
+
if (getProperty(node, LIVE_REGION_PROPS)) {
|
|
41
|
+
hasLiveRegion = true;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
MemberExpression(node) {
|
|
45
|
+
if (node.property.type !== AST_NODE_TYPES.Identifier) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (node.property.name === "announceForAccessibility") {
|
|
49
|
+
announces.push(node);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Matched structurally, so `somePlatform.OSCheck` is not a gate
|
|
53
|
+
if (node.property.name === "OS" &&
|
|
54
|
+
node.object.type === AST_NODE_TYPES.Identifier &&
|
|
55
|
+
node.object.name === "Platform") {
|
|
56
|
+
platformChecks.push(node);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"Program:exit"() {
|
|
60
|
+
if (!hasLiveRegion) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// ponytail: any Platform.OS in the same function counts as the gate.
|
|
64
|
+
// Lenient on purpose — a guard clause, a switch and a named boolean are
|
|
65
|
+
// all real gates, and none of them is an ancestor of the call.
|
|
66
|
+
const gatedScopes = new Set(platformChecks.map((check) => enclosingScope(check)));
|
|
67
|
+
for (const announce of announces) {
|
|
68
|
+
if (gatedScopes.has(enclosingScope(announce))) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
context.report({ node: announce, messageId: "ungatedAnnounce" });
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
export default gateAnnounceForAccessibility;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Options } from "../models/optional-options.js";
|
|
2
|
+
type MessageIds = "missingAutoComplete" | "missingAutofillHint" | "platformSpecificToken";
|
|
3
|
+
declare const inputPurpose: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export default inputPurpose;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { TEXT_INPUTS } from "../constants/react-native-components.js";
|
|
2
|
+
import schemaProperties from "../constants/schema-properties.js";
|
|
3
|
+
import createRule from "../utils/create-rule.js";
|
|
4
|
+
import getComponents from "../utils/get-components.js";
|
|
5
|
+
import getElementName from "../utils/get-element-name.js";
|
|
6
|
+
import getLiteralPropertyValue from "../utils/get-literal-property-value.js";
|
|
7
|
+
import getProperty from "../utils/get-property.js";
|
|
8
|
+
import hasSpreadProps from "../utils/has-spread-props.js";
|
|
9
|
+
/**
|
|
10
|
+
* React Native derives iOS `textContentType` from a standard `autoComplete`
|
|
11
|
+
* token but never the reverse, so `autoComplete` alone is fine and
|
|
12
|
+
* `textContentType` alone leaves Android with nothing.
|
|
13
|
+
*
|
|
14
|
+
* ponytail: one token, the only one the skill names. Extend the map if another
|
|
15
|
+
* platform-specific token shows up in review.
|
|
16
|
+
*/
|
|
17
|
+
const PLATFORM_SPECIFIC_TOKENS = {
|
|
18
|
+
"sms-otp": "one-time-code",
|
|
19
|
+
};
|
|
20
|
+
const inputPurpose = createRule({
|
|
21
|
+
name: "input-purpose",
|
|
22
|
+
meta: {
|
|
23
|
+
defaultOptions: [
|
|
24
|
+
{
|
|
25
|
+
additionalComponents: [],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
type: "problem",
|
|
29
|
+
docs: {
|
|
30
|
+
description: "Enforce a cross-platform autofill hint on inputs with a common purpose.",
|
|
31
|
+
},
|
|
32
|
+
messages: {
|
|
33
|
+
missingAutoComplete: "'textContentType' only covers iOS. Add the matching 'autoComplete' token so Android autofill identifies this field too.",
|
|
34
|
+
missingAutofillHint: "This input has no 'autoComplete', so the OS keyboard, autofill and password managers cannot identify what it collects. Name the purpose, or set 'autoComplete=\"off\"' to say there is none.",
|
|
35
|
+
platformSpecificToken: "'autoComplete=\"{{ token }}\"' is platform-specific. Prefer the standard token '{{ standard }}', which covers both platforms.",
|
|
36
|
+
},
|
|
37
|
+
schema: [
|
|
38
|
+
{
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: schemaProperties,
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
create(context) {
|
|
46
|
+
const components = getComponents({
|
|
47
|
+
context,
|
|
48
|
+
defaultComponents: TEXT_INPUTS,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
JSXOpeningElement(node) {
|
|
52
|
+
const componentName = getElementName(node);
|
|
53
|
+
if (componentName === undefined) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!components.includes(componentName)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// The missing token may arrive through the spread; an explicit one is
|
|
60
|
+
// already wrong whatever the spread carries
|
|
61
|
+
const autoComplete = getProperty(node, "autoComplete");
|
|
62
|
+
const textContentType = getProperty(node, "textContentType");
|
|
63
|
+
// One fix either way, so one finding: the sharper message where only
|
|
64
|
+
// the iOS half is already set
|
|
65
|
+
if (!autoComplete && !hasSpreadProps(node)) {
|
|
66
|
+
context.report(textContentType
|
|
67
|
+
? { node: textContentType, messageId: "missingAutoComplete" }
|
|
68
|
+
: { node, messageId: "missingAutofillHint" });
|
|
69
|
+
}
|
|
70
|
+
const token = getLiteralPropertyValue(node, "autoComplete");
|
|
71
|
+
if (autoComplete &&
|
|
72
|
+
typeof token === "string" &&
|
|
73
|
+
PLATFORM_SPECIFIC_TOKENS[token]) {
|
|
74
|
+
context.report({
|
|
75
|
+
node: autoComplete,
|
|
76
|
+
messageId: "platformSpecificToken",
|
|
77
|
+
data: { token, standard: PLATFORM_SPECIFIC_TOKENS[token] },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
export default inputPurpose;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import { ALIAS_TO_LEGACY, STATE_ALIAS_KEYS, VALUE_ALIAS_KEYS, } from "../constants/accessibility-props.js";
|
|
3
|
+
import createRule from "../utils/create-rule.js";
|
|
4
|
+
import getProperty from "../utils/get-property.js";
|
|
5
|
+
/**
|
|
6
|
+
* Both spellings map to the same native behaviour and only one of them reaches
|
|
7
|
+
* the OS, so the pair drifts the moment one of them is updated.
|
|
8
|
+
*/
|
|
9
|
+
const hasStateKey = (node, legacyProperty, key) => {
|
|
10
|
+
const attribute = getProperty(node, legacyProperty);
|
|
11
|
+
if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const { expression } = attribute.value;
|
|
15
|
+
// A state built elsewhere cannot be read here; leave it to manual review
|
|
16
|
+
return expression.type === AST_NODE_TYPES.ObjectExpression && expression.properties.some((property) => property.type === AST_NODE_TYPES.Property &&
|
|
17
|
+
property.key.type === AST_NODE_TYPES.Identifier &&
|
|
18
|
+
property.key.name === key);
|
|
19
|
+
};
|
|
20
|
+
const noAriaLegacyMix = createRule({
|
|
21
|
+
name: "no-aria-legacy-mix",
|
|
22
|
+
meta: {
|
|
23
|
+
defaultOptions: [],
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: {
|
|
26
|
+
description: "Disallow an ARIA alias and its legacy accessibility prop on the same node.",
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
ariaLegacyMix: "'{{ alias }}' and '{{ legacy }}' are the same prop. Keep one, since only one of them reaches the OS.",
|
|
30
|
+
},
|
|
31
|
+
schema: [],
|
|
32
|
+
},
|
|
33
|
+
create(context) {
|
|
34
|
+
return {
|
|
35
|
+
JSXOpeningElement(node) {
|
|
36
|
+
for (const [alias, legacy] of Object.entries(ALIAS_TO_LEGACY)) {
|
|
37
|
+
if (!getProperty(node, alias)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const stateKey = STATE_ALIAS_KEYS[alias] ?? VALUE_ALIAS_KEYS[alias];
|
|
41
|
+
// A grouped prop only clashes on the key the alias maps onto
|
|
42
|
+
const isClashes = stateKey
|
|
43
|
+
? hasStateKey(node, legacy, stateKey)
|
|
44
|
+
: getProperty(node, legacy) !== undefined;
|
|
45
|
+
if (isClashes) {
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
messageId: "ariaLegacyMix",
|
|
49
|
+
data: { alias, legacy },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
export default noAriaLegacyMix;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
declare const noDeprecatedAccessibilityFocus: import("@typescript-eslint/utils/ts-eslint").RuleModule<"deprecatedSetAccessibilityFocus", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
2
|
+
name: string;
|
|
3
|
+
};
|
|
4
|
+
export default noDeprecatedAccessibilityFocus;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import createRule from "../utils/create-rule.js";
|
|
3
|
+
const noDeprecatedAccessibilityFocus = createRule({
|
|
4
|
+
name: "no-deprecated-accessibility-focus",
|
|
5
|
+
meta: {
|
|
6
|
+
defaultOptions: [],
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow setAccessibilityFocus in favour of sendAccessibilityEvent.",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
deprecatedSetAccessibilityFocus: "'setAccessibilityFocus' takes a node handle that 'findNodeHandle' can return as null, focusing nothing while looking like it worked, and is deprecated since React Native 0.85. Call 'AccessibilityInfo.sendAccessibilityEvent(ref.current, \"focus\")' with the component ref instead.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
// Matched on the property so an aliased import is caught too
|
|
19
|
+
MemberExpression(node) {
|
|
20
|
+
if (node.property.type === AST_NODE_TYPES.Identifier &&
|
|
21
|
+
node.property.name === "setAccessibilityFocus") {
|
|
22
|
+
context.report({
|
|
23
|
+
node,
|
|
24
|
+
messageId: "deprecatedSetAccessibilityFocus",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
export default noDeprecatedAccessibilityFocus;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Options } from "../models/optional-options.js";
|
|
2
|
+
declare const noDroppedAriaOnText: import("@typescript-eslint/utils/ts-eslint").RuleModule<"droppedAriaProp", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default noDroppedAriaOnText;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ARIA_DROPPED_ON_TEXT } from "../constants/accessibility-props.js";
|
|
2
|
+
import { TEXTS } from "../constants/react-native-components.js";
|
|
3
|
+
import schemaProperties from "../constants/schema-properties.js";
|
|
4
|
+
import createRule from "../utils/create-rule.js";
|
|
5
|
+
import getComponents from "../utils/get-components.js";
|
|
6
|
+
import getElementName from "../utils/get-element-name.js";
|
|
7
|
+
import getProperty from "../utils/get-property.js";
|
|
8
|
+
const LEGACY_EQUIVALENT = {
|
|
9
|
+
"aria-labelledby": "accessibilityLabelledBy",
|
|
10
|
+
"aria-live": "accessibilityLiveRegion",
|
|
11
|
+
"aria-valuemax": "accessibilityValue",
|
|
12
|
+
"aria-valuemin": "accessibilityValue",
|
|
13
|
+
"aria-valuenow": "accessibilityValue",
|
|
14
|
+
"aria-valuetext": "accessibilityValue",
|
|
15
|
+
};
|
|
16
|
+
const noDroppedAriaOnText = createRule({
|
|
17
|
+
name: "no-dropped-aria-on-text",
|
|
18
|
+
meta: {
|
|
19
|
+
defaultOptions: [
|
|
20
|
+
{
|
|
21
|
+
additionalComponents: [],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: {
|
|
26
|
+
description: "Disallow ARIA aliases that Text silently drops instead of forwarding to the OS.",
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
droppedAriaProp: "'<Text />' does not convert '{{ alias }}', so it never reaches the OS. Use '{{ legacy }}' instead.",
|
|
30
|
+
},
|
|
31
|
+
schema: [
|
|
32
|
+
{
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: schemaProperties,
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
create(context) {
|
|
40
|
+
const components = getComponents({
|
|
41
|
+
context,
|
|
42
|
+
defaultComponents: TEXTS,
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
JSXOpeningElement(node) {
|
|
46
|
+
const componentName = getElementName(node);
|
|
47
|
+
if (componentName === undefined) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!components.includes(componentName)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const alias of ARIA_DROPPED_ON_TEXT) {
|
|
54
|
+
if (getProperty(node, alias)) {
|
|
55
|
+
context.report({
|
|
56
|
+
node,
|
|
57
|
+
messageId: "droppedAriaProp",
|
|
58
|
+
data: { alias, legacy: LEGACY_EQUIVALENT[alias] },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
export default noDroppedAriaOnText;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type MessageIds = "fontScalingDisabled" | "fontScalingCapped";
|
|
2
|
+
declare const noFontScalingDisabled: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default noFontScalingDisabled;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import createRule from "../utils/create-rule.js";
|
|
2
|
+
import getLiteralPropertyValue from "../utils/get-literal-property-value.js";
|
|
3
|
+
import getProperty from "../utils/get-property.js";
|
|
4
|
+
/** WCAG 1.4.4 asks for text to resize up to 200%. */
|
|
5
|
+
const MINIMUM_MULTIPLIER = 2;
|
|
6
|
+
/** React Native reads `0` as "no maximum", which is the opposite of a cap. */
|
|
7
|
+
const NO_MAXIMUM = 0;
|
|
8
|
+
const noFontScalingDisabled = createRule({
|
|
9
|
+
name: "no-font-scaling-disabled",
|
|
10
|
+
meta: {
|
|
11
|
+
defaultOptions: [],
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Disallow opting text out of the system font scale below the level WCAG 1.4.4 requires.",
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
fontScalingDisabled: "'allowFontScaling={false}' stops this text respecting the system font size. Cap it with 'maxFontSizeMultiplier' instead if one element cannot grow.",
|
|
18
|
+
fontScalingCapped: "'maxFontSizeMultiplier={{{ multiplier }}}' caps text below the 200% WCAG 1.4.4 requires. Reserve a cap under {{ minimum }} for chrome whose information is available elsewhere.",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
return {
|
|
24
|
+
JSXOpeningElement(node) {
|
|
25
|
+
const allowFontScaling = getProperty(node, "allowFontScaling");
|
|
26
|
+
if (allowFontScaling &&
|
|
27
|
+
getLiteralPropertyValue(node, "allowFontScaling") === false) {
|
|
28
|
+
context.report({
|
|
29
|
+
node: allowFontScaling,
|
|
30
|
+
messageId: "fontScalingDisabled",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const cap = getProperty(node, "maxFontSizeMultiplier");
|
|
34
|
+
const multiplier = getLiteralPropertyValue(node, "maxFontSizeMultiplier");
|
|
35
|
+
if (cap &&
|
|
36
|
+
typeof multiplier === "number" &&
|
|
37
|
+
multiplier !== NO_MAXIMUM &&
|
|
38
|
+
multiplier < MINIMUM_MULTIPLIER) {
|
|
39
|
+
context.report({
|
|
40
|
+
node: cap,
|
|
41
|
+
messageId: "fontScalingCapped",
|
|
42
|
+
data: { multiplier, minimum: MINIMUM_MULTIPLIER },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
export default noFontScalingDisabled;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type MessageIds = "listItemRole" | "alertRole" | "hiddenWithLabel" | "labelledByWithoutLabel" | "hiddenWithAccessibleFalse" | "accessibleFalseIsDefault";
|
|
2
|
+
/**
|
|
3
|
+
* Props that read as accessibility work but do not reach a user: a role with no
|
|
4
|
+
* native mapping, a label on a hidden node, an Android-only association.
|
|
5
|
+
*/
|
|
6
|
+
declare const noIneffectiveAccessibilityProps: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
export default noIneffectiveAccessibilityProps;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { IMAGES, VIEWS } from "../constants/react-native-components.js";
|
|
2
|
+
import createRule from "../utils/create-rule.js";
|
|
3
|
+
import getElementName from "../utils/get-element-name.js";
|
|
4
|
+
import getLiteralPropertyValue from "../utils/get-literal-property-value.js";
|
|
5
|
+
import getProperty from "../utils/get-property.js";
|
|
6
|
+
import { hasLabelProperty as hasLabelProperty } from "../utils/has-accessible-name.js";
|
|
7
|
+
import hasSpreadProps from "../utils/has-spread-props.js";
|
|
8
|
+
import { isDefinitelyHidden } from "../utils/is-hidden-from-accessibility.js";
|
|
9
|
+
const LABELLED_BY_PROPS = ["accessibilityLabelledBy", "aria-labelledby"];
|
|
10
|
+
/** Hosts whose `accessible` is false until a prop opts them in. */
|
|
11
|
+
const DEFAULTS_TO_INACCESSIBLE = new Set([...VIEWS, ...IMAGES]);
|
|
12
|
+
/**
|
|
13
|
+
* Props that read as accessibility work but do not reach a user: a role with no
|
|
14
|
+
* native mapping, a label on a hidden node, an Android-only association.
|
|
15
|
+
*/
|
|
16
|
+
const noIneffectiveAccessibilityProps = createRule({
|
|
17
|
+
name: "no-ineffective-accessibility-props",
|
|
18
|
+
meta: {
|
|
19
|
+
defaultOptions: [],
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: {
|
|
22
|
+
description: "Disallow accessibility props that never reach the user on either platform.",
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
listItemRole: "'listitem' has no native mapping on either platform. Set 'accessibilityCollectionItem' where the position in a list matters.",
|
|
26
|
+
alertRole: "The 'alert' role does not interrupt on either platform. Use 'accessibilityLiveRegion=\"assertive\"' paired with an iOS 'announceForAccessibility' call for an error that must interrupt.",
|
|
27
|
+
hiddenWithLabel: "This node is hidden from assistive tech, so its label is never announced. Drop the label, or stop hiding the node.",
|
|
28
|
+
labelledByWithoutLabel: "'{{ prop }}' is Android-only in React Native, so iOS derives no name from it. Add an explicit 'accessibilityLabel' as well.",
|
|
29
|
+
hiddenWithAccessibleFalse: "This node is already hidden from assistive tech, so 'accessible={false}' beside it sets nothing. Keep the prop that hides and drop this one.",
|
|
30
|
+
accessibleFalseIsDefault: "'accessible' already defaults to false on '{{ name }}', so this sets nothing. To hide the node use 'aria-hidden'; only a touchable or a Text is a focus stop to opt out of.",
|
|
31
|
+
},
|
|
32
|
+
schema: [],
|
|
33
|
+
},
|
|
34
|
+
create(context) {
|
|
35
|
+
return {
|
|
36
|
+
JSXOpeningElement(node) {
|
|
37
|
+
const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
|
|
38
|
+
if (role === "listitem") {
|
|
39
|
+
context.report({ node, messageId: "listItemRole" });
|
|
40
|
+
}
|
|
41
|
+
if (role === "alert") {
|
|
42
|
+
context.report({ node, messageId: "alertRole" });
|
|
43
|
+
}
|
|
44
|
+
const isHidden = isDefinitelyHidden(node);
|
|
45
|
+
if (isHidden && hasLabelProperty(node)) {
|
|
46
|
+
context.report({ node, messageId: "hiddenWithLabel" });
|
|
47
|
+
}
|
|
48
|
+
if (getLiteralPropertyValue(node, "accessible") === false) {
|
|
49
|
+
const name = getElementName(node);
|
|
50
|
+
if (isHidden) {
|
|
51
|
+
context.report({ node, messageId: "hiddenWithAccessibleFalse" });
|
|
52
|
+
}
|
|
53
|
+
else if (name !== undefined &&
|
|
54
|
+
DEFAULTS_TO_INACCESSIBLE.has(name) &&
|
|
55
|
+
// A spread may carry the `accessible` this one is overriding.
|
|
56
|
+
!hasSpreadProps(node)) {
|
|
57
|
+
context.report({
|
|
58
|
+
node,
|
|
59
|
+
messageId: "accessibleFalseIsDefault",
|
|
60
|
+
data: { name },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const labelledBy = getProperty(node, LABELLED_BY_PROPS);
|
|
65
|
+
if (labelledBy && !hasLabelProperty(node)) {
|
|
66
|
+
context.report({
|
|
67
|
+
node,
|
|
68
|
+
messageId: "labelledByWithoutLabel",
|
|
69
|
+
data: { prop: labelledBy.name.name },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
export default noIneffectiveAccessibilityProps;
|