@hypersolid-dev/eslint-config 1.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 +23 -0
- package/README.md +28 -1
- package/dist/base.d.ts +1 -0
- package/dist/base.js +63 -8
- 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 +85 -1
- package/dist/shared.d.ts +8 -0
- package/dist/shared.js +22 -1
- package/package.json +4 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { IMAGES, INPUTS, TOUCHABLES } from "../constants/react-native-components.js";
|
|
2
|
+
import schemaProperties from "../constants/schema-properties.js";
|
|
3
|
+
import createRule from "../utils/create-rule.js";
|
|
4
|
+
import getAmountOfTextChildren from "../utils/get-amount-of-text-children.js";
|
|
5
|
+
import getComponents from "../utils/get-components.js";
|
|
6
|
+
import getElementName from "../utils/get-element-name.js";
|
|
7
|
+
import { getLiteralLabel, hasEmptyLabel, hasLabelProperty as hasLabelProperty, } from "../utils/has-accessible-name.js";
|
|
8
|
+
import getLiteralTextChild from "../utils/get-literal-text-child.js";
|
|
9
|
+
import hasSpreadProps from "../utils/has-spread-props.js";
|
|
10
|
+
import isHiddenFromAccessibility from "../utils/is-hidden-from-accessibility.js";
|
|
11
|
+
import isInsideFocusStop from "../utils/is-inside-focus-stop.js";
|
|
12
|
+
import isNotAFocusStop from "../utils/is-not-a-focus-stop.js";
|
|
13
|
+
const accessibilityLabel = createRule({
|
|
14
|
+
name: "accessibility-label",
|
|
15
|
+
meta: {
|
|
16
|
+
defaultOptions: [
|
|
17
|
+
{
|
|
18
|
+
additionalComponents: [],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
type: "problem",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "Enforce accessibilityLabel prop is present on controls without visible text.",
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
missingAccessibilityLabel: "The '<{{ componentName }} />' component must have an 'accessibilityLabel' prop.",
|
|
27
|
+
labelMissingVisibleText: "The label '{{ label }}' does not contain the visible text '{{ text }}', so a Voice Control user saying what they can see will not reach this control.",
|
|
28
|
+
emptyAccessibilityLabel: "The '<{{ componentName }} />' component has an empty 'accessibilityLabel', which leaves it unnamed. Give it a name or hide it with 'aria-hidden'.",
|
|
29
|
+
},
|
|
30
|
+
schema: [
|
|
31
|
+
{
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: schemaProperties,
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
create(context) {
|
|
39
|
+
const components = getComponents({
|
|
40
|
+
context,
|
|
41
|
+
defaultComponents: [...IMAGES, ...TOUCHABLES, ...INPUTS],
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
JSXOpeningElement(node) {
|
|
45
|
+
const componentName = getElementName(node);
|
|
46
|
+
if (componentName === undefined) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!components.includes(componentName)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Decorative content is hidden rather than labelled
|
|
53
|
+
// The surface above answers for its children
|
|
54
|
+
if (isHiddenFromAccessibility(node) ||
|
|
55
|
+
isNotAFocusStop(node) ||
|
|
56
|
+
isInsideFocusStop(node)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (hasEmptyLabel(node)) {
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: "emptyAccessibilityLabel",
|
|
63
|
+
data: {
|
|
64
|
+
componentName,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// The label may arrive through the spread
|
|
70
|
+
if (hasSpreadProps(node)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Visible text already names the control
|
|
74
|
+
const { children } = node.parent;
|
|
75
|
+
const amountOfTextChildren = getAmountOfTextChildren(children);
|
|
76
|
+
// 2.5.3: a spoken command matches the accessible name, so the name
|
|
77
|
+
// has to contain what the user can read
|
|
78
|
+
const label = getLiteralLabel(node);
|
|
79
|
+
const visible = getLiteralTextChild(children);
|
|
80
|
+
if (label !== undefined &&
|
|
81
|
+
visible !== undefined &&
|
|
82
|
+
!label.toLowerCase().includes(visible.toLowerCase())) {
|
|
83
|
+
context.report({
|
|
84
|
+
node,
|
|
85
|
+
messageId: "labelMissingVisibleText",
|
|
86
|
+
data: { label, text: visible },
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!hasLabelProperty(node) && amountOfTextChildren === 0) {
|
|
91
|
+
context.report({
|
|
92
|
+
node,
|
|
93
|
+
messageId: "missingAccessibilityLabel",
|
|
94
|
+
data: {
|
|
95
|
+
componentName,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
export default accessibilityLabel;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Options } from "../models/optional-options.js";
|
|
2
|
+
declare const accessibilityRole: import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingAccessibilityRole", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default accessibilityRole;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { IMAGES, TOUCHABLES } 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 hasProperty from "../utils/has-property.js";
|
|
7
|
+
import hasSpreadProps from "../utils/has-spread-props.js";
|
|
8
|
+
import isHiddenFromAccessibility from "../utils/is-hidden-from-accessibility.js";
|
|
9
|
+
import isInsideFocusStop from "../utils/is-inside-focus-stop.js";
|
|
10
|
+
import isNotAFocusStop from "../utils/is-not-a-focus-stop.js";
|
|
11
|
+
const accessibilityRole = createRule({
|
|
12
|
+
name: "accessibility-role",
|
|
13
|
+
meta: {
|
|
14
|
+
defaultOptions: [
|
|
15
|
+
{
|
|
16
|
+
additionalComponents: [],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description: "Enforce accessibilityRole prop is present on interactive components and meaningful images.",
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
missingAccessibilityRole: "The '<{{ componentName }} />' component must have an 'accessibilityRole' prop.",
|
|
25
|
+
},
|
|
26
|
+
schema: [
|
|
27
|
+
{
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: schemaProperties,
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
create(context) {
|
|
35
|
+
const components = getComponents({
|
|
36
|
+
context,
|
|
37
|
+
defaultComponents: [...TOUCHABLES, ...IMAGES],
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
JSXOpeningElement(node) {
|
|
41
|
+
const componentName = getElementName(node);
|
|
42
|
+
if (componentName === undefined) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!components.includes(componentName)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Decorative content is hidden rather than given a role
|
|
49
|
+
// The surface above answers for its children
|
|
50
|
+
if (isHiddenFromAccessibility(node) ||
|
|
51
|
+
isNotAFocusStop(node) ||
|
|
52
|
+
isInsideFocusStop(node)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// The role may arrive through the spread
|
|
56
|
+
if (hasSpreadProps(node)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!hasProperty(node, ["accessibilityRole", "role"])) {
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: "missingAccessibilityRole",
|
|
63
|
+
data: {
|
|
64
|
+
componentName,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
export default accessibilityRole;
|
|
@@ -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;
|