critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

The thumbnail image node has a Semantics label that dynamically reads: 'Receipt attached: {filename}, {size}. Double-tap to preview.' when a receipt is present, and 'No receipt attached' in placeholder state
The Remove button Semantics label reads 'Remove receipt' and the Replace button reads 'Replace receipt' — these are explicit semanticsLabel overrides, not derived from tooltip text
The upload progress overlay announces its state changes: 'Uploading receipt, {N}%', 'Receipt uploaded successfully', and 'Receipt upload failed. Activate to retry' at appropriate transitions using SemanticsService.announce or liveRegion
Focus traversal order is thumbnail → Remove → Replace as verified by navigating with VoiceOver (iOS) swipe-right and TalkBack (Android) swipe-right
All interactive elements (Remove, Replace, thumbnail itself if tappable) report touch target size of at least 44×44 dp — no interactive element smaller than this exists in the widget tree
Decorative sub-elements (file format badge, size text) are excluded from the accessibility tree via ExcludeSemantics or Semantics(excludeSemantics: true) to avoid redundant announcements
The widget passes Flutter's built-in accessibility guidelines checker (SemanticsChecker) with zero violations
Manual VoiceOver test on iOS (physical device or simulator) confirms all states are announced correctly and no focus traps occur
Manual TalkBack test on Android confirms all states are announced correctly and no focus traps occur
Test results for both VoiceOver and TalkBack are documented in the PR with a brief written summary or screen recording link

Technical Requirements

frameworks
Flutter
Dart
apis
SemanticsService.announce
MediaQuery (disableAnimations)
Flutter Semantics API
data models
Receipt
performance requirements
Semantics tree modifications must not cause extra layout passes — use Semantics widget wrapping, not custom RenderObject overrides
security requirements
Semantics labels must not include sensitive personal data beyond filename and file size — do not include claim amounts or user names in announced strings
ui components
Semantics widget (label, hint, button, liveRegion properties)
ExcludeSemantics widget
MergeSemantics where appropriate
FocusTraversalGroup for explicit ordering

Execution Context

Execution Tier
Tier 3

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 checks, (4) liveRegion announcements fire on upload progress state changes. Enable semanticsEnabled: true in testWidgets. Additionally, perform and document manual device testing on VoiceOver (iOS 17+) and TalkBack (Android 13+) covering all four widget states: placeholder, with-receipt, uploading, and upload-error.

Epic Risks (2)
medium impact medium prob technical

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.

medium impact low prob integration

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.