critical priority low complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

OrgCard accepts a required `onTap: void Function(Organization)` callback parameter
The entire card surface area is tappable — no dead zones at padding edges or icon regions
InkWell splash is clipped to card bounds (use Material + InkWell or Ink widget) so ripple does not bleed outside rounded corners
Tapping the card invokes onTap with exactly the Organization object bound to that card instance
onTap is never called with a null or incorrect Organization — verified by unit test asserting identity equality
When onTap is null/not provided, the card renders without InkWell splash and ignores taps (graceful no-op, not a crash)
No internal Navigator.push or GoRouter navigation occurs inside OrgCard — all routing is delegated to parent
Tap interaction does not cause any visible delay before splash appears (immediate visual feedback)

Technical Requirements

frameworks
Flutter
Riverpod
performance requirements
Tap response must be perceptible within one frame (~16ms) — no async operations block the InkWell splash
OrgCard widget must be const-constructible when possible to reduce rebuild cost in ListView
security requirements
No Organization PII logged or printed during tap event handling
ui components
InkWell (or Ink + InkWell combo for proper splash clipping)
Material widget wrapping card content to host InkWell splash layer
OrgCard widget (existing, being wired)

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use `Material(type: MaterialType.transparency, child: InkWell(...))` pattern inside the card's existing Container/Card to ensure the splash layer clips correctly to the card's border radius. Pass `borderRadius` matching the card's radius to InkWell's `borderRadius` parameter. The callback signature must be `void Function(Organization org)` — avoid dynamic types. Mark the callback field as `final` in the widget.

Since this is purely parent-driven, OrgCard should have zero awareness of app routing or state management — it is a dumb presentational widget. Avoid wrapping the entire card in GestureDetector as it does not provide Material splash feedback; InkWell is required per the design system.

Testing Requirements

Write widget tests using flutter_test. Test 1: render OrgCard with a mock onTap callback, simulate tap via WidgetTester.tap(), assert callback was called once with correct Organization instance. Test 2: render OrgCard with onTap=null, simulate tap, assert no exception thrown. Test 3: verify splash is clipped — confirm Material ancestor exists in widget tree above InkWell.

Test 4: assert no Navigator/GoRouter calls are made inside OrgCard on tap. Minimum coverage: 100% of tap-handling code paths.

Component
Organization Card Widget
ui low
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.