Skip to content

fix(Anchor): resolve scroll animation conflict when clicking links ra…#55814

Merged
afc163 merged 6 commits intoant-design:masterfrom
tuzixiangs:fix/anchor-animation-conflict
Nov 24, 2025
Merged

fix(Anchor): resolve scroll animation conflict when clicking links ra…#55814
afc163 merged 6 commits intoant-design:masterfrom
tuzixiangs:fix/anchor-animation-conflict

Conversation

@tuzixiangs
Copy link
Copy Markdown
Contributor

English Template / 英文模板

🤔 这个变动的性质是?

  • 🆕 新特性提交
  • 🐞 Bug 修复
  • 📝 站点、文档改进
  • 📽️ 演示代码改进
  • 💄 组件样式/交互改进
  • 🤖 TypeScript 定义更新
  • 📦 包体积优化
  • ⚡️ 性能优化
  • ⭐️ 功能增强
  • 🌐 国际化改进
  • 🛠 重构
  • 🎨 代码风格优化
  • ✅ 测试用例
  • 🔀 分支合并
  • ⏩ 工作流程
  • ⌨️ 无障碍改进
  • ❓ 其他改动(是关于什么的改动?)

🔗 相关 Issue

Fixes #55813

💡 需求背景和解决方案

问题背景: 目前 Anchor 组件在快速点击不同链接时,由于没有取消上一次的滚动动画,导致多个 raf 循环同时运行,引发页面滚动抖动。

解决方案:

  1. components/_util/scrollTo.ts:
  • 更新 scrollTo 函数,使其返回一个 cancel 函数。
  • 在闭包中追踪最新的 rafId,确保可以随时取消正在运行的动画帧。

2. components/anchor/Anchor.tsx:

  • 引入 scrollRequestId ref 来存储当前动画的取消句柄。
  • 在 handleScrollTo 中,如果检测到正在进行动画且目标 Link 不同,先调用 scrollRequestId.current() 取消旧动画,再开始新动画。
  • 增加同名 Link 点击的判断:如果目标 Link 与当前正在滚动的 Link 相同,直接忽略,避免重复触发。

修复后效果:

Jietu20251122-153658-HD.mp4

📝 更新日志

语言 更新描述
🇺🇸 英文 Fix scroll animation conflict in Anchor component when clicking links rapidly.
🇨🇳 中文 修复 Anchor 组件在快速点击链接时滚动动画冲突的问题。

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 22, 2025

👁 Visual Regression Report for PR #55814 Passed ✅

🎯 Target branch: master (ef52e15)
📖 View Full Report ↗︎
📖 Alternative Report ↗︎

🎊 Congrats! No visual-regression diff found.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 22, 2025

Preview is ready

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @tuzixiangs, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves an issue in the Anchor component where rapid clicking of links could lead to conflicting scroll animations and a 'jittery' page experience. The solution introduces a mechanism to gracefully cancel any ongoing scroll animations before initiating a new one, ensuring a smoother and more predictable scrolling behavior for users interacting with anchor links.

Highlights

  • Scroll Animation Cancellation: The scrollTo utility function has been enhanced to return a cancellation function, allowing ongoing scroll animations to be stopped programmatically.
  • Anchor Component Animation Conflict Resolution: The Anchor component now utilizes the new cancellation capability of the scrollTo function. When a new link is clicked while an animation is in progress, the previous animation is cancelled before a new one begins, preventing conflicts and page jitter.
  • Preventing Redundant Animations: The Anchor component now includes logic to prevent re-triggering a scroll animation if the user clicks on the same active link, further optimizing the user experience.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 22, 2025

📝 Walkthrough

Summary by CodeRabbit

发行说明

  • 新功能

    • 锚点链接滚动动画现支持取消机制。用户在快速点击不同锚点时,系统会自动取消前一个滚动动画并立即开始新的滚动,改进导航体验。
  • 测试

    • 新增滚动取消、多次点击和状态清理的测试用例。

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

为平滑滚动引入可取消的动画机制:scrollTo 现在返回取消函数并保存 RAF id;Anchor 在启动新滚动前会调用并替换先前的取消函数以防止并发滚动冲突和页面抖动。

Changes

内聚 / 文件 变更摘要
滚动工具
components/_util/scrollTo.ts
scrollTo(y: number, options: ScrollToOptions = {}) 签名从 void 改为 () => void;内部保存并使用 rafId 调度帧循环,返回可取消当前滚动动画的清理函数,替换之前直接递归调用 RAF 的实现。
锚点组件
components/anchor/Anchor.tsx
新增 scrollRequestId ref 用于保存当前滚动的取消函数;在 handleScrollTo 中若存在进行中动画且目标不同则调用取消函数;启动新 smooth scroll 时保存新的取消函数并维护 animating 状态与回调行为。
单元测试
components/_util/__tests__/scrollTo.test.ts, components/anchor/__tests__/Anchor.test.tsx
新增测试覆盖:取消正在进行的滚动、带回调的取消不应调用回调、多次快速调用导致前一个被取消并开始新滚动、重复调用取消函数行为及 Anchor 在快速点击与卸载时的状态清理验证。

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Anchor
    participant ScrollUtil as scrollTo
    participant RAF as requestAnimationFrame

    User->>Anchor: 点击链接 A
    Anchor->>ScrollUtil: scrollTo(targetA)
    Note right of ScrollUtil: 创建并保存 rafId,开始帧循环
    ScrollUtil-->>Anchor: 返回取消函数 cancelA
    Anchor->>Anchor: 保存 cancelA 到 scrollRequestId.current

    Note over Anchor: 滚动进行中...

    User->>Anchor: 快速点击链接 B
    Anchor->>Anchor: 调用 scrollRequestId.current() (取消 cancelA)
    Note right of ScrollUtil: 使用保存的 rafId 取消 RAF 回调
    Anchor->>ScrollUtil: scrollTo(targetB)
    Note right of ScrollUtil: 保存新 rafId 并开始新帧循环
    ScrollUtil-->>Anchor: 返回取消函数 cancelB
    Anchor->>Anchor: 更新 scrollRequestId.current = cancelB
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 分钟

需要额外关注:

  • components/_util/scrollTo.ts:取消函数在完成、取消和异常路径上的清理(RAF 取消、回调抑制、id 重入);
  • components/anchor/Anchor.tsx:并发点击、相同目标重复点击的早期返回逻辑是否会影响状态或回调;
  • 两处测试文件:对 window.scrollTo 与 requestAnimationFrame 的模拟是否健壮,边界断言是否可靠。

Poem

🐰 快速点我别慌张,
旧帧停下新帧忙,
取消一声风轻过,
目标稳稳到眼前,
草地安静月光香 ✨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR标题准确描述了主要变动:修复快速点击链接时Anchor组件的滚动动画冲突问题。
Description check ✅ Passed PR描述详细说明了问题背景、解决方案、受影响的文件及修复效果演示,与变动集相关。
Linked Issues check ✅ Passed 代码变动完整实现了#55813的需求:返回取消函数、追踪rafId、中断前一个动画、忽略重复点击。
Out of Scope Changes check ✅ Passed 所有变动都围绕修复滚动动画冲突问题,包括scrollTo工具函数、Anchor组件和相关测试,无超出范围的改动。
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06fcc09 and ce4a1cb.

📒 Files selected for processing (1)
  • components/anchor/Anchor.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/anchor/Anchor.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (16)
  • GitHub Check: visual-diff snapshot (2/2)
  • GitHub Check: visual-diff snapshot (1/2)
  • GitHub Check: build
  • GitHub Check: build preview
  • GitHub Check: test lib/es module (lib, 2/2)
  • GitHub Check: test lib/es module (es, 2/2)
  • GitHub Check: test lib/es module (es, 1/2)
  • GitHub Check: test lib/es module (lib, 1/2)
  • GitHub Check: build
  • GitHub Check: test-react-latest (dom, 2/2)
  • GitHub Check: test-node
  • GitHub Check: test-react-latest (dom, 1/2)
  • GitHub Check: test-react-legacy (18, 1/2)
  • GitHub Check: test-react-legacy (18, 2/2)
  • GitHub Check: size
  • GitHub Check: lint

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 and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

这个 Pull Request 旨在解决 Anchor 组件在快速点击不同链接时出现的滚动动画冲突和页面抖动问题。您的解决方案非常清晰且实现得当:通过修改 scrollTo 工具函数使其返回一个取消句柄,然后在 Anchor 组件中追踪并管理这个句柄。在用户点击新链接时,如果存在正在进行的动画,会先取消旧动画再启动新动画,并且会忽略对同一链接的重复点击。这些改动从根本上解决了动画冲突,代码实现简洁有效。整体来看,这是一个高质量的修复。

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/anchor/Anchor.tsx (1)

295-300: 组件卸载时需要取消进行中的动画。

当前代码缺少在组件卸载时取消正在进行的滚动动画的清理逻辑。如果组件在动画进行期间卸载,可能会导致尝试操作已销毁的 DOM 元素,引发错误。

建议添加清理 Effect:

React.useEffect(() => {
  return () => {
    // Cancel any ongoing scroll animation on unmount
    scrollRequestId.current?.();
  };
}, []);

可以将此 Effect 添加在其他 useEffect 调用附近(例如第 367-374 行之后)。

🧹 Nitpick comments (1)
components/_util/scrollTo.ts (1)

21-22: 建议明确 rafId 的初始类型。

rafId 声明为 number 类型但未初始化。虽然 TypeScript 允许这样做,但更明确的类型声明会提高代码可读性。

可以考虑以下改进:

-  let rafId: number;
+  let rafId: number | undefined;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2e000f and 4e9eb84.

📒 Files selected for processing (2)
  • components/_util/scrollTo.ts (2 hunks)
  • components/anchor/Anchor.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
components/anchor/Anchor.tsx (1)
components/_util/scrollTo.ts (1)
  • scrollTo (15-45)
🔇 Additional comments (2)
components/_util/scrollTo.ts (2)

23-39: 代码逻辑正确!

动画帧循环正确地更新了 rafId,确保取消函数始终引用最新的帧 ID。当动画被中断时不调用 callback 是符合预期的行为。


40-44: 取消机制实现正确!

通过闭包捕获 rafId 引用,返回的清理函数能够正确取消最新的动画帧。这个实现有效解决了动画冲突问题。

Comment thread components/anchor/Anchor.tsx Outdated
Comment thread components/anchor/Anchor.tsx Outdated
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Nov 22, 2025

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 22, 2025

Bundle Report

Changes will increase total bundle size by 200 bytes (0.01%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
antd.min-array-push 3.91MB 200 bytes (0.01%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: antd.min-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
antd-with-locales.min.js 100 bytes 2.15MB 0.0%
antd.min.js 100 bytes 1.76MB 0.01%

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 22, 2025

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.99%. Comparing base (ef52e15) to head (ce4a1cb).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
components/anchor/Anchor.tsx 75.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##            master   #55814      +/-   ##
===========================================
- Coverage   100.00%   99.99%   -0.01%     
===========================================
  Files          803      803              
  Lines        14804    14809       +5     
  Branches      3911     3912       +1     
===========================================
+ Hits         14804    14808       +4     
- Misses           0        1       +1     

☔ View full report in Codecov by Sentry.
📢 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.

@meet-student
Copy link
Copy Markdown
Member

meet-student commented Nov 24, 2025

test 覆盖率掉了, 需要看一下

meet-student
meet-student previously approved these changes Nov 24, 2025
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Nov 24, 2025
Copy link
Copy Markdown
Member

@afc163 afc163 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

覆盖率掉了

@dosubot dosubot Bot removed the lgtm This PR has been approved by a maintainer label Nov 24, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
components/_util/__tests__/scrollTo.test.ts (2)

73-85: 验证滚动动画是否真正被取消。

当前测试仅验证 scrollToSpy 被调用过,但没有验证取消后滚动是否真的停止了。应该验证最终滚动位置没有达到目标位置 1000。

建议应用此修改:

    const cancel = scrollTo(1000);
    jest.advanceTimersByTime(50);
    cancel();
    await waitFakeTimer();
    expect(scrollToSpy).toHaveBeenCalled();
+   // Verify that scrolling did not reach the target position
+   expect(window.pageYOffset).toBeLessThan(1000);
    scrollToSpy.mockRestore();

99-113: 考虑增强断言以验证第一个动画被取消。

测试验证了第二个滚动完成到 2000,但可以额外验证第一个滚动没有达到其目标位置 1000。另外,Line 111 的 cancel2() 调用是在动画完成后,虽然无害但不必要。

建议应用此修改:

    const cancel1 = scrollTo(1000);
    jest.advanceTimersByTime(50);
+   const positionAfterCancel = window.pageYOffset;
+   expect(positionAfterCancel).toBeLessThan(1000);
    cancel1();
    const cancel2 = scrollTo(2000);
    await waitFakeTimer();
    expect(window.pageYOffset).toBe(2000);
-   cancel2();
    scrollToSpy.mockRestore();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 217b001 and 06fcc09.

📒 Files selected for processing (2)
  • components/_util/__tests__/scrollTo.test.ts (1 hunks)
  • components/anchor/__tests__/Anchor.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
components/anchor/__tests__/Anchor.test.tsx (1)
tests/utils.tsx (1)
  • waitFakeTimer (77-89)
components/_util/__tests__/scrollTo.test.ts (2)
components/_util/scrollTo.ts (1)
  • scrollTo (15-45)
tests/utils.tsx (1)
  • waitFakeTimer (77-89)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (16)
  • GitHub Check: size
  • GitHub Check: build preview
  • GitHub Check: test lib/es module (es, 1/2)
  • GitHub Check: test lib/es module (lib, 1/2)
  • GitHub Check: test lib/es module (es, 2/2)
  • GitHub Check: test-react-latest (dom, 2/2)
  • GitHub Check: build
  • GitHub Check: test lib/es module (lib, 2/2)
  • GitHub Check: test-react-latest (dom, 1/2)
  • GitHub Check: test-node
  • GitHub Check: test-react-legacy (18, 1/2)
  • GitHub Check: lint
  • GitHub Check: test-react-legacy (18, 2/2)
  • GitHub Check: build
  • GitHub Check: visual-diff snapshot (2/2)
  • GitHub Check: visual-diff snapshot (1/2)
🔇 Additional comments (2)
components/_util/__tests__/scrollTo.test.ts (2)

87-97: LGTM!

此测试正确验证了取消滚动后回调函数不会被调用。逻辑清晰,断言准确。


115-120: LGTM!

此测试正确验证了取消函数可以多次调用而不抛出错误,并且返回 undefined。这是良好的防御性编程验证。

Comment thread components/anchor/__tests__/Anchor.test.tsx
Comment thread components/anchor/__tests__/Anchor.test.tsx
Comment thread components/anchor/__tests__/Anchor.test.tsx
Comment thread components/anchor/Anchor.tsx Outdated
Co-authored-by: afc163 <afc163@gmail.com>
Signed-off-by: 遇见同学 <1875694521@qq.com>
@meet-student meet-student requested a review from afc163 November 24, 2025 06:42
@tuzixiangs
Copy link
Copy Markdown
Contributor Author

感谢 @meet-student@afc163 的帮助!学到了很多 🙏

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Nov 24, 2025
@afc163 afc163 merged commit e81da48 into ant-design:master Nov 24, 2025
36 of 37 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🎉 Thank you for your contribution! If you have not yet joined our DingTalk community group, please feel free to join us (when joining, please provide the link to this PR).

🎉 感谢您的贡献!如果您还没有加入钉钉社区群,请扫描下方二维码加入我们(加群时请提供此 PR 链接)。

钉钉社区群二维码

tuzixiangs added a commit to tuzixiangs/ant-design that referenced this pull request Nov 24, 2025
@PeachScript PeachScript mentioned this pull request Dec 2, 2025
17 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Anchor] 快速点击链接时滚动动画冲突导致的页面抖动问题

3 participants