Implement WCAG 2.2 AA accessibility across all components
epic-bufdir-report-history-ui-task-007 — Audit and enforce WCAG 2.2 AA compliance across ReportHistoryScreen, ReportHistoryListItem, and ReportSummaryMetricsWidget. This includes: minimum 4.5:1 contrast ratios for all text using design token color palette, 44x44dp minimum touch targets for all interactive controls, meaningful Semantics labels on all widgets, live region announcements for loading and error state transitions, and correct focus order for keyboard and switch access navigation.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Use ExcludeSemantics on purely decorative icons and dividers to reduce screen reader noise. Use MergeSemantics to group list item content so VoiceOver reads it as a single announcement rather than individual fragments. For live region announcements, call SemanticsService.announce() inside the BLoC/notifier state listener — not inside build() — to avoid firing on every render. Enforce 44x44dp touch targets by wrapping smaller widgets in a SizedBox with explicit constraints and setting the Semantics.onTap callback.
Document which design token pairs were checked for contrast and their measured ratios — this serves as a record for audits. Blindeforbundet users include visually impaired users with screen readers as a primary interface, so this task has real end-user impact beyond compliance.
Testing Requirements
Widget tests (flutter_test): use tester.getSemantics() to assert every interactive widget has a non-empty label; assert touch target sizes are >= 44x44dp using tester.getSize(); assert selected/checked semantic properties on filter controls match state. Use Flutter's SemanticsDebugger in a test harness to capture the full semantics tree and assert no unlabeled interactive nodes exist. Manual testing: run VoiceOver (iOS) and TalkBack (Android) through all three components and verify announcements are correct and timely. Contrast testing: use a contrast analyzer tool against the design token color palette to document pass/fail for each text color/background combination.
The ReportSummaryMetricsWidget must display metrics as they were at time of submission, not recalculated from current data. If the metrics are not stored as a JSON snapshot in the history record at export time, the widget will either show wrong data or require a full re-aggregation on every list load.
Mitigation & Contingency
Mitigation: Ensure the Bufdir export pipeline (bufdir-report-export feature) writes a summary_metrics JSONB column in the history record at export time, containing total_activities, total_hours, and participants_reached. The UI widget reads only from this snapshot field — never from live aggregation queries.
Contingency: If snapshot data is missing for historical records (e.g., older exports before the column existed), display a 'Metrics not available for this report' placeholder in the widget rather than showing zeros or triggering a live aggregation that could return different figures.
Re-export can take several seconds (it runs the full generation pipeline). Without adequate progress feedback, coordinators may tap the button multiple times, triggering duplicate exports and duplicate history records.
Mitigation & Contingency
Mitigation: Disable the re-export button immediately on first tap and show an inline progress indicator on the list item. Guard against duplicate invocations at the service layer using an in-progress flag keyed by report ID. Display a loading state on the list item throughout the operation.
Contingency: If duplicate re-export records are created (e.g., due to a race condition), the history table will show multiple entries for the same original report — which is harmless but confusing. Add a deduplication UI hint ('Re-exported N times') and a backend guard that prevents more than one in-flight re-export for the same source record ID simultaneously.