Apply WCAG 2.2 AA semantics to attachment indicator
epic-receipt-capture-and-attachment-ui-accessibility-task-008 — Add Semantics wrappers to the attachment indicator. Labels must communicate both the requirement status and the current attachment state, e.g. 'Receipt required. No receipt attached. Tap to add receipt.' Ensure color is never the sole differentiator by pairing icons with text labels. Touch target must be at least 44×44 dp. Validate with VoiceOver and TalkBack.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Compose the semantic label dynamically in the widget's build method using a helper function buildSemanticLabel(ReceiptThresholdState threshold, bool hasReceipt) → String. Keep the label concise but complete — screen readers read every character. Follow the pattern: '[status]. [attachment state].
[action hint].' Use Semantics(label: ..., hint: ..., button: true, child: ...) rather than MergeSemantics to maintain explicit control. Wrap internal Icon widget with ExcludeSemantics to prevent the icon's implicit label from being read in addition to the parent label. For touch target, wrap the visible indicator in a SizedBox(width: 44, height: 44) with alignment, or use a Material InkWell with a minimum size constraint. Consult Flutter's accessibility guidelines and test with both iOS VoiceOver rotor navigation and TalkBack explore-by-touch.
Testing Requirements
Automated widget tests (flutter_test with SemanticsHandle): enable semantics with tester.ensureSemantics(), find the indicator by semantic label pattern, assert label contains all three components (status, attachment state, hint) for each of the four state combinations (required+no receipt, required+attached, optional+no receipt, optional+attached). Assert touch target size >= 44dp via tester.getSize(). Assert button role present. Manual device tests documented in task-010 VoiceOver audit and task-011 TalkBack audit.
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.