Implement WCAG 2.2 AA semantics on OrgCard
epic-organization-selection-ui-task-004 — Wrap OrgCard in a Semantics widget that announces the organization name, the role label resolved via OrgLabelsProvider, and the interactive selected/unselected state. Ensure VoiceOver (iOS) and TalkBack (Android) read: '[OrgName], [role label], selected/not selected, button'. Validate excludeSemantics on decorative logo image.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
OrgCard must consume OrgLabelsProvider, so if it is currently a StatelessWidget it needs to become a ConsumerWidget (Riverpod) to call ref.watch(orgLabelsProvider). Alternatively, accept the resolved roleLabel as a constructor parameter to keep OrgCard pure and push the provider read to the parent (OrgSelectionScreen) — this is preferable for testability since tests can inject any label string without a ProviderContainer. The Semantics label should be built as a single string: '${organization.displayName}, $roleLabel' because screen readers announce the full label at once. Do not use separate Semantics.label and Semantics.hint for name and role — combine them in label for a more natural announcement flow.
The semanticLabel on the Text('checkmark icon') inside the card should be set to '' (empty) or the icon widget excluded since the outer Semantics already covers the selected state via the selected flag.
Testing Requirements
Widget tests using flutter_test with SemanticsHandle enabled (tester.ensureSemantics()): (1) pump OrgCard with isSelected=true and assert tester.getSemantics(find.byType(OrgCard)).label contains the displayName and roleLabel, (2) assert tester.getSemantics(...).isSelected == true when isSelected=true and == false when isSelected=false, (3) assert the semantic node hasFlag(SemanticsFlag.isButton) == true, (4) assert no semantic child node corresponds to the logo image (excludeSemantics working). Dispose the SemanticsHandle in tearDown. Manual testing checklist: test with iOS Simulator VoiceOver and Android Emulator TalkBack before marking done.
Flutter's Semantics system requires explicit configuration for custom card widgets — the default widget tree semantics may merge child nodes incorrectly, causing VoiceOver to announce partial or garbled text for each OrgCard. This is a critical failure for Blindeforbundet and violates the contractual WCAG 2.2 AA requirement.
Mitigation & Contingency
Mitigation: Wrap each OrgCard in a Semantics widget with an explicit label combining organization name and role label, set button role, and excludeSemantics: true on all child widgets to prevent double-announcement. Test on a physical iOS device with VoiceOver enabled before marking the epic complete.
Contingency: If automated semantics are insufficient, implement a dedicated AccessibilityLabel builder in the OrgCard that constructs the announcement string from OrgLabelsProvider output and passes it directly to the Semantics label field.
Organization logos must load before the screen is considered ready. If assets are fetched from a remote URL and network latency is high (a realistic scenario for rural NHF users), the screen may render with broken image placeholders, degrading the first-impression quality and potentially confusing screen reader users whose VoiceOver announcements would reference an empty image.
Mitigation & Contingency
Mitigation: Cache organization logos locally after first load using the org-branding-cache pattern. For the initial load, render a styled placeholder with the organization's initials and primary color while the logo loads asynchronously. Ensure the Semantics label is derived from the organization name, not the image asset, so screen reader announcements are never dependent on image load state.
Contingency: If caching implementation is out of scope for this epic, ship with initials-based placeholders as permanent fallbacks and defer logo loading to the org-branding-cache epic. The screen remains fully functional and accessible without logos.
The user story specifies that single-organization users bypass the selection screen and are routed directly into the app. If the bypass logic is implemented in the UI layer rather than in OrgSelectionService, it creates a tight coupling that breaks if the routing logic is ever reused or tested in isolation. It also creates a potential timing issue where the screen briefly renders before the bypass fires.
Mitigation & Contingency
Mitigation: Implement the single-org bypass entirely in OrgSelectionService.getPersistedSelection() or as a route guard — the screen should never be pushed onto the navigation stack for single-org users. The OrgSelectionScreen widget itself has no knowledge of this bypass.
Contingency: If the route guard approach is deferred to the routing epic, add a local guard in the OrgSelectionScreen's initState that calls OrgSelectionService, auto-selects if count is 1, and navigates forward before the first frame is painted, using a loading indicator to prevent the flash.