Add live-region announcements to preview panel
epic-expense-type-selection-user-interface-task-010 — Integrate SemanticsService live-region announcements into the ExpenseCalculationPreview so that screen readers announce the updated total amount within one render cycle after a selection change. Use accessibility-live-region-announcer patterns consistent with the rest of the app.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 5 - 253 tasks
Can start after Tier 4 completes
Implementation Notes
Use a BlocListener alongside the existing BlocBuilder — do not merge them into a single BlocConsumer unless the codebase already uses that pattern, as it complicates testability. In the listener: compare previous.calculationResult?.totalAmount != current.calculationResult?.totalAmount before calling SemanticsService.announce() to implement deduplication. The announcement text must be in Norwegian Bokmål as this is a Norwegian-market product used by accessibility-dependent users (Blindeforbundet users in particular). Format the amount using the same NumberFormat instance as the display widget to guarantee consistency.
Wrap the SemanticsService call in a WidgetsBinding.instance.addPostFrameCallback if needed to ensure the visual update has completed before the audio announcement.
Testing Requirements
Widget tests using flutter_test with SemanticsController enabled (tester.ensureSemantics()). Cover: (1) no announcement on initial widget mount, (2) announcement fires with correct Norwegian text when total changes from null to a value, (3) announcement fires when total changes from one amount to another, (4) announcement does NOT fire when an unrelated BLoC state field changes (buildWhen deduplication), (5) announcement is deduplicated when the same total is emitted twice. Use a spy/mock on SemanticsService or verify via tester.getSemantics if the framework supports it.
If the expense calculation preview subscribes to the full BLoC state stream, every unrelated state property change (e.g. a loading flag toggle) triggers a widget rebuild. With complex card animations for the disabled-state transition, this could cause frame drops on low-end Android devices used by some peer mentors.
Mitigation & Contingency
Mitigation: Use select() on the Riverpod provider to subscribe only to the specific state slice each widget needs; write a performance test asserting rebuild count on a rapid sequence of toggle events.
Contingency: If jank is detected in device testing, replace animated disabled-state transitions with instant opacity changes and defer animation polish to a follow-up sprint.
The disabled card state requires a specific contrast-safe colour combination that communicates unavailability without relying solely on colour (WCAG 1.4.1). If the current design token palette does not include a disabled-state token with sufficient contrast for text on the disabled background, the widget will either fail WCAG AA or require a last-minute design token addition that could break other components.
Mitigation & Contingency
Mitigation: Audit the existing design token manifest for disabled-state tokens at the start of the epic; if missing, raise with the design lead and add a contrast-validated token before widget implementation begins.
Contingency: If no design review is available, use the established --color-text-disabled and --color-surface-disabled tokens with an added strikethrough or lock icon to satisfy WCAG 1.4.1 non-colour requirement, and document the deviation for design review.