fix(dashboard): stop mobile board jump on tap during momentum
Re-touch while the board is coasting now cancels the pending directional settle and re-baselines the gesture at the current scrollLeft, so a stop-tap no longer hard-jumps using the original swipe.
This commit is contained in:
@@ -343,6 +343,63 @@ describe("useColumnScrollSnap", () => {
|
||||
expect(scroller.scrollLeft).toBe(0);
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:BoardNavigation 2026-07-22-15:10:
|
||||
Tap-to-stop during post-lift momentum must not hard-jump using the original swipe direction.
|
||||
*/
|
||||
it("does not jump when the user taps during momentum after a swipe", () => {
|
||||
const scroller = createScroller(3, 0);
|
||||
renderHook(() => useColumnScrollSnap(scroller, { mobileOnly: true, isUserInteraction: () => true }));
|
||||
|
||||
act(() => {
|
||||
// Start a forward swipe and lift so the idle settle is armed.
|
||||
dispatchPointerEvent(scroller, "pointerdown", 200);
|
||||
dispatchPointerEvent(scroller, "pointermove", 160);
|
||||
scroller.scrollLeft = 30;
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
dispatchPointerEvent(scroller, "pointerup", 160);
|
||||
|
||||
// Coast a bit more (native fling), then the user taps to stop mid-travel.
|
||||
scroller.scrollLeft = 55;
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
dispatchPointerEvent(scroller, "pointerdown", 100);
|
||||
dispatchPointerEvent(scroller, "pointerup", 100);
|
||||
});
|
||||
|
||||
// Without re-baselining the second touch, the original rightward settle would jump to COLUMN_WIDTH.
|
||||
settleAfterMomentum();
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
expect(scroller.scrollLeft).toBe(55);
|
||||
});
|
||||
|
||||
it("starts a new directional settle after a pan that continues from a mid-momentum re-touch", () => {
|
||||
const scroller = createScroller(3, 0);
|
||||
renderHook(() => useColumnScrollSnap(scroller, { mobileOnly: true, isUserInteraction: () => true }));
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent(scroller, "pointerdown", 200);
|
||||
dispatchPointerEvent(scroller, "pointermove", 160);
|
||||
scroller.scrollLeft = 30;
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
dispatchPointerEvent(scroller, "pointerup", 160);
|
||||
|
||||
// Interrupt fling, then pan back left so settle must use the new gesture only.
|
||||
scroller.scrollLeft = 55;
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
dispatchPointerEvent(scroller, "pointerdown", 100);
|
||||
dispatchPointerEvent(scroller, "pointermove", 140);
|
||||
scroller.scrollLeft = 20;
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
dispatchPointerEvent(scroller, "pointerup", 140);
|
||||
});
|
||||
settleAfterMomentum();
|
||||
|
||||
expect(scroller.scrollLeft).toBe(0);
|
||||
});
|
||||
|
||||
it("does not attach on non-phone desktop", () => {
|
||||
stubViewport("wide-short-desktop");
|
||||
expect(isMobileViewport()).toBe(false);
|
||||
|
||||
@@ -8,6 +8,11 @@ rubber-band/fling end often reverses for a frame — and (2) origin±nearest hyb
|
||||
Direction is locked at finger-up from net gesture delta only (never post-lift ticks). Target
|
||||
is always the next column in that scroll direction from the current viewport (classic
|
||||
directional page snap). Pin until next touch; hard-jump kills residual fling.
|
||||
|
||||
FNXC:BoardNavigation 2026-07-22-15:10:
|
||||
A tap during post-lift momentum must cancel the pending directional settle and re-baseline
|
||||
the gesture at the current scrollLeft (pointerHeld true). Otherwise the original swipe's
|
||||
idle timer still hard-jumps the board away from where the user stopped.
|
||||
*/
|
||||
/** After lift/cancel/wheel: wait for scroll idle (momentum finished) before paging. */
|
||||
const SCROLL_IDLE_SETTLE_MS = 48;
|
||||
@@ -389,12 +394,35 @@ export function useColumnScrollSnap(
|
||||
idleTimer = setTimeout(snapInScrollDirection, SCROLL_IDLE_SETTLE_MS);
|
||||
};
|
||||
|
||||
/*
|
||||
FNXC:BoardNavigation 2026-07-22-15:10:
|
||||
A second touch during post-lift momentum must cancel the pending directional settle and start a fresh gesture at the current scrollLeft.
|
||||
Previously, re-touch while interactionActive only re-captured the pointer and returned early — pointerHeld stayed false, the idle timer kept the original swipe direction, and the board hard-jumped away from where the user tapped to stop.
|
||||
*/
|
||||
const beginInteraction = (event: Event) => {
|
||||
if (!isUserInteraction(event)) return;
|
||||
|
||||
clearPin();
|
||||
|
||||
// Mid-momentum re-touch (or duplicate pointerdown+touchstart): cancel pending snap and re-baseline.
|
||||
if (interactionActive) {
|
||||
clearIdleTimer();
|
||||
lockedDirection = 0;
|
||||
sawHorizontalMovement = false;
|
||||
gestureStartScrollLeft = scroller.scrollLeft;
|
||||
lastScrollLeft = scroller.scrollLeft;
|
||||
const clientX = getClientX(event);
|
||||
gestureStartClientX = clientX;
|
||||
lastClientX = clientX;
|
||||
|
||||
if (event.type === "wheel") {
|
||||
pointerHeld = false;
|
||||
suspendNativeSnap();
|
||||
armIdleSettle();
|
||||
return;
|
||||
}
|
||||
|
||||
pointerHeld = true;
|
||||
if (event.type === "pointerdown" && "pointerId" in event) {
|
||||
try {
|
||||
scroller.setPointerCapture((event as PointerEvent).pointerId);
|
||||
|
||||
Reference in New Issue
Block a user