critical priority low complexity frontend pending frontend specialist Tier 9

Acceptance Criteria

The screen title/logo region is wrapped in `Semantics(header: true, child: ...)` so VoiceOver/TalkBack announces it as a page heading
The ListView of OrgCards is wrapped in `Semantics(label: 'Velg din organisasjon', explicitChildNodes: true, child: ...)` providing a semantic list label
While loading, a `Semantics(liveRegion: true, child: Text('Laster organisasjoner...'))` widget is present so screen readers announce the loading state without user focus action
The error state widget is also wrapped in `Semantics(liveRegion: true, ...)` to announce errors immediately upon appearance
Each OrgCard exposes its organization name and selected state via Semantics: `Semantics(label: orgName, selected: isSelected, button: true, child: ...)`
Focus traversal order on VoiceOver (iOS) is: heading → first OrgCard → subsequent OrgCards → CTA button — verified manually
Focus traversal order on TalkBack (Android) matches the same logical top-to-bottom order
No focus trap exists: after the last OrgCard, focus naturally moves to the CTA button, then exits the screen
CTA button disabled state is communicated to screen readers (`Semantics(enabled: false)` or Flutter's built-in disabled semantics)
Minimum touch target size for all interactive elements is 48×48 dp per WCAG 2.5.5 (applies to OrgCards and CTA button)
Color contrast ratio for all text elements meets WCAG 2.2 AA minimum of 4.5:1 (normal text) and 3:1 (large text/UI components)

Technical Requirements

frameworks
Flutter
Riverpod
data models
accessibility_preferences
performance requirements
Semantics tree overhead must not measurably affect frame rate — avoid deeply nested Semantics wrappers
security requirements
Screen reader announcements must not expose internal IDs or technical error strings — only user-friendly Norwegian text
ui components
Semantics widget (Flutter core) — header, label, liveRegion, selected, button, enabled
MergeSemantics where appropriate to group related content
ExcludeSemantics for purely decorative elements (logo icon if announced separately as text)

Execution Context

Execution Tier
Tier 9

Tier 9 - 22 tasks

Can start after Tier 8 completes

Implementation Notes

Flutter's built-in widgets handle many semantics automatically (e.g., ElevatedButton's disabled state, InkWell's button role). The primary additions needed are: (1) `Semantics(header: true)` around the logo/title — Flutter does not infer this automatically; (2) `Semantics(liveRegion: true)` around the loading and error state widgets — these are the most impactful for screen reader UX; (3) explicit `label` and `selected` on OrgCard since it is a custom widget without default semantics inference. Use `MergeSemantics` inside OrgCard to combine the org name, logo, and selection indicator into a single announced node (e.g. 'Norges Handikapforbund, valgt, knapp').

Test with `debugDumpSemanticsTree()` during development to inspect the live semantics tree. This work is critical for Blindeforbundet users (VoiceOver primary users) and NHF users with cognitive challenges — both groups were identified in workshops as requiring the highest accessibility standards.

Testing Requirements

Automated widget tests using flutter_test semantics API: Test 1: call `tester.getSemantics(find.byType(OrgSelectionScreen))` and assert a node with `SemanticsFlag.isHeader` is present. Test 2: in loading state, assert a node with `SemanticsFlag.isLiveRegion` is present. Test 3: in error state, assert live region node is present with error text. Test 4: for a selected OrgCard, assert semantics node has `SemanticsFlag.isSelected`.

Test 5: assert CTA button semantics has `SemanticsFlag.isEnabled = false` when no org selected. Manual testing required: use VoiceOver on iOS (real device or simulator) to verify heading announcement and card list navigation. Use TalkBack on Android to verify focus order. Use accessibility inspector / Contrast Checker to validate color contrast ratios.

TestFlight distribution to a screen reader user from Blindeforbundet test group for real-world validation.

Epic Risks (3)
high impact medium prob technical

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.

medium impact medium prob technical

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.

medium impact low prob scope

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.