Add accessibility semantics to stat card widget
epic-annual-impact-summary-ui-components-task-006 — Wire SummaryAccessibilityProvider into StatCardWidget to attach Semantics nodes with the generated announcement strings. Ensure the count-up animation does not trigger repeated screen reader announcements mid-animation; only announce the final value on animation completion using a live region.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Wrap the entire StatCardWidget content in a single Semantics(label: announcementString, excludeSemantics: true, child: ...) node. Compute announcementString once at build time using the final target value — never derive it from the animation's current value. To trigger the post-animation live-region announcement, add an addStatusListener to the AnimationController and call SemanticsService.announce(announcementString, TextDirection.ltr) when status == AnimationStatus.completed. When reduced-motion is true (no animation), call SemanticsService.announce in initState / after the first frame via WidgetsBinding.instance.addPostFrameCallback.
Avoid calling announce repeatedly — use a bool _hasAnnounced guard. The Semantics label must be the full human-readable sentence from the generator (e.g. '47 activities logged'), not a raw number.
Testing Requirements
Write widget tests using tester.semantics (or ensureSemantics()) to assert: (1) each card has exactly one top-level Semantics node; (2) the node's label matches generateStatCardAnnouncement output for the given inputs; (3) the label does not change during animation — pump to 50% duration and assert label is still the final-value string; (4) SemanticsService.announce is called once on animation completion and once on mount when reduced-motion is true. Mock SemanticsService.announce using a test double to count invocations.
Simultaneous count-up animations across multiple stat cards and chart draw-in animations on lower-end Android devices may cause frame drops below 60fps, degrading the premium Wrapped experience and making the feature feel unpolished.
Mitigation & Contingency
Mitigation: Stagger animation starts using AnimationController with staggered intervals rather than starting all animations simultaneously. Use RepaintBoundary around each animated widget to isolate rasterisation. Profile on a mid-range Android device (e.g., equivalent to Pixel 4a) during development, not just at QA.
Contingency: If frame rate targets cannot be met on low-end devices, implement a device-capability check at startup and substitute simpler fade-in animations for the count-up and chart draw-in on devices below a CPU performance threshold.
The activity-type-breakdown-widget must render organisation-specific activity type labels sourced from the terminology system. If the terminology provider is not yet integrated at the time this widget is built, the widget will display hardcoded system labels, which is a regression risk for multi-org support.
Mitigation & Contingency
Mitigation: Accept activity type labels as a typed parameter in the widget constructor rather than reading from the terminology provider directly inside the widget. The BLoC or repository layer resolves labels before passing them to the widget, maintaining clean separation and testability.
Contingency: If terminology resolution is unavailable at widget integration time, display internal activity type keys as a temporary fallback with a localised suffix '(label pending)' visible only in non-production builds so QA can identify unresolved labels.