high priority medium complexity backend pending frontend specialist Tier 2

Acceptance Criteria

moveFocusTo(GlobalKey) moves accessibility focus to the target widget within one frame after the widget is confirmed mounted
If the target GlobalKey is not yet mounted when moveFocusTo() is called, the service polls for up to 2 seconds (configurable) in post-frame callbacks, then gives up silently with a debug-level log
Inline validation errors: when a form field's error text widget appears (e.g., after failed submission), moveFocusTo() on the error widget causes VoiceOver/TalkBack to announce the error label
Expanded accordion sections: after an accordion expands, focus moves to the first content node inside the newly visible region
Newly loaded list items: after a paginated list appends items, focus optionally moves to the first new item when the caller passes moveFocusTo(firstNewItemKey)
Async content (Supabase response): moveFocusTo() called before a Supabase fetch completes still successfully focuses the content when it arrives, within the 2-second polling window
moveFocusTo() is safe to call from any isolate-safe context (no BuildContext required at call site)
Calling moveFocusTo() when the app is in the background does not throw; focus is deferred until app returns to foreground

Technical Requirements

frameworks
Flutter
Riverpod
Supabase
apis
GlobalKey.currentContext
FocusNode.requestFocus
WidgetsBinding.instance.addPostFrameCallback
SchedulerBinding.instance.scheduleFrameCallback
SemanticsService.announce
data models
PendingFocusRequest (GlobalKey + expiry timestamp + retryCount)
performance requirements
Polling must not spin more than once per frame — use addPostFrameCallback not a Timer loop
Maximum 120 polling frames (approx 2 seconds at 60fps) before abandoning a pending request
No more than 5 concurrent pending focus requests; oldest is dropped when the queue overflows
security requirements
Error messages focused via this API must not include raw server error payloads — callers are responsible for sanitizing label text before assigning to Semantics
ui components
InlineErrorText widget (should expose a GlobalKey for error label)
AccordionSection widget (exposes a GlobalKey for first content node)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

The polling mechanism must use WidgetsBinding.instance.addPostFrameCallback recursively — after each frame check GlobalKey.currentContext != null. If still null, register another addPostFrameCallback. Do NOT use Timer.periodic as it fires even when no frame is scheduled, wasting CPU. Store pending requests in a List inside FocusManagementService; each request carries a createdAt timestamp and a maxFrames counter.

For async Supabase content, the typical pattern is: BLoC emits loaded state → widget calls WidgetsBinding.instance.addPostFrameCallback → calls FocusManagementService.moveFocusTo(contentKey). Document this pattern in the service's dartdoc as the recommended async usage. For inline validation errors, the moveFocusTo() call should be placed in the form's onValidationFailed callback. Also expose a announceToScreenReader(String) convenience method using SemanticsService.announce() for cases where a GlobalKey target is not appropriate (e.g., transient snackbar messages).

Testing Requirements

Unit tests: verify moveFocusTo() with an immediately mounted key focuses within one frame; verify polling logic retries up to the configured limit then stops; verify queue overflow discards the oldest request. Widget tests: pump a form, trigger validation error, call moveFocusTo(errorKey), assert the error Semantics node has focus. Async tests: use flutter_test FakeAsync to simulate a 500ms Supabase delay, call moveFocusTo() before the delay, verify focus is placed after the widget appears. Edge-case tests: moveFocusTo() with a null GlobalKey logs a warning and returns immediately; moveFocusTo() when app lifecycle is paused queues the request.

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.