@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,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;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ASTUtils, AST_NODE_TYPES, } from "@typescript-eslint/utils";
|
|
2
|
+
import { ACCESSIBILITY_STATE_KEYS } from "../constants/accessibility-state.js";
|
|
3
|
+
import createRule from "../utils/create-rule.js";
|
|
4
|
+
import getProperty from "../utils/get-property.js";
|
|
5
|
+
/** `{ … } as const` and `{ … } satisfies T` are still that object. */
|
|
6
|
+
const unwrap = (node) => {
|
|
7
|
+
if (!node) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
if (node.type === AST_NODE_TYPES.TSAsExpression ||
|
|
11
|
+
node.type === AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
12
|
+
return unwrap(node.expression);
|
|
13
|
+
}
|
|
14
|
+
return node.type === AST_NODE_TYPES.ObjectExpression ? node : undefined;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The object a binding was declared with, where that is safe to read: a single
|
|
18
|
+
* declaration, never reassigned. Anything else is left to manual review.
|
|
19
|
+
*/
|
|
20
|
+
const resolveObjectBinding = (scope, identifier) => {
|
|
21
|
+
const variable = ASTUtils.findVariable(scope, identifier);
|
|
22
|
+
if (!variable || variable.defs.length !== 1) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const [definition] = variable.defs;
|
|
26
|
+
if (definition.node.type !== AST_NODE_TYPES.VariableDeclarator) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (variable.references.filter((reference) => reference.isWrite()).length > 1) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
return unwrap(definition.node.init);
|
|
33
|
+
};
|
|
34
|
+
const noInvalidAccessibilityState = createRule({
|
|
35
|
+
name: "no-invalid-accessibility-state",
|
|
36
|
+
meta: {
|
|
37
|
+
defaultOptions: [],
|
|
38
|
+
type: "problem",
|
|
39
|
+
docs: {
|
|
40
|
+
description: "Disallow accessibilityState keys React Native does not support, including through a variable.",
|
|
41
|
+
},
|
|
42
|
+
messages: {
|
|
43
|
+
invalidStateKey: "'accessibilityState' has no '{{ key }}' key, so it is dropped. Supported keys: {{ supported }}.",
|
|
44
|
+
},
|
|
45
|
+
schema: [],
|
|
46
|
+
},
|
|
47
|
+
create(context) {
|
|
48
|
+
return {
|
|
49
|
+
JSXOpeningElement(node) {
|
|
50
|
+
const attribute = getProperty(node, "accessibilityState");
|
|
51
|
+
if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const { expression } = attribute.value;
|
|
55
|
+
// An object literal here is already rejected by the React Native types
|
|
56
|
+
if (expression.type !== AST_NODE_TYPES.Identifier) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const resolved = resolveObjectBinding(context.sourceCode.getScope(expression), expression);
|
|
60
|
+
if (!resolved) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (const property of resolved.properties) {
|
|
64
|
+
if (property.type !== AST_NODE_TYPES.Property ||
|
|
65
|
+
property.key.type !== AST_NODE_TYPES.Identifier ||
|
|
66
|
+
ACCESSIBILITY_STATE_KEYS.includes(property.key.name)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
context.report({
|
|
70
|
+
node: property,
|
|
71
|
+
messageId: "invalidStateKey",
|
|
72
|
+
data: {
|
|
73
|
+
key: property.key.name,
|
|
74
|
+
supported: ACCESSIBILITY_STATE_KEYS.join(", "),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
export default noInvalidAccessibilityState;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Options } from "../models/optional-options.js";
|
|
2
|
+
declare const noNestedFocusStops: import("@typescript-eslint/utils/ts-eslint").RuleModule<"nestedFocusStop", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default noNestedFocusStops;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { 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 getJsxDescendants from "../utils/get-jsx-descendants.js";
|
|
7
|
+
import isHiddenFromAccessibility from "../utils/is-hidden-from-accessibility.js";
|
|
8
|
+
import { hasInteractiveRole, hasTouchHandler } from "../utils/is-interactive.js";
|
|
9
|
+
import isDefinitelyGrouped from "../utils/is-definitely-grouped.js";
|
|
10
|
+
const noNestedFocusStops = createRule({
|
|
11
|
+
name: "no-nested-focus-stops",
|
|
12
|
+
meta: {
|
|
13
|
+
defaultOptions: [
|
|
14
|
+
{
|
|
15
|
+
additionalComponents: [],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
type: "problem",
|
|
19
|
+
docs: {
|
|
20
|
+
description: "Disallow controls inside a container marked accessible, which collapses them into one focus stop.",
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
nestedFocusStop: "'<{{ componentName }} />' is marked 'accessible', so '<{{ childName }} />' inside it can no longer be reached on its own. Group only non-interactive content, or drop 'accessible' from the container.",
|
|
24
|
+
},
|
|
25
|
+
schema: [
|
|
26
|
+
{
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: schemaProperties,
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
create(context) {
|
|
34
|
+
// The touchables plus whatever this project wraps them in
|
|
35
|
+
const components = getComponents({
|
|
36
|
+
context,
|
|
37
|
+
defaultComponents: TOUCHABLES,
|
|
38
|
+
});
|
|
39
|
+
const isControl = (node) => {
|
|
40
|
+
const name = getElementName(node);
|
|
41
|
+
// A nested group is structure, not a control
|
|
42
|
+
return ((name !== undefined && components.includes(name)) ||
|
|
43
|
+
hasInteractiveRole(node) ||
|
|
44
|
+
hasTouchHandler(node));
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
JSXOpeningElement(node) {
|
|
48
|
+
const componentName = getElementName(node);
|
|
49
|
+
if (componentName === undefined ||
|
|
50
|
+
!isDefinitelyGrouped(node) ||
|
|
51
|
+
isHiddenFromAccessibility(node)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const swallowed = getJsxDescendants(node).find((descendant) => isControl(descendant));
|
|
55
|
+
if (!swallowed) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: "nestedFocusStop",
|
|
61
|
+
data: {
|
|
62
|
+
componentName,
|
|
63
|
+
childName: getElementName(swallowed) ?? "child",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
export default noNestedFocusStops;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Options } from "../models/optional-options.js";
|
|
2
|
+
declare const noPressInActivation: import("@typescript-eslint/utils/ts-eslint").RuleModule<"activatesOnPressIn", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default noPressInActivation;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { 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 getProperty from "../utils/get-property.js";
|
|
7
|
+
import hasProperty from "../utils/has-property.js";
|
|
8
|
+
import hasSpreadProps from "../utils/has-spread-props.js";
|
|
9
|
+
const noPressInActivation = createRule({
|
|
10
|
+
name: "no-press-in-activation",
|
|
11
|
+
meta: {
|
|
12
|
+
defaultOptions: [
|
|
13
|
+
{
|
|
14
|
+
additionalComponents: [],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
type: "problem",
|
|
18
|
+
docs: {
|
|
19
|
+
description: "Disallow activating on touch-down, which leaves the user no way to abort by dragging off.",
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
activatesOnPressIn: "'onPressIn' fires on touch-down, so the user cannot drag off '<{{ componentName }} />' to cancel. Activate on 'onPress' instead, and keep 'onPressIn' for press feedback only.",
|
|
23
|
+
},
|
|
24
|
+
schema: [
|
|
25
|
+
{
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: schemaProperties,
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
const components = getComponents({
|
|
34
|
+
context,
|
|
35
|
+
defaultComponents: TOUCHABLES,
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
JSXOpeningElement(node) {
|
|
39
|
+
const componentName = getElementName(node);
|
|
40
|
+
if (componentName === undefined) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!components.includes(componentName) || hasSpreadProps(node)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const pressIn = getProperty(node, "onPressIn");
|
|
47
|
+
if (!pressIn || hasProperty(node, ["onPress", "onLongPress"])) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
context.report({
|
|
51
|
+
node: pressIn,
|
|
52
|
+
messageId: "activatesOnPressIn",
|
|
53
|
+
data: { componentName },
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
export default noPressInActivation;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Options } from "../models/optional-options.js";
|
|
2
|
+
declare const noUnnamedInteractive: import("@typescript-eslint/utils/ts-eslint").RuleModule<"unnamedInteractive", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default noUnnamedInteractive;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NAME_PROPS } from "../constants/interactive.js";
|
|
2
|
+
import { IMAGES, INPUTS, TEXTS, TOUCHABLES, } from "../constants/react-native-components.js";
|
|
3
|
+
import schemaProperties from "../constants/schema-properties.js";
|
|
4
|
+
import createRule from "../utils/create-rule.js";
|
|
5
|
+
import getAmountOfTextChildren from "../utils/get-amount-of-text-children.js";
|
|
6
|
+
import getComponents from "../utils/get-components.js";
|
|
7
|
+
import getElementName from "../utils/get-element-name.js";
|
|
8
|
+
import hasProperty from "../utils/has-property.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 isInteractive from "../utils/is-interactive.js";
|
|
13
|
+
/**
|
|
14
|
+
* Already covered by accessibility-label, which knows these by name. `Text` is
|
|
15
|
+
* here too: it is named by whatever it renders, which may be a variable.
|
|
16
|
+
*/
|
|
17
|
+
const ALREADY_COVERED = new Set([...IMAGES, ...TOUCHABLES, ...INPUTS, ...TEXTS]);
|
|
18
|
+
const noUnnamedInteractive = createRule({
|
|
19
|
+
name: "no-unnamed-interactive",
|
|
20
|
+
meta: {
|
|
21
|
+
defaultOptions: [
|
|
22
|
+
{
|
|
23
|
+
additionalComponents: [],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
type: "problem",
|
|
27
|
+
docs: {
|
|
28
|
+
description: "Enforce an accessible name on a React Native host a user can operate.",
|
|
29
|
+
},
|
|
30
|
+
messages: {
|
|
31
|
+
unnamedInteractive: "'<{{ componentName }} />' can be operated but has no accessible name. Add an 'accessibilityLabel', or hide it with 'aria-hidden' if it is not a control.",
|
|
32
|
+
},
|
|
33
|
+
schema: [
|
|
34
|
+
{
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: schemaProperties,
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
create(context) {
|
|
42
|
+
const components = getComponents({ context });
|
|
43
|
+
return {
|
|
44
|
+
JSXOpeningElement(node) {
|
|
45
|
+
const componentName = getElementName(node);
|
|
46
|
+
if (componentName === undefined) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// accessibility-label owns the components it knows by name, so a
|
|
50
|
+
// control does not collect a finding from both rules
|
|
51
|
+
if (ALREADY_COVERED.has(componentName) &&
|
|
52
|
+
!components.includes(componentName)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!isInteractive(node, components) ||
|
|
56
|
+
isHiddenFromAccessibility(node) ||
|
|
57
|
+
isInsideFocusStop(node) ||
|
|
58
|
+
hasSpreadProps(node)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// A container collapsed into one stop is announced by its own text
|
|
62
|
+
if (hasProperty(node, NAME_PROPS) ||
|
|
63
|
+
getAmountOfTextChildren(node.parent.children) > 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
context.report({
|
|
67
|
+
node,
|
|
68
|
+
messageId: "unnamedInteractive",
|
|
69
|
+
data: { componentName },
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
export default noUnnamedInteractive;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Options } from "../models/optional-options.js";
|
|
2
|
+
declare const requireInvertColorsOptOut: import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingIgnoresInvertColors", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default requireInvertColorsOptOut;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { IMAGES } 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
|
+
const requireInvertColorsOptOut = createRule({
|
|
10
|
+
name: "require-invert-colors-opt-out",
|
|
11
|
+
meta: {
|
|
12
|
+
defaultOptions: [
|
|
13
|
+
{
|
|
14
|
+
additionalComponents: [],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
type: "problem",
|
|
18
|
+
docs: {
|
|
19
|
+
description: "Enforce accessibilityIgnoresInvertColors on images, which iOS Smart Invert would otherwise render as a negative.",
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
missingIgnoresInvertColors: "'<{{ componentName }} />' is inverted into a negative when iOS Smart Invert is on. Add 'accessibilityIgnoresInvertColors', or hide it if it carries no content.",
|
|
23
|
+
},
|
|
24
|
+
schema: [
|
|
25
|
+
{
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: schemaProperties,
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
// Matched on the element name, so an image from expo-image or a wrapper
|
|
34
|
+
// named through additionalComponents is checked the same way. Deliberately
|
|
35
|
+
// not gated on the import statement: that reads the source text and misses
|
|
36
|
+
// a codebase whose quote style differs.
|
|
37
|
+
const components = getComponents({
|
|
38
|
+
context,
|
|
39
|
+
defaultComponents: IMAGES,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
JSXOpeningElement(node) {
|
|
43
|
+
const componentName = getElementName(node);
|
|
44
|
+
if (componentName === undefined) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!components.includes(componentName) ||
|
|
48
|
+
hasSpreadProps(node) ||
|
|
49
|
+
isHiddenFromAccessibility(node) ||
|
|
50
|
+
hasProperty(node, "accessibilityIgnoresInvertColors")) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
context.report({
|
|
54
|
+
node,
|
|
55
|
+
messageId: "missingIgnoresInvertColors",
|
|
56
|
+
data: { componentName },
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
export default requireInvertColorsOptOut;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Options } from "../models/optional-options.js";
|
|
2
|
+
declare const requireModalA11yOnSheets: import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingViewIsModal", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default requireModalA11yOnSheets;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import schemaProperties from "../constants/schema-properties.js";
|
|
2
|
+
import createRule from "../utils/create-rule.js";
|
|
3
|
+
import getComponents from "../utils/get-components.js";
|
|
4
|
+
import getElementName from "../utils/get-element-name.js";
|
|
5
|
+
import hasProperty from "../utils/has-property.js";
|
|
6
|
+
import hasSpreadProps from "../utils/has-spread-props.js";
|
|
7
|
+
const requireModalA11yOnSheets = createRule({
|
|
8
|
+
name: "require-modal-a11y-on-sheets",
|
|
9
|
+
meta: {
|
|
10
|
+
defaultOptions: [
|
|
11
|
+
{
|
|
12
|
+
additionalComponents: [],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
type: "problem",
|
|
16
|
+
docs: {
|
|
17
|
+
description: "Enforce accessibilityViewIsModal on sheet and overlay roots, so VoiceOver ignores the content behind them.",
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
missingViewIsModal: "'<{{ componentName }} />' covers the screen but leaves everything behind it reachable by VoiceOver. Set 'accessibilityViewIsModal' on its root view.",
|
|
21
|
+
},
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: schemaProperties,
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
// No default list: a sheet is whatever this project calls one, and
|
|
32
|
+
// hardcoding one library's component names would fire on a codebase that
|
|
33
|
+
// happens to reuse them. Configure through additionalComponents.
|
|
34
|
+
const components = getComponents({ context });
|
|
35
|
+
return {
|
|
36
|
+
JSXOpeningElement(node) {
|
|
37
|
+
const componentName = getElementName(node);
|
|
38
|
+
if (componentName === undefined) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!components.includes(componentName) ||
|
|
42
|
+
hasSpreadProps(node) ||
|
|
43
|
+
hasProperty(node, ["accessibilityViewIsModal", "aria-modal"])) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
messageId: "missingViewIsModal",
|
|
49
|
+
data: { componentName },
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
export default requireModalA11yOnSheets;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export default createRule;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* Counts children that render visible text, which is what makes an
|
|
4
|
+
* `accessibilityLabel` redundant. Whitespace between JSX tags is not text, and
|
|
5
|
+
* `<Text>{t("Save")}</Text>` is, so neither can be judged on node type alone.
|
|
6
|
+
*/
|
|
7
|
+
declare const getAmountOfTextChildren: (children: TSESTree.JSXChild[], oldAmount?: number) => number;
|
|
8
|
+
export default getAmountOfTextChildren;
|