# Gestures and scrolling

Swipe, scroll and drag in a flow, reach controls further down a list, and deal with a tap on a control that is off screen.

## swipe and scroll go opposite ways

`swipe` names where your finger goes. `scroll` names where the content goes. To see what’s further down a list, you `scroll: down`, and the finger moves up. Pick a step to see which way each one moves, and a position to see a drag to it:

```yaml
- swipe: { direction: left }                   # the finger moves left
- scroll: { direction: down, amount: 300 }      # the content moves down 300 points
```

`amount` is in points. A `scroll` holds the finger still before lifting it, so the content moves exactly that far, with no momentum. A `swipe` lifts while the finger is still moving, as a flick does, so a list keeps going and a sheet that closes on a fast swipe closes.

## Drag to a position

For a slider, a segmented control or a colour picker, the value is where the finger stops. A distance in points means something different on every device, so say where the drag ends instead, as a percentage of the control:

```yaml
- swipe: { testID: brightness, to: 25% }        # a quarter of the way across
- swipe: { testID: brightness, to: 100% }       # all the way right
- swipe: { testID: canvas, to: "10%, 75%" }     # across and down
```

The drag starts at the control’s centre. One value sets how far across and keeps the finger centred vertically. Use either `to` or `direction`, not both. Then assert the new value: a swipe that passes only proves the drag was sent.

## Drag after a long press

A list you reorder by holding a row and then dragging it only picks the row up after the finger has stayed still for a moment. A drag that moves at once scrolls the list instead. Add `hold`, in milliseconds, to press and hold before the drag starts. A held drag starts at the middle of the control you name and moves `amount` from there, so you pick up the row itself:

```yaml
- swipe: { testID: row-milk, direction: down, amount: 88, hold: 400 }
```

Make `hold` longer than the list’s long-press delay. It takes a whole number from 1 to 3000, and only on a `swipe`: a held finger picks something up, which is not a scroll. On a macOS app the step fails rather than drag without the hold.

## Drag slowly

A `swipe` takes about 300 milliseconds, as a flick does. Add `duration`, in milliseconds, to make the drag take longer. The finger also lifts more slowly, so a sheet that closes on a fast flick stays open unless the drag went far enough:

```yaml
- swipe: { testID: sheet, direction: down, amount: 400, duration: 1500 }
```

It takes a whole number from 50 to 5000, and only on a `swipe`: a `scroll` holds the finger still before it lifts, so its speed changes nothing. With `hold`, the hold comes first and `duration` times the drag after it.

## Scroll until a control appears

```yaml
- scrollUntilVisible:
    element: { text: Log out }
    direction: down
    maxScrolls: 10
```

`scrollUntilVisible` scrolls until the control is fully on screen and a tap at its centre would reach it, up to `maxScrolls` times (15 by default). If the control is already visible, it returns at once. If it still isn’t visible after the last scroll, the step fails with `not visible after 15 scroll(s)`. If it still isn’t visible after the last scroll, the step fails with `not visible after 15 scroll(s)`.

Add `mode: direct` to move the list in one step instead of dragging, which is much faster. Leave it out when scrolling is what you’re testing: a direct scroll skips momentum, paging, snapping and pull-to-refresh. To make `direct` the default for a run, see [Run settings](https://squiggle.sh/docs/run/settings).

## A tap on an off-screen control

When a `tap` or `type` targets a control outside the screen, the step fails and names the control’s position and the screen size. Squiggle won’t tap it anyway, because the tap would land on whatever is at the edge of the screen instead.

Add a `scrollUntilVisible` step before it. Or run with `--auto-scroll`, which scrolls the control into view and tries again, and marks the step `scrolled into view`. In Studio, it is **Auto-scroll** in the Replay options.

> **Auto-scroll is off by default**
>
> It moves the screen in a step that didn’t ask for it, which is wrong for a flow that tests the scroll position. A flow that needs it can turn it on for itself with `config: { autoScroll: true }`.
