critical priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

Each ExpenseTypeOptionCard is wrapped in a Semantics widget with: label sourced from accessibility service, hint describing the tap action, selected reflecting isSelected, enabled reflecting !isDisabled
The Semantics node role is set to 'button' (via button: true) so VoiceOver and TalkBack announce it correctly
When a card transitions from enabled to selected, the screen reader announces the new selected state without requiring a full page refresh
When a card transitions to disabled, the screen reader announces it as dimmed/unavailable and skips it in the focus order if excludeSemantics is set appropriately
The accessibility service returns localized strings — no hardcoded English labels in the Semantics widget
VoiceOver on iOS correctly reads: '[label], button, [selected/not selected]' and TalkBack on Android reads equivalently
No duplicate semantic labels exist on the same card
Widget tests using flutter_test semantics finder confirm all required semantic properties are present for each state

Technical Requirements

frameworks
Flutter
flutter_test
apis
ExpenseTypeAccessibilityService (internal service)
data models
ExpenseTypeOption
ExpenseTypeSemanticLabel
performance requirements
Semantics wrapper must not cause measurable layout performance regression
security requirements
Accessibility labels must not expose internal IDs or raw database keys to the accessibility tree
ui components
ExpenseTypeOptionCard (existing, from task-001)
Semantics (Flutter built-in)

Execution Context

Execution Tier
Tier 1

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.

Component
Expense Type Picker Widget
ui medium
Epic Risks (2)
medium impact medium prob technical

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.

medium impact low prob integration

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.