critical priority medium complexity backend pending frontend specialist Tier 0

Acceptance Criteria

FocusManagementService is registered as a singleton (Riverpod Provider or BLoC service) and accessible app-wide without direct instantiation
On every GoRouter push transition, focus is moved to the first meaningful semantic node of the destination screen within one frame after the transition animation completes
'First meaningful semantic node' is defined as: the screen's h1-equivalent Semantics label, or the first focusable interactive element if no heading is present
On GoRouter pop, focus returns to the widget identified by the GlobalKey registered before the push — verified by checking SemanticsNode.hasFocus on that widget
If the trigger element no longer exists in the tree on pop (e.g., list item was removed), focus falls back to the nearest ancestor that is still mounted
FocusManagementService exposes a registerNavigationTrigger(GlobalKey) method that must be called before triggering any navigation
The service correctly handles GoRouter's StatefulShellRoute tab switches — each tab remembers its own last focus position
No focus flicker: focus must not briefly land on an intermediate node during the transition animation
The service works correctly on both iOS (VoiceOver) and Android (TalkBack) — verified via integration tests on each platform

Technical Requirements

frameworks
Flutter
GoRouter
Riverpod
flutter/semantics
apis
GoRouter.routerDelegate
RouterDelegate.addListener
FocusNode
FocusScopeNode
SemanticsNode
WidgetsBinding.instance.addPostFrameCallback
GlobalKey.currentContext
data models
FocusTriggerEntry (GlobalKey + route path)
RouteTransitionRecord
performance requirements
Focus placement must complete within a single post-frame callback (< 16ms) after transition animation end
The trigger registry must not grow unbounded — entries older than 10 navigation hops are evicted
security requirements
Do not store any screen content or user data in the trigger registry — only GlobalKey references and route paths

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Listen to GoRouter state changes via GoRouter.routerDelegate.addListener or a custom NavigatorObserver. On push, schedule focus placement with WidgetsBinding.instance.addPostFrameCallback to ensure the incoming screen is fully mounted. To find the 'first meaningful node', traverse the new route's BuildContext semantic tree looking for a node with SemanticsFlag.isHeader first, then fall back to the first node with SemanticsAction.tap or SemanticsFlag.isTextField. Implement the trigger registry as a simple stack (List) — push appends, pop retrieves and removes the last entry.

For StatefulShellRoute, maintain a per-branch Map of last focused nodes. Avoid using FocusScope.of(context).requestFocus() directly on the node — instead call node.requestFocus() after verifying node.canRequestFocus to prevent exceptions on unmounted nodes. This service has no dependencies on other tasks in this epic and can be developed in parallel.

Testing Requirements

Unit tests: mock GoRouter delegate and verify registerNavigationTrigger() + push produces a focus placement call on the correct GlobalKey; verify pop restores focus to the registered trigger key; verify fallback to ancestor when trigger key is unmounted. Widget tests with flutter_test: pump a two-screen Navigator, call push, assert SemanticsNode.hasFocus on the destination heading. Integration tests: full activity registration flow — navigate from home to wizard, assert each screen's heading receives focus on arrival; pop from wizard, assert the originating button regains focus. Platform integration tests on iOS and Android via TestFlight builds.

Component
Focus Management Service
service medium
Epic Risks (3)
high impact high prob technical

Flutter's build pipeline and SemanticsService.announce() operate asynchronously. Announcements triggered too early (before the semantic tree settles) may be swallowed silently on both platforms, causing acceptance criteria around the 500ms window to fail intermittently in CI and on device, which would block pilot launch.

Mitigation & Contingency

Mitigation: Implement the LiveRegionAnnouncer with a post-frame callback delay and an internal timing guard. Write integration tests using WidgetTester.pump() sequences that verify announcement delivery across multiple frame boundaries. Validate on physical devices at each sprint boundary, not only in CI.

Contingency: If consistent announcement timing cannot be achieved within Flutter's semantic pipeline, switch to a platform channel approach that calls native UIAccessibility.post (iOS) and AccessibilityManager.announce (Android) directly, bypassing Flutter's intermediary.

high impact medium prob technical

Flutter does not natively emit a focus-gain event to Dart code when VoiceOver or TalkBack moves focus to a specific widget. If the intercept mechanism for the SensitiveFieldWarningDialog relies on an unsupported or undocumented hook in the semantics tree, it may miss focus events for some field types or in some navigation contexts, leaving sensitive data unprotected.

Mitigation & Contingency

Mitigation: Prototype the focus-intercept mechanism during the first sprint of this epic, before building the dialog UI. Evaluate Flutter's SemanticsBinding.instance callbacks and custom SemanticsActions as intercept points. Document the chosen mechanism with platform compatibility notes.

Contingency: If no reliable focus-intercept is available, implement an alternative where sensitive fields show a static 'Screen reader active — tap to reveal' overlay instead of an OS dialog, which is less seamless but achieves equivalent privacy protection without relying on an unreliable event hook.

medium impact low prob dependency

The AccessibilityTestHarness depends on internal flutter_test semantic tree APIs that can change between Flutter minor versions. If the project upgrades Flutter during this epic, the harness may break silently, causing CI accessibility tests to pass while actually skipping assertions.

Mitigation & Contingency

Mitigation: Pin the Flutter SDK version in pubspec.yaml for the duration of this epic. Document which flutter_test APIs are used and their stability tier. Add a canary test that explicitly fails if the semantic tree API surface changes.

Contingency: If a forced Flutter upgrade breaks the harness, prioritise patching the harness as a blocking task before any other epic work continues, using the canary test failure as the trigger.