Enforce 44×44 dp touch targets on all interactive elements
epic-no-access-screen-ui-task-009 — Wrap every interactive element (portal link, logout button) in a minimum 44×44 dp touch target using accessible-touch-target-wrapper pattern (e.g., SizedBox + InkWell with minWidth/minHeight constraints or GestureDetector with HitTestBehavior). Verify touch target sizes with Flutter inspector.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Prefer ButtonStyle.minimumSize (Size(44, 44)) for ElevatedButton and TextButton components — this is the idiomatic Flutter approach and avoids adding extra wrapper widgets. For text-only links rendered as a TextSpan inside a Text.rich, wrap the tappable span in a GestureDetector with a SizedBox(width: double.infinity, height: 44) or use a ConstrainedBox. Do not use Padding alone to increase tap area — padding does not contribute to hit-testing on all widget types. Test on a physical device or emulator at 1× DPI after implementing to confirm the tap zone feels comfortable.
Note that the 44 dp minimum applies to both width AND height independently — a wide but short button (e.g., 200×36 dp) still fails and needs the height increased. Use the accessible-touch-target-wrapper pattern as a shared widget if multiple locations across the app need this treatment, to avoid repeating the boilerplate.
Testing Requirements
Extend the widget test suite with a group 'touch_targets'. For each interactive element: (1) pump the widget, (2) use tester.getSize(find.byKey(Key('portal-link-touch-target'))) and assert width >= 44 and height >= 44, (3) repeat for the logout button. Also call meetsGuideline(androidTapTargetGuideline) and meetsGuideline(iOSTapTargetGuideline) on the full widget. Integration: tap the portal link touch zone at its corner coordinate and assert the url_launcher mock receives the correct URL.
Tap the logout button at the edge and assert the sign-out BLoC event fires.
Flutter's live region (SemanticsProperties.liveRegion) announcement may be delayed or swallowed by the OS accessibility engine if the Semantics tree is not fully built when the screen mounts, causing screen-reader users to miss the denial announcement.
Mitigation & Contingency
Mitigation: Trigger the live region announcement from a post-frame callback (WidgetsBinding.addPostFrameCallback) to ensure the Semantics tree is committed before the announcement fires. Test on both VoiceOver (iOS) and TalkBack (Android) physical devices.
Contingency: If live region timing is unreliable, fall back to using SemanticsService.announce() directly in the initState post-frame callback, which provides more deterministic announcement timing.
The organisation logo may fail to load (network error, missing asset) leaving a broken image in an otherwise functional screen, degrading the professional appearance and potentially confusing users.
Mitigation & Contingency
Mitigation: Wrap the logo widget in an error builder that renders a styled fallback (organisation name text or a generic icon) when the logo asset or network image fails to load.
Contingency: If logo loading is persistently unreliable across organisations, remove the logo from the no-access screen entirely in favour of a text-only header using the organisation's display name from the design token system.