Skip to content

fix(scroll): keep directional swipe endpoints within the resolved scrollable #2714

Description

@thymikee

Purpose

A directional scroll must not deliver a swipe outside the scrollable it resolved. Endpoints are currently viewport-centred with a span of amount x viewport height and are never clamped, so on an inset scrollable the drag can start or end in the navigation bar and the gesture lands on the wrong surface.

This issue originally reported two defects. The disclosure half — a scroll that observed nothing answering with the distance it was asked for — has landed; what remains is the geometry half below.

Status: what already landed

On main at 913e22545 (layers #2770, #2776, #2771, #2772; unreleased at the time of writing, 0.21.12 being the newest published version):

  • A directional scroll answers with observed movement (moved | at-edge | unchanged | unobserved) instead of the requested distance, and a scroll that made no progress while the container still reports hidden content fails with details.reason === 'scroll_no_progress', carrying containerRect, hiddenContentAt, and requestedPixels.
  • moved is confined to the resolved scroller, so an unrelated on-screen change cannot be credited to the gesture. Measured on one AVD: a status-bar battery icon changing alone used to produce movement: 'moved' attempts:1; on the fixed build the same experiment returns unobserved with change-outside-container while the app's own row geometry is identical across the gesture.
  • The defect below is now observable rather than silent: when the resolved container does not hold the swipe midpoint, the answer is movement: 'unobserved' with reason container-outside-swipe and a containerRect (src/daemon/scroll-movement.ts:465). That detects the condition; it does not correct the gesture.

Remaining defect: unclamped geometry

Gesture endpoints are centred on the viewport with a span of amount x viewport height, and are never clamped to the resolved scrollable:

amount gesture viewport
0.5 656 -> 218 402x874
0.6 699 -> 175
0.75 765 -> 109

The /automation scrollable is inset from the window:

ScrollView / role=RCTEnhancedScrollView subrole=UIScrollView rect=(18,178,366,662)

so a 0.75 gesture ends at y=109, which is 69pt above the scrollable's own top edge, in the navigation bar. On /catalog the same build reports rect=(18,62,366,729), every amount stays inside it, and scrolling works in both directions.

Required behavior

  • When a scroll container is resolved, both directional swipe endpoints are computed inside its containerRect.
  • Where the container can carry the requested span, the requested direction and amount are preserved exactly.
  • Where it cannot, the dispatched span is reduced to the container and the answer states the span actually dispatched, rather than silently repeating the requested one.
  • container-outside-swipe ceases to be reachable for an inset-but-adequate scrollable, because the gesture no longer leaves the container.

Observable completion conditions

  • Regression coverage for a container inset from the viewport: the emitted gesture endpoints lie inside containerRect, and the reported distance is the span actually dispatched.
  • Regression coverage for a container that already fits the span: endpoints and reported distance are unchanged from today's behavior.
  • An inset route of the shape above moves content (movement: 'moved') where it previously returned unobserved / container-outside-swipe.

Open question before this is AFK-ready

When the resolved container is shorter than the span the caller asked for: scale the span down to the container, or refuse with a typed reason? Both are defensible. Either way requestedPixels must stay distinguishable from the span actually dispatched, so the caller can tell a reduced gesture from a full one.

Environment and provenance

Reported against the SDK 58 fixture's /automation route; run 35523907559, job 106112615304. That build cannot launch on the 27.x runtimes available here, so reproduction on this host is the tester app's inset surfaces together with the geometry table above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions