high priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

When the user switches to any of the 5 tabs, a live region announcement fires with the format '<Tab name> tab' (e.g., 'Contacts tab')
When the Notifications tab has unread notifications, the announcement includes the count: 'Notifications tab, 3 unread'
Announcements use polite priority (not assertive) so they do not interrupt ongoing VoiceOver/TalkBack speech
If the user rapidly switches tabs (3+ tabs in under 1 second), only the final destination tab is announced — intermediate tabs are debounced and dropped
No announcement fires on app launch or initial tab render — only on user-initiated tab switches
Announcement text uses org-specific terminology when available (consistent with task-006 OrgLabelResolver)
Announcement text is localised (Norwegian: 'Kontakter-fane')
LiveRegionAnnouncer is used — direct calls to SemanticsService.announce are NOT made from this widget
The implementation does not cause double-announcements when VoiceOver already reads the tab label via the selected state change implemented in task-006
WCAG 2.2 AA criterion 4.1.3 (Status Messages) is satisfied for tab navigation context changes

Technical Requirements

frameworks
Flutter SemanticsService via LiveRegionAnnouncer abstraction
BLoC/Riverpod for navigation state
Dart async debounce utility
apis
LiveRegionAnnouncer.announcePolite(String message)
StatefulShellRoute onNavigate callback or go_router route listener
data models
device_token
accessibility_preferences
performance requirements
Debounce window: 400ms — if another tab switch occurs within 400ms, cancel the pending announcement
Announcement must fire after the new tab's page starts rendering, not before, to align with visual context
security requirements
Announcement must not include notification message content — only counts
No PII in any announcement string
ui components
LiveRegionAnnouncer (polite mode)
TabSwitchDebouncer utility
ContextualTabLabelBuilder (composes tab name + badge count)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The key design challenge here is avoiding double-announcement: task-006 sets Semantics.selected=true on the new tab (which VoiceOver may read as 'selected'), and this task posts a live region announcement. On iOS VoiceOver, the selected state change is typically read when the user next moves focus to the tab bar — it does NOT fire automatically on programmatic tab switches. Therefore, the live region announcement IS necessary for proactive notification. On Android TalkBack, the behaviour may differ — test empirically.

To implement debounce: use a CancelableOperation from the async package. On each tab switch event from the BLoC, cancel any pending CancelableOperation and schedule a new one with a 400ms delay. For the initial launch guard: store a bool _hasRendered initialized to false; set it to true in addPostFrameCallback on first build; only call announcer if _hasRendered is true. Inject LiveRegionAnnouncer via Riverpod, consistent with how task-003 and task-005 use it, so the same announcer instance queues across all components.

Testing Requirements

Unit tests: verify TabSwitchDebouncer cancels pending announcement when a second switch fires within 400ms; verify the final announcement is the last switch target. Verify ContextualTabLabelBuilder returns 'Contacts tab' when count is 0 and 'Notifications tab, 5 unread' when count is 5. Widget tests: mount AccessibleBottomNavigation; simulate tab switch and assert LiveRegionAnnouncer.announcePolite was called exactly once with correct string. Simulate 3 rapid tab switches and assert announcer called exactly once (debounce).

Assert no announcement fires during initial widget mount. Localisation test: switch locale to Norwegian and verify announcement format. Integration test: run full tab-switching flow with VoiceOver enabled on a physical iOS device and confirm no double-announcements. Coverage target: 85%.

Component
Accessible Bottom Navigation
ui high
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.