VoiceOver accessibility audit for receipt capture flow
epic-receipt-capture-and-attachment-ui-accessibility-task-010 — Conduct a structured VoiceOver (iOS) audit of the full receipt capture flow: expense form → attachment indicator (optional) → threshold crossed → indicator updates → camera sheet opens → receipt captured → thumbnail appears → remove/replace actions. Document each screen reader interaction, confirm all interactive elements have meaningful labels, and that live region announcements fire at correct moments. Fix any failures found.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Use a standardized audit checklist template shared across the VoiceOver (task-010) and TalkBack (task-011) audits to ensure consistent coverage. Enable 'Speak Hints' in VoiceOver settings during the audit to catch missing hint strings. Use VoiceOver's rotor to test custom actions on the thumbnail. When fixing issues found during audit, isolate each fix to the minimal Semantics change required — avoid broad refactors that could introduce regressions in other flows.
After fixes, run the automated semantic label widget tests from tasks 007 and 008 to confirm no regressions. TestFlight build recommended for audit to match production binary behavior (vs debug mode which may differ in semantics tree handling).
Testing Requirements
Manual structured audit on a physical iPhone (iOS 17+) with VoiceOver enabled. Use a structured audit checklist covering: (1) label completeness, (2) role accuracy, (3) focus order, (4) live region timing, (5) action discoverability, (6) no focus traps. Document each finding with: element name, expected behavior, actual VoiceOver output, pass/fail classification (blocking/non-blocking), and fix reference if applicable. Fixes must be validated with a re-test pass on the same device before sign-off.
Flutter's accessibility live region support (SemanticsProperties.liveRegion) has known inconsistencies between iOS VoiceOver and Android TalkBack, and between Flutter versions. Threshold-crossing announcements may fail to fire or double-fire, breaking the accessibility contract for Blindeforbundet users.
Mitigation & Contingency
Mitigation: Test live region announcements on physical devices with VoiceOver and TalkBack enabled from the first iteration. Use the AccessibilityLiveRegionAnnouncer component pattern already established in the project. Verify announcement timing relative to Bloc state emissions to avoid double-fires.
Contingency: If Flutter live regions prove unreliable, implement a platform-channel fallback that calls UIAccessibility.post(notification:) on iOS and AccessibilityManager.sendAccessibilityEvent() on Android directly, bypassing Flutter's abstraction.
The org-configurable threshold must be available at form-render time. If the threshold configuration is fetched asynchronously and not cached, the indicator may briefly show the wrong state (e.g., 'optional' before the threshold loads), confusing users and potentially allowing invalid submissions.
Mitigation & Contingency
Mitigation: Ensure the receipt threshold validator loads and caches the org configuration at app startup or organization selection time, not lazily on form open. Use a loading state in the indicator widget rather than defaulting to 'optional' while configuration is pending.
Contingency: If startup caching is not feasible, treat an unknown threshold as 'receipt required' (fail safe) and surface a clear loading indicator until the configuration resolves, preventing invalid submissions while the config loads.