Add VoiceOver/JAWS Semantics to search input
epic-contact-search-accessible-ui-task-002 — Extend the AccessibleSearchInputField with full screen-reader support: announce input field purpose via Semantics(label:), mark as text field, and set textField: true. Validate that VoiceOver on iOS and TalkBack on Android read the label and hint correctly. Meets Blindeforbundet mandatory accessibility requirement.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Flutter's TextField widget already sets isTextField: true in its internal SemanticsNode. To avoid double-announcing, do NOT wrap the inner AppTextField with another Semantics(textField: true) node — this creates a conflict. Instead, use Semantics on the outer container with only label and hint; the inner TextField handles the textField role. To provide the label to the inner TextField's semantics, pass it via TextField(decoration: InputDecoration(semanticCounterText: ..., labelText: ...)) or use the TextField's own semanticsLabel if supported by the version in use.
Test for duplicate nodes using the SemanticsHandle debugDumpSemanticsTree output. For the clear button exclusion: use ExcludeSemantics(excluding: controller.text.isEmpty, child: clearButton) to toggle semantic visibility. This is preferable to Visibility(maintainState: ...) for semantic-only exclusion.
Testing Requirements
Automated: use flutter_test SemanticsHandle to assert the semantics tree contains a node with isTextField: true, hasEnabledState: true, label matching the expected string, and hint matching the expected string. Assert that when field is empty, no node with label 'Clear search' is present in the semantics tree. Assert that when field contains text, a node with label 'Clear search' IS present. Manual: test on a physical iOS device with VoiceOver enabled — swipe to each element and verify announcements match expected strings.
Test on Android emulator with TalkBack enabled. Document manual test results in the PR description.
Flutter's Semantics live region support for announcing dynamic result count changes may behave inconsistently between VoiceOver (iOS) and TalkBack (Android), particularly regarding announcement throttling and focus management, causing the feature to pass testing on one platform and fail on the other.
Mitigation & Contingency
Mitigation: Test live region announcements on both iOS (VoiceOver) and Android (TalkBack) early in development using the existing accessibility test harness. Reference the existing LiveRegionAnnouncer component (608-live-region-announcer) patterns used elsewhere in the app.
Contingency: If cross-platform consistency cannot be achieved, implement a platform-specific announcement strategy using the SemanticsService.announce API with platform-conditional announcement timing to work around OS-specific throttling behaviour.
Voice-to-text progressive enhancement for Blindeforbundet may not be available or may behave unpredictably on all device/OS combinations, particularly older Android devices, potentially causing crashes or silent failures that degrade the search experience.
Mitigation & Contingency
Mitigation: Implement voice-to-text as a strictly optional enhancement: detect availability at runtime, show the microphone button only when the platform speech API reports availability, and wrap all voice invocations in try/catch with graceful degradation to standard text input.
Contingency: If voice-to-text causes instability on a subset of devices discovered during TestFlight/beta, disable the feature flag for that platform version while a fix is investigated, without impacting the core text-based search functionality.