@hypersolid-dev/eslint-config 2.0.0 → 4.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +12 -1
  3. package/dist/plugins/react-native-a11y/constants/accessibility-props.d.ts +32 -0
  4. package/dist/plugins/react-native-a11y/constants/accessibility-props.js +50 -0
  5. package/dist/plugins/react-native-a11y/constants/accessibility-state.d.ts +2 -0
  6. package/dist/plugins/react-native-a11y/constants/accessibility-state.js +8 -0
  7. package/dist/plugins/react-native-a11y/constants/interactive.d.ts +6 -0
  8. package/dist/plugins/react-native-a11y/constants/interactive.js +33 -0
  9. package/dist/plugins/react-native-a11y/constants/react-native-components.d.ts +7 -0
  10. package/dist/plugins/react-native-a11y/constants/react-native-components.js +13 -0
  11. package/dist/plugins/react-native-a11y/constants/schema-properties.d.ts +4 -0
  12. package/dist/plugins/react-native-a11y/constants/schema-properties.js +10 -0
  13. package/dist/plugins/react-native-a11y/index.d.ts +3 -0
  14. package/dist/plugins/react-native-a11y/index.js +48 -0
  15. package/dist/plugins/react-native-a11y/models/optional-options.d.ts +3 -0
  16. package/dist/plugins/react-native-a11y/models/optional-options.js +1 -0
  17. package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.d.ts +5 -0
  18. package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +82 -0
  19. package/dist/plugins/react-native-a11y/rules/accessibility-hint.d.ts +5 -0
  20. package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +74 -0
  21. package/dist/plugins/react-native-a11y/rules/accessibility-label.d.ts +6 -0
  22. package/dist/plugins/react-native-a11y/rules/accessibility-label.js +103 -0
  23. package/dist/plugins/react-native-a11y/rules/accessibility-role.d.ts +5 -0
  24. package/dist/plugins/react-native-a11y/rules/accessibility-role.js +72 -0
  25. package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.d.ts +4 -0
  26. package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +49 -0
  27. package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.d.ts +4 -0
  28. package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.js +40 -0
  29. package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.d.ts +4 -0
  30. package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +77 -0
  31. package/dist/plugins/react-native-a11y/rules/input-purpose.d.ts +6 -0
  32. package/dist/plugins/react-native-a11y/rules/input-purpose.js +84 -0
  33. package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.d.ts +4 -0
  34. package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.js +57 -0
  35. package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.d.ts +4 -0
  36. package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.js +31 -0
  37. package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.d.ts +5 -0
  38. package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.js +66 -0
  39. package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.d.ts +5 -0
  40. package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +49 -0
  41. package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.d.ts +9 -0
  42. package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.js +76 -0
  43. package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.d.ts +5 -0
  44. package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.js +82 -0
  45. package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.d.ts +5 -0
  46. package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.js +70 -0
  47. package/dist/plugins/react-native-a11y/rules/no-press-in-activation.d.ts +5 -0
  48. package/dist/plugins/react-native-a11y/rules/no-press-in-activation.js +59 -0
  49. package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.d.ts +5 -0
  50. package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +75 -0
  51. package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.d.ts +5 -0
  52. package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +62 -0
  53. package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.d.ts +5 -0
  54. package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +55 -0
  55. package/dist/plugins/react-native-a11y/utils/create-rule.d.ts +5 -0
  56. package/dist/plugins/react-native-a11y/utils/create-rule.js +3 -0
  57. package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.d.ts +8 -0
  58. package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +67 -0
  59. package/dist/plugins/react-native-a11y/utils/get-components.d.ts +7 -0
  60. package/dist/plugins/react-native-a11y/utils/get-components.js +11 -0
  61. package/dist/plugins/react-native-a11y/utils/get-element-name.d.ts +10 -0
  62. package/dist/plugins/react-native-a11y/utils/get-element-name.js +66 -0
  63. package/dist/plugins/react-native-a11y/utils/get-jsx-descendants.d.ts +4 -0
  64. package/dist/plugins/react-native-a11y/utils/get-jsx-descendants.js +49 -0
  65. package/dist/plugins/react-native-a11y/utils/get-literal-property-value.d.ts +7 -0
  66. package/dist/plugins/react-native-a11y/utils/get-literal-property-value.js +21 -0
  67. package/dist/plugins/react-native-a11y/utils/get-literal-text-child.d.ts +8 -0
  68. package/dist/plugins/react-native-a11y/utils/get-literal-text-child.js +40 -0
  69. package/dist/plugins/react-native-a11y/utils/get-property.d.ts +4 -0
  70. package/dist/plugins/react-native-a11y/utils/get-property.js +9 -0
  71. package/dist/plugins/react-native-a11y/utils/has-accessible-name.d.ts +6 -0
  72. package/dist/plugins/react-native-a11y/utils/has-accessible-name.js +27 -0
  73. package/dist/plugins/react-native-a11y/utils/has-property.d.ts +3 -0
  74. package/dist/plugins/react-native-a11y/utils/has-property.js +3 -0
  75. package/dist/plugins/react-native-a11y/utils/has-spread-props.d.ts +8 -0
  76. package/dist/plugins/react-native-a11y/utils/has-spread-props.js +8 -0
  77. package/dist/plugins/react-native-a11y/utils/is-definitely-grouped.d.ts +7 -0
  78. package/dist/plugins/react-native-a11y/utils/is-definitely-grouped.js +20 -0
  79. package/dist/plugins/react-native-a11y/utils/is-hidden-from-accessibility.d.ts +19 -0
  80. package/dist/plugins/react-native-a11y/utils/is-hidden-from-accessibility.js +51 -0
  81. package/dist/plugins/react-native-a11y/utils/is-inside-focus-stop.d.ts +9 -0
  82. package/dist/plugins/react-native-a11y/utils/is-inside-focus-stop.js +36 -0
  83. package/dist/plugins/react-native-a11y/utils/is-interactive.d.ts +16 -0
  84. package/dist/plugins/react-native-a11y/utils/is-interactive.js +38 -0
  85. package/dist/plugins/react-native-a11y/utils/is-not-a-focus-stop.d.ts +7 -0
  86. package/dist/plugins/react-native-a11y/utils/is-not-a-focus-stop.js +19 -0
  87. package/dist/react-native.js +81 -1
  88. package/package.json +5 -2
@@ -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,5 @@
1
+ import { type TSESLint } from "@typescript-eslint/utils";
2
+ declare const noInvalidAccessibilityState: TSESLint.RuleModule<"invalidStateKey", [], unknown, TSESLint.RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default noInvalidAccessibilityState;
@@ -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,3 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+ const createRule = ESLintUtils.RuleCreator((name) => name);
3
+ 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;
@@ -0,0 +1,67 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { TEXTS } from "../constants/react-native-components.js";
3
+ // ponytail: `Text` by name only. A renamed import or a differently named text
4
+ // atom reads as no text; pass it through `additionalComponents` if that bites.
5
+ const isTextElement = (node) => node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
6
+ TEXTS.includes(node.openingElement.name.name);
7
+ /** `<Text>{null}</Text>` and `<Text> </Text>` render nothing a user can read. */
8
+ const hasRenderedContent = (child) => {
9
+ if (child.type === AST_NODE_TYPES.JSXText) {
10
+ return child.value.trim().length > 0;
11
+ }
12
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
13
+ const { expression } = child;
14
+ return (expression.type !== AST_NODE_TYPES.JSXEmptyExpression &&
15
+ !(expression.type === AST_NODE_TYPES.Literal && expression.value === null) &&
16
+ !(expression.type === AST_NODE_TYPES.Identifier &&
17
+ expression.name === "undefined"));
18
+ }
19
+ return true;
20
+ };
21
+ /** The JSX a conditional can render, so a branch carrying text still counts. */
22
+ const branchesOf = (expression) => {
23
+ if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
24
+ return [expression.consequent, expression.alternate];
25
+ }
26
+ if (expression.type === AST_NODE_TYPES.LogicalExpression) {
27
+ return [expression.left, expression.right];
28
+ }
29
+ return [];
30
+ };
31
+ const isJsxChild = (expression) => expression.type === AST_NODE_TYPES.JSXElement ||
32
+ expression.type === AST_NODE_TYPES.JSXFragment;
33
+ /**
34
+ * Counts children that render visible text, which is what makes an
35
+ * `accessibilityLabel` redundant. Whitespace between JSX tags is not text, and
36
+ * `<Text>{t("Save")}</Text>` is, so neither can be judged on node type alone.
37
+ */
38
+ const getAmountOfTextChildren = (children, oldAmount = 0) => children.reduce((amount, child) => {
39
+ if (child.type === AST_NODE_TYPES.JSXText) {
40
+ return child.value.trim().length > 0 ? amount + 1 : amount;
41
+ }
42
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
43
+ const { expression } = child;
44
+ if (expression.type === AST_NODE_TYPES.Literal ||
45
+ expression.type === AST_NODE_TYPES.TemplateLiteral) {
46
+ return amount + 1;
47
+ }
48
+ // JSX inside an expression renders like any other child, and a branch
49
+ // that renders text still names the control
50
+ const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
51
+ return rendered.length > 0
52
+ ? getAmountOfTextChildren(rendered, amount)
53
+ : amount;
54
+ }
55
+ if (child.type === AST_NODE_TYPES.JSXFragment) {
56
+ return getAmountOfTextChildren(child.children, amount);
57
+ }
58
+ if (child.type === AST_NODE_TYPES.JSXElement) {
59
+ // A `Text` renders whatever it is given, including an i18n call
60
+ if (isTextElement(child)) {
61
+ return child.children.some(hasRenderedContent) ? amount + 1 : amount;
62
+ }
63
+ return getAmountOfTextChildren(child.children, amount);
64
+ }
65
+ return amount;
66
+ }, oldAmount);
67
+ export default getAmountOfTextChildren;
@@ -0,0 +1,7 @@
1
+ import type { RuleContext } from "@typescript-eslint/utils/ts-eslint";
2
+ import type { Options } from "../models/optional-options.js";
3
+ declare const getComponents: <T extends string, P extends Options>({ context, defaultComponents, }: {
4
+ context: RuleContext<T, P>;
5
+ defaultComponents?: string[];
6
+ }) => string[];
7
+ export default getComponents;
@@ -0,0 +1,11 @@
1
+ const getComponents = ({ context, defaultComponents, }) => {
2
+ const options = context.options.length > 0 ? context.options[0] : {};
3
+ const { additionalComponents } = options;
4
+ // Combine default React Native components with user-defined custom components
5
+ const componentsToCheck = [
6
+ ...(defaultComponents ?? []),
7
+ ...(additionalComponents ?? []),
8
+ ];
9
+ return componentsToCheck;
10
+ };
11
+ export default getComponents;
@@ -0,0 +1,10 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * The name to match a component by. A qualified name resolves to its last part,
4
+ * since `Animated.Image` is an `Image` wrapped for animation and owes the same
5
+ * accessibility props. A renamed import resolves to the name it was imported
6
+ * under. Returns undefined for a namespaced name (`svg:rect`), which is not a
7
+ * React Native component.
8
+ */
9
+ declare const getElementName: (node: TSESTree.JSXOpeningElement) => string | undefined;
10
+ export default getElementName;