critical priority medium complexity backend pending frontend specialist Tier 1

Acceptance Criteria

When showDialog() or showModalBottomSheet() is called, focus moves to the dialog title Semantics node within one post-frame callback after the dialog is mounted
If the dialog has no title, focus moves to the first interactive element (button, text field) within the dialog
When the dialog is dismissed (any method: button, barrier tap, system back), focus returns to the GlobalKey registered as the trigger before showDialog() was called
FocusManagementService.pushDialogTrigger(GlobalKey) must be called before every dialog open; a missing push logs a warning but does not throw
Nested dialogs are supported: opening a confirmation dialog from within a main dialog stacks triggers correctly; closing the inner dialog restores focus to the inner trigger, not the outermost trigger
Bottom sheets follow the same focus lifecycle as dialogs
Focus is trapped within the open dialog — tab/swipe traversal does not escape to content behind the barrier (WCAG 2.2 SC 2.1.2 No Keyboard Trap compliant)
On dialog close with no valid trigger in the stack, focus falls back to the nearest visible focusable element on the underlying screen
Behaviour is verified on both VoiceOver (iOS) and TalkBack (Android)

Technical Requirements

frameworks
Flutter
Riverpod
apis
showDialog
showModalBottomSheet
FocusTrapArea
FocusScope
FocusScopeNode
ModalRoute.of
WidgetsBinding.instance.addPostFrameCallback
GlobalKey.currentContext
data models
DialogTriggerStack (List<GlobalKey>)
DialogFocusRecord
performance requirements
Focus placement after dialog open must occur within one frame (< 16ms post-mount)
The dialog trigger stack must be bounded — overflow beyond depth 10 emits a warning and discards oldest entries
security requirements
The FocusScope trap must prevent assistive technology from reading sensitive content on screens behind the dialog barrier
ui components
AppDialog (reusable dialog widget)
AppBottomSheet (reusable bottom sheet widget)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement the trigger stack as a List inside FocusManagementService — pushDialogTrigger() appends, popDialogTrigger() removes and returns last. To trap focus inside the dialog use Flutter's built-in FocusTrapArea widget wrapping the dialog content — do not implement a custom trap. For the 'first meaningful element' search inside the dialog, walk the dialog's BuildContext semantic tree using the same helper built in task-003 (extract it to a shared _findFirstFocusableNode(BuildContext) private method). Register a RouteAware or ModalRoute listener on the dialog route to detect dismissal and trigger focus restoration.

Be careful with AnimatedDialog transitions — the dialog widget is mounted before the animation completes; delay focus until SchedulerBinding.instance.scheduleFrameCallback to ensure the semantic node is laid out. Coordinate with task-003 to ensure the dialog trigger stack and route trigger stack share no state and do not interfere.

Testing Requirements

Unit tests: verify pushDialogTrigger() + open + close cycle restores the correct GlobalKey; verify nested push/pop stack behavior with depth 3; verify missing push produces a warning log not an exception. Widget tests: pump a screen with a button that opens a dialog, simulate dialog open, assert dialog title has semantic focus; simulate dialog dismiss, assert the button GlobalKey has semantic focus. Edge-case tests: rapid open/close cycle; dialog dismissed via barrier tap; bottom sheet dismissed via drag. Integration tests: run the full activity wizard flow, open a confirmation dialog, close it, assert focus returned to the confirm button.

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.