Skip to content

Conversation

@anxLiang
Copy link
Contributor

@anxLiang anxLiang commented Nov 28, 2025

中文版模板 / Chinese template

🤔 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
  • 🛠 Refactoring
  • 🎨 Code style optimization
  • ✅ Test Case
  • 🔀 Branch merge
  • ⏩ Workflow
  • ⌨️ Accessibility improvement
  • ❓ Other (about what?)

🔗 Related Issues

  • Describe the source of related requirements, such as links to relevant issue discussions.
  • For example: close #xxxx, fix #xxxx

💡 Background and Solution

  • The specific problem to be addressed.
  • List the final API implementation and usage if needed.
  • If there are UI/interaction changes, consider providing screenshots or GIFs.

📝 Change Log

Language Changelog
🇺🇸 English Bubble.List scroll compatible with safari
🇨🇳 Chinese Bubble.List 滚动行为兼容 safari

Summary by CodeRabbit

发布说明

  • 新功能

    • 改进了气泡列表的滚动锁定机制,增强了Safari浏览器兼容性。
  • 测试

    • 新增详细的滚动行为测试套件,覆盖各类边界情景。
  • 文档

    • 更新了组件文档,调整演示示例列表。

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

@github-actions
Copy link
Contributor

github-actions bot commented Nov 28, 2025

Preview is ready

@dosubot dosubot bot added the bug Something isn't working label Nov 28, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 28, 2025

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • packages/x/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap is excluded by !**/*.snap

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

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

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

🎯 概览

新增 useCompatibleScroll React 钩子实现 Safari 兼容的反向弹性容器滚动锁定,集成至 BubbleList 组件,新增comprehensive测试套件,更新文档演示及样式注释。

📋 变更

群组 / 文件 变更摘要
滚动逻辑实现
packages/x/components/bubble/hooks/useCompatibleScroll.ts, packages/x/components/bubble/BubbleList.tsx
新增 useCompatibleScroll 钩子,利用 IntersectionObserver 和 MutationObserver 实现 column-reverse 布局下的滚动锁定;在 BubbleList 中集成钩子并在滚动前调用 reset()
测试套件
packages/x/components/bubble/__tests__/list-scroll.test.tsx
新增全面的单元测试,涵盖初始化流程、哨兵元素、观察器行为、滚动处理、重置、滚动锁定、变动处理及边界情况
文档与样式
packages/x/components/bubble/index.zh-CN.md, packages/x/components/bubble/style/list.ts
重组文档演示列表并新增"editable"演示;在样式中添加位置属性注释

🔄 序列图

sequenceDiagram
    participant App as 应用
    participant BubbleList as BubbleList<br/>组件
    participant useCSHook as useCompatibleScroll<br/>钩子
    participant DOM as DOM/<br/>容器
    participant IO as IntersectionObserver
    participant MO as MutationObserver
    participant ScrollEvent as 滚动事件

    App->>BubbleList: 挂载/传入 autoScroll
    BubbleList->>useCSHook: 初始化钩子<br/>(scrollBoxRef)
    
    alt 启用滚动锁定
        useCSHook->>DOM: 创建哨兵元素<br/>(底部标记)
        useCSHook->>IO: 注册IntersectionObserver<br/>监听哨兵可见性
        useCSHook->>MO: 注册MutationObserver<br/>监听DOM变化
    end
    
    Note over DOM,ScrollEvent: 用户交互或内容变化

    DOM->>ScrollEvent: 触发滚动事件
    ScrollEvent->>useCSHook: 捕获阶段监听<br/>更新 lockedScrollBottomPos
    
    alt 内容高度变化
        DOM->>MO: 变动回调触发
        MO->>useCSHook: 检测是否需要<br/>锁定滚动
        alt 需要锁定
            useCSHook->>useCSHook: enforceScrollLock()<br/>计算目标 scrollTop
            useCSHook->>DOM: 应用 scrollTop
        end
    end
    
    IO->>useCSHook: 哨兵可见性变化
    useCSHook->>useCSHook: 更新 isAtBottom 状态

    BubbleList->>useCSHook: 调用 reset()<br/>清除锁定状态
    
    alt 卸载/禁用
        useCSHook->>IO: 移除观察器
        useCSHook->>MO: 移除观察器
        useCSHook->>DOM: 移除哨兵元素
    end
Loading

⏱️ 代码审查工作量

🎯 3 (中等) | ⏱️ ~25 分钟

需要重点关注的区域:

  • useCompatibleScroll.ts 中复杂的状态管理逻辑(多个 refs、观察器初始化、滚动锁定计算)
  • 测试套件中的观察器模拟和 DOM 操作的准确性
  • BubbleList 中 reset() 调用时序与滚动效果的协调

🐰 诗赋

滚卷容器巧妙锁,
Safari 来也不用愁,
哨兵守底观变化,
丝滑滚动伴君侧,
响应式布局新篇章 🎉✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Pull request title clearly and specifically describes the main change: fixing Bubble.List scroll behavior to be compatible with Safari.

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @anxLiang, 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 a bug affecting the Bubble.List component's scrolling behavior in Safari browsers. It introduces a specialized React hook, useCompatibleScroll, to ensure that lists configured with flex-direction: column-reverse maintain their scroll position correctly, particularly when new items are dynamically added. This enhancement improves the user experience by providing consistent and reliable auto-scrolling across different browsers.

Highlights

  • Safari Scroll Compatibility: Implemented a new React hook (useCompatibleScroll) to address and fix scrolling behavior issues in Safari, specifically for Bubble.List components using flex-direction: column-reverse.
  • New Scroll Management Hook: Introduced useCompatibleScroll to manage scroll locking and position, ensuring a consistent user experience when new items are added to a reverse-scrolling list.
  • Comprehensive Testing: Added a dedicated test file (list-scroll.test.tsx) with extensive test cases for the new useCompatibleScroll hook, covering different browser user agents and scroll scenarios.
  • Demo Updates: Included new demo files (_test.md, _test.tsx) to showcase and test the Bubble.List component's auto-scrolling functionality with dynamic content.
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.

Copy link
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

这个 PR 旨在修复 Bubble.List 组件在 auto-scroll 模式下(即 flex-direction: column-reverse)于 Safari 浏览器中的滚动兼容性问题。核心实现是新增了一个自定义 Hook useCompatibleScroll,它通过 IntersectionObserverMutationObserver 来实现当用户向上滚动后锁定视窗位置的功能。这是一个很棒的解决方案。同时,作者为这个新 Hook 编写了非常全面的测试用例,值得称赞。

我对 useCompatibleScroll 这个 Hook 有两个建议,以提升其代码健壮性和性能。其中一个是关于 handleScroll 中存在陈旧闭包(stale closure)的高优先级 bug 修复,另一个是关于缓存一个昂贵计算的性能优化。

总体而言,这是一个高质量的 PR。

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Nov 28, 2025

Deploying ant-design-x with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7d45a4a
Status: ✅  Deploy successful!
Preview URL: https://a46beb82.ant-design-x.pages.dev
Branch Preview URL: https://fix-bubble.ant-design-x.pages.dev

View logs

Copy link
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: 6

🧹 Nitpick comments (11)
packages/x/components/bubble/style/list.ts (1)

28-28: 移除无用的注释代码或添加说明。

这行被注释的代码看起来像是调试或探索性代码的遗留。如果不需要,建议删除;如果有保留的原因(例如作为未来参考),请添加说明注释。

   [`${componentCls}-list-scroll-box`]: {
-      // position: 'relative',
       overflowY: 'auto',
packages/x/components/bubble/demo/_test.md (1)

5-7: 英文文档的语法可以优化。

根据静态分析提示,建议进行以下改进:

  • so as toto(更简洁)
  • high performancehigh-performance(作为形容词修饰 rendering 时应使用连字符)
-Bubble list with preset styles, supports automatic scrolling, supports using `role` to define different types of bubbles and set properties. **Bubble.List** is a controlled component, and **Bubble** is internally memoized, so it is recommended to use **setState** callback form to modify the `items` property, and try to ensure the stability of the configuration of non-essential rendering data items, so as to ensure the high performance rendering of **Bubble.List**.
+Bubble list with preset styles, supports automatic scrolling, supports using `role` to define different types of bubbles and set properties. **Bubble.List** is a controlled component, and **Bubble** is internally memoized, so it is recommended to use **setState** callback form to modify the `items` property, and try to ensure the stability of the configuration of non-essential rendering data items to ensure the high-performance rendering of **Bubble.List**.
packages/x/components/bubble/demo/_test.tsx (2)

21-23: 全局变量 id 在热重载时可能导致问题。

let id = 0 是模块级变量,在热模块替换(HMR)期间不会重置,可能导致 key 不一致或调试困难。对于演示代码这可能是可接受的,但建议使用 useRef 来管理 ID 计数器。


63-66: useBubbleList 返回值未完全使用。

hook 返回 [items, setItems, add, update],但只解构了 [items, set]addupdate 函数未被使用。如果这些函数是为了演示更多用法而设计的,这是合理的;否则可以简化 hook 的返回值。

packages/x/components/bubble/hooks/useCompatibleScroll.ts (1)

35-75: enforceScrollLock 应添加到 effect 的依赖数组中。

enforceScrollLock 在 MutationObserver 回调中被调用,但未包含在 effect 的依赖数组中。虽然当前实现因为 enforceScrollLock 只依赖 dom(已在依赖中)而可能不会出问题,但这不符合 React hooks 的最佳实践,且 ESLint exhaustive-deps 规则会报警。

    return () => {
      intersectionObserver.disconnect();
      mutationObserver.disconnect();
    };
- }, [dom, disable]);
+ }, [dom, disable, enforceScrollLock]);

注意:这需要将 enforceScrollLock 的定义移到 effect 之前,或者使用 useRef 来存储函数引用以避免循环依赖。

packages/x/components/bubble/__tests__/list-scroll.test.tsx (6)

4-20: UA mock 建议恢复原值以提升测试隔离性

这里通过 Object.defineProperty(navigator, 'userAgent', …) 改写全局 UA,但没有在 afterEach/afterAll 中恢复原值。当前文件内每个依赖 UA 的用例都会显式调用 mockUserAgent,所以功能上没问题,但如果后续有其他测试依赖真实 UA,可能被这里的 mock 污染。建议在 beforeAll 里缓存原始 UA,在 afterAll 里恢复,或者使用 jest.spyOn(window.navigator, 'userAgent', 'get') 方式更易维护。


90-130: 全局重写 IntersectionObserver/MutationObserver/requestAnimationFrame 建议显式恢复

beforeEach 中直接重写了 global.IntersectionObserverglobal.MutationObserverglobal.requestAnimationFrameafterEach 只做了 jest.clearAllMocks(),不会还原全局对象本身。这意味着一旦这个文件被执行,其 mock 会在整个测试进程中常驻,可能影响其他依赖这些 Web API 的用例。建议:

  • beforeAll 缓存原始引用;
  • afterAll 显式恢复全局到原始实现。

这样既不影响本文件测试,又能避免对其它测试套件的隐性影响。


244-275: 部分滚动相关用例断言过于薄弱,未真正验证目标行为

it('should handle intersection observer callback correctly', …)it('should handle scroll event and update locked position', …)

  • 前者只在调用 intersectionCallback 后断言 mockDom.scrollTop === 0,但 scrollTop 初始就是 0,而且 IntersectionObserver 回调实现本身并不会修改 scrollTop,所以即使回调完全不生效,这个断言也会通过。
  • 后者的描述是 “update locked position”,但测试只断言 scrollTop 仍然是 -500,并没有验证 lockedScrollBottomPos.current 是否按预期更新。

这类测试名和实际断言不匹配,容易给人一种“覆盖到了逻辑”的错觉。建议要么删除这些用例,要么改成更贴近实现的断言(例如通过触发 mutation 并检查最终 scrollTop 是否按锁定位置计算变化),避免纯覆盖驱动的“空心”测试。


345-360: resetToBottom 用例与描述不符,实质只重复类型检查

it('should reset to bottom correctly', …) 用例内部只是调用了一次 result.current.resetToBottom(),最后又断言了一次 typeof result.current.resetToBottom === 'function'。这与文件开头 it('should return resetToBottom function', …) 的用例基本重复,并没有验证“重置到底部”的行为(例如内部 ref 状态或后续滚动效果)。

建议要么移除这个用例,要么改为通过先模拟离开底部(intersection false + scroll),再调用 resetToBottom() 并结合 BubbleList 行为验证“回到底部”的实际效果。


362-372: “cleanup observers on unmount” 用例未真正断言清理逻辑

it('should cleanup observers on unmount', …) 当前只校验了 mockIntersectionObservermockMutationObserver 被调用过,这证明观察者被创建,但并没有断言 disconnect 是否在 unmount() 时被调用。

如果目的是验证卸载清理,建议在 mock 返回的对象上挂载可观察的 disconnect

mockIntersectionObserver.mockImplementation((callback) => {
  intersectionCallback = callback;
  return {
-   observe: jest.fn(),
-   disconnect: jest.fn(),
+   observe: jest.fn(),
+   disconnect: jest.fn(),
    unobserve: jest.fn(),
  };
});

然后在用例中断言 disconnect 被调用一次,才能真正覆盖清理逻辑。


485-553: 多组 disable 分支相关用例存在明显重复且偏向覆盖驱动

it('should skip initialization when disable is true (dom is null)', …)it('should verify disable branch coverage for enforceScrollLock', …) 这一大段测试,大多是:

  • 与前文已有用例功能重复(例如前面已经验证过 dom === nullonlySafari && !Safari 时不会初始化 observer);
  • 或仅通过“mock 未被调用 / 不抛异常”来声明覆盖了某个“disable 分支”,而不是从外部行为角度验证逻辑;
  • 部分描述还直接引用实现文件的“行号”,后续一旦重构实现,描述会很快失效。

建议适当收缩这一组用例,例如:

  • 保留一两条典型场景(dom === nullonlySafari + 非 Safari)验证 disable 行为;
  • 去掉那些仅为覆盖率而写、行为重复或依赖具体行号的测试;
  • 如果确实需要保证分支覆盖,可以通过参数化测试来减少重复代码。

这样可以让测试套件更聚焦于行为,而不是行号级别的覆盖。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b946b1 and ba0098c.

⛔ Files ignored due to path filters (2)
  • packages/x/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap is excluded by !**/*.snap
  • packages/x/components/bubble/__tests__/__snapshots__/demo.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • packages/x/components/bubble/BubbleList.tsx (3 hunks)
  • packages/x/components/bubble/__tests__/list-scroll.test.tsx (1 hunks)
  • packages/x/components/bubble/demo/_test.md (1 hunks)
  • packages/x/components/bubble/demo/_test.tsx (1 hunks)
  • packages/x/components/bubble/hooks/useCompatibleScroll.ts (1 hunks)
  • packages/x/components/bubble/index.zh-CN.md (1 hunks)
  • packages/x/components/bubble/style/list.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: afc163
Repo: ant-design/x PR: 0
File: :0-0
Timestamp: 2025-04-11T14:47:09.527Z
Learning: 当评审 ant-design/x 仓库中的 PR 时,需要用中文回复中文评论。该项目的文档支持中英双语。
Learnt from: afc163
Repo: ant-design/x PR: 0
File: :0-0
Timestamp: 2025-04-11T14:47:09.527Z
Learning: 当评审 ant-design/x 仓库中的 PR 时,需要用中文回复中文评论。该项目的文档支持中英双语。
📚 Learning: 2025-01-27T09:36:11.490Z
Learnt from: YumoImer
Repo: ant-design/x PR: 479
File: components/bubble/demo/debug-list.tsx:39-57
Timestamp: 2025-01-27T09:36:11.490Z
Learning: In the Bubble.List debug demo component, the different typing behavior between pushBubble (sets typing: true) and unshiftBubble (no typing property) operations is intentional to facilitate testing different scenarios.

Applied to files:

  • packages/x/components/bubble/demo/_test.md
  • packages/x/components/bubble/demo/_test.tsx
  • packages/x/components/bubble/style/list.ts
  • packages/x/components/bubble/__tests__/list-scroll.test.tsx
  • packages/x/components/bubble/index.zh-CN.md
🧬 Code graph analysis (2)
packages/x/components/bubble/BubbleList.tsx (1)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (1)
  • useCompatibleScroll (18-114)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (2)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (2)
  • isSafari (3-12)
  • useCompatibleScroll (18-114)
packages/x-sdk/tests/utils.tsx (1)
  • renderHook (41-53)
🪛 LanguageTool
packages/x/components/bubble/demo/_test.md

[style] ~7-~7: ‘So as to’ expresses purpose and is used in formal texts. Consider using “to”.
Context: ... of non-essential rendering data items, so as to ensure the high performance rendering o...

(SO_AS_TO)


[grammar] ~7-~7: Use a hyphen to join words.
Context: ...ing data items, so as to ensure the high performance rendering of Bubble.List...

(QB_NEW_EN_HYPHEN)

⏰ 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). (5)
  • GitHub Check: build preview
  • GitHub Check: size
  • GitHub Check: test
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
packages/x/components/bubble/BubbleList.tsx (1)

169-173: 滚动逻辑改进看起来合理。

在执行标准滚动逻辑之前调用 resetToBottom() 来重置状态,这符合 Safari 兼容性修复的目的。依赖数组中正确包含了 resetToBottom

packages/x/components/bubble/index.zh-CN.md (1)

22-32: 确认隐藏演示代码是否为预期行为。

多个列表相关的演示代码被注释掉了(包括 list.tsxlist-scroll.tsxsemantic-list-custom.tsxlist-extra.tsx)。如果这是为了暂时隐藏调试用的演示或者尚未准备好发布的功能,这是合理的。

请确认这些演示是否应该在后续重新启用,或者这只是针对此 PR 的临时调整。

@github-actions
Copy link
Contributor

github-actions bot commented Nov 28, 2025

size-limit report 📦

Path Size
packages/x/dist/antdx.min.js 55.81 KB (+395 B 🔺)
packages/x-sdk/dist/x-sdk.min.js 7.29 KB
packages/x-markdown/dist/x-markdown.min.js 50.66 KB
packages/x-markdown/dist/plugins/code-high-light.min.js 313.47 KB
packages/x-markdown/dist/plugins/latex.min.js 61.95 KB
packages/x-markdown/dist/plugins/mermaid.min.js 881.82 KB

@codecov
Copy link

codecov bot commented Nov 28, 2025

Bundle Report

Changes will increase total bundle size by 5.15MB (124.9%) ⬆️⚠️, exceeding the configured threshold of 5%.

Bundle name Size Change
x-markdown-array-push 9.14MB 5.01MB (121.54%) ⬆️⚠️
antdx-array-push* 138.56kB 138.56kB (100%) ⬆️⚠️

ℹ️ *Bundle size includes cached data from a previous commit

Affected Assets, Files, and Routes:

view changes for bundle: x-markdown-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
mermaid.js (New) 9.14MB 9.14MB 100.0% 🚀
mermaid.min.js (Deleted) -4.12MB 0 bytes -100.0% 🗑️

@kimteayon kimteayon changed the title Fix/bubble fix: Bubble.List scroll compatible with safari Dec 1, 2025
Copy link
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: 1

♻️ Duplicate comments (1)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (1)

16-16: 建议使用 useMemo 缓存 disable 计算结果。

disable 常量在每次渲染时都会重新计算,其中包含开销较大的 getComputedStyle(dom) 调用,可能触发样式重新计算。建议使用 useMemo 缓存这个计算结果,以避免不必要的重复计算,特别是当使用此 Hook 的组件频繁重新渲染时。

应用此 diff 来缓存计算结果:

+import { useCallback, useEffect, useMemo, useRef } from 'react';

// ...

-  const disable = !dom || getComputedStyle(dom).flexDirection !== 'column-reverse';
+  const disable = useMemo(
+    () => !dom || getComputedStyle(dom).flexDirection !== 'column-reverse',
+    [dom],
+  );
🧹 Nitpick comments (2)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (2)

66-82: 添加空值检查以避免运行时错误。

回调函数在第 67 行使用了 dom! 非空断言,但没有运行时检查。如果 dom 在渲染之间变为 null,访问其属性会导致运行时错误。

应用此 diff 添加安全检查:

  const handleScroll = useCallback(() => {
+   if (!dom) return;
    const { scrollTop, scrollHeight } = dom;
    // 倒序, top 在变化,但 bottom 固定
    lockedScrollBottomPos.current = scrollHeight + scrollTop;
    // 检测并恢复自然触发状态
    if (callOnScrollNotNative.current) {
      callOnScrollNotNative.current = false;
      return;
    }
    if (scrolling.current) {
      clearTimeout(scrolling.current);
    }
    scrolling.current = setTimeout(() => {
      clearTimeout(scrolling.current);
      scrolling.current = undefined;
    }, 50);
  }, [dom]);

111-116: 代码逻辑正确,但可以更明确地处理类型安全。

虽然第 112 行的 disable 检查已经隐式保证了 dom 不为空(因为 disable!dom 时为 true),但 TypeScript 可能无法识别这种控制流。可以考虑添加显式的空值检查以提高类型安全性和代码可读性。

可选的改进方案:

  const resetToBottom = useCallback(() => {
-   if (disable) return;
+   if (!dom || disable) return;
    isAtBottom.current = true;
    shouldLock.current = false;
    lockedScrollBottomPos.current = dom.scrollHeight;
  }, [dom, disable]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba0098c and d406b1f.

⛔ Files ignored due to path filters (1)
  • packages/x/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • packages/x/components/bubble/__tests__/list-scroll.test.tsx (1 hunks)
  • packages/x/components/bubble/demo/_test.tsx (1 hunks)
  • packages/x/components/bubble/hooks/useCompatibleScroll.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/x/components/bubble/tests/list-scroll.test.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-01-27T09:36:11.490Z
Learnt from: YumoImer
Repo: ant-design/x PR: 479
File: components/bubble/demo/debug-list.tsx:39-57
Timestamp: 2025-01-27T09:36:11.490Z
Learning: In the Bubble.List debug demo component, the different typing behavior between pushBubble (sets typing: true) and unshiftBubble (no typing property) operations is intentional to facilitate testing different scenarios.

Applied to files:

  • packages/x/components/bubble/demo/_test.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). (4)
  • GitHub Check: size
  • GitHub Check: build preview
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (1)

19-64: 哨兵元素和观察器的管理实现良好。

哨兵元素的创建、插入和清理逻辑实现正确:

  • 哨兵元素在需要时被创建并插入 DOM
  • IntersectionObserver 和 MutationObserver 正确设置并观察相应的目标
  • 清理函数完整地断开观察器连接并移除哨兵元素(第 59-62 行)
  • 依赖数组包含了所有必要的依赖项

这解决了之前关于哨兵元素清理的问题。

packages/x/components/bubble/demo/_test.tsx (4)

70-70: 定时器清理逻辑正确实现。

第 70 行的 useEffect 清理函数正确地在组件卸载时清理定时器,解决了之前关于定时器未清理的问题。各个按钮的点击处理中也正确地调用了 clearInterval 并将定时器引用设置为 null


36-61: 自定义 Hook 实现良好。

useBubbleList Hook 的实现正确:

  • 使用 useCallback 正确地记忆化了 addupdate 回调
  • 两个回调都使用函数式更新 setItems,因此空依赖数组是正确的
  • Hook 接口清晰,可复用性强

72-88: 角色配置正确使用了记忆化。

memoRole 配置正确地使用 useMemo 进行记忆化,避免每次渲染时创建新的角色配置对象。配置结构合理,包含了 AI 和用户角色的头像、标题和操作项等属性。


90-139: 演示组件实现完整且正确。

App 组件提供了完整的测试演示界面:

  • 控制面板提供了启动、停止、清空和自动滚动切换功能
  • 定时器逻辑正确地在 AI 和用户消息之间交替添加内容
  • 使用 Bubble.List 的所有相关属性(ref、role、items、autoScroll)
  • 这个演示很好地测试了新的 Safari 兼容滚动行为

Copy link
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

♻️ Duplicate comments (1)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (1)

409-455: 关于“unmount 后 dom 变为 null/undefined”的注释依然容易产生误导

在以下两个 edge case 用例中:

  • Line 435 注释:// Unmount to set dom to null
  • Line 532 注释:// Unmount to set dom to undefined

实际实现里,useCompatibleScroll 接收的是一个普通的 dom 参数(非 ref),React 组件卸载并不会把这个闭包变量改成 null/undefined。unmount 只会触发 effect cleanup,让 IntersectionObserver / MutationObserver 停止工作,但闭包里的 dom 仍然指向原始元素。

因此:

  • it('should not throw error when enforcing scroll lock with null dom', …) 实际验证的是“在已 unmount 后手动调用 mutationCallback 不会抛错”,但并没有真正覆盖 domnull 的逻辑分支;
  • it('should not throw error when dom becomes undefined after mount', …) 同理,是在卸载后调用 resetToBottom 不抛错,而不是 dom 真的变成 undefined

建议:

  • 如果当前目标仅是“卸载后调用相关回调不抛异常”,可以把注释改成类似“// Unmount to simulate calls after cleanup” 并适度调整用例名称,避免提到“set dom to null/undefined”;
  • 如果确实需要覆盖 dom === null/undefined 的防御分支,可以考虑让 hook 从一个 ref.current 读取 DOM,并在 cleanup 时显式置空,然后在测试里通过修改 ref 来驱动该分支。

这些点与之前 review 中对“dom 为空分支未真正覆盖”的反馈很接近,建议一并调整,减少后续阅读和维护时的困惑。

Also applies to: 521-541

🧹 Nitpick comments (2)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (2)

190-212: 多处测试用例断言过弱或缺失,容易产生“虚假覆盖”

以下几个用例的名称与实际断言不太匹配,基本只是在验证“代码不抛异常”,而没有真正检查行为是否符合预期:

  • Line 191–211:'should update isAtBottom state when sentinel intersects' —— 只调用了 intersectionCallback,没有任何断言,看不出 isAtBottom/shouldLock 是否真的被更新。
  • Line 298–318:'should reset internal state to bottom position' —— 实际只验证 resetToBottom 是函数,这一点前面的“Hook Return Value”用例已经覆盖,并未检查滚动锁定状态/内部 ref 是否被重置。
  • Line 333–364:'should calculate correct target scroll position' —— 只是手工算了一遍 700 - 1200 = -500,没有通过触发 mutationCallback 等路径去验证 hook 实际算出来的 target scroll 是否正确。
  • Line 459–490:'should handle multiple rapid mutations'、以及 Line 492–518:'should not enforce scroll lock when at bottom' —— 都是多次触发 mutationCallback/scroll,但没有检查最终 scrollTop、锁定位置或者任何状态变化。
  • Line 543–563:'should handle scroll event correctly when not at bottom' —— 只触发了 scroll,没有断言 lockedScrollBottomPos 或相关效果。

从测试可维护性和信心角度来看,建议:

  • 要么为这些用例补充针对性断言(例如通过更精细的 mock 捕获 enforceScrollLock 内部计算结果、最终 scrollTop 或内部状态 ref 的变化);
  • 要么将用例名称改成更符合当前行为的描述,如“… should not throw when …”,避免给人“关键逻辑已被验证”的错觉。

这样可以显著提升这些复杂滚动场景测试的真实覆盖度。

Also applies to: 298-318, 332-364, 458-490, 492-518, 543-563


566-587: Cleanup 用例目前只验证了“创建过 observer”,并未真正覆盖清理逻辑

describe('Cleanup', …) 的用例中:

const { unmount } = renderHook(() => useCompatibleScroll(mockDom));

expect(mockDom.firstChild).toBeTruthy();

unmount();

expect(mockIntersectionObserver).toHaveBeenCalled();
expect(mockMutationObserver).toHaveBeenCalled();

这里的断言只保证了在 mount 阶段确实 new 过 IntersectionObserver / MutationObserver,但并没有检查:

  • disconnect / unobserve 是否在 unmount 时被调用;
  • 底部 sentinel 元素是否从 DOM 中被移除;
  • 滚动监听器是否被 removeEventListener 正确解绑。

也就是说,即使把 effect 的 cleanup 完全删掉,只保留创建逻辑,这个测试也仍然会通过。

建议:

  1. 在 mock 实现里保存每个 observer 实例的 disconnect/unobserve 函数,并在用例里断言它们在 unmount() 后被调用。
  2. 在 unmount 前后分别检查 mockDom.firstChild(或通过 querySelector)以确认 sentinel 元素被移除。
  3. 如有需要,可以复用 addEventListener/removeEventListener 的 mock 模式,断言 scroll 监听也在 cleanup 中解绑。

这样可以让 “Cleanup” 这个用例名与真正验证的行为更一致,提高对资源释放、避免泄漏的信心。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d406b1f and 3d47899.

📒 Files selected for processing (2)
  • packages/x/components/bubble/__tests__/list-scroll.test.tsx (1 hunks)
  • packages/x/components/bubble/hooks/useCompatibleScroll.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/x/components/bubble/hooks/useCompatibleScroll.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (2)
packages/x-sdk/tests/utils.tsx (1)
  • renderHook (41-53)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (1)
  • useCompatibleScroll (7-120)
⏰ 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). (5)
  • GitHub Check: size
  • GitHub Check: build preview
  • GitHub Check: test
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (2)

41-88: 整体测试结构和 mock 布局设计得比较清晰

整体的 beforeEach/afterEach 里集中创建 mockDom、挂载到 document.body,并统一 mock IntersectionObserver / MutationObserver,结构清晰、可读性和复用性都不错,为后续细分用例打了一个比较扎实的基础。


240-266: 滚动监听 capture 选项的用例覆盖点很好

这里通过替换 addEventListener/removeEventListener 并断言:

expect(mockAddEventListener).toHaveBeenCalledWith(
  'scroll',
  expect.any(Function),
  { capture: true },
);

明确保证了 hook 在 Safari 兼容场景下是用捕获阶段监听 scroll 事件的(避免只在冒泡阶段拿不到事件的问题),这一点与 PR 目标高度相关,测试写得比较到位。

Copy link
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: 0

♻️ Duplicate comments (4)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (2)

16-16: 避免在每次渲染中重复调用 getComputedStyle,建议用 useMemo 缓存 disable

当前 disable 每次渲染都会调用一次 getComputedStyle(dom),在频繁重渲染场景下可能触发不必要的样式计算。可以用 useMemo 缓存结果,只在 dom 变化时重新计算,例如:

-import { useCallback, useEffect, useRef } from 'react';
+import { useCallback, useEffect, useMemo, useRef } from 'react';

-  const disable = !dom || getComputedStyle(dom).flexDirection !== 'column-reverse';
+  const disable = useMemo(
+    () => !dom || getComputedStyle(dom).flexDirection !== 'column-reverse',
+    [dom],
+  );

这对 Safari 兼容逻辑没有行为变化,只是减少 layout 相关的开销。


91-120: enforceScrollLock 增加 DOM 空值防御,提升未来改动下的健壮性

目前实现依赖“只在 dom 有值且非 disable 时才注册 MutationObserver”,因此运行时 dom 理论上不会为 nulldom! 也不会真的报错。不过一旦未来将 dom 换成 ref.current 或在外层出现动态置空(例如卸载前后复用 hook),这里会是第一处潜在 NPE。

可以通过在函数开头增加一次空值检查,同时合并对 scrolling.current 的判断,去掉 ! 断言,更安全一些:

-  const enforceScrollLock = useCallback(() => {
+  const enforceScrollLock = useCallback(() => {
+    if (!dom || scrolling.current) return;
@@
-    // requestAnimationFrame(() => {
-    if (scrolling.current) return;
-    const targetScroll = lockedScrollBottomPos.current - dom!.scrollHeight;
-    dom!.scrollTop = targetScroll;
+    // requestAnimationFrame(() => {
+    const targetScroll = lockedScrollBottomPos.current - dom.scrollHeight;
+    dom.scrollTop = targetScroll;
@@
-  }, [dom]);
+  }, [dom]);

这不会改变当前行为,只是为后续重构留出更大的安全空间。

packages/x/components/bubble/__tests__/list-scroll.test.tsx (2)

85-88: afterEach 中增加 jest.restoreAllMocks(),避免 getComputedStyle 等 spy 泄漏到其他用例

多个用例中用 jest.spyOn(window, 'getComputedStyle') 并调用 mockImplementation,但 afterEach 只做了 jest.clearAllMocks()。这只会清空调用记录,不会把被 spy 的属性还原,导致后续测试仍然使用 mock 版本的 getComputedStyle,容易产生测试之间的隐式耦合。

建议在 afterEach 中同时恢复所有 spy:

-  afterEach(() => {
-    document.body.innerHTML = '';
-    jest.clearAllMocks();
-  });
+  afterEach(() => {
+    document.body.innerHTML = '';
+    jest.restoreAllMocks();
+    jest.clearAllMocks();
+  });

这样每个用例都会从干净的浏览器 API 状态开始。


268-295: 避免直接覆盖 global.clearTimeout,改用 jest.spyOn 以防全局状态污染

当前测试通过赋值方式替换全局 clearTimeout

const mockClearTimeout = jest.fn();
global.clearTimeout = mockClearTimeout;

问题在于:

  • 这不是 Jest 的 spy,对应的原实现不会在 jest.clearAllMocks() 中自动恢复;
  • 钩子在 effect cleanup 里依赖真实的 clearTimeout 进行资源回收,被替换后可能影响其他测试的行为。

更推荐用 jest.spyOn 并在用例结束后恢复:

-      // Set up a timeout to simulate existing scrolling.current
-      const mockClearTimeout = jest.fn();
-      global.clearTimeout = mockClearTimeout;
+      const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
@@
-      expect(mockClearTimeout).toHaveBeenCalled();
+      expect(clearTimeoutSpy).toHaveBeenCalled();
+      clearTimeoutSpy.mockRestore();

如果配合上方 afterEach 中的 jest.restoreAllMocks(),可以省略显式的 mockRestore()

🧹 Nitpick comments (1)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (1)

50-83: 全局替换 IntersectionObserver / MutationObserver 建议在测试结束时显式恢复

beforeEach 中通过赋值方式把 global.IntersectionObserverglobal.MutationObserver 替换为 jest.fn 实现,这对本文件内部测试是方便的,但同样会在文件级别之外保留 mock 版本,潜在影响其他依赖这些原生 API 的测试。

可以考虑:

  • beforeAll 保存原实现,在 afterAll 里还原;或
  • afterEach 中显式恢复为保存的原实现。

示例:

let OriginalIntersectionObserver = global.IntersectionObserver;
let OriginalMutationObserver = global.MutationObserver;

beforeAll(() => {
  OriginalIntersectionObserver = global.IntersectionObserver;
  OriginalMutationObserver = global.MutationObserver;
});

afterAll(() => {
  global.IntersectionObserver = OriginalIntersectionObserver;
  global.MutationObserver = OriginalMutationObserver;
});

这样既保留当前文件的可测试性,又降低整个测试套件的全局副作用。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d47899 and e794d73.

📒 Files selected for processing (2)
  • packages/x/components/bubble/__tests__/list-scroll.test.tsx (1 hunks)
  • packages/x/components/bubble/hooks/useCompatibleScroll.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (2)
packages/x-sdk/tests/utils.tsx (1)
  • renderHook (41-53)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (1)
  • useCompatibleScroll (7-121)
⏰ 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). (5)
  • GitHub Check: test
  • GitHub Check: build preview
  • GitHub Check: size
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (1)

19-64: 哨兵插入与观察者清理逻辑整体合理

  • 只在 disable === false 时创建 sentinel 和两个 Observer,避免在非倒序/无 DOM 时做多余工作。
  • cleanup 中同时 disconnect()clearTimeout(scrolling.current),并从父节点移除 sentinel,避免 DOM 污染和计时器泄漏。
  • effect 依赖 [dom, disable],在容器/布局变化时会重新挂载观察逻辑,和滚动锁定语义一致。

这块实现从鲁棒性和资源清理角度看都比较到位。

packages/x/components/bubble/__tests__/list-scroll.test.tsx (1)

190-583: 测试覆盖面很完整,基本行为与边界情况都已覆盖

整体来看,这个测试文件已经很好地覆盖了新 hook 的关键路径:

  • 初始化/禁用条件(domnull、非 column-reverse);
  • sentinel 创建及样式;
  • IntersectionObserver / MutationObserver 配置与回调行为;
  • 滚动事件监听(包括 capture 选项、timeout 清理)、滚动锁定与 resetToBottom
  • 多次 mutation、滚动中/在底部等边界行为;
  • 卸载后的清理和不会抛错的场景。

在解决上面几个 mock 污染问题后,这套测试对于防止未来改动引入 Safari 回归会非常有帮助。

Copy link
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: 0

🧹 Nitpick comments (1)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (1)

333-364: 目标滚动位置计算测试可增强断言

当前测试仅验证了 expectedTargetScroll 的计算结果(-500),但未直接验证 hook 是否真正将 scrollTop 设置为该值。虽然测试计算逻辑本身是有效的,但如果能够在触发 mutationCallback 后验证 mockDom.scrollTop 的实际值,测试会更加健壮。

可以考虑在计算后触发 mutation 并验证实际的 scrollTop 值:

 // Change scrollHeight to simulate content being added
 Object.defineProperty(mockDom, 'scrollHeight', { value: 1200, writable: true });

+// Trigger mutation to enforce scroll lock
+act(() => {
+  intersectionCallback([{ isIntersecting: false }]); // Ensure shouldLock is true
+  mutationCallback();
+});
+
+// Verify scrollTop was set to the calculated target
+expect(mockDom.scrollTop).toBe(-500);
+
 // Check that the target scroll position would be calculated correctly
 // targetScroll = lockedScrollBottomPos.current - dom!.scrollHeight
 // targetScroll = 700 - 1200 = -500
 // We can't directly access lockedScrollBottomPos, but we can verify the calculation
 // by checking the expected result
 const expectedTargetScroll = 700 - 1200;
 expect(expectedTargetScroll).toBe(-500);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72c6c8c and f94a01a.

📒 Files selected for processing (1)
  • packages/x/components/bubble/__tests__/list-scroll.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (2)
packages/x-sdk/tests/utils.tsx (1)
  • renderHook (41-53)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (1)
  • useCompatibleScroll (7-121)
⏰ 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). (5)
  • GitHub Check: build preview
  • GitHub Check: test
  • GitHub Check: size
  • GitHub Check: Cloudflare Pages
  • GitHub Check: test
🔇 Additional comments (6)
packages/x/components/bubble/__tests__/list-scroll.test.tsx (6)

85-89: 测试清理逻辑完善

afterEach 钩子中正确调用了 jest.restoreAllMocks()jest.clearAllMocks(),确保了测试之间的隔离性,避免了 mock 状态泄漏。这很好地解决了之前评审中指出的问题。


98-165: 初始化场景测试覆盖全面

测试套件充分覆盖了各种初始化场景:

  • dom 为 null 时不初始化
  • flexDirection 非 column-reverse 时不初始化
  • 正常场景下正确初始化 observers
  • MutationObserver 配置选项验证

测试使用 jest.spyOn 方式 mock getComputedStyle,配合 afterEach 中的 restoreAllMocks() 确保了测试间的隔离。


269-294: 快速滚动时 timeout 清理测试改进

测试现在正确使用 jest.spyOn(global, 'clearTimeout') 代替直接赋值,避免了全局状态污染。spy 会在 afterEach 中通过 jest.restoreAllMocks() 自动恢复,确保不会影响后续测试。


297-330: Reset to Bottom 功能测试充分

测试覆盖了 resetToBottom() API 的正常场景和边界情况(hook 被禁用时),验证了该方法不会在禁用状态下抛出异常,体现了良好的防御性设计。


454-514: Mutation 处理测试覆盖完整

测试充分验证了 MutationObserver 的行为:

  • 快速多次 DOM 变更场景
  • 在底部时不强制锁定滚动位置

循环模拟 5 次快速 mutation 的压力测试很好地验证了 hook 在高频变更下的稳定性。


516-584: 边界情况和清理逻辑测试完备

测试套件的收尾部分充分验证了:

  • 组件卸载后调用 API 不抛异常
  • 非底部滚动事件的正确处理
  • observers 和哨兵元素的正确清理

整体测试文件结构清晰、覆盖全面,充分验证了 useCompatibleScroll hook 在各种场景下的行为,包括正常流程、边界情况和异常处理。

@codecov
Copy link

codecov bot commented Dec 1, 2025

Codecov Report

❌ Patch coverage is 96.77419% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.25%. Comparing base (8fbbdbb) to head (7d45a4a).

Files with missing lines Patch % Lines
...s/x/components/bubble/hooks/useCompatibleScroll.ts 96.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1392      +/-   ##
==========================================
+ Coverage   94.21%   94.25%   +0.04%     
==========================================
  Files         144      145       +1     
  Lines        3701     3763      +62     
  Branches     1036     1043       +7     
==========================================
+ Hits         3487     3547      +60     
- Misses        212      214       +2     
  Partials        2        2              

☔ 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.

Copy link
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: 0

♻️ Duplicate comments (2)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (2)

16-17: disable 每次渲染都会触发一次 getComputedStyle,建议做轻量缓存

当前实现功能上是正确的,但在频繁重渲染的场景下,getComputedStyle(dom) 可能成为不必要的性能开销;可以用 useMemo 包一层,只在 dom(以及后续若增加的开关,例如 onlySafari)变化时重新计算 disable。


91-109: enforceScrollLock 建议增加 dom 判空以提升防御性

从当前调用链看(只有在 disable === false 且 dom 已存在时创建 Observer 并触发回调),dom! 理论上不会为 null,但这里完全依赖调用方不误用 Hook;在函数顶部加一行 if (!dom) return; 并去掉后续的非空断言,可以在保持现有行为不变的前提下,让未来扩展或复用时更安全可预期。

🧹 Nitpick comments (2)
packages/x/components/bubble/hooks/useCompatibleScroll.ts (2)

8-15: 部分内部 ref(例如 isAtBottom)当前未被读取,容易造成困惑

现在只在 IntersectionObserver 回调里更新 isAtBottom.current,但从未被读出;如果短期没有对外暴露或内部使用的计划,可以考虑删除这个 ref,或在返回值中一起暴露状态,减少“看起来要用但其实没用”的干扰。


66-82: 滚动处理逻辑清晰,可以考虑抽出 debounce 时间为常量

handleScroll 通过 lockedScrollBottomPos 记录“视窗底部”的绝对位置,并用 callOnScrollNotNative 区分程序触发与自然滚动,依赖数组也更新为 [dom] 避免陈旧闭包;可以把 50ms 提取成常量(例如 SCROLL_IDLE_DELAY),方便后续调参和在单测中缩短等待时间。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f94a01a and 09fadf5.

📒 Files selected for processing (3)
  • packages/x/components/bubble/BubbleList.tsx (3 hunks)
  • packages/x/components/bubble/__tests__/list-scroll.test.tsx (1 hunks)
  • packages/x/components/bubble/hooks/useCompatibleScroll.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/x/components/bubble/tests/list-scroll.test.tsx
🔇 Additional comments (5)
packages/x/components/bubble/BubbleList.tsx (2)

10-10: 引入 useCompatibleScroll 作为独立 Hook 的方向是合理的

BubbleList 只负责选择性地把 scrollBox DOM 传给 Hook,本身不再关心 Safari 兼容细节,职责边界更清晰。


142-173: 在滚动副作用中调用 reset 与滚动指令的顺序是合理的

if (!scrollBoxRef.current) return; 的保护下,再调用 reset()scrollTo,既避免了首帧 ref 为空的异常,也能在最后一条消息变化或 autoScroll 切换时先重置锁定基准,再执行滚动,行为符合预期;依赖数组里加入 reset 也避免了闭包旧函数的问题。

packages/x/components/bubble/hooks/useCompatibleScroll.ts (3)

18-64: 哨兵元素与 Observer 的初始化和清理现在比较完备

disable 为真时直接返回,启用时只在 sentinelRef.current 为空时插入一个哨兵节点,并在清理阶段断开 Intersection/MutationObserver、清除 scrolling 定时器并从 DOM 中移除 sentinel,这样多次挂载或切换容器时不会遗留脏节点,整体资源管理是可靠的。


84-89: 滚动事件监听的绑定与清理路径是合理的

仅在 disable === false 时才绑定捕获阶段的 scroll 监听,并在清理时通过 dom?.removeEventListener 解注册,既避免了 dom 为空时的异常,也不会在禁用场景下多余地监听滚动事件。


111-120: reset 的语义与 BubbleList 的使用方式契合

先在 reset 中检查 disable 再读取 dom.scrollHeight,避免 dom 为空时访问属性的问题,同时把 lockedScrollBottomPos 重置为当前内容高度,使得在外部(如 BubbleList)追加消息前先调用 reset() 时,后续的锁底逻辑有一个正确的基准点。

Copy link
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: 0

🧹 Nitpick comments (1)
packages/x/components/bubble/index.zh-CN.md (1)

155-162: 建议补充 Bubble.List API 文档

根据 PR 摘要,新增了 useCompatibleScroll 钩子并在 BubbleList 中集成,包括 reset() 方法。建议在 Bubble.List 的 API 部分补充相关说明,例如:

  • 说明新的 Safari 兼容性改进
  • 文档化 reset() 方法的用途和用法(如果这是公开 API)
  • 更新 autoScroll 行为的说明

这将帮助用户更好地理解和使用新功能。


总体评价

这份文档文件的改动主要是表现层调整,包括:

  1. ✅ 添加了 <!-- prettier-ignore --> 注释以控制代码风格
  2. ✅ 重新组织了演示代码的格式
  3. ⚠️ 新增的测试演示被注释掉(请确认这是预期行为)

建议

  • 如果测试演示已准备好,请取消注释以发布到文档
  • 考虑在 Bubble.List API 部分补充 Safari 兼容性改进的相关说明(如新增的 reset() 方法的用途)

Based on learnings, 这份回复使用中文以符合 ant-design/x 仓库的评审习惯。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09fadf5 and 442f0c3.

📒 Files selected for processing (1)
  • packages/x/components/bubble/index.zh-CN.md (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: afc163
Repo: ant-design/x PR: 0
File: :0-0
Timestamp: 2025-04-11T14:47:09.527Z
Learning: 当评审 ant-design/x 仓库中的 PR 时,需要用中文回复中文评论。该项目的文档支持中英双语。
Learnt from: afc163
Repo: ant-design/x PR: 0
File: :0-0
Timestamp: 2025-04-11T14:47:09.527Z
Learning: 当评审 ant-design/x 仓库中的 PR 时,需要用中文回复中文评论。该项目的文档支持中英双语。
📚 Learning: 2025-01-27T09:36:11.490Z
Learnt from: YumoImer
Repo: ant-design/x PR: 479
File: components/bubble/demo/debug-list.tsx:39-57
Timestamp: 2025-01-27T09:36:11.490Z
Learning: In the Bubble.List debug demo component, the different typing behavior between pushBubble (sets typing: true) and unshiftBubble (no typing property) operations is intentional to facilitate testing different scenarios.

Applied to files:

  • packages/x/components/bubble/index.zh-CN.md
⏰ 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). (5)
  • GitHub Check: size
  • GitHub Check: build preview
  • GitHub Check: test
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
packages/x/components/bubble/index.zh-CN.md (1)

21-35: 注意:测试演示已被注释掉

第 21 行的测试演示被注释掉了,这表示该演示还未准备好发布到正式文档中。请确认这是预期行为。如果测试演示已经准备好,应取消注释以供用户查看。

格式化改动(添加 <!-- prettier-ignore --> 和重新整理 demo 条目)本身没有问题,有助于保持代码风格一致。

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Dec 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants