diff --git a/.claude/skills/coding-standards/SKILL.md b/.claude/skills/coding-standards/SKILL.md index d34adf0f723b5..f2450a4cc2fcf 100644 --- a/.claude/skills/coding-standards/SKILL.md +++ b/.claude/skills/coding-standards/SKILL.md @@ -15,6 +15,7 @@ Coding standards for the Expensify App. Each standard is a standalone file in `r | Performance | `PERF-*` | Render optimization, memo patterns, useEffect hygiene, data selection | | Consistency | `CONSISTENCY-*` | Platform checks, magic values, unused props, ESLint discipline | | Clean React Patterns | `CLEAN-REACT-PATTERNS-*` | Composition, component ownership, state structure | +| Accessibility | `A11Y-*` | WCAG 2.2 AA compliance, screen reader support, inclusive interaction patterns | ## Quick Reference @@ -50,6 +51,23 @@ Coding standards for the Expensify App. Each standard is a standalone file in `r - [CLEAN-REACT-PATTERNS-4](rules/clean-react-4-no-side-effect-spaghetti.md) — No side-effect spaghetti - [CLEAN-REACT-PATTERNS-5](rules/clean-react-5-narrow-state.md) — Keep state narrow +### Accessibility (WCAG 2.2 AA) + +**Use React Native accessibility props.** React Native Web translates them to ARIA attributes automatically. Only use `aria-*` when a React Native equivalent isn't available. Reference: [React Native Accessibility](https://reactnative.dev/docs/accessibility) + +- [A11Y-1](rules/a11y-1-label-interactive-elements.md) — Label interactive elements +- [A11Y-2](rules/a11y-2-semantic-roles.md) — Semantic accessibilityRole +- [A11Y-3](rules/a11y-3-communicate-state.md) — Communicate component state +- [A11Y-4](rules/a11y-4-touch-target-size.md) — Minimum 44x44 touch targets +- [A11Y-5](rules/a11y-5-announce-dynamic-content.md) — Announce dynamic content +- [A11Y-6](rules/a11y-6-accessible-images.md) — Accessible images +- [A11Y-7](rules/a11y-7-no-color-only-info.md) — No color-only information +- [A11Y-8](rules/a11y-8-modal-focus-management.md) — Modal focus management +- [A11Y-9](rules/a11y-9-drag-alternatives.md) — Drag interaction alternatives +- [A11Y-10](rules/a11y-10-respect-text-scaling.md) — Respect text scaling +- [A11Y-11](rules/a11y-11-form-accessibility.md) — Form accessibility +- [A11Y-12](rules/a11y-12-logical-focus-order.md) — Logical focus order + ## Usage **During development**: When writing or modifying `src/` files, consult the relevant standard files for detailed conditions, examples, and exceptions. diff --git a/.claude/skills/coding-standards/rules/a11y-1-label-interactive-elements.md b/.claude/skills/coding-standards/rules/a11y-1-label-interactive-elements.md new file mode 100644 index 0000000000000..12ad37c03aa0e --- /dev/null +++ b/.claude/skills/coding-standards/rules/a11y-1-label-interactive-elements.md @@ -0,0 +1,65 @@ +--- +ruleId: A11Y-1 +title: Interactive elements must have accessible labels +--- + +## [A11Y-1] Interactive elements must have accessible labels + +### Reasoning + +Screen readers (VoiceOver/TalkBack) cannot convey the purpose of an interactive element without a text label. Icon-only buttons, image-only touchables, and components whose visible text is insufficient for context must provide `accessibilityLabel`. Without it, assistive technology announces the element as an unnamed control, making the app unusable for screen reader users. (WCAG 1.1.1, 4.1.2) + +### Incorrect + +```tsx +// Icon-only button with no label — screen reader says "button" + + + + +// Image-only touchable with no description + + + +``` + +### Correct + +```tsx + + + + + + + +``` + +--- + +### Review Metadata + +Flag ONLY when ALL of these are true: + +- Element is interactive (`Pressable`, `TouchableOpacity`, `TouchableWithoutFeedback`, `PressableWithFeedback`, `Button`, or has `onPress`/`onLongPress`) +- Element contains **no visible `` child** (icon-only, image-only, or SVG-only) +- Element has **no** `accessibilityLabel` prop + +**DO NOT flag if:** + +- Element has a `` child that clearly describes the action +- Element is explicitly hidden from accessibility (`accessible={false}`, `accessibilityElementsHidden={true}` on iOS, `importantForAccessibility="no"` on Android) +- Element is a list item wrapper where the child component handles its own accessibility + +**Search Patterns** (hints for reviewers): +- `Account Balance + +// Capping font scaling to prevent layout issues instead of fixing the layout +$2,450.00 + +// Fixed height container that clips scaled text + + This text will be clipped at larger scales + +``` + +### Correct + +```tsx +// Text respects system scaling (default behavior — don't override it) +Account Balance + +// Flexible container that accommodates scaled text + + + This text grows with the container at larger scales + + + +// Acceptable: limiting scaling on tiny decorative text only (e.g., badge count) +3 +``` + +--- + +### Review Metadata + +Flag ONLY when ANY of these patterns is found: + +- `allowFontScaling={false}` on `` or `` displaying user-facing content +- `maxFontSizeMultiplier={1}` effectively disabling scaling on readable text +- Fixed `height` on a container with text content and no `minHeight` or overflow accommodation + +**DO NOT flag if:** + +- `allowFontScaling={false}` on purely decorative text (icons rendered as text, single-character badges) +- `maxFontSizeMultiplier` set to a reasonable value (>= 1.5) to prevent extreme layout breakage +- Container uses `minHeight` instead of `height` +- Text is inside a component that manages scaling internally + +**Search Patterns** (hints for reviewers): +- `allowFontScaling={false}` +- `maxFontSizeMultiplier={1}` +- `maxFontSizeMultiplier={1.0}` +- Fixed `height:` on text containers diff --git a/.claude/skills/coding-standards/rules/a11y-11-form-accessibility.md b/.claude/skills/coding-standards/rules/a11y-11-form-accessibility.md new file mode 100644 index 0000000000000..4cdcbb398fa28 --- /dev/null +++ b/.claude/skills/coding-standards/rules/a11y-11-form-accessibility.md @@ -0,0 +1,90 @@ +--- +ruleId: A11Y-11 +title: Forms must have accessible labels, errors, and instructions +--- + +## [A11Y-11] Forms must have accessible labels, errors, and instructions + +### Reasoning + +Screen reader users navigate forms field by field. Each input must be associated with a descriptive label so users know what to enter. While React Native uses `placeholder` as a fallback accessible name, it disappears once the user starts typing, leaving the field unlabeled. An explicit `accessibilityLabel` (or `accessibilityLabelledBy` on Android) persists regardless of input state. Error messages must be announced when they appear using `accessibilityLiveRegion` (Android) and `AccessibilityInfo.announceForAccessibility()` (iOS). (WCAG 1.3.1, 3.3.1, 3.3.2) + +### Incorrect + +```tsx +// Input with no accessible label — screen reader says "edit text" + + +// Error not announced, not associated with input + +{emailError && {emailError}} +``` + +### Correct + +```tsx +// Input with accessible label + + +// Android: label linked to input via nativeID +{translate('common.email')} + + +// Error announced via live region (Android) + announceForAccessibility (iOS) + +{emailError && ( + + {emailError} + +)} + +// iOS: announce error to VoiceOver +useEffect(() => { + if (emailError) { + AccessibilityInfo.announceForAccessibility(emailError); + } +}, [emailError]); +``` + +--- + +### Review Metadata + +Flag ONLY when ANY of these patterns is found: + +- `` with **no** `accessibilityLabel` and **no** `accessibilityLabelledBy` +- `` relying **only** on `placeholder` for labeling (placeholder disappears once user types, leaving the field unlabeled for screen readers) +- Form validation error text rendered without `accessibilityLiveRegion` or `accessibilityRole="alert"` + +**DO NOT flag if:** + +- Using a form component library that wraps inputs with labels internally +- `accessibilityLabel` is set on the input or a parent `accessible` container +- `accessibilityLabelledBy` links to a visible label via `nativeID` (Android-only — ensure `accessibilityLabel` is also set as iOS fallback) + +**Search Patterns** (hints for reviewers): +- ` + Submit + Cancel + + +// Header visually on top via absolute positioning, but last in JSX + + Body content + + Title + + +``` + +### Correct + +```tsx +// JSX order matches visual reading order + + Cancel + Submit + + +// Header first in JSX, matching visual order + + + Title + + Body content + +``` + +--- + +### Review Metadata + +Flag ONLY when ANY of these patterns is found: + +- `flexDirection: 'row-reverse'` or `'column-reverse'` on a container with multiple interactive children +- Absolute positioning causing an element to appear visually before its JSX siblings +- `zIndex` layering that places interactive elements in a different visual order than JSX order + +**DO NOT flag if:** + +- `row-reverse` is used on a container with a single child or non-interactive children +- Visual reordering is purely decorative (no interactive elements affected) +- The component uses `experimental_accessibilityOrder` to explicitly control focus order + +**Search Patterns** (hints for reviewers): +- `flexDirection: 'row-reverse'` / `'column-reverse'` +- `position: 'absolute'` on interactive elements +- `zIndex` on containers with multiple interactive children diff --git a/.claude/skills/coding-standards/rules/a11y-2-semantic-roles.md b/.claude/skills/coding-standards/rules/a11y-2-semantic-roles.md new file mode 100644 index 0000000000000..72eb299f006f2 --- /dev/null +++ b/.claude/skills/coding-standards/rules/a11y-2-semantic-roles.md @@ -0,0 +1,75 @@ +--- +ruleId: A11Y-2 +title: Use semantic accessibilityRole for interactive elements +--- + +## [A11Y-2] Use semantic accessibilityRole for interactive elements + +### Reasoning + +React Native components have no implicit semantic meaning — assistive technology treats them as generic containers. Interactive elements must declare their role via `accessibilityRole` or `role` so screen readers can convey what the element does. Note: `role` is a cross-platform alias that takes precedence over `accessibilityRole` when both are set. (WCAG 4.1.2) + +### Incorrect + +```tsx +// Pressable with no role — screen reader doesn't convey it's a button + + Submit + + +// Pressable acting as link with no role + openURL(href)}> + Learn more + + +// Section heading with no role +Account Settings +``` + +### Correct + +```tsx + + Submit + + + openURL(href)} +> + Learn more + + + + Account Settings + +``` + +--- + +### Review Metadata + +Flag ONLY when ANY of these patterns is found: + +- `` or interactive component with `onPress`/`onLongPress` handler but **no** `accessibilityRole` or `role` prop +- `` or `` navigating to a URL without `accessibilityRole="link"` +- Text styled as a heading (large/bold, section title) without `accessibilityRole="header"` +- Toggle/switch UI without `accessibilityRole="switch"` or `"checkbox"` +- Tab UI without `accessibilityRole="tab"` + +**DO NOT flag if:** + +- Component already has `accessibilityRole` or `role` set +- Using a design system component that sets the role internally (e.g., `