@hypersolid-dev/eslint-config 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +11 -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 +49 -0
  88. package/package.json +4 -2
@@ -0,0 +1,49 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ const isJsx = (node) => node.type === AST_NODE_TYPES.JSXElement ||
3
+ node.type === AST_NODE_TYPES.JSXFragment;
4
+ /** The JSX an expression can render, including through a conditional. */
5
+ const renderedBy = (expression) => {
6
+ if (isJsx(expression)) {
7
+ return [expression];
8
+ }
9
+ if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
10
+ return [
11
+ ...renderedBy(expression.consequent),
12
+ ...renderedBy(expression.alternate),
13
+ ];
14
+ }
15
+ if (expression.type === AST_NODE_TYPES.LogicalExpression) {
16
+ return [...renderedBy(expression.left), ...renderedBy(expression.right)];
17
+ }
18
+ // `items.map(() => <Row />)` renders its callback's JSX
19
+ if (expression.type === AST_NODE_TYPES.CallExpression) {
20
+ return expression.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES.ArrowFunctionExpression &&
21
+ argument.body.type !== AST_NODE_TYPES.BlockStatement
22
+ ? renderedBy(argument.body)
23
+ : []);
24
+ }
25
+ return [];
26
+ };
27
+ const collect = (children, found) => {
28
+ for (const child of children) {
29
+ if (child.type === AST_NODE_TYPES.JSXElement) {
30
+ found.push(child.openingElement);
31
+ collect(child.children, found);
32
+ continue;
33
+ }
34
+ if (child.type === AST_NODE_TYPES.JSXFragment) {
35
+ collect(child.children, found);
36
+ continue;
37
+ }
38
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
39
+ collect(renderedBy(child.expression), found);
40
+ }
41
+ }
42
+ };
43
+ /** Every JSX element rendered below this one, at any depth. */
44
+ const getJsxDescendants = (node) => {
45
+ const found = [];
46
+ collect(node.parent.children, found);
47
+ return found;
48
+ };
49
+ export default getJsxDescendants;
@@ -0,0 +1,7 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * The literal value of a prop, or `undefined` when it is absent or computed.
4
+ * A computed value cannot be judged statically, so callers skip it.
5
+ */
6
+ declare const getLiteralPropertyValue: (node: TSESTree.JSXOpeningElement, propertyNames: string | string[]) => TSESTree.Literal["value"] | undefined;
7
+ export default getLiteralPropertyValue;
@@ -0,0 +1,21 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import getProperty from "./get-property.js";
3
+ /**
4
+ * The literal value of a prop, or `undefined` when it is absent or computed.
5
+ * A computed value cannot be judged statically, so callers skip it.
6
+ */
7
+ const getLiteralPropertyValue = (node, propertyNames) => {
8
+ const attribute = getProperty(node, propertyNames);
9
+ if (!attribute?.value) {
10
+ return undefined;
11
+ }
12
+ if (attribute.value.type === AST_NODE_TYPES.Literal) {
13
+ return attribute.value.value;
14
+ }
15
+ if (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
16
+ attribute.value.expression.type === AST_NODE_TYPES.Literal) {
17
+ return attribute.value.expression.value;
18
+ }
19
+ return undefined;
20
+ };
21
+ export default getLiteralPropertyValue;
@@ -0,0 +1,8 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * The visible text a node renders, where every part of it is a plain literal.
4
+ * Anything resolved at runtime — an i18n call, an interpolation — reads as
5
+ * undefined, since a substring check against it would be guesswork.
6
+ */
7
+ declare const getLiteralTextChild: (children: TSESTree.JSXChild[]) => string | undefined;
8
+ export default getLiteralTextChild;
@@ -0,0 +1,40 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { TEXTS } from "../constants/react-native-components.js";
3
+ /** A childless non-text element renders no words of its own. */
4
+ const hasNoWords = (node) => node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
5
+ !TEXTS.includes(node.openingElement.name.name) &&
6
+ node.children.length === 0;
7
+ /** The literal words in these children, or undefined if any part is computed. */
8
+ const wordsIn = (children) => children.reduce((words, child) => {
9
+ if (!words) {
10
+ return;
11
+ }
12
+ if (child.type === AST_NODE_TYPES.JSXText) {
13
+ const text = child.value.trim();
14
+ return text.length > 0 ? [...words, text] : words;
15
+ }
16
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
17
+ // A value only known at runtime cannot be compared against
18
+ return child.expression.type === AST_NODE_TYPES.Literal
19
+ ? [...words, String(child.expression.value)]
20
+ : undefined;
21
+ }
22
+ if (child.type === AST_NODE_TYPES.JSXElement) {
23
+ if (hasNoWords(child)) {
24
+ return words;
25
+ }
26
+ const nested = wordsIn(child.children);
27
+ return nested ? [...words, ...nested] : undefined;
28
+ }
29
+ return words;
30
+ }, []);
31
+ /**
32
+ * The visible text a node renders, where every part of it is a plain literal.
33
+ * Anything resolved at runtime — an i18n call, an interpolation — reads as
34
+ * undefined, since a substring check against it would be guesswork.
35
+ */
36
+ const getLiteralTextChild = (children) => {
37
+ const words = wordsIn(children);
38
+ return words && words.length > 0 ? words.join(" ") : undefined;
39
+ };
40
+ export default getLiteralTextChild;
@@ -0,0 +1,4 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /** The first attribute on `node` matching one of `propNames`, if any. */
3
+ declare const getProperty: (node: TSESTree.JSXOpeningElement, propertyNames: string | string[]) => TSESTree.JSXAttribute | undefined;
4
+ export default getProperty;
@@ -0,0 +1,9 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ /** The first attribute on `node` matching one of `propNames`, if any. */
3
+ const getProperty = (node, propertyNames) => {
4
+ const names = Array.isArray(propertyNames) ? propertyNames : [propertyNames];
5
+ return node.attributes.find((attribute) => attribute.type === AST_NODE_TYPES.JSXAttribute &&
6
+ attribute.name.type === AST_NODE_TYPES.JSXIdentifier &&
7
+ names.includes(attribute.name.name));
8
+ };
9
+ export default getProperty;
@@ -0,0 +1,6 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /** An `accessibilityLabel=""` clears the name rather than setting one. */
3
+ export declare const hasEmptyLabel: (node: TSESTree.JSXOpeningElement) => boolean;
4
+ /** The label's text where it is a plain literal, otherwise undefined. */
5
+ export declare const getLiteralLabel: (node: TSESTree.JSXOpeningElement) => string | undefined;
6
+ export declare const hasLabelProperty: (node: TSESTree.JSXOpeningElement) => boolean;
@@ -0,0 +1,27 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import getLiteralPropertyValue from "./get-literal-property-value.js";
3
+ import getProperty from "./get-property.js";
4
+ const LABEL_PROPS = ["accessibilityLabel", "aria-label"];
5
+ /** An `accessibilityLabel=""` clears the name rather than setting one. */
6
+ export const hasEmptyLabel = (node) => {
7
+ const attribute = getProperty(node, LABEL_PROPS);
8
+ if (!attribute?.value) {
9
+ return false;
10
+ }
11
+ if (attribute.value.type === AST_NODE_TYPES.Literal) {
12
+ return attribute.value.value === "";
13
+ }
14
+ if (attribute.value.type !== AST_NODE_TYPES.JSXExpressionContainer) {
15
+ return false;
16
+ }
17
+ const { expression } = attribute.value;
18
+ return expression.type === AST_NODE_TYPES.Literal ? expression.value === "" : (expression.type === AST_NODE_TYPES.TemplateLiteral &&
19
+ expression.expressions.length === 0 &&
20
+ expression.quasis.every((quasi) => quasi.value.raw === ""));
21
+ };
22
+ /** The label's text where it is a plain literal, otherwise undefined. */
23
+ export const getLiteralLabel = (node) => {
24
+ const value = getLiteralPropertyValue(node, LABEL_PROPS);
25
+ return typeof value === "string" && value.length > 0 ? value : undefined;
26
+ };
27
+ export const hasLabelProperty = (node) => getProperty(node, LABEL_PROPS) !== undefined;
@@ -0,0 +1,3 @@
1
+ import type { TSESTree } from "@typescript-eslint/types";
2
+ declare const hasProperty: (node: TSESTree.JSXOpeningElement, propertyNames: string | string[]) => boolean;
3
+ export default hasProperty;
@@ -0,0 +1,3 @@
1
+ import getProperty from "./get-property.js";
2
+ const hasProperty = (node, propertyNames) => getProperty(node, propertyNames) !== undefined;
3
+ export default hasProperty;
@@ -0,0 +1,8 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * A node spreading unknown props may already carry the accessibility prop a
4
+ * rule is looking for. Wrapper components pass them down this way constantly,
5
+ * so reporting one is a false positive rather than a finding.
6
+ */
7
+ declare const hasSpreadProps: (node: TSESTree.JSXOpeningElement) => boolean;
8
+ export default hasSpreadProps;
@@ -0,0 +1,8 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ /**
3
+ * A node spreading unknown props may already carry the accessibility prop a
4
+ * rule is looking for. Wrapper components pass them down this way constantly,
5
+ * so reporting one is a false positive rather than a finding.
6
+ */
7
+ const hasSpreadProps = (node) => node.attributes.some((attribute) => attribute.type === AST_NODE_TYPES.JSXSpreadAttribute);
8
+ export default hasSpreadProps;
@@ -0,0 +1,7 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * Whether the node is collapsed into one focus stop for certain. Reporting
4
+ * needs that certainty: `accessible={isGrouped}` may well be false at runtime.
5
+ */
6
+ declare const isDefinitelyGrouped: (node: TSESTree.JSXOpeningElement) => boolean;
7
+ export default isDefinitelyGrouped;
@@ -0,0 +1,20 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import getProperty from "./get-property.js";
3
+ /**
4
+ * Whether the node is collapsed into one focus stop for certain. Reporting
5
+ * needs that certainty: `accessible={isGrouped}` may well be false at runtime.
6
+ */
7
+ const isDefinitelyGrouped = (node) => {
8
+ const attribute = getProperty(node, "accessible");
9
+ if (!attribute) {
10
+ return false;
11
+ }
12
+ // A bare `accessible` is `true`
13
+ if (!attribute.value) {
14
+ return true;
15
+ }
16
+ return (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
17
+ attribute.value.expression.type === AST_NODE_TYPES.Literal &&
18
+ attribute.value.expression.value === true);
19
+ };
20
+ export default isDefinitelyGrouped;
@@ -0,0 +1,19 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * Whether this node hides everything below it. `accessible={false}` does not:
4
+ * it drops the node as a focus stop and leaves its children reachable.
5
+ */
6
+ export declare const isHidingSubtree: (node: TSESTree.JSXOpeningElement) => boolean;
7
+ /**
8
+ * Whether the node is hidden for certain. Reporting a node needs this
9
+ * certainty, since a value computed at runtime may well be `false`.
10
+ */
11
+ export declare const isDefinitelyHidden: (node: TSESTree.JSXOpeningElement) => boolean;
12
+ /**
13
+ * Decorative content is hidden from assistive tech rather than labelled.
14
+ *
15
+ * A value computed at runtime counts as hidden here: suppressing a report on a
16
+ * node that may be hidden beats reporting one that is.
17
+ */
18
+ declare const isHiddenFromAccessibility: (node: TSESTree.JSXOpeningElement) => boolean;
19
+ export default isHiddenFromAccessibility;
@@ -0,0 +1,51 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { HIDDEN_PROPS } from "../constants/accessibility-props.js";
3
+ import getProperty from "./get-property.js";
4
+ const literalValue = (attribute) => {
5
+ // A bare `aria-hidden` is `true`
6
+ if (!attribute.value) {
7
+ return true;
8
+ }
9
+ if (attribute.value.type === AST_NODE_TYPES.Literal) {
10
+ return attribute.value.value;
11
+ }
12
+ if (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
13
+ attribute.value.expression.type === AST_NODE_TYPES.Literal) {
14
+ return attribute.value.expression.value;
15
+ }
16
+ return;
17
+ };
18
+ const isHidingDescendants = (node) => {
19
+ const attribute = getProperty(node, "importantForAccessibility");
20
+ return attribute !== undefined && literalValue(attribute) === "no-hide-descendants";
21
+ };
22
+ /**
23
+ * Whether this node hides everything below it. `accessible={false}` does not:
24
+ * it drops the node as a focus stop and leaves its children reachable.
25
+ */
26
+ export const isHidingSubtree = (node) => {
27
+ const hidden = getProperty(node, HIDDEN_PROPS);
28
+ return ((hidden !== undefined && literalValue(hidden) !== false) ||
29
+ isHidingDescendants(node));
30
+ };
31
+ /**
32
+ * Whether the node is hidden for certain. Reporting a node needs this
33
+ * certainty, since a value computed at runtime may well be `false`.
34
+ */
35
+ export const isDefinitelyHidden = (node) => {
36
+ const hidden = getProperty(node, HIDDEN_PROPS);
37
+ return ((hidden !== undefined && literalValue(hidden) === true) ||
38
+ isHidingDescendants(node));
39
+ };
40
+ /**
41
+ * Decorative content is hidden from assistive tech rather than labelled.
42
+ *
43
+ * A value computed at runtime counts as hidden here: suppressing a report on a
44
+ * node that may be hidden beats reporting one that is.
45
+ */
46
+ const isHiddenFromAccessibility = (node) => {
47
+ const hidden = getProperty(node, HIDDEN_PROPS);
48
+ return ((hidden !== undefined && literalValue(hidden) !== false) ||
49
+ isHidingDescendants(node));
50
+ };
51
+ export default isHiddenFromAccessibility;
@@ -0,0 +1,9 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * Whether an ancestor already answers for this node: a touchable is one focus
4
+ * stop, so its children are not stepped through separately and the role and
5
+ * label belong on the surface rather than on every child. The same holds for a
6
+ * composite control grouped with `accessible`, and for a hidden subtree.
7
+ */
8
+ declare const isInsideFocusStop: (node: TSESTree.JSXOpeningElement) => boolean;
9
+ export default isInsideFocusStop;
@@ -0,0 +1,36 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { TOUCHABLES } from "../constants/react-native-components.js";
3
+ import getElementName from "./get-element-name.js";
4
+ import getProperty from "./get-property.js";
5
+ import { isHidingSubtree } from "./is-hidden-from-accessibility.js";
6
+ const jsxAncestors = (node) => {
7
+ const ancestors = [];
8
+ // The node's own JSXElement is `node.parent`, so ancestry starts above it
9
+ let current = node.parent.parent;
10
+ while (current) {
11
+ if (current.type === AST_NODE_TYPES.JSXElement) {
12
+ ancestors.push(current.openingElement);
13
+ }
14
+ current = current.parent;
15
+ }
16
+ return ancestors;
17
+ };
18
+ /** A composite control marked as one focus stop carries the name for all of it. */
19
+ const isGrouped = (node) => {
20
+ const accessible = getProperty(node, "accessible");
21
+ return accessible !== undefined && !(accessible.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
22
+ accessible.value.expression.type === AST_NODE_TYPES.Literal &&
23
+ accessible.value.expression.value === false);
24
+ };
25
+ const isTouchable = (node) => {
26
+ const name = getElementName(node);
27
+ return name !== undefined && TOUCHABLES.includes(name);
28
+ };
29
+ /**
30
+ * Whether an ancestor already answers for this node: a touchable is one focus
31
+ * stop, so its children are not stepped through separately and the role and
32
+ * label belong on the surface rather than on every child. The same holds for a
33
+ * composite control grouped with `accessible`, and for a hidden subtree.
34
+ */
35
+ const isInsideFocusStop = (node) => jsxAncestors(node).some((ancestor) => isTouchable(ancestor) || isGrouped(ancestor) || isHidingSubtree(ancestor));
36
+ export default isInsideFocusStop;
@@ -0,0 +1,16 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * `accessible` makes the node a single focus stop. It defaults to true on the
4
+ * touchables and false elsewhere, so only an explicit prop is read here.
5
+ */
6
+ export declare const isMarkedAccessible: (node: TSESTree.JSXOpeningElement) => boolean;
7
+ export declare const hasInteractiveRole: (node: TSESTree.JSXOpeningElement) => boolean;
8
+ export declare const hasTouchHandler: (node: TSESTree.JSXOpeningElement) => boolean;
9
+ /**
10
+ * A node a user can operate: an RN host (or a wrapper listed as one) that
11
+ * announces itself as a control, handles a touch, or has been collapsed into
12
+ * one focus stop on purpose. An unlisted wrapper's `onPress` / `role` /
13
+ * `accessible` are its own API, not a sign that this JSX node is the control.
14
+ */
15
+ declare const isInteractive: (node: TSESTree.JSXOpeningElement, extraHosts?: string[]) => boolean;
16
+ export default isInteractive;
@@ -0,0 +1,38 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { INTERACTIVE_ROLES, TOUCH_HANDLER_PROPS } from "../constants/interactive.js";
3
+ import { IMAGES, INPUTS, TEXTS, TOUCHABLES, } from "../constants/react-native-components.js";
4
+ import getElementName from "./get-element-name.js";
5
+ import getLiteralPropertyValue from "./get-literal-property-value.js";
6
+ import getProperty from "./get-property.js";
7
+ import hasProperty from "./has-property.js";
8
+ const RN_HOSTS = new Set(["View", ...TOUCHABLES, ...TEXTS, ...IMAGES, ...INPUTS]);
9
+ /**
10
+ * `accessible` makes the node a single focus stop. It defaults to true on the
11
+ * touchables and false elsewhere, so only an explicit prop is read here.
12
+ */
13
+ export const isMarkedAccessible = (node) => {
14
+ const attribute = getProperty(node, "accessible");
15
+ return attribute !== undefined && !(attribute.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
16
+ attribute.value.expression.type === AST_NODE_TYPES.Literal &&
17
+ attribute.value.expression.value === false);
18
+ };
19
+ export const hasInteractiveRole = (node) => {
20
+ const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
21
+ return typeof role === "string" && INTERACTIVE_ROLES.includes(role);
22
+ };
23
+ export const hasTouchHandler = (node) => hasProperty(node, TOUCH_HANDLER_PROPS);
24
+ const isHost = (node, extraHosts) => {
25
+ const name = getElementName(node);
26
+ return (name !== undefined && (RN_HOSTS.has(name) || extraHosts.includes(name)));
27
+ };
28
+ /**
29
+ * A node a user can operate: an RN host (or a wrapper listed as one) that
30
+ * announces itself as a control, handles a touch, or has been collapsed into
31
+ * one focus stop on purpose. An unlisted wrapper's `onPress` / `role` /
32
+ * `accessible` are its own API, not a sign that this JSX node is the control.
33
+ */
34
+ const isInteractive = (node, extraHosts = []) => isHost(node, extraHosts) &&
35
+ (isMarkedAccessible(node) ||
36
+ hasInteractiveRole(node) ||
37
+ hasTouchHandler(node));
38
+ export default isInteractive;
@@ -0,0 +1,7 @@
1
+ import { type TSESTree } from "@typescript-eslint/types";
2
+ /**
3
+ * `accessible={false}` only opts a touchable out of being a focus stop. On an
4
+ * `Image` or `View` it is not a hide, and cannot excuse a missing name or role.
5
+ */
6
+ declare const isNotAFocusStop: (node: TSESTree.JSXOpeningElement) => boolean;
7
+ export default isNotAFocusStop;
@@ -0,0 +1,19 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/types";
2
+ import { TOUCHABLES } from "../constants/react-native-components.js";
3
+ import getElementName from "./get-element-name.js";
4
+ import getProperty from "./get-property.js";
5
+ /**
6
+ * `accessible={false}` only opts a touchable out of being a focus stop. On an
7
+ * `Image` or `View` it is not a hide, and cannot excuse a missing name or role.
8
+ */
9
+ const isNotAFocusStop = (node) => {
10
+ const name = getElementName(node);
11
+ if (name === undefined || !TOUCHABLES.includes(name)) {
12
+ return false;
13
+ }
14
+ const attribute = getProperty(node, "accessible");
15
+ return (attribute?.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
16
+ attribute.value.expression.type === AST_NODE_TYPES.Literal &&
17
+ attribute.value.expression.value === false);
18
+ };
19
+ export default isNotAFocusStop;
@@ -1,6 +1,8 @@
1
1
  import globals from 'globals';
2
2
  import noSecrets from 'eslint-plugin-no-secrets';
3
+ import reactNativeA11y from './plugins/react-native-a11y/index.js';
3
4
  import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, REACT_BOOLEAN_NAME_OPTIONS, reactConfigs, TEST_FILES, } from './shared.js';
5
+ const A11Y = '@hypersolid-dev/react-native-a11y';
4
6
  // Derived instead of hardcoded: the frozen `confusing-browser-globals` list (last released 2021) misses `document`,
5
7
  // `localStorage` and `getComputedStyle`, and bans `self`, which React Native defines. `globals` is maintained,
6
8
  // so new browser APIs are picked up without editing this file.
@@ -85,5 +87,52 @@ export default function reactNative() {
85
87
  'no-restricted-globals': 'off',
86
88
  },
87
89
  },
90
+ {
91
+ // React Native accessibility, each rule citing its WCAG criterion. Docs: src/plugins/react-native-a11y/docs.
92
+ name: '@hypersolid-dev/react-native/a11y',
93
+ files: CODE_FILES,
94
+ plugins: { [A11Y]: reactNativeA11y },
95
+ rules: {
96
+ // Custom accessibility actions need an `onAccessibilityAction`. Bad: `<ListItem accessibilityActions={[{ name: 'delete' }]} />`.
97
+ [`${A11Y}/accessibility-actions-need-handler`]: 'error',
98
+ // A hint describes the outcome, never the only name. Bad: `<TouchableOpacity accessibilityHint="Go to the next page"><Icon /></TouchableOpacity>`.
99
+ [`${A11Y}/accessibility-hint`]: 'error',
100
+ // Controls without visible text need a label, and a label contains any visible text. Bad: `<Pressable onPress={onPress}><Icon /></Pressable>`.
101
+ [`${A11Y}/accessibility-label`]: 'error',
102
+ // Interactive elements declare a role. Bad: `<Pressable onPress={onPress} />`.
103
+ [`${A11Y}/accessibility-role`]: 'error',
104
+ // A numeric `accessibilityValue` needs `min` and `max`. Bad: `<View accessibilityRole="adjustable" accessibilityValue={{ now: 5 }} />`.
105
+ [`${A11Y}/accessibility-value-needs-range`]: 'error',
106
+ // An adjustable element announces its value. Bad: `<View accessibilityRole="adjustable" />`.
107
+ [`${A11Y}/adjustable-needs-value`]: 'error',
108
+ // `announceForAccessibility` next to an Android live region needs a `Platform.OS` check, or TalkBack hears it twice. Bad: an unguarded `AccessibilityInfo.announceForAccessibility(message)` in a file with `accessibilityLiveRegion`.
109
+ [`${A11Y}/gate-announce-for-accessibility`]: 'error',
110
+ // Inputs declare their autofill purpose with `autoComplete`, which covers both platforms. Bad: `<TextInput textContentType="emailAddress" />`.
111
+ [`${A11Y}/input-purpose`]: 'error',
112
+ // Don't set both the ARIA and the legacy spelling of a prop. Bad: `<Pressable role="button" accessibilityRole="button" />`.
113
+ [`${A11Y}/no-aria-legacy-mix`]: 'error',
114
+ // `setAccessibilityFocus` is deprecated in favour of `sendAccessibilityEvent`. Bad: `AccessibilityInfo.setAccessibilityFocus(findNodeHandle(ref.current))`.
115
+ [`${A11Y}/no-deprecated-accessibility-focus`]: 'error',
116
+ // `Text` silently drops some ARIA props. Bad: `<Text aria-live="polite">{status}</Text>`.
117
+ [`${A11Y}/no-dropped-aria-on-text`]: 'error',
118
+ // Text keeps scaling with the user's font size. Bad: `allowFontScaling={false}` on a `Text`.
119
+ [`${A11Y}/no-font-scaling-disabled`]: 'error',
120
+ // Accessibility props on an element that never becomes an accessibility element do nothing. Bad: `<View role="listitem" />`.
121
+ [`${A11Y}/no-ineffective-accessibility-props`]: 'error',
122
+ // A state object built outside the JSX holds only supported keys. Bad: `const state = { invalid: true }; <Pressable accessibilityState={state} />`.
123
+ [`${A11Y}/no-invalid-accessibility-state`]: 'error',
124
+ // A focus stop inside another one is unreachable for screen readers. Bad: `<View accessible accessibilityLabel="Card"><Pressable accessibilityLabel="Save" /></View>`.
125
+ [`${A11Y}/no-nested-focus-stops`]: 'error',
126
+ // Activate on release so a press can be cancelled. Bad: `<Pressable accessibilityLabel="Save" onPressIn={submit} />`.
127
+ [`${A11Y}/no-press-in-activation`]: 'error',
128
+ // An element made accessible needs a name. Bad: `<View accessible accessibilityRole="button" />`.
129
+ [`${A11Y}/no-unnamed-interactive`]: 'error',
130
+ // Images opt out of iOS Smart Invert or render as a negative. Bad: `<Image source={source} accessibilityLabel="A cat" />`.
131
+ [`${A11Y}/require-invert-colors-opt-out`]: 'error',
132
+ // Off: a sheet is whatever a project calls one, so the rule has no defaults. Enable it with the project's sheet components:
133
+ // `['error', { additionalComponents: ['BottomSheet'] }]`. Bad then: `<BottomSheet><Text>Body</Text></BottomSheet>`.
134
+ [`${A11Y}/require-modal-a11y-on-sheets`]: 'off',
135
+ },
136
+ },
88
137
  ]);
89
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypersolid-dev/eslint-config",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Shared ESLint flat config for Hypersolid TypeScript projects",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -48,7 +48,7 @@
48
48
  "scripts": {
49
49
  "build": "tsc -p tsconfig.build.json",
50
50
  "lint": "pnpm build && eslint .",
51
- "test": "pnpm build && node --test 'test/*.test.ts'",
51
+ "test": "pnpm build && node --test 'test/*.test.ts' 'test/rules/**/*.test.ts'",
52
52
  "prepublishOnly": "pnpm test"
53
53
  },
54
54
  "peerDependencies": {
@@ -70,6 +70,8 @@
70
70
  "@eslint/js": "^10.0.1",
71
71
  "@eslint/json": "^2.1.1",
72
72
  "@next/eslint-plugin-next": "^16.4.3",
73
+ "@typescript-eslint/types": "^8.72.1",
74
+ "@typescript-eslint/utils": "^8.72.1",
73
75
  "eslint-import-resolver-typescript": "^4.4.5",
74
76
  "eslint-plugin-import-x": "^4.17.1",
75
77
  "eslint-plugin-jsx-a11y": "^6.10.2",