critical priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

ContactSearchBar renders a styled TextField with a search icon prefix and a clear (X) icon suffix
The clear button only appears when the input has non-empty text β€” it is hidden when the field is empty
Tapping the clear button resets the TextField value to empty and returns focus to the input field
onChanged is invoked with the debounced value exactly 300ms after the user stops typing β€” rapid keystroke bursts produce exactly one callback per pause interval, not one per keystroke
Debounce timer is cancelled and reset on each new keystroke β€” no stale callbacks fire after the widget is disposed
The widget exposes a TextEditingController via a controller parameter so the parent can programmatically clear or set the search value
The TextField has a semanticsLabel parameter applied so screen readers announce the field purpose
The decoration hintText is set to an appropriate placeholder (e.g., 'Search contacts…')
Pressing the keyboard's clear/return action does not submit a form β€” the field is search-mode only
Widget is properly disposed (timer cancelled, controller not disposed if externally provided)

Technical Requirements

frameworks
Flutter
flutter_test
Riverpod
data models
ContactSearchQuery
performance requirements
Debounce implementation uses dart:async Timer β€” no third-party debounce packages
onChanged callback must never fire more than once per 300ms burst regardless of typing speed
security requirements
Search input must not be logged or persisted to any storage β€” it is ephemeral session state only
Input must be sanitized before passing to any provider or Supabase query to prevent injection patterns
ui components
TextField with InputDecoration (prefixIcon: search, suffixIcon: clear)
IconButton for clear action
Semantics label on TextField
Timer-based debounce in StatefulWidget or custom hook

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Implement debounce using a nullable Timer _debounce field in a StatefulWidget. In the TextField's onChanged handler: cancel any existing timer, then start a new Timer(Duration(milliseconds: 300), () => widget.onChanged(value)). In dispose(), call _debounce?.cancel() to prevent timer leaks. For the clear button: use a StatefulWidget local bool _hasText and update it via setState in the same onChanged handler β€” conditionally render the suffix icon based on this flag.

For the controller: if widget.controller is provided, use it directly; otherwise create an internal _controller and dispose it in dispose(). This dual-controller pattern avoids memory leaks when the parent manages state. The parent (ContactListScreen or Riverpod provider) should use the debounced value to filter the contact list β€” the ContactSearchBar itself is purely presentational and has no knowledge of providers.

Testing Requirements

Write flutter_test widget tests: (1) assert clear button is absent when field is empty and present after typing; (2) tap clear button and assert field value is empty and focus is returned; (3) simulate rapid keypresses using tester.enterText() in quick succession, then advance fake timer by 300ms and assert onChanged fires exactly once with the final value; (4) advance timer by 299ms and assert onChanged has NOT fired; (5) dispose widget mid-debounce and assert no callbacks fire after disposal (no late timer exceptions); (6) provide an external TextEditingController, programmatically set text, and assert onChanged fires after debounce. Use flutter_test's FakeAsync or pump(Duration) to control timer advancement without real-time waits.

Component
Contact Search Bar
ui low
Epic Risks (2)
medium impact medium prob technical

Design token color values used in role badges, certification status indicators, and availability chips may not meet the WCAG 2.2 AA contrast ratio of 4.5:1 when rendered against card backgrounds, requiring rework after accessibility review and potentially blocking acceptance sign-off.

Mitigation & Contingency

Mitigation: Run the contrast-ratio-validator on every new token combination during widget development. Enforce the CI accessibility lint runner on all PRs touching visualization components, and validate against the contrast-safe-color-palette before finalizing card designs.

Contingency: If contrast failures are found late, adjust token values in the design token theme centrally β€” since all widgets consume design tokens rather than hardcoded colors, all affected widgets will be corrected by a single token update without per-widget changes.

low impact medium prob dependency

The ContactViewSwitcher is required for Barnekreftforeningen but must not appear for other organizations. If the organization labels provider does not yet expose a reliable feature flag for this widget, it may render universally or be conditionally hidden in an inconsistent way, breaking the role-specific layout contract.

Mitigation & Contingency

Mitigation: Implement view switcher visibility as a constructor parameter on ContactListScreen injected from a provider, defaulting to hidden. Document the integration point for the org labels provider so the flag can be wired without changing the widget's API.

Contingency: If org labels integration is delayed beyond this epic, use a feature flag constant keyed to the Barnekreftforeningen organization ID as a temporary gate, with a tracked issue to replace it with the runtime labels provider before general release.