critical priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

VerticalScrollContainer accepts a required child widget and an optional controller parameter
Scroll physics is ClampingScrollPhysics with no horizontal component; bouncing overscroll does not occur on either axis
scrollDirection is always Axis.vertical and cannot be overridden by a parent ScrollConfiguration
Horizontal swipe gestures within the widget do not trigger the iOS edge-swipe navigation or Android back gesture
The widget works correctly when nested inside a PageView, a GoRouter ShellRoute, and a standard Scaffold body
ScrollConfiguration.of(context).buildScrollbar() is overridden so the default platform glow/overscroll indicator does not appear horizontally
The widget renders with zero horizontal scroll extent; LayoutBuilder confirms no horizontal overflow
Visual regression: existing screens using SingleChildScrollView are unaffected until explicitly migrated to VerticalScrollContainer
Widget can be exported from the shared widget library and imported with a single import line

Technical Requirements

frameworks
Flutter
apis
ScrollConfiguration
ScrollBehavior
SingleChildScrollView
ClampingScrollPhysics
GestureDetector (for gesture disambiguation)
performance requirements
Widget must not introduce additional layers that trigger unnecessary paint/composite passes
ScrollController attachment and detachment must be handled correctly to avoid memory leaks when controller is externally provided
ui components
VerticalScrollContainer (new reusable widget)
ScrollConfiguration
SingleChildScrollView or CustomScrollView

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Create a custom ScrollBehavior subclass (e.g., VerticalOnlyScrollBehavior) that overrides getScrollPhysics() to always return ClampingScrollPhysics() and overrides buildOverscrollIndicator() to return child directly (suppress glow). Wrap the child in a ScrollConfiguration with this behavior. Use SingleChildScrollView with scrollDirection: Axis.vertical and physics: const NeverScrollableScrollPhysics() is NOT the right approach — instead use the custom ScrollBehavior so the user can still scroll vertically. For gesture isolation, if nested inside a horizontal PageView, the scroll view must win the gesture arena for vertical gestures: this is handled automatically by Flutter's gesture arena when scrollDirection is vertical.

Test this specifically in the context of the 5-tab StatefulShellRoute bottom nav, where a horizontal swipe on the shell should NOT be absorbed by the inner scroll view.

Testing Requirements

Write widget tests that: (1) confirm no horizontal scroll extent using tester.binding.window and LayoutBuilder, (2) simulate horizontal drag gestures and assert no page-level navigation occurs (mock GoRouter), (3) simulate vertical drag and assert content scrolls, (4) verify ClampingScrollPhysics is resolved from the widget's ScrollConfiguration. Add a golden test for a list of 20 items to detect visual regressions. Test on both iOS (BouncingScrollPhysics default) and Android (ClampingScrollPhysics default) simulation modes.

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.