critical priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

TalkBack (Android) announces scroll position as 'X of Y' when user swipes within the scroll container
VoiceOver (iOS) announces scroll position and scrollable region label when focus enters the container
Semantics(container: true) groups the scroll region as a single semantics node so assistive technology treats it as one scrollable unit
scrollSemanticDescription is exposed and can be customized via a widget parameter (e.g., 'Contact list' or 'Activity feed')
Switch Access users can trigger scrollUp and scrollDown via SemanticsAction, causing the container to scroll by one viewport step
SemanticsAction.scrollUp and SemanticsAction.scrollDown are registered and functional, scrolling the container by a configurable step (default: 0.5× viewport height)
The semantics tree does not contain redundant or conflicting scroll nodes when VerticalScrollContainer is nested inside a ListView
Screen reader focus is not trapped inside the scroll container; users can navigate out using standard accessibility gestures
Accessibility audit via flutter_test semantics tester finds no WCAG 2.2 violations related to this widget

Technical Requirements

frameworks
Flutter
apis
Semantics widget
SemanticsAction.scrollUp
SemanticsAction.scrollDown
ScrollController
SemanticsService.announce()
RenderObject.sendSemanticsEvent()
performance requirements
Semantics tree updates must not cause full-widget rebuilds on every scroll frame; use addPostFrameCallback for position announcement
ui components
VerticalScrollContainer (enhanced)
Semantics wrapper
MergeSemantics (where applicable)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

In VerticalScrollContainer, wrap the scrollable content in a Semantics widget with: container: true, label equal to the customizable scrollSemanticDescription, and onScrollDown/onScrollUp callbacks that call _controller.animateTo() with a step of 0.5 * _controller.position.viewportDimension. Expose a scrollSemanticDescription parameter defaulting to 'Scrollable list'. For position announcement ('3 of 10'), attach a scroll listener to the ScrollController and use SemanticsService.announce() only when the user pauses scrolling (debounce by 300 ms) to avoid flooding the screen reader. Be careful not to double-announce when Flutter's own Scrollable widget already provides scroll semantics; test with SemanticsDebugger overlay to check for duplication.

This widget is especially critical for Blindeforbundet users who rely on screen readers.

Testing Requirements

Use flutter_test SemanticsController to verify: (1) scroll container node has scrollUp and scrollDown actions registered, (2) invoking SemanticsAction.scrollDown moves the scroll offset by the expected amount, (3) Semantics label and hint are correctly set. Use tester.pumpWidget with AccessibilityGuideline checks (e.g., meetsGuideline(androidTapTargetGuideline)). Manually test with TalkBack on a physical or emulated Android device and VoiceOver on iOS simulator; document pass/fail in PR description. Test Switch Access navigation through a 10-item list.

Component
Vertical Scroll Container
ui low
Epic Risks (3)
high impact high prob technical

Flutter's ModalBottomSheet and showDialog do not automatically confine VoiceOver or TalkBack focus to the modal's subtree on all platform versions. Background content may remain reachable by screen readers, confusing users and violating WCAG 2.2 criterion 1.3.1.

Mitigation & Contingency

Mitigation: Wrap modal content in an ExcludeSemantics or BlockSemantics widget for background content. Use a Semantics node with liveRegion on the modal container and manually request focus via FocusScope after the modal animation completes. Test on both iOS (VoiceOver) and Android (TalkBack) during widget development.

Contingency: If platform-level focus trapping is unreliable, implement a custom modal wrapper widget that uses a FocusTrap widget (available in Flutter's internal tooling) and an Overlay entry with semantics blocking on the dimmed background layer.

medium impact medium prob technical

On iOS, the system-level swipe-back gesture (UINavigationController) can bypass PopScope and GoRouter's gesture suppression, meaning users can still accidentally dismiss screens via swipe even after the component is implemented. This breaks the gesture-free contract for motor-impaired users.

Mitigation & Contingency

Mitigation: Set popGestureEnabled: false in GoRouter route configurations where swipe-back is suppressed. Test specifically against Flutter's CupertinoPageRoute, which respects this flag, and verify that GoRouter generates Cupertino routes on iOS rather than Material routes with gesture enabled.

Contingency: If go_router's popGestureEnabled flag does not propagate correctly, wrap affected routes in a WillPopScope replacement (PopScope with canPop: false) and file a bug with the go_router maintainers. Document the workaround in the navigation-route-config component for future maintainers.

medium impact medium prob scope

The feature description implies migrating all existing ModalBottomSheet and dialog call sites across the app to use the new accessible helpers, which is a cross-cutting change. Scope underestimation could mean the epic finishes the new components but leaves many call sites un-migrated, leaving the accessibility promise partially broken.

Mitigation & Contingency

Mitigation: Audit all existing modal call sites at the start of the epic (grep for showModalBottomSheet, showDialog, showCupertinoDialog) and add the count to the task list. Treat migration as explicit tasks, not an implied post-step.

Contingency: If migration scope grows beyond the epic's estimate, create a follow-up tech-debt epic scoped only to call-site migration, and gate the release on at minimum all flows used by the accessibility user-story acceptance criteria being migrated.