diff --git a/src/hooks/useMobileTouchMove.ts b/src/hooks/useMobileTouchMove.ts
index cd3dc36..35ec3d8 100644
--- a/src/hooks/useMobileTouchMove.ts
+++ b/src/hooks/useMobileTouchMove.ts
@@ -54,7 +54,7 @@ export default function useMobileTouchMove(
} else {
offsetY *= SMOOTH_PTG;
}
- const offset = Math.floor(isHorizontal ? offsetX : offsetY);
+ const offset = Math.trunc(isHorizontal ? offsetX : offsetY);
if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) {
clearInterval(intervalRef.current);
}
diff --git a/tests/touch.test.js b/tests/touch.test.js
index d0a5072..ae53fb6 100644
--- a/tests/touch.test.js
+++ b/tests/touch.test.js
@@ -1,6 +1,7 @@
import { act, fireEvent, render } from '@testing-library/react';
import React from 'react';
import List from '../src';
+import useMobileTouchMove from '../src/hooks/useMobileTouchMove';
import { spyElementPrototypes } from './utils/domHook';
// Mock ScrollBar
@@ -188,4 +189,100 @@ describe('List.Touch', () => {
'0',
);
});
+
+ describe('smooth scroll ease out', () => {
+ function setupProbe(impl = () => true) {
+ const callback = jest.fn(impl);
+
+ function Probe() {
+ const listRef = React.useRef(null);
+ useMobileTouchMove(true, listRef, callback);
+ return (
+
+ item
+
+ );
+ }
+
+ const { container } = render();
+ return { container, callback };
+ }
+
+ function dispatchTouch(el, type, pageY) {
+ const ev = new Event(type, { bubbles: true, cancelable: true });
+ ev.touches = [{ pageY, pageX: 0 }];
+ el.dispatchEvent(ev);
+ }
+
+ // Offset is `start - current`, so moving the finger DOWN gives a negative
+ // offset (content scrolls up). `Math.floor` never returns 0 for a negative
+ // value (-0.9 -> -1), which used to keep the interval running forever.
+ [200, -200].forEach((move) => {
+ it(`stops easing out when the finger moved ${move > 0 ? 'down' : 'up'}`, () => {
+ const { container, callback } = setupProbe();
+ const item = container.querySelector('.item');
+
+ act(() => {
+ dispatchTouch(item, 'touchstart', 100);
+ });
+ act(() => {
+ dispatchTouch(item, 'touchmove', 100 + move);
+ });
+
+ // Let the smooth interval run longer than the easing needs.
+ act(() => {
+ jest.advanceTimersByTime(5000);
+ });
+ const settledCalls = callback.mock.calls.length;
+ expect(settledCalls).toBeGreaterThan(0);
+
+ // Nothing may happen after it settled.
+ act(() => {
+ jest.advanceTimersByTime(5000);
+ });
+ expect(callback.mock.calls.length).toBe(settledCalls);
+ });
+ });
+
+ it('stops as soon as the list reports it cannot scroll further', () => {
+ // First call (the touchmove itself) reports handled, then the list is at
+ // its limit, so the interval must give up right away.
+ let handled = true;
+ const { container, callback } = setupProbe(() => {
+ const result = handled;
+ handled = false;
+ return result;
+ });
+ const item = container.querySelector('.item');
+
+ act(() => {
+ dispatchTouch(item, 'touchstart', 100);
+ });
+ act(() => {
+ dispatchTouch(item, 'touchmove', 300);
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(5000);
+ });
+ // touchmove + a single interval frame, then it bailed out.
+ expect(callback.mock.calls.length).toBe(2);
+
+ act(() => {
+ jest.advanceTimersByTime(5000);
+ });
+ expect(callback.mock.calls.length).toBe(2);
+ });
+
+ it('ignores a touchmove that never had a touchstart', () => {
+ const { container, callback } = setupProbe();
+ const item = container.querySelector('.item');
+
+ act(() => {
+ dispatchTouch(item, 'touchmove', 300);
+ });
+
+ expect(callback).not.toHaveBeenCalled();
+ });
+ });
});