Hooks
useDragScroll
Прокрутка протяжкой мыши с инерцией и растворением краёв.
Зажали ленту, потянули — она едет за курсором и по инерции доезжает после отпускания. Как панорамирование в графе, только для обычного контейнера с прокруткой.
Пример
Зажмите и потяните. Отпустите на движении — лента доедет по инерции. Края растворяются там, где спрятан контент.
import { useDragScroll } from "@toimetdev/pathlogs-hooks";
function Board() {
const ref = useDragScroll<HTMLDivElement>({ axis: "x", keyboard: true });
return (
<div ref={ref} className="flex gap-3 overflow-x-auto">
{columns.map((c) => <Column key={c.id} {...c} />)}
</div>
);
}Хук возвращает ref-колбэк — его достаточно повесить на контейнер. Классы прокрутки (overflow-x-auto и прочее) задаёте вы: хук отвечает за поведение, а не за раскладку.
Настройки
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
axis | "x" | "y" | "both" | "x" | Ось прокрутки. Клавиши поперёк оси не перехватываются — их ждёт страница. |
momentum | boolean | true | Инерция после броска. Автоматически выключается при prefers-reduced-motion. |
keyboard | boolean | false | Лента встаёт в порядок табуляции и слушает стрелки, Page и Home/End — но только когда фокус на ней самой, а не на элементе внутри. |
enabled | boolean | true | Временно выключить протяжку, не снимая хук. |
axis"x" | "y" | "both"по умолчанию: "x"
Ось прокрутки. Клавиши поперёк оси не перехватываются — их ждёт страница.
momentumbooleanпо умолчанию: true
Инерция после броска. Автоматически выключается при prefers-reduced-motion.
keyboardbooleanпо умолчанию: false
Лента встаёт в порядок табуляции и слушает стрелки, Page и Home/End — но только когда фокус на ней самой, а не на элементе внутри.
enabledbooleanпо умолчанию: true
Временно выключить протяжку, не снимая хук.
Обе оси
С axis: "both" полотно тянется в любую сторону — так листается диаграмма Ганта:
const ref = useDragScroll<HTMLDivElement>({ axis: "both", momentum: false });
<div ref={ref} className="h-56 overflow-auto">
<div className="w-[48rem]">…</div>
</div>Как это работает
Здесь много неочевидного, и почти каждое решение — ответ на конкретную поломку:
- Клик не ломается. Протяжка включается только после порога сдвига в 5 px, а «пойманный» ею клик гасится на фазе захвата. Без этого дрожание руки при клике по карточке открывало бы её вместо прокрутки — или наоборот.
- Нативный drag&drop важнее. На
dragstartпротяжка отменяется, а лента у края начинает крутиться сама. Иначе карточку было бы нечем донести до колонки за пределами экрана: во время переноса указатель принадлежит браузеру. - Тач не трогаем. Там прокрутка пальцем и так родная, и перехват только испортил бы её.
- Курсор и края — по факту. «Рука» и растворение краёв появляются, только когда ленте действительно есть куда ехать.
- Инерция гаснет о край. Упёрлись — скорость обнуляется, лента не «дрожит» у границы. Долгий кадр (вкладка была в фоне) не телепортирует ленту: шаг ограничен 50 мс.
Стили
Визуальная часть живёт в @toimetdev/pathlogs-tokens/styles/scroll.css и цепляется за data-атрибуты, которые ставит хук:
<div data-pl-drag-scroll="true" <!-- есть куда прокручивать: курсор «рука» -->
data-pl-scroll-edge="both" <!-- контент спрятан слева и справа -->
data-pl-scroll-edge-y="end" <!-- и снизу -->
data-pl-scroll-keys="true"> <!-- слушает клавиши -->Математика отдельно
Вся арифметика вынесена из хука и не знает про DOM — поэтому она покрыта тестами, а не проверяется руками:
import {
isDragIntent, // сдвиг превысил порог — это протяжка, а не клик
flingVelocity, // скорость броска по треку указателя
decayVelocity, // затухание инерции за кадр
edgeScrollSpeed, // скорость автопрокрутки у края при drag&drop
hiddenEdges, // какой край «продолжается» за экран
keyboardScroll, // что сделать по нажатой клавише
} from "@toimetdev/pathlogs-hooks";Там же лежит attachDragScroll(el, getOptions) — привязка к элементу без React, если поведение нужно вне React-дерева.