PathLogs UI
Русский

Hooks

useDragScroll

Drag-to-scroll with momentum and fading edges.

Grab the strip, pull — it follows the cursor and coasts to a stop after you let go. Like panning a graph, but for an ordinary scrollable container.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14

Зажмите и потяните. Отпустите на движении — лента доедет по инерции. Края растворяются там, где спрятан контент.

The hook returns a ref callback — hanging it on the container is enough. The scroll classes (overflow-x-auto and friends) are yours to set: the hook is responsible for behaviour, not layout.

Options

axis"x" | "y" | "both"

defaults to: "x"

The scrolling axis. Keys across the axis are not intercepted — the page is waiting for those.

momentumboolean

defaults to: true

Inertia after a fling. Switched off automatically under prefers-reduced-motion.

keyboardboolean

defaults to: false

The strip joins the tab order and listens for arrows, Page and Home/End — but only while focus is on the strip itself, not on an element inside it.

enabledboolean

defaults to: true

Temporarily disable dragging without removing the hook.

Both axes

With axis: "both" the canvas drags in any direction — this is how the Gantt chart is panned:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

How it works

There is a lot here that is not obvious, and almost every decision is an answer to a specific breakage:

  • Clicks do not break. Dragging engages only past a 5px movement threshold, and a click «caught» by it is suppressed during the capture phase. Without this, a shaky hand while clicking a card would open the card instead of scrolling — or the other way round.
  • Native drag&drop wins. On dragstart the drag is cancelled and the strip starts auto-scrolling near the edge. Otherwise there would be no way to carry a card to a column off-screen: during a native drag the pointer belongs to the browser.
  • Touch is left alone. Finger scrolling is already native there, and intercepting it would only make it worse.
  • Cursor and edges follow reality. The «grab» cursor and the fading edges appear only when the strip actually has somewhere to go.
  • Inertia dies against the edge. Hit the end and velocity is zeroed, so the strip does not judder at the boundary. A long frame (the tab was in the background) does not teleport the strip: the step is capped at 50ms.

Styles

The visual half lives in @toimetdev/pathlogs-tokens/styles/scroll.css and hooks onto the data attributes the hook sets:

html
<div data-pl-drag-scroll="true"      <!-- there is room to scroll: «grab» cursor -->
     data-pl-scroll-edge="both"     <!-- content is hidden left and right -->
     data-pl-scroll-edge-y="end"    <!-- and below -->
     data-pl-scroll-keys="true">    <!-- listening for keys -->

The maths, separately

All the arithmetic is lifted out of the hook and knows nothing about the DOM — which is why it is covered by tests rather than checked by hand:

tsx
import {
  isDragIntent,     // movement passed the threshold — a drag, not a click
  flingVelocity,    // fling speed from the pointer track
  decayVelocity,    // inertia decay per frame
  edgeScrollSpeed,  // auto-scroll speed near the edge during drag&drop
  hiddenEdges,      // which edge «continues» off-screen
  keyboardScroll,   // what a given key press should do
} from "@toimetdev/pathlogs-hooks";

The same module holds attachDragScroll(el, getOptions) — binding to an element without React, if you need the behaviour outside the React tree.