diff --git a/apps/web/src/components/HistorySwipeNavigation.browser.test.tsx b/apps/web/src/components/HistorySwipeNavigation.browser.test.tsx index b2e6fcaaaf402..1edd2c591627a 100644 --- a/apps/web/src/components/HistorySwipeNavigation.browser.test.tsx +++ b/apps/web/src/components/HistorySwipeNavigation.browser.test.tsx @@ -58,4 +58,33 @@ describe("HistorySwipeNavigation", () => { expect(navigateHistory).toHaveBeenCalledTimes(navigates ? 1 : 0); }); + + it("a vertical scroll that started slightly diagonal and drifts sideways keeps the thread", async () => { + navigateHistory.mockReset(); + await using screen = await render(); + const content = screen.container.appendChild(document.createElement("div")); + const wheel = (deltaX: number, deltaY: number) => + content.dispatchEvent(new WheelEvent("wheel", { deltaX, deltaY, bubbles: true })); + + for (let index = 0; index < 2; index++) wheel(-4, -3); + for (let index = 0; index < 40; index++) wheel(-3, -25); + wheel(0, 0); + + expect(navigateHistory).not.toHaveBeenCalled(); + }); + + it("a swipe that wobbles vertically at the start still navigates", async () => { + navigateHistory.mockReset(); + await using screen = await render(); + const content = screen.container.appendChild(document.createElement("div")); + const wheel = (deltaX: number, deltaY: number) => + content.dispatchEvent(new WheelEvent("wheel", { deltaX, deltaY, bubbles: true })); + + wheel(-5, -4); + wheel(0, -2); + for (let index = 0; index < 4; index++) wheel(-40, 0); + wheel(0, 0); + + expect(navigateHistory).toHaveBeenCalledWith("back"); + }); }); diff --git a/apps/web/src/components/HistorySwipeNavigation.tsx b/apps/web/src/components/HistorySwipeNavigation.tsx index 767b4285c696f..3e7341106dddc 100644 --- a/apps/web/src/components/HistorySwipeNavigation.tsx +++ b/apps/web/src/components/HistorySwipeNavigation.tsx @@ -17,6 +17,8 @@ type SwipeGesture = direction: HistoryDirection; distance: number; deltas: number[]; + travelX: number; + travelY: number; y: number; bounds: SwipeBounds; } @@ -68,6 +70,8 @@ function lockSwipeAxis( direction, distance: Math.abs(sumX), deltas: [], + travelX: Math.abs(sumX), + travelY: Math.abs(sumY), y: pending.y, bounds: readMainContentBounds(), }; @@ -96,10 +100,14 @@ export function HistorySwipeNavigation() { useEffect(() => { let gesture: SwipeGesture | null = null; const hideIndicator = () => - setSwipe((current) => (current === null ? null : { ...current, tracking: false })); + setSwipe((current) => (current?.tracking ? { ...current, tracking: false } : current)); // The fingers lifted: navigate if the swipe went far enough, then ignore its momentum. const release = () => { - if (gesture?.kind === "tracking" && gesture.distance >= SWIPE_TRIGGER_DISTANCE) { + if ( + gesture?.kind === "tracking" && + gesture.travelX >= gesture.travelY && + gesture.distance >= SWIPE_TRIGGER_DISTANCE + ) { navigateHistory(gesture.direction); } gesture = { kind: "ignored" }; @@ -130,9 +138,13 @@ export function HistorySwipeNavigation() { } else { const delta = gesture.direction === "back" ? -event.deltaX : event.deltaX; if (event.deltaX === 0 && event.deltaY === 0) return release(); + gesture.travelX += Math.abs(event.deltaX); + gesture.travelY += Math.abs(event.deltaY); gesture.deltas = [...gesture.deltas.slice(1 - MOMENTUM_SAMPLE_SIZE), delta]; if (isMomentumTail(gesture.deltas)) return release(); gesture.distance = Math.max(0, gesture.distance + delta); + // A scroll that locked on a diagonal start can turn out to be vertical. + if (gesture.travelY > gesture.travelX) return hideIndicator(); } setSwipe({ direction: gesture.direction,