critical priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

When the wizard advances to a new step, keyboard/screen reader focus is programmatically moved to the primary input widget of the new step within one frame after the transition animation completes
On back navigation, focus is restored to the last focused field on the previous step (not necessarily the first field)
Each wizard step's FocusNodes are disposed when the step is no longer in the widget tree — no orphaned FocusNodes remain after stepping forward through all 5 steps
FocusManagementService correctly tracks focus history per step index
On the Contact step, focus lands on the contact search field
On the Date step, focus lands on the date picker's primary trigger button
On the Time step, focus lands on the hour input
On the Duration step, focus lands on the duration selector
On the Summary step, focus lands on the summary heading (H1-equivalent semantic label)
Focus management does not interfere with the step-transition announcer from task-003 — announcement fires before focus moves
FocusScope.of(context).unfocus() is called before disposing a step's FocusNodes to prevent Flutter FocusManager assertion errors

Technical Requirements

frameworks
Flutter FocusNode and FocusScope
Flutter FocusManager
BLoC for wizard state
Riverpod for FocusManagementService injection
apis
FocusNode.requestFocus()
FocusScope.of(context).requestFocus()
FocusNode.dispose()
WidgetsBinding.instance.addPostFrameCallback
data models
activity
activity_type
performance requirements
Focus transfer must occur in the first frame after animation completion — use addPostFrameCallback, not Future.delayed
FocusNode creation must not occur in build() — create in initState or a dedicated service to avoid re-creation on rebuild
security requirements
Focus history must not persist sensitive field values — store only the FocusNode reference, never the field content
FocusNodes must be disposed deterministically to prevent memory leaks in long-running app sessions
ui components
FocusManagementService (per-wizard-session scoped)
Per-step FocusNode registry
Focus history stack (List<int> stepFocusHistory)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Create FocusNodes in FocusManagementService.registerStep(stepIndex, primaryNodeKey) during the wizard's initState, not in build(). Use a Map for O(1) lookup. Store focus history as a Stack — push on forward navigation, pop on back. In the BLocListener, after the step transition event, call WidgetsBinding.instance.addPostFrameCallback((_) => focusService.focusPrimaryNodeForStep(newStep)) so the focus transfer happens after Flutter has laid out the new step's widget tree.

Before disposing, call FocusScope.of(context).unfocus() to cleanly release focus from the tree. For the back-navigation restoration: the popped step index from the history stack gives you the FocusNode to request. Important: on the Summary step, the 'primary focus target' is a Semantics widget with a heading role, not a TextField — use a FocusNode attached via Focus(focusNode: ..., child: Semantics(header: true, ...)).

Testing Requirements

Unit tests: verify FocusManagementService.registerStep() creates and returns a FocusNode; verify .disposeStep() calls FocusNode.dispose() and removes the node from the registry; verify .focusPrimaryNodeForStep(stepIndex) calls requestFocus on the correct node. Widget tests: mount the full 5-step wizard; simulate tapping 'Next' and assert that the primary input FocusNode of each step has hasFocus==true after transition. Simulate 'Back' and assert focus restoration. Memory test: step through all 5 steps and back, then check FocusManager.instance.debugGetActiveScopeChain() contains no wizard-owned nodes after the wizard is popped.

Run on flutter_test with semanticsEnabled: true. Coverage target: 85%.

Component
Activity Wizard Step Semantics
ui medium
Epic Risks (3)
high impact high prob integration

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.

high impact medium prob technical

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.

medium impact medium prob scope

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.