Wire accessibility semantics onto option card
epic-expense-type-selection-user-interface-task-002 — Integrate expense-type-accessibility-service labels into the ExpenseTypeOptionCard widget. Each card must expose a Semantics widget with role, label, hint, and selected/disabled state sourced from the accessibility service. Verify VoiceOver and TalkBack announce state transitions correctly.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Use Flutter's Semantics widget directly rather than MergeSemantics to maintain granular control. The ExpenseTypeAccessibilityService should be injected as a dependency (constructor or Riverpod provider) rather than accessed as a singleton to keep the widget testable. Define a getSemanticLabel(ExpenseTypeOption option, {required bool isSelected, required bool isDisabled}) method in the service. For state-change announcements, use SemanticsService.announce() or a LiveRegion pattern to notify screen readers of selection changes — do not rely solely on widget rebuild.
Refer to WCAG 2.2 AA success criterion 4.1.2 (Name, Role, Value). Note that the project serves users with visual impairments (Blindeforbundet) — accessibility is a first-class requirement, not an afterthought.
Testing Requirements
Widget tests using flutter_test: (1) use tester.getSemantics() to assert label, hint, button role, selected, and enabled properties for all three card states, (2) test that state transition from enabled→selected updates the semantic 'selected' flag without requiring a widget rebuild of the parent, (3) test that disabled cards have enabled: false in the semantics tree. Additionally, perform manual testing on a physical iOS device with VoiceOver enabled and an Android device with TalkBack enabled, confirming state change announcements.
Document results in the PR description.
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.