Restore trigger focus on AccessibleModalSheet close
epic-screen-reader-support-complex-widgets-task-010 — Extend AccessibleModalSheet to track the widget or button that triggered the sheet's opening. On close (whether via dismiss, button, or back gesture), programmatically restore focus to that trigger element. Handle cases where the trigger widget may no longer be in the tree and fall back gracefully to the nearest focusable ancestor.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Accept an optional `triggerFocusNode` parameter on AccessibleModalSheet (or infer it by capturing `FocusManager.instance.primaryFocus` at the instant the modal is pushed, before focus moves inside). Store it in a `WeakReference`-style pattern: keep the FocusNode reference but check `mounted`/`context` validity before restoring. Attach an `AnimationStatusListener` to the modal route's animation controller; when status is `AnimationStatus.dismissed`, fire `WidgetsBinding.instance.addPostFrameCallback` and call `triggerFocusNode.requestFocus()`. In the fallback path, walk `FocusManager.instance.primaryFocus?.ancestors` to find the first node that can request focus and is within the current page's FocusScopeNode — stop at the first RouteAware scope boundary.
Avoid calling `requestFocus` on a FocusNode whose context is no longer attached; guard with a try-catch or a mounted check via the BuildContext. Coordinate with task-009 so both the trap and the restore use the same modal lifecycle hooks.
Testing Requirements
Write flutter_test widget tests that: (1) mount a page with a button that opens AccessibleModalSheet, capture the button's FocusNode, open the modal, close it, and assert the button's FocusNode has focus; (2) mount the same setup but remove the triggering button from the tree before closing and assert no exception is thrown and a fallback ancestor receives focus; (3) open and close the modal three times in sequence and assert focus is correct each time. Use pumpAndSettle to allow animations to complete before asserting focus. Aim for 100% branch coverage of the restoration and fallback logic.
Flutter does not natively enforce a focus trap within a bottom sheet or modal dialog in the semantic tree — VoiceOver and TalkBack can navigate outside the sheet to background content. Implementing a reliable focus trap requires overriding the semantic tree, which may conflict with the existing modal helper infrastructure in the app and require changes to shared components beyond this feature's scope.
Mitigation & Contingency
Mitigation: Prototype the focus trap on the first modal sheet implementation before building the remaining sheets. Evaluate Flutter's ExcludeSemantics and BlockSemantics widgets as the trap mechanism, and coordinate with the team owning the shared modal helpers to agree on a non-breaking integration point before writing production code.
Contingency: If a complete semantic focus trap cannot be implemented without breaking existing modal patterns, implement a partial solution using FocusScope with autofocus on the modal's first element and a prominent 'Return to main content' semantic action, documenting the deviation from WCAG 2.4.3 with a scheduled remediation item.
The activity wizard uses BLoC state management and the UI rebuilds the entire step widget subtree on transition. If the semantic tree is traversed by VoiceOver before the build cycle settles, focus may land on a stale or partially rendered step, causing the wrong step label or progress value to be announced. This is particularly problematic for blind users who cannot visually verify the announcement against the screen.
Mitigation & Contingency
Mitigation: Coordinate ActivityWizardStepSemantics with FocusManagementService (from the core services epic) to delay focus placement until the post-build callback confirms the new step's semantic tree is complete. Write integration tests using the AccessibilityTestHarness that assert the full announcement sequence across all five wizard steps.
Contingency: If post-build focus delay is insufficient due to async BLoC emission timing, add an explicit semantic notification barrier in the wizard cubit that emits a 'step ready' event only after the new widget tree has been marked as built, decoupling the announcement trigger from the raw state transition.
Automated WCAG contrast ratio checking on widget tree snapshots may produce false positives for gradient backgrounds, dark-mode overrides, or design token overrides that are resolved at runtime but appear as unresolvable colours at static analysis time. Excessive false positives would erode team trust in the CI gate, leading to suppression rules that also mask real violations.
Mitigation & Contingency
Mitigation: Scope the WCAGComplianceChecker to check only solid-colour backgrounds in the first iteration, explicitly excluding gradients from contrast checks with documented rationale. Design the check output to distinguish 'undetermined' (gradient/unknown) from 'fail' (solid colour below threshold) so the team can take targeted action on genuine failures only.
Contingency: If false positive rates exceed 20% of reported violations during initial CI runs, switch the CI gate from a hard build failure to a warning annotation on the pull request, combined with a mandatory manual review step, until the checker's rule set has been tuned to match actual design token values.