@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [3.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv2.0.0&targetVersion=GTv3.0.0) (2026-11-09)
2
+
3
+ ### ⚠ BREAKING CHANGES
4
+
5
+ * **react-native:** the react-native preset enables 18 new error-level
6
+ accessibility rules.
7
+
8
+ ### Features
9
+
10
+ * **react-native:** add in-house react-native-a11y plugin ([755d725](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/755d725061b8c1f09fd8c33b73e2ae0f2b046426)), closes [AB#5177](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5177)
11
+
1
12
  ## [2.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv1.0.0&targetVersion=GTv2.0.0) (2026-11-09)
2
13
 
3
14
  ### ⚠ BREAKING CHANGES
package/README.md CHANGED
@@ -69,6 +69,17 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), ...reactNativ
69
69
 
70
70
  `react-native` bans browser globals that don't exist on React Native (`document`, `localStorage`, …), and ignores `.expo`, `android`, `ios` and `expo-env.d.ts`.
71
71
 
72
+ It also enables the bundled `@hypersolid-dev/react-native-a11y` rules, each citing the WCAG criterion it enforces ([rule docs](src/plugins/react-native-a11y/docs)). Custom components are matched by name, so tell a rule about your wrappers with `additionalComponents`. `require-modal-a11y-on-sheets` has no defaults and stays off until you list your sheet components:
73
+
74
+ ```js
75
+ {
76
+ rules: {
77
+ '@hypersolid-dev/react-native-a11y/accessibility-label': ['error', { additionalComponents: ['IconButton'] }],
78
+ '@hypersolid-dev/react-native-a11y/require-modal-a11y-on-sheets': ['error', { additionalComponents: ['BottomSheet'] }],
79
+ },
80
+ }
81
+ ```
82
+
72
83
  ### Next.js
73
84
 
74
85
  ```js
@@ -171,7 +182,7 @@ pnpm lint
171
182
  pnpm test
172
183
  ```
173
184
 
174
- `pnpm lint` builds the package and lints this repo with its own `base` preset. `pnpm test` runs fixture tests: for each preset, every invalid fixture in `test/fixtures/<preset>/invalid` must trigger the rule it's named after, and the valid fixtures must produce no messages. See [AGENTS.md](AGENTS.md) for conventions.
185
+ `pnpm lint` builds the package and lints this repo with its own `base` preset. `pnpm test` runs fixture tests: for each preset, every invalid fixture in `test/fixtures/<preset>/invalid` must trigger the rule it's named after, and the valid fixtures must produce no messages. Rules this package owns also have `RuleTester` tests in `test/rules`. See [AGENTS.md](AGENTS.md) for conventions.
175
186
 
176
187
  ## Versioning
177
188
 
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ARIA aliases React Native maps onto the legacy `accessibility*` props.
3
+ * Both spellings reach the same native behaviour, so a rule that only knows
4
+ * the legacy name reports a component that is already accessible.
5
+ */
6
+ export declare const ALIAS_TO_LEGACY: {
7
+ readonly role: "accessibilityRole";
8
+ readonly "aria-label": "accessibilityLabel";
9
+ readonly "aria-labelledby": "accessibilityLabelledBy";
10
+ readonly "aria-live": "accessibilityLiveRegion";
11
+ readonly "aria-modal": "accessibilityViewIsModal";
12
+ readonly "aria-busy": "accessibilityState";
13
+ readonly "aria-checked": "accessibilityState";
14
+ readonly "aria-disabled": "accessibilityState";
15
+ readonly "aria-expanded": "accessibilityState";
16
+ readonly "aria-selected": "accessibilityState";
17
+ readonly "aria-valuemax": "accessibilityValue";
18
+ readonly "aria-valuemin": "accessibilityValue";
19
+ readonly "aria-valuenow": "accessibilityValue";
20
+ readonly "aria-valuetext": "accessibilityValue";
21
+ };
22
+ /** The `accessibilityState` keys their `aria-*` aliases map onto. */
23
+ export declare const STATE_ALIAS_KEYS: Record<string, string>;
24
+ /** The `accessibilityValue` keys their `aria-*` aliases map onto. */
25
+ export declare const VALUE_ALIAS_KEYS: Record<string, string>;
26
+ /**
27
+ * `Text` does not implement the ARIA-to-legacy conversion for these, so they
28
+ * are silently dropped rather than reaching the OS.
29
+ */
30
+ export declare const ARIA_DROPPED_ON_TEXT: string[];
31
+ /** Props that hide a node (or its subtree) from assistive tech. */
32
+ export declare const HIDDEN_PROPS: string[];
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ARIA aliases React Native maps onto the legacy `accessibility*` props.
3
+ * Both spellings reach the same native behaviour, so a rule that only knows
4
+ * the legacy name reports a component that is already accessible.
5
+ */
6
+ export const ALIAS_TO_LEGACY = {
7
+ role: "accessibilityRole",
8
+ "aria-label": "accessibilityLabel",
9
+ "aria-labelledby": "accessibilityLabelledBy",
10
+ "aria-live": "accessibilityLiveRegion",
11
+ "aria-modal": "accessibilityViewIsModal",
12
+ "aria-busy": "accessibilityState",
13
+ "aria-checked": "accessibilityState",
14
+ "aria-disabled": "accessibilityState",
15
+ "aria-expanded": "accessibilityState",
16
+ "aria-selected": "accessibilityState",
17
+ "aria-valuemax": "accessibilityValue",
18
+ "aria-valuemin": "accessibilityValue",
19
+ "aria-valuenow": "accessibilityValue",
20
+ "aria-valuetext": "accessibilityValue",
21
+ };
22
+ /** The `accessibilityState` keys their `aria-*` aliases map onto. */
23
+ export const STATE_ALIAS_KEYS = {
24
+ "aria-busy": "busy",
25
+ "aria-checked": "checked",
26
+ "aria-disabled": "disabled",
27
+ "aria-expanded": "expanded",
28
+ "aria-selected": "selected",
29
+ };
30
+ /** The `accessibilityValue` keys their `aria-*` aliases map onto. */
31
+ export const VALUE_ALIAS_KEYS = {
32
+ "aria-valuemax": "max",
33
+ "aria-valuemin": "min",
34
+ "aria-valuenow": "now",
35
+ "aria-valuetext": "text",
36
+ };
37
+ /**
38
+ * `Text` does not implement the ARIA-to-legacy conversion for these, so they
39
+ * are silently dropped rather than reaching the OS.
40
+ */
41
+ export const ARIA_DROPPED_ON_TEXT = [
42
+ "aria-labelledby",
43
+ "aria-live",
44
+ "aria-valuemax",
45
+ "aria-valuemin",
46
+ "aria-valuenow",
47
+ "aria-valuetext",
48
+ ];
49
+ /** Props that hide a node (or its subtree) from assistive tech. */
50
+ export const HIDDEN_PROPS = ["aria-hidden", "accessibilityElementsHidden"];
@@ -0,0 +1,2 @@
1
+ /** The only keys `accessibilityState` supports. Notably absent: `invalid`. */
2
+ export declare const ACCESSIBILITY_STATE_KEYS: string[];
@@ -0,0 +1,8 @@
1
+ /** The only keys `accessibilityState` supports. Notably absent: `invalid`. */
2
+ export const ACCESSIBILITY_STATE_KEYS = [
3
+ "disabled",
4
+ "selected",
5
+ "checked",
6
+ "busy",
7
+ "expanded",
8
+ ];
@@ -0,0 +1,6 @@
1
+ /** Roles that imply the node is operated, so it owes an accessible name. */
2
+ export declare const INTERACTIVE_ROLES: string[];
3
+ /** Props through which a node handles a touch itself. */
4
+ export declare const TOUCH_HANDLER_PROPS: string[];
5
+ /** Props that can supply an accessible name. */
6
+ export declare const NAME_PROPS: string[];
@@ -0,0 +1,33 @@
1
+ /** Roles that imply the node is operated, so it owes an accessible name. */
2
+ export const INTERACTIVE_ROLES = [
3
+ "button",
4
+ "checkbox",
5
+ "combobox",
6
+ "imagebutton",
7
+ "link",
8
+ "menuitem",
9
+ "radio",
10
+ "search",
11
+ "spinbutton",
12
+ "switch",
13
+ "tab",
14
+ "togglebutton",
15
+ "adjustable",
16
+ ];
17
+ /** Props through which a node handles a touch itself. */
18
+ export const TOUCH_HANDLER_PROPS = [
19
+ "onPress",
20
+ "onPressIn",
21
+ "onPressOut",
22
+ "onLongPress",
23
+ "onTouchStart",
24
+ "onTouchEnd",
25
+ "onAccessibilityTap",
26
+ ];
27
+ /** Props that can supply an accessible name. */
28
+ export const NAME_PROPS = [
29
+ "accessibilityLabel",
30
+ "aria-label",
31
+ "accessibilityLabelledBy",
32
+ "aria-labelledby",
33
+ ];
@@ -0,0 +1,7 @@
1
+ export declare const IMAGES: string[];
2
+ export declare const TEXT_INPUTS: string[];
3
+ export declare const INPUTS: string[];
4
+ export declare const TEXTS: string[];
5
+ /** Hosts that are not accessibility elements until something opts them in. */
6
+ export declare const VIEWS: string[];
7
+ export declare const TOUCHABLES: string[];
@@ -0,0 +1,13 @@
1
+ export const IMAGES = ["Image", "ImageBackground"];
2
+ export const TEXT_INPUTS = ["TextInput"];
3
+ export const INPUTS = [...TEXT_INPUTS, "Switch"];
4
+ export const TEXTS = ["Text"];
5
+ /** Hosts that are not accessibility elements until something opts them in. */
6
+ export const VIEWS = ["View", "ScrollView", "SafeAreaView"];
7
+ export const TOUCHABLES = [
8
+ "Pressable",
9
+ "TouchableOpacity",
10
+ "TouchableHighlight",
11
+ "TouchableWithoutFeedback",
12
+ "TouchableNativeFeedback",
13
+ ];
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ additionalComponents: import("@typescript-eslint/utils/json-schema").JSONSchema4ArraySchema;
3
+ };
4
+ export default _default;
@@ -0,0 +1,10 @@
1
+ const ADDITIONAL_COMPONENTS = {
2
+ type: "array",
3
+ items: {
4
+ type: "string",
5
+ },
6
+ description: "Additional components to include in the rule, e.g.: ['CustomComponent']",
7
+ };
8
+ export default {
9
+ additionalComponents: ADDITIONAL_COMPONENTS,
10
+ };
@@ -0,0 +1,3 @@
1
+ import type { ESLint } from "eslint";
2
+ declare const _default: ESLint.Plugin;
3
+ export default _default;
@@ -0,0 +1,48 @@
1
+ import accessibilityActionsNeedHandler from "./rules/accessibility-actions-need-handler.js";
2
+ import accessibilityHint from "./rules/accessibility-hint.js";
3
+ import accessibilityLabel from "./rules/accessibility-label.js";
4
+ import accessibilityRole from "./rules/accessibility-role.js";
5
+ import accessibilityValueNeedsRange from "./rules/accessibility-value-needs-range.js";
6
+ import adjustableNeedsValue from "./rules/adjustable-needs-value.js";
7
+ import gateAnnounceForAccessibility from "./rules/gate-announce-for-accessibility.js";
8
+ import inputPurpose from "./rules/input-purpose.js";
9
+ import noAriaLegacyMix from "./rules/no-aria-legacy-mix.js";
10
+ import noDeprecatedAccessibilityFocus from "./rules/no-deprecated-accessibility-focus.js";
11
+ import noDroppedAriaOnText from "./rules/no-dropped-aria-on-text.js";
12
+ import noFontScalingDisabled from "./rules/no-font-scaling-disabled.js";
13
+ import noIneffectiveAccessibilityProps from "./rules/no-ineffective-accessibility-props.js";
14
+ import noInvalidAccessibilityState from "./rules/no-invalid-accessibility-state.js";
15
+ import noNestedFocusStops from "./rules/no-nested-focus-stops.js";
16
+ import noPressInActivation from "./rules/no-press-in-activation.js";
17
+ import noUnnamedInteractive from "./rules/no-unnamed-interactive.js";
18
+ import requireInvertColorsOptOut from "./rules/require-invert-colors-opt-out.js";
19
+ import requireModalA11yOnSheets from "./rules/require-modal-a11y-on-sheets.js";
20
+ const rules = {
21
+ "accessibility-actions-need-handler": accessibilityActionsNeedHandler,
22
+ "accessibility-hint": accessibilityHint,
23
+ "accessibility-label": accessibilityLabel,
24
+ "accessibility-role": accessibilityRole,
25
+ "accessibility-value-needs-range": accessibilityValueNeedsRange,
26
+ "adjustable-needs-value": adjustableNeedsValue,
27
+ "gate-announce-for-accessibility": gateAnnounceForAccessibility,
28
+ "input-purpose": inputPurpose,
29
+ "no-aria-legacy-mix": noAriaLegacyMix,
30
+ "no-deprecated-accessibility-focus": noDeprecatedAccessibilityFocus,
31
+ "no-dropped-aria-on-text": noDroppedAriaOnText,
32
+ "no-font-scaling-disabled": noFontScalingDisabled,
33
+ "no-ineffective-accessibility-props": noIneffectiveAccessibilityProps,
34
+ "no-invalid-accessibility-state": noInvalidAccessibilityState,
35
+ "no-nested-focus-stops": noNestedFocusStops,
36
+ "no-press-in-activation": noPressInActivation,
37
+ "no-unnamed-interactive": noUnnamedInteractive,
38
+ "require-invert-colors-opt-out": requireInvertColorsOptOut,
39
+ "require-modal-a11y-on-sheets": requireModalA11yOnSheets,
40
+ };
41
+ // React Native accessibility rules, maintained in-house. Originally @hypersolid-dev/eslint-config (react-native preset) v2.2.0 (MIT), from
42
+ // before Triple became Hypersolid. Rule docs live in ./docs.
43
+ const plugin = {
44
+ meta: { name: "@hypersolid-dev/react-native-a11y" },
45
+ rules,
46
+ };
47
+ // typescript-eslint rule types are stricter than ESLint's `Plugin` type and not assignable to it; the runtime shape is the same.
48
+ export default plugin;
@@ -0,0 +1,3 @@
1
+ export type Options = {
2
+ additionalComponents?: string[];
3
+ }[];
@@ -0,0 +1,5 @@
1
+ type MessageIds = "actionsWithoutHandler" | "handlerWithoutActions" | "customActionWithoutLabel";
2
+ declare const accessibilityActionsNeedHandler: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default accessibilityActionsNeedHandler;
@@ -0,0 +1,82 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
+ import createRule from "../utils/create-rule.js";
3
+ import getProperty from "../utils/get-property.js";
4
+ import hasSpreadProps from "../utils/has-spread-props.js";
5
+ /** Names the platforms already describe to the user. */
6
+ const STANDARD_ACTIONS = new Set([
7
+ "activate",
8
+ "increment",
9
+ "decrement",
10
+ "longpress",
11
+ "magicTap",
12
+ "escape",
13
+ ]);
14
+ const accessibilityActionsNeedHandler = createRule({
15
+ name: "accessibility-actions-need-handler",
16
+ meta: {
17
+ defaultOptions: [],
18
+ type: "problem",
19
+ docs: {
20
+ description: "Enforce accessibilityActions and onAccessibilityAction are declared together.",
21
+ },
22
+ messages: {
23
+ actionsWithoutHandler: "'accessibilityActions' are offered to assistive tech but nothing handles them. Add 'onAccessibilityAction'.",
24
+ handlerWithoutActions: "'onAccessibilityAction' never fires without 'accessibilityActions' to declare what can be invoked.",
25
+ customActionWithoutLabel: "The custom action '{{ name }}' has no 'label', so assistive tech lists it by its internal name. Add a label users can read.",
26
+ },
27
+ schema: [],
28
+ },
29
+ create(context) {
30
+ const reportUnlabelledActions = (expression) => {
31
+ if (expression.type !== AST_NODE_TYPES.ArrayExpression) {
32
+ return;
33
+ }
34
+ for (const element of expression.elements) {
35
+ if (element?.type !== AST_NODE_TYPES.ObjectExpression) {
36
+ continue;
37
+ }
38
+ const entries = element.properties.flatMap((property) => property.type === AST_NODE_TYPES.Property &&
39
+ property.key.type === AST_NODE_TYPES.Identifier
40
+ ? [[property.key.name, property.value]]
41
+ : []);
42
+ const name = entries.find(([key]) => key === "name")?.[1];
43
+ if (name?.type !== AST_NODE_TYPES.Literal ||
44
+ typeof name.value !== "string" ||
45
+ STANDARD_ACTIONS.has(name.value) ||
46
+ entries.some(([key]) => key === "label")) {
47
+ continue;
48
+ }
49
+ context.report({
50
+ node: element,
51
+ messageId: "customActionWithoutLabel",
52
+ data: { name: name.value },
53
+ });
54
+ }
55
+ };
56
+ return {
57
+ JSXOpeningElement(node) {
58
+ if (hasSpreadProps(node)) {
59
+ return;
60
+ }
61
+ const actions = getProperty(node, "accessibilityActions");
62
+ const handler = getProperty(node, "onAccessibilityAction");
63
+ if (actions && !handler) {
64
+ context.report({
65
+ node: actions,
66
+ messageId: "actionsWithoutHandler",
67
+ });
68
+ }
69
+ if (actions?.value?.type === AST_NODE_TYPES.JSXExpressionContainer) {
70
+ reportUnlabelledActions(actions.value.expression);
71
+ }
72
+ if (handler && !actions) {
73
+ context.report({
74
+ node: handler,
75
+ messageId: "handlerWithoutActions",
76
+ });
77
+ }
78
+ },
79
+ };
80
+ },
81
+ });
82
+ export default accessibilityActionsNeedHandler;
@@ -0,0 +1,5 @@
1
+ import type { Options } from "../models/optional-options.js";
2
+ declare const accessibilityHint: import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingAccessibilityLabel", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default accessibilityHint;
@@ -0,0 +1,74 @@
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 getAmountOfTextChildren from "../utils/get-amount-of-text-children.js";
5
+ import getComponents from "../utils/get-components.js";
6
+ import getElementName from "../utils/get-element-name.js";
7
+ import { hasLabelProperty as hasLabelProperty } from "../utils/has-accessible-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 isNotAFocusStop from "../utils/is-not-a-focus-stop.js";
13
+ const accessibilityHint = createRule({
14
+ name: "accessibility-hint",
15
+ meta: {
16
+ defaultOptions: [
17
+ {
18
+ additionalComponents: [],
19
+ },
20
+ ],
21
+ type: "problem",
22
+ docs: {
23
+ description: "Enforce a hint is paired with an accessible name, since a hint is not a name.",
24
+ },
25
+ messages: {
26
+ missingAccessibilityLabel: "The '<{{ componentName }} />' component must have an 'accessibilityLabel' in order to use the 'accessibilityHint'.",
27
+ },
28
+ schema: [
29
+ {
30
+ type: "object",
31
+ properties: schemaProperties,
32
+ additionalProperties: false,
33
+ },
34
+ ],
35
+ },
36
+ create(context) {
37
+ const components = getComponents({
38
+ context,
39
+ defaultComponents: TOUCHABLES,
40
+ });
41
+ return {
42
+ JSXOpeningElement(node) {
43
+ const componentName = getElementName(node);
44
+ if (componentName === undefined) {
45
+ return;
46
+ }
47
+ if (!components.includes(componentName)) {
48
+ return;
49
+ }
50
+ if (!hasProperty(node, "accessibilityHint") ||
51
+ hasSpreadProps(node) ||
52
+ isHiddenFromAccessibility(node) ||
53
+ isNotAFocusStop(node) ||
54
+ isInsideFocusStop(node)) {
55
+ return;
56
+ }
57
+ // Visible text already names the control, so the hint has a name to
58
+ // hang off even without a label
59
+ const { children } = node.parent;
60
+ if (hasLabelProperty(node) || getAmountOfTextChildren(children) > 0) {
61
+ return;
62
+ }
63
+ context.report({
64
+ node,
65
+ messageId: "missingAccessibilityLabel",
66
+ data: {
67
+ componentName,
68
+ },
69
+ });
70
+ },
71
+ };
72
+ },
73
+ });
74
+ export default accessibilityHint;
@@ -0,0 +1,6 @@
1
+ import type { Options } from "../models/optional-options.js";
2
+ type MessageIds = "missingAccessibilityLabel" | "emptyAccessibilityLabel" | "labelMissingVisibleText";
3
+ declare const accessibilityLabel: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
4
+ name: string;
5
+ };
6
+ export default accessibilityLabel;
@@ -0,0 +1,103 @@
1
+ import { IMAGES, INPUTS, TOUCHABLES } from "../constants/react-native-components.js";
2
+ import schemaProperties from "../constants/schema-properties.js";
3
+ import createRule from "../utils/create-rule.js";
4
+ import getAmountOfTextChildren from "../utils/get-amount-of-text-children.js";
5
+ import getComponents from "../utils/get-components.js";
6
+ import getElementName from "../utils/get-element-name.js";
7
+ import { getLiteralLabel, hasEmptyLabel, hasLabelProperty as hasLabelProperty, } from "../utils/has-accessible-name.js";
8
+ import getLiteralTextChild from "../utils/get-literal-text-child.js";
9
+ import hasSpreadProps from "../utils/has-spread-props.js";
10
+ import isHiddenFromAccessibility from "../utils/is-hidden-from-accessibility.js";
11
+ import isInsideFocusStop from "../utils/is-inside-focus-stop.js";
12
+ import isNotAFocusStop from "../utils/is-not-a-focus-stop.js";
13
+ const accessibilityLabel = createRule({
14
+ name: "accessibility-label",
15
+ meta: {
16
+ defaultOptions: [
17
+ {
18
+ additionalComponents: [],
19
+ },
20
+ ],
21
+ type: "problem",
22
+ docs: {
23
+ description: "Enforce accessibilityLabel prop is present on controls without visible text.",
24
+ },
25
+ messages: {
26
+ missingAccessibilityLabel: "The '<{{ componentName }} />' component must have an 'accessibilityLabel' prop.",
27
+ labelMissingVisibleText: "The label '{{ label }}' does not contain the visible text '{{ text }}', so a Voice Control user saying what they can see will not reach this control.",
28
+ emptyAccessibilityLabel: "The '<{{ componentName }} />' component has an empty 'accessibilityLabel', which leaves it unnamed. Give it a name or hide it with 'aria-hidden'.",
29
+ },
30
+ schema: [
31
+ {
32
+ type: "object",
33
+ properties: schemaProperties,
34
+ additionalProperties: false,
35
+ },
36
+ ],
37
+ },
38
+ create(context) {
39
+ const components = getComponents({
40
+ context,
41
+ defaultComponents: [...IMAGES, ...TOUCHABLES, ...INPUTS],
42
+ });
43
+ return {
44
+ JSXOpeningElement(node) {
45
+ const componentName = getElementName(node);
46
+ if (componentName === undefined) {
47
+ return;
48
+ }
49
+ if (!components.includes(componentName)) {
50
+ return;
51
+ }
52
+ // Decorative content is hidden rather than labelled
53
+ // The surface above answers for its children
54
+ if (isHiddenFromAccessibility(node) ||
55
+ isNotAFocusStop(node) ||
56
+ isInsideFocusStop(node)) {
57
+ return;
58
+ }
59
+ if (hasEmptyLabel(node)) {
60
+ context.report({
61
+ node,
62
+ messageId: "emptyAccessibilityLabel",
63
+ data: {
64
+ componentName,
65
+ },
66
+ });
67
+ return;
68
+ }
69
+ // The label may arrive through the spread
70
+ if (hasSpreadProps(node)) {
71
+ return;
72
+ }
73
+ // Visible text already names the control
74
+ const { children } = node.parent;
75
+ const amountOfTextChildren = getAmountOfTextChildren(children);
76
+ // 2.5.3: a spoken command matches the accessible name, so the name
77
+ // has to contain what the user can read
78
+ const label = getLiteralLabel(node);
79
+ const visible = getLiteralTextChild(children);
80
+ if (label !== undefined &&
81
+ visible !== undefined &&
82
+ !label.toLowerCase().includes(visible.toLowerCase())) {
83
+ context.report({
84
+ node,
85
+ messageId: "labelMissingVisibleText",
86
+ data: { label, text: visible },
87
+ });
88
+ return;
89
+ }
90
+ if (!hasLabelProperty(node) && amountOfTextChildren === 0) {
91
+ context.report({
92
+ node,
93
+ messageId: "missingAccessibilityLabel",
94
+ data: {
95
+ componentName,
96
+ },
97
+ });
98
+ }
99
+ },
100
+ };
101
+ },
102
+ });
103
+ export default accessibilityLabel;
@@ -0,0 +1,5 @@
1
+ import type { Options } from "../models/optional-options.js";
2
+ declare const accessibilityRole: import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingAccessibilityRole", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default accessibilityRole;
@@ -0,0 +1,72 @@
1
+ import { IMAGES, TOUCHABLES } from "../constants/react-native-components.js";
2
+ import schemaProperties from "../constants/schema-properties.js";
3
+ import createRule from "../utils/create-rule.js";
4
+ import getComponents from "../utils/get-components.js";
5
+ import getElementName from "../utils/get-element-name.js";
6
+ import hasProperty from "../utils/has-property.js";
7
+ import hasSpreadProps from "../utils/has-spread-props.js";
8
+ import isHiddenFromAccessibility from "../utils/is-hidden-from-accessibility.js";
9
+ import isInsideFocusStop from "../utils/is-inside-focus-stop.js";
10
+ import isNotAFocusStop from "../utils/is-not-a-focus-stop.js";
11
+ const accessibilityRole = createRule({
12
+ name: "accessibility-role",
13
+ meta: {
14
+ defaultOptions: [
15
+ {
16
+ additionalComponents: [],
17
+ },
18
+ ],
19
+ type: "problem",
20
+ docs: {
21
+ description: "Enforce accessibilityRole prop is present on interactive components and meaningful images.",
22
+ },
23
+ messages: {
24
+ missingAccessibilityRole: "The '<{{ componentName }} />' component must have an 'accessibilityRole' prop.",
25
+ },
26
+ schema: [
27
+ {
28
+ type: "object",
29
+ properties: schemaProperties,
30
+ additionalProperties: false,
31
+ },
32
+ ],
33
+ },
34
+ create(context) {
35
+ const components = getComponents({
36
+ context,
37
+ defaultComponents: [...TOUCHABLES, ...IMAGES],
38
+ });
39
+ return {
40
+ JSXOpeningElement(node) {
41
+ const componentName = getElementName(node);
42
+ if (componentName === undefined) {
43
+ return;
44
+ }
45
+ if (!components.includes(componentName)) {
46
+ return;
47
+ }
48
+ // Decorative content is hidden rather than given a role
49
+ // The surface above answers for its children
50
+ if (isHiddenFromAccessibility(node) ||
51
+ isNotAFocusStop(node) ||
52
+ isInsideFocusStop(node)) {
53
+ return;
54
+ }
55
+ // The role may arrive through the spread
56
+ if (hasSpreadProps(node)) {
57
+ return;
58
+ }
59
+ if (!hasProperty(node, ["accessibilityRole", "role"])) {
60
+ context.report({
61
+ node,
62
+ messageId: "missingAccessibilityRole",
63
+ data: {
64
+ componentName,
65
+ },
66
+ });
67
+ }
68
+ },
69
+ };
70
+ },
71
+ });
72
+ export default accessibilityRole;
@@ -0,0 +1,4 @@
1
+ declare const accessibilityValueNeedsRange: import("@typescript-eslint/utils/ts-eslint").RuleModule<"valueWithoutRange", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ export default accessibilityValueNeedsRange;