Apply WCAG 2.2 AA semantics to thumbnail widget
epic-receipt-capture-and-attachment-ui-accessibility-task-004 — Wrap the thumbnail widget with Flutter Semantics nodes. Provide descriptive labels such as 'Receipt attached: filename.jpg, 45 KB. Double-tap to preview. Buttons available: Remove, Replace.' Ensure focus order is logical (thumbnail → remove → replace) and touch targets meet 44×44 dp minimum. Verify with VoiceOver on iOS and TalkBack on Android.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
This task retrofits Semantics onto the widget built in tasks 001–003. Start by auditing the current widget tree with the Flutter inspector's Semantics layer view (toggle in DevTools). Key patterns: (1) wrap the image container in a Semantics widget with label and hint; (2) use Semantics(button: true, label: '...') on Remove and Replace instead of relying on IconButton's auto-generated semantics, which may read icon names rather than intent; (3) for the progress overlay, use Semantics(liveRegion: true) on the percentage text so screen readers announce changes without requiring focus; (4) wrap decorative elements (format badge, raw size text) in ExcludeSemantics since the thumbnail Semantics label already contains this information. The Blindeforbundet organization has blind users as primary audience — this task is critical for that use case.
Test with actual VoiceOver on a physical device if possible, as simulator behavior can differ for haptic and announcement timing.
Testing Requirements
Write flutter_test semantic tests using tester.getSemantics() and SemanticsHandle to assert: (1) thumbnail node has correct label in attached and placeholder states, (2) Remove and Replace button nodes have correct semantic labels, (3) no interactive node has size below 44×44 dp using tester.widget
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.