critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

The attachment indicator is wrapped in a Semantics widget with a dynamic label that includes: (1) requirement status ('Receipt required' or 'Receipt optional'), (2) attachment state ('No receipt attached' or 'Receipt attached'), and (3) action hint ('Double tap to add receipt' or 'Double tap to view or remove receipt')
The Semantics label updates automatically when either the threshold state or the attachment state changes — no stale labels
Color contrast ratio between indicator icon/text and background meets WCAG 2.2 AA minimum of 4.5:1 for normal text and 3:1 for large text/UI components, verified with a contrast checker
The indicator icon is always accompanied by a visible text label — color alone does not differentiate between required and optional states
The interactive touch target of the indicator is at least 44×44 dp (use SizedBox or Padding to extend the tappable area if the visual indicator is smaller)
The indicator is NOT marked as a live region (liveRegion: false in Semantics wrapper) — live region announcements are handled separately by task-007
The Semantics widget uses button role (button: true) or equivalent so VoiceOver/TalkBack identifies it as an actionable element
ExcludeSemantics is applied to decorative child icons/images inside the Semantics wrapper to prevent double-reading
Automated accessibility test using flutter_test's SemanticsHandle confirms correct label format and button role for both 'required/no receipt' and 'optional/receipt attached' states
Manual validation on VoiceOver (iOS): focusing the indicator reads the full composite label in a single announcement
Manual validation on TalkBack (Android): swipe focus lands on indicator and reads the full composite label

Technical Requirements

frameworks
Flutter
performance requirements
Semantics tree update must not introduce a rebuild cycle — use const Semantics constructors where state is static
security requirements
Semantics labels must not expose the exact threshold amount in the spoken string — use only 'required' or 'optional' language
Comply with GDPR: no PII in accessibility labels
ui components
Semantics wrapper with dynamic label and hint
ExcludeSemantics for decorative icons
SizedBox/GestureDetector for 44dp touch target enforcement
Icon + text label pair for non-color-only differentiation

Execution Context

Execution Tier
Tier 3

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.

Component
Receipt Attachment Indicator
ui low
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.