critical priority medium complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

LiveRegionAnnouncer is a Flutter widget backed by a Riverpod StateNotifierProvider that exposes the announcement queue state
announce(message, priority) API accepts AnnouncementPriority.polite and AnnouncementPriority.assertive enum values
announcePolite(message) is a convenience wrapper that calls announce with polite priority
Assertive announcements preempt the current queue and are dispatched immediately via SemanticsService.announce()
Polite announcements are appended to a FIFO queue and dispatched only after the current utterance completes
When 3+ polite announcements arrive within 200ms, older polite messages are coalesced or dropped to prevent queue flooding
Queue drains to empty without hanging when rapid successive announcements arrive within the same frame
SemanticsService.announce() is called with the correct TextDirection (ltr by default, rtl when locale demands)
The provider is globally accessible via a top-level ProviderScope and does not require widget tree access to enqueue messages
LiveRegionAnnouncer widget renders no visible UI (zero size, transparent) and does not affect layout
Queue state is reset on full app restart; no persistence between sessions
All public API methods are null-safe and handle empty/whitespace messages gracefully (no-op)

Technical Requirements

frameworks
Flutter
Riverpod
apis
SemanticsService.announce()
flutter/semantics.dart
data models
AnnouncementPriority (enum: polite, assertive)
AnnouncementEntry (message, priority, timestamp)
performance requirements
Queue dispatch latency must not exceed 50ms after the current utterance ends
No more than one SemanticsService.announce() call per animation frame to avoid speech stuttering
Queue processing must run on the main isolate without blocking UI rendering
security requirements
Announcement messages must be sanitised — strip any HTML/markup before passing to SemanticsService
Sensitive field content must never be passed directly to LiveRegionAnnouncer without explicit consent from SensitiveFieldWarningDialog

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use a StateNotifier> backed by a final provider. Implement a drain loop using Future.microtask() or a debounce timer (50ms) to batch-check whether the TTS engine is idle before dispatching the next polite item — Flutter does not expose a 'speech ended' callback from SemanticsService, so use a fixed delay heuristic (estimated utterance duration based on word count, ~130ms per word). For assertive priority, call SemanticsService.announce() synchronously and flush the polite queue. Expose the provider ref via a global accessor pattern (e.g., a static NavigatorKey-style holder) so BLoCs can enqueue without BuildContext.

Keep the widget itself a const StatelessWidget with a Semantics(explicitChildNodes: false) wrapper to stay invisible to the accessibility tree.

Testing Requirements

Write flutter_test unit tests using a mock SemanticsService binding. Test: (1) polite messages are queued in FIFO order, (2) assertive messages bypass the queue, (3) rapid-fire polite messages coalesce correctly, (4) empty/whitespace messages are no-ops, (5) queue is empty after draining. Use fake_async to control time-based dispatch. Aim for ≥90% branch coverage on the StateNotifier logic.

Verify provider is accessible from a ProviderScope in widget tests without throwing.

Component
Live Region Announcer
ui 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.