Skip to content

fix: stop smooth touch scrolling once a step is under one pixel - #388

Open
Ye-YiChen wants to merge 1 commit into
react-component:masterfrom
Ye-YiChen:fix-mobile-smooth-scroll-never-stops
Open

Ye-YiChen wants to merge 1 commit into
react-component:masterfrom
Ye-YiChen:fix-mobile-smooth-scroll-never-stops

Conversation

@Ye-YiChen

@Ye-YiChen Ye-YiChen commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

🤔 This is a ...

  • 🆕 New feature
  • 🐞 Bug fix
  • 📝 Site / documentation improvement
  • 📽️ Demo improvement
  • 💄 Component style improvement
  • 🤖 TypeScript definition improvement
  • 📦 Bundle size optimization
  • ⚡️ Performance optimization
  • ⭐️ Feature enhancement
  • 🌐 Internationalization improvement
  • 🛠 Refactoring
  • 🎨 Code style optimization
  • ✅ Test Case
  • 🔀 Branch merge
  • ⏩ Workflow
  • ⌨️ Accessibility improvement
  • ❓ Other (about what?)

🔗 Probably related Issues

Fixes #275, #305

💡 Background and Solution

A touch swipe that scrolls a virtual list upwards never comes to rest. After the finger lifts, the list keeps moving at a constant 1px per frame until something else interrupts it, instead of easing out the way the opposite direction does.

The smooth-scroll interval decays the offset by SMOOTH_PTG and stops once there is nothing left to move:

const offset = Math.floor(isHorizontal ? offsetX : offsetY);
if (!callback(...) || Math.abs(offset) <= 0.1) clearInterval(...);

Math.floor never returns 0 for a negative value — Math.floor(-0.9) is -1. Once the offset decays into (-1, 0), offset stays -1 forever, so the stop check never passes and the interval keeps firing. The other direction is unaffected because Math.floor(0.9) is 0 — which is exactly why only one direction ever hangs.

The fix is one line: Math.trunc rounds towards zero, so the existing Math.abs(offset) <= 0.1 check finally does what it was written to do, for both directions.

-          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);
           }

The original threshold and the original structure are kept.

Reproduces only when all of these hold:

  1. the list is virtual (height + itemHeight)
  2. the input is touch (not wheel — useMobileTouchMove is only wired up when virtual; the wheel path has no interval of its own)
  3. the list is not already at the top — at the top useOriginScroll hands the gesture to the browser and no interval is ever created

✅ Verification

Red / green with the change reverted and re-applied, full suite both times:

with fix without fix
finger down (offset < 0) ✅ passed failed
finger up (offset > 0) ✅ passed ✅ passed

Only the negative direction goes red, which matches the Math.floor analysis.

Also confirmed in a real browser (Chrome device emulation) on an rc-tree virtual list:

behaviour
before the list scrolls at a constant speed and never stops; the interval keeps firing with the step approaching 0 while the rounded offset stays -1
after the list eases out and stops on its own

useMobileTouchMove.ts is at 100% line / statement / function coverage (the one uncovered branch is if (touchedRef.current), unreachable because the listeners are only attached after a successful touchstart).

Full suite: 9 suites / 288 tests passed. tsc --noEmit clean, eslint 0 errors, prettier clean.

📝 Change Log

Language Changelog
🇺🇸 English Fix virtual-list smooth touch scrolling never stopping when a swipe scrolls upwards
🇨🇳 Chinese 修复 virtual-list 在向上滑动时触摸惯性滚动不会停止的问题

Summary by CodeRabbit

  • Bug 修复

    • 调整移动端触摸平滑滚动的停止条件,提升滚动结束判断的准确性。
  • 测试

    • 增加移动端触摸平滑滚动测试,覆盖不同移动方向、滚动边界及缺少触摸起始事件等场景。

@vercel

vercel Bot commented Sep 21, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the afc163's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 8cba0fd6-cada-4a1b-a66b-babf1382e120

📥 Commits

Reviewing files that changed from the base of the PR and between 2fb57c0 and 88497c0.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 204a3e05-32c5-495c-a456-d5d22574423b

📥 Commits

Reviewing files that changed from the base of the PR and between 00f0ba5 and 36cdf75.

📒 Files selected for processing (1)
  • src/hooks/useMobileTouchMove.ts

Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.


Walkthrough

本次修改更新 useMobileTouchMove 的平滑滚动步长和停止条件,并新增测试覆盖上下滑动、无法继续滚动及缺少 touchstart 的情况。

Changes

平滑滚动修复

Layer / File(s) Summary
调整平滑滚动停止逻辑
src/hooks/useMobileTouchMove.ts
使用 Math.trunc 截断步长。定时器在回调未处理或截断后的偏移量绝对值不超过 0.1 时停止。
补充触摸滚动测试
tests/touch.test.js
新增测试,验证上下滑动最终停止、列表无法继续滚动时停止,以及没有先触发 touchstart 时不处理 touchmove

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~15 minutes

Change: Bug fix · Severity of issue fixed: Medium

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed 变更满足直接关联议题 #275 的编码目标。src/hooks/useMobileTouchMove.ts 使用 Math.trunc 处理平滑滚动步长。负数步长会向零截断,因此小于一个像素时得到 0,现有 Math.abs(offset) <= 0.1 停止条件可以清除定时器。tests/touch.test.js 覆盖手指向上和向下移动后的停止行为,并验证列表无法继续滚动时…
Out of Scope Changes check ✅ Passed 变更范围与议题 #275 一致。源码变更只调整移动端触摸平滑滚动的取整行为。新增测试只验证该修复的双向停止、边界停止和无效触摸移动处理。未发现无关变更。
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了主要变更:当平滑触摸滚动步长小于一个像素时停止滚动。该内容与代码修改、测试和 PR 目标一致。
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

小兔挥爪触发滚动,
上下滑动步伐分明。
无法前行立即停下,
细小偏移不再徘徊。
测试守护触摸路径。

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.62%. Comparing base (92e18b1) to head (88497c0).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #388   +/-   ##
=======================================
  Coverage   97.62%   97.62%           
=======================================
  Files          19       19           
  Lines         843      843           
  Branches      206      210    +4     
=======================================
  Hits          823      823           
  Misses         20       20           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Ye-YiChen
Ye-YiChen force-pushed the fix-mobile-smooth-scroll-never-stops branch from 36cdf75 to 2fb57c0 Compare September 21, 2026 06:31
A touch swipe that scrolls a virtual list upwards never comes to rest. After the
finger lifts, the list keeps moving at a constant 1px per frame until something
else interrupts it, instead of easing out the way the opposite direction does.

The smooth-scroll interval decays the offset by SMOOTH_PTG and stops once there
is nothing left to move:

    const offset = Math.floor(isHorizontal ? offsetX : offsetY);
    if (!callback(...) || Math.abs(offset) <= 0.1) clearInterval(...);

Math.floor never returns 0 for a negative value - Math.floor(-0.9) is -1. Once
the offset decays into (-1, 0), `offset` stays -1 forever, so the stop check
never passes and the interval keeps firing. The other direction is unaffected
because Math.floor(0.9) is 0, which is why only one direction ever hangs.

Use Math.trunc, which rounds towards zero, so the existing
`Math.abs(offset) <= 0.1` check finally does what it was written to do - in both
directions. The original threshold and structure are kept.

Needs all of: a virtual list (height + itemHeight), touch input, and not already
being at the top - at the top `useOriginScroll` hands the gesture to the browser
and no interval is created at all.

Fixes react-component#275
@Ye-YiChen
Ye-YiChen force-pushed the fix-mobile-smooth-scroll-never-stops branch from 2fb57c0 to 88497c0 Compare September 21, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

手机下上划会出现一直滚动的情况

1 participant