high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

NavigationAccessibilityService is registered as a singleton (Riverpod provider or service locator) and initialized once at app startup
On every GoRouter route change, SemanticsService.announce() is called with the human-readable screen title within 100 ms of the route transition completing
Screen titles are resolved from NavigationRouteConfig (not hard-coded) so adding a new route automatically gains an announcement without code changes to this service
The announcement includes the screen title and role, e.g., 'Contacts, screen' or 'Activity detail, screen'
The service correctly handles shell route tab switches (bottom nav) by announcing the tab name, not the shell route path
The service does not announce duplicate routes when the same route is pushed twice in succession (debounce by route name equality)
The service is a no-op when accessibility services (TalkBack/VoiceOver) are not active, to avoid unnecessary SemanticsService calls
The service can be disposed without memory leaks; GoRouter listener is removed on dispose
Unit tests cover: route change → title resolution → announcement, duplicate suppression, and tab switch announcement

Technical Requirements

frameworks
Flutter
GoRouter
Riverpod
apis
GoRouter.routeInformationProvider
RouteMatchList
SemanticsService.announce()
WidgetsBinding.instance.accessibilityFeatures
Riverpod (for singleton provider)
performance requirements
Announcement must fire within 100 ms of route transition animation completing (use WidgetsBinding.addPostFrameCallback to wait for settle)
Listener registration must not prevent GoRouter garbage collection
security requirements
Screen title strings must not include any user-specific PII (names, IDs) that could be read aloud unexpectedly; use generic titles only

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement as a plain Dart class with a Riverpod Provider. In the constructor, accept a GoRouter instance and a NavigationRouteConfig map (route name → display title). Call goRouter.routeInformationProvider.addListener(_onRouteChange) and remove it in dispose(). In _onRouteChange, read goRouter.routerDelegate.currentConfiguration, extract the last matched route name, resolve the title from NavigationRouteConfig, then call SemanticsService.announce(title, TextDirection.ltr).

Gate the call with ServicesBinding.instance.accessibilityFeatures.accessibleNavigation to skip when no screen reader is running. For StatefulShellRoute tab switches, the route name will be the shell branch's child route — ensure NavigationRouteConfig includes entries for all five tab roots. Initialize the service in main.dart after GoRouter is constructed, or in the root widget's initState.

Testing Requirements

Write unit tests using flutter_test with a mock GoRouter and mock SemanticsService: (1) simulate route push → verify announce() called with correct title, (2) simulate same route pushed twice → verify announce() called only once, (3) simulate tab switch → verify tab name announced, (4) simulate accessibility off → verify announce() not called. Write an integration test that navigates through three screens and captures announcements via a mock SemanticsService. Verify no memory leaks with FlutterMemoryAllocations in test tearDown.

Component
Navigation Accessibility Service
service medium
Epic Risks (3)
high impact high prob technical

Flutter's ModalBottomSheet and showDialog do not automatically confine VoiceOver or TalkBack focus to the modal's subtree on all platform versions. Background content may remain reachable by screen readers, confusing users and violating WCAG 2.2 criterion 1.3.1.

Mitigation & Contingency

Mitigation: Wrap modal content in an ExcludeSemantics or BlockSemantics widget for background content. Use a Semantics node with liveRegion on the modal container and manually request focus via FocusScope after the modal animation completes. Test on both iOS (VoiceOver) and Android (TalkBack) during widget development.

Contingency: If platform-level focus trapping is unreliable, implement a custom modal wrapper widget that uses a FocusTrap widget (available in Flutter's internal tooling) and an Overlay entry with semantics blocking on the dimmed background layer.

medium impact medium prob technical

On iOS, the system-level swipe-back gesture (UINavigationController) can bypass PopScope and GoRouter's gesture suppression, meaning users can still accidentally dismiss screens via swipe even after the component is implemented. This breaks the gesture-free contract for motor-impaired users.

Mitigation & Contingency

Mitigation: Set popGestureEnabled: false in GoRouter route configurations where swipe-back is suppressed. Test specifically against Flutter's CupertinoPageRoute, which respects this flag, and verify that GoRouter generates Cupertino routes on iOS rather than Material routes with gesture enabled.

Contingency: If go_router's popGestureEnabled flag does not propagate correctly, wrap affected routes in a WillPopScope replacement (PopScope with canPop: false) and file a bug with the go_router maintainers. Document the workaround in the navigation-route-config component for future maintainers.

medium impact medium prob scope

The feature description implies migrating all existing ModalBottomSheet and dialog call sites across the app to use the new accessible helpers, which is a cross-cutting change. Scope underestimation could mean the epic finishes the new components but leaves many call sites un-migrated, leaving the accessibility promise partially broken.

Mitigation & Contingency

Mitigation: Audit all existing modal call sites at the start of the epic (grep for showModalBottomSheet, showDialog, showCupertinoDialog) and add the count to the task list. Treat migration as explicit tasks, not an implied post-step.

Contingency: If migration scope grows beyond the epic's estimate, create a follow-up tech-debt epic scoped only to call-site migration, and gate the release on at minimum all flows used by the accessibility user-story acceptance criteria being migrated.