Skip to content

Conversation

@aojunhao123
Copy link
Collaborator

@aojunhao123 aojunhao123 commented Nov 15, 2025

Summary by CodeRabbit

发行说明

  • 新功能

    • 新增 Listy 组件,支持高性能虚拟化列表渲染
    • 支持数据分组与粘性分组表头
    • 支持无限滚动、按需加载等交互模式
    • 支持程序化滚动控制接口
  • 文档

    • 新增完整的组件示例演示和使用文档

@coderabbitai
Copy link

coderabbitai bot commented Nov 15, 2025

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.

变更总结

概览

将项目从 Trigger 组件库(弹出层管理)完全重构为 Listy 组件库(虚拟列表)。包括包名称变更、组件实现替换、新增虚拟列表、分组、粘性标题和无限滚动等功能,以及对应的测试和文档。

变更明细

组件/文件 变更摘要
配置与包管理
package.json, .dumirc.ts, tsconfig.json, jest.config.js, now.json
包名从 @rc-component/trigger 改为 @rc-component/listy;版本升至 1.0.0;更新依赖项(增加 rc-virtual-list);删除 now.json;更新别名映射和主题配置
核心组件实现
src/List.tsx, src/index.ts, index.js
新增 Listy 列表组件及其 forwardRef 包装;支持虚拟化、分组、粘性标题、无限滚动等功能;导出 ListyRef 和 ListyProps 类型;删除旧的 Trigger 相关导出
自定义 Hooks
src/hooks/*
新增 useGroupSegments(分组分割)、useFlattenRows(行展平)、useStickyGroupHeader(粘性头)、useOnEndReached(底部加载)4 个 Hook 及其索引文件
类型定义
src/interface.ts
替换为 Listy 专用类型:ListyRef、ListyProps、Group、RowKey;移除旧的 Trigger 对齐/偏移配置类型;新增 ScrollAlign 和相关滚动配置类型
工具函数
src/util.ts, src/util/index.ts
删除原 Trigger 相关工具函数;新增 isGroupScrollConfig 类型守卫函数
示例与文档
docs/examples/*, docs/demos/*, docs/index.md, README.md
新增 5 个示例文件(basic、endless-scrolling、group、load-on-demand、no-virtual)及对应的 markdown 文档;更新 README 和首页描述
样式
assets/index.less
重构样式:删除动画和 arrow 相关规则;新增分组头和粘性头样式;更新前缀类名变量
测试
tests/*
新增 4 个测试文件:hooks.test.tsx(Hook 单元测试)、listy.behavior.test.tsx(行为测试)、listy.test.tsx(组件功能测试)、onEndReached.test.tsx(无限滚动集成测试)

序列图

sequenceDiagram
    participant User as 用户
    participant Listy as Listy 组件
    participant Hooks as 自定义 Hooks
    participant VirtualList as rc-virtual-list
    participant DOM as DOM

    User->>Listy: 传入 items, group, itemRender
    Listy->>Hooks: useGroupSegments(items, group)
    Hooks-->>Listy: 返回分组分割数据
    Listy->>Hooks: useFlattenRows(items, group, segments)
    Hooks-->>Listy: 返回展平的行数组(含头行)
    Listy->>Hooks: useStickyGroupHeader(enabled, group, ...)
    Hooks-->>Listy: 返回粘性头渲染函数
    Listy->>VirtualList: 传入行数据、itemRender、extraRender
    VirtualList->>VirtualList: 虚拟渲染可见行
    VirtualList->>DOM: 渲染项行和粘性头
    
    Note over User,DOM: 用户滚动到底部
    VirtualList->>Listy: 触发 scroll 事件
    Listy->>Hooks: useOnEndReached 检测底部
    Hooks->>User: 调用 onEndReached 回调
    User->>Listy: 更新 items
    Listy->>VirtualList: 重新渲染更新数据
    VirtualList->>DOM: 增量渲染新项
Loading

预估代码审查工作量

🎯 5 (Critical) | ⏱️ ~120 分钟

需重点关注的区域:

  • src/List.tsx — 新增核心列表组件,包含复杂的分组、虚拟化、粘性头逻辑整合
  • src/hooks/useStickyGroupHeader.tsx — Portal 渲染、滚动位置计算和二分查找逻辑,容易出现边界问题
  • tests/onEndReached.test.tsx — 密集的无限滚动测试用例,覆盖多种动态数据场景,逻辑复杂
  • tests/listy.test.tsx — 组件快照测试和分组功能验证,需确保正确性
  • src/interface.ts — API 表面完全替换,从对齐配置转向列表专用配置,影响下游使用
  • package.json — 依赖项升级和包元数据变更,需验证兼容性

诗歌

🐰 从弹窗到列表,我的小兔心欢喜

虚拟渲染闪闪发光,性能飞快真是棒

分组粘头无限滚,每一行都闪闪亮

Listy 新生,兔子献礼,代码焕然一新光~ ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
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.
Title check ❓ Inconclusive 标题 'feat: listy' 过于简洁和宽泛,没有充分说明主要变更内容。该PR涉及从Trigger组件到Listy列表组件的完整重构,但标题未能体现这一核心变更。 建议更新标题为更具体的描述,例如:'refactor: migrate from Trigger popup to Listy virtualized list component',以更清晰地传达主要变更意图。
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 125d7ae and 77a440d.

📒 Files selected for processing (1)
  • tests/listy.behavior.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/listy.behavior.test.tsx (1)
src/hooks/useFlattenRows.ts (1)
  • FlattenRowsResult (9-13)
🪛 GitHub Check: CodeQL
tests/listy.behavior.test.tsx

[warning] 76-76: Useless conditional
This use of variable 'mockFlattenRows' always evaluates to false.

🔇 Additional comments (9)
tests/listy.behavior.test.tsx (9)

1-6: 导入语句正确且必要。

所有导入的依赖项都在测试中使用,类型导入确保了类型安全。


7-58: 模拟实现结构良好。

rc-virtual-list 的模拟提供了充分的测试控制:

  • 通过闭包变量管理状态(extraInfo、scrollHandler、lastProps)
  • 暴露测试钩子方法用于配置模拟行为
  • 正确实现 forwardRef 和 scrollTo API

这种模拟模式为测试 Listy 组件与虚拟列表的集成提供了良好的基础。


60-68: 类型定义增强了模拟的类型安全性。

为测试工具方法添加类型注解确保了在测试中使用这些方法时的类型检查。


80-89: 测试隔离设置正确。

beforeEach 钩子正确重置了所有模拟状态,确保测试之间的独立性。


91-117: 辅助函数设计良好。

renderList 辅助函数:

  • 正确使用 Object.prototype.hasOwnProperty.call 区分显式传入 undefined 与属性不存在
  • 提供合理的默认值减少测试代码重复
  • 支持通过 ref 访问组件实例

这为编写简洁清晰的测试用例奠定了良好基础。


119-131: scrollTo 引用转发测试清晰准确。

该测试正确验证了:

  • Listy 组件通过 ref 暴露 scrollTo 方法
  • 调用被正确转发到底层的虚拟列表

测试逻辑简洁且有针对性。


133-138: 边界情况测试有价值。

该测试验证了当 itemsundefined 时组件能优雅处理,传递空数组给虚拟列表。这是良好的防御性编程实践。


140-157: 粘性头部测试覆盖了关键行为。

该测试验证了非虚拟模式下的粘性头部功能:

  • 粘性头部元素使用正确的 CSS 类
  • 组标题函数被正确调用
  • 渲染的内容符合预期

测试覆盖了虚拟模式关闭时的核心行为。


159-181: 分组滚动测试完整验证了 API 行为。

该测试正确验证了:

  • 通过 groupKey 滚动到指定分组
  • 滚动参数(alignoffset)被正确传递
  • 分组键被正确转换为虚拟列表的键

测试覆盖了分组滚动的完整流程。


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

Summary of Changes

Hello @aojunhao123, 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 represents a significant transformation of the project, pivoting from a generic React Trigger Component to a specialized React Listy Component. The core purpose has shifted to providing a high-performance, feature-rich virtualized list component, complete with grouping, sticky headers, and infinite scrolling capabilities. This change involved a comprehensive renaming, refactoring of internal logic, and a complete refresh of documentation and examples to align with the new component's functionality.

Highlights

  • Project Renaming and Re-scoping: The project has been completely re-scoped and renamed from @rc-component/trigger to @rc-component/listy. This change is reflected across all configuration files, documentation, and source code.
  • Introduction of Listy Component: A new React component, Listy, has been introduced, focusing on efficient rendering of large lists. It leverages rc-virtual-list for virtualization and includes features like grouping and sticky headers.
  • New Core Functionality: New hooks (useGroupSegments, useFlattenRows, useStickyGroupHeader, useOnEndReached) have been added to support the advanced features of the Listy component, such as segmenting data for grouping and managing sticky headers.
  • Documentation and Examples Overhaul: All previous documentation and examples related to the rc-trigger component have been removed and replaced with new ones tailored for the Listy component, demonstrating its usage for basic lists, endless scrolling, grouped lists, and load-on-demand scenarios.
  • Dependency Updates and Cleanup: The package.json has been updated with new keywords, rc-virtual-list as a dependency, and various development dependencies have been refreshed. Obsolete files and styles related to the old rc-trigger component have been removed.
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

@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

This pull request introduces a significant change by replacing the rc-trigger component with a new rc-listy component. This is a complete rewrite, affecting project configuration, dependencies, source code, and documentation. The new rc-listy component is a high-performance list component that supports virtualization, grouping, and sticky headers. The implementation appears robust and is accompanied by a comprehensive test suite. I have one suggestion in a demo file to enhance type safety.

Copy link

@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: 5

🧹 Nitpick comments (11)
tests/listy.test.tsx (1)

1-87: 测试用例覆盖较完整,但对分组顺序有隐式假设

  • 基础渲染、快照、函数式 rowKey 以及分组标题回调的行为都做了验证,能够很好地保护 Listy 的关键用法(特别是 rowKey 为函数、以及 group.title 的参数形态)。
  • renders group headers with title callback 中通过 toHaveBeenNthCalledWith 断言回调调用顺序(先 A 再 B),这等于把“分组标题按首个出现顺序生成”的行为固化到测试里。如果未来实现改为按 key 排序或其它策略,该测试会失败——如果这是预期的对外行为,这样的断言是合理的;否则可以改成只断言调用参数集合,而不依赖调用顺序。

整体而言,这个测试文件设计清晰,可以在后续改动 Listy 行为时提供不错的安全网。

src/hooks/useGroupSegments.ts (1)

1-52: 分组切段算法正确,只有一些小的实现细节可微调

  • 算法本身:通过遍历 items,在 group.key 变化时关闭上一个 segment 并开启新 segment,最后补上尾段,能够正确生成按相邻分组的区间 [startIndex, endIndex],逻辑上没有明显边界问题(空数组、单元素、多段都能覆盖)。
  • group.key 同时支持常量和函数的写法,getGroupKey 的封装也比较清晰。
  • useMemo 依赖是 [items, group],从语义上是对的。不过在常见用法中 items / group 往往是每次 render 都重新创建的对象,这会让 useMemo 在多数场景下每次都重新计算,只是这一点性能上的开销在 O(n) 算法下一般也可以接受。

可以考虑的几个小优化(可选):

  • items 在类型上已经是必填数组,if (!group || !items.length) 就足够了,去掉 ?. 能让代码更整洁。
  • 如果希望在大数据量场景下稍微减少重算,可以在上层组件中给 itemsgroup 提供更稳定的引用(例如通过 useMemo 包裹 group 配置),这里的实现则可以保持不变。

整体实现已经足够清晰易维护。

docs/examples/group.tsx (1)

1-71: 示例逻辑清晰,但 renderHeader 的类型断言可以更优雅

功能层面:

  • 分组数据、动态 header 高度和行高度都符合 Listy 分组 + 虚拟滚动的展示目的,示例整体可读性很好。
  • sticky + virtual 的组合也能很好展示分组吸顶效果。

类型和实现上有两点可以微调,避免不必要的断言:

  1. 当前写法:

    group={{
      key: (item) => item.groupId,
      title: (groupKey, groupItems) => renderHeader(groupKey as string, groupItems as any),
    }}

    这里对 groupKey 做了 as string,对 groupItems 做了 as any,会削弱示例对用户的类型示范价值。

  2. 建议改成让 renderHeader 直接满足 Group<T>['title'] 的签名,比如:

  • function renderHeader(groupKey: string, groupItems: typeof items) {
  • const groupIndex = Number(groupKey.slice(1));
  • function renderHeader(groupKey: React.Key, groupItems: typeof items) {
  • const groupIndex = Number(String(groupKey).slice(1));
    const heights = [32, 56, 80];
    const h = heights[groupIndex % heights.length];
    return (
    Group {groupKey} (size: {groupItems.length})
    );

}

// 使用时可直接:

  • group={{
  • key: (item) => item.groupId,
  • title: (groupKey, groupItems) => renderHeader(groupKey as string, groupItems as any),
  • }}
  • group={{
  • key: (item) => item.groupId,
  • title: renderHeader,
  • }}
    
    

这样既保持了示例行为不变,又去掉了 as any 等不必要的类型断言,更利于用户参考和复制。

docs/examples/load-on-demand.tsx (1)

1-91: 按需加载示例逻辑正确,可考虑增加一点健壮性(可选)

  • appendItems 利用 loading 状态防止重复点击,批量生成数据并追加到末尾,整体逻辑清晰、易懂,适合文档示例。
  • 使用 nextIdRef 保证 ID 递增、scrollTo 跳转到最后一项,也很好地演示了 ListyRef 的用法。

有两点可以在不增加太多复杂度的前提下进一步优化(可选):

  1. 定时器清理
    当前 window.setTimeout 没有在组件卸载时清理,如果示例在文档页里被频繁切换,理论上可能出现卸载后仍然尝试 setState 的情况。可以用一个 useEffect 在卸载时 clearTimeout,提高健壮性。

  2. 与其他示例复用工具函数(可选)
    RowItemBATCH_SIZEcreateBatch 的逻辑和 endless-scrolling 示例非常相似,如果希望减轻维护成本,可以考虑在 docs 模块里抽个小工具(比如 examples/utils.ts)统一导出这些生成函数,不过这完全是维护层面的优化,并非必须。

在当前规模下,即便保持现状,这个示例也已经足够直观实用。

tests/hooks.test.tsx (1)

26-35: Sticky 头部 hook 的测试场景合理,可考虑补一条负向断言

  • 通过 StickyHeaderTester + 自建 containerRef 验证 Portal 渲染位置与标题内容,场景基本覆盖了二分查找 headerRow 及 virtual 开关逻辑。
  • 第二个用例仅检查 virtual: false 时不渲染 sticky 节点,如再断言一次 title 未被调用,可以更明确保证「非虚拟列表 + stickyHeader 不生效」这一契约,防止未来修改时误触发标题渲染。

Also applies to: 105-187

docs/examples/endless-scrolling.tsx (1)

22-46: 无限滚动示例可考虑增加定时器清理,防止被直接 copy 出问题

当前用 window.setTimeout 模拟异步加载,在 demo 场景下没问题。但如果用户直接 copy 这段到业务中,组件在定时器回调执行前卸载时,setItems / setLoading 可能触发 “卸载组件后更新 state” 的 React 警告。

可以作为可选增强,增加一个简单的清理逻辑,例如:

-  const loadMore = useCallback(() => {
+  const loadMore = useCallback(() => {
@@
-    window.setTimeout(() => {
+    const timerId = window.setTimeout(() => {
       setItems((prevItems) => {
@@
-      setLoading(false);
-    }, LOAD_DELAY);
-  }, [loading]);
+      setLoading(false);
+    }, LOAD_DELAY);
+    return () => window.clearTimeout(timerId);
+  }, [loading]);

或在外层用 useEffect 统一管理定时器,避免示例在生产代码中被直接复用时留下隐患。

docs/examples/no-virtual.tsx (1)

5-61: 非虚拟分组示例完整且贴合组件能力

  • 通过 GROUP_META + GroupId 类型,把分组标题、描述、配色等元数据集中管理,再在 group.title 中一次性渲染,示例非常清晰。
  • virtual={false} + sticky + rowKey="id" 的组合,也很好地演示了 Listy 在非虚拟场景下的分组粘头能力以及编程滚动到指定分组。
  • GROUP_IDS 使用 Object.keys(GROUP_META) as GroupId[] 在当前常量结构下是安全的;如果以后扩展/调整 GROUP_META 的 key,记得同步更新 GroupId 类型以保持类型与运行时一致。

整体无需改动,仅作文档示例已足够完善。

Also applies to: 63-159

src/hooks/useFlattenRows.ts (1)

5-51: useFlattenRows 扁平化逻辑整体清晰可靠

  • !group || !segments.length 时退化为纯 item 列表的分支设计合理,能够兼容未启用分组的场景。
  • 分组模式下,headerRows.rowIndex 使用 flatRows.length 记录插入位置,随后按 [startIndex, endIndex] 闭区间推入 item,行序与原数据保持一致,没有明显边界问题。
  • groupKeyToSeg 直接镜像 GroupSegment 的起止索引,方便后续 sticky 逻辑或跳转使用。

目前看不到功能性问题,这部分可以直接合入。仅需确保 useGroupSegments 中的 startIndex / endIndex 定义持续保持为闭区间即可。

src/interface.ts (1)

1-7: ScrollTo 建议改为类型导入,和 GetKey 保持一致

目前:

import * as React from 'react';
import { ScrollTo } from 'rc-virtual-list/lib/List';
import type { GetKey } from 'rc-virtual-list/lib/interface';

ScrollTo 只在 ListyRef 的类型位置使用,推荐改为 import type,既与下方 GetKey 一致,也可以在开启 verbatimModuleSyntax/importsNotUsedAsValues 时避免生成不必要的运行时代码:

-import { ScrollTo } from 'rc-virtual-list/lib/List';
+import type { ScrollTo } from 'rc-virtual-list/lib/List';

除此之外,ListyRefGroup<T>ListyProps<T> 的设计与文档和测试用例保持一致,看起来是合理的。

tests/onEndReached.test.tsx (1)

562-567: 分组用例中额外的二次 fireEvent.scroll 可以考虑去掉

在“Integration with group feature” 用例中:

scrollToBottom(scrollContainer, items.length + groupCount);
fireEvent.scroll(scrollContainer);

scrollToBottom 内部已经调用了一次 fireEvent.scroll,这里再次手动触发实际上依赖了“相同 scrollHeight 只触发一次”的去重逻辑,功能上没问题,但略显冗余,调试时也不太直观。

可以考虑删除这一行,让用例更聚焦于“分组后仍然能触发一次 onEndReached”的核心意图。

README.md (1)

18-21: 可以考虑清理未使用的 david-* 链接引用

静态分析提示以下引用定义未在文档中使用:

  • david-url
  • david-image
  • david-dev-url
  • david-dev-image

如果后续不再展示这些依赖状态徽章,建议直接删除对应引用定义;若计划保留,可在正文中补充相关 badge 以消除 lint 告警。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41ce994 and edb7851.

⛔ Files ignored due to path filters (1)
  • tests/__snapshots__/listy.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (95)
  • .dumirc.ts (1 hunks)
  • .gitignore (1 hunks)
  • HISTORY.md (0 hunks)
  • README.md (4 hunks)
  • assets/index.less (1 hunks)
  • assets/index/Mask.less (0 hunks)
  • assets/index/Mobile.less (0 hunks)
  • docs/demos/basic.md (1 hunks)
  • docs/demos/body-overflow.md (0 hunks)
  • docs/demos/case.md (0 hunks)
  • docs/demos/click-nested.md (0 hunks)
  • docs/demos/clip.md (0 hunks)
  • docs/demos/container.md (0 hunks)
  • docs/demos/endless-scrolling.md (1 hunks)
  • docs/demos/group.md (1 hunks)
  • docs/demos/inside.md (0 hunks)
  • docs/demos/large-popup.md (0 hunks)
  • docs/demos/load-on-demand.md (1 hunks)
  • docs/demos/mobile.md (0 hunks)
  • docs/demos/nested.md (0 hunks)
  • docs/demos/no-virtual.md (1 hunks)
  • docs/demos/point.md (0 hunks)
  • docs/demos/portal.md (0 hunks)
  • docs/demos/shadow.md (0 hunks)
  • docs/demos/simple.md (0 hunks)
  • docs/demos/static-scroll.md (0 hunks)
  • docs/demos/visible-fallback.md (0 hunks)
  • docs/examples/basic.tsx (1 hunks)
  • docs/examples/body-overflow.tsx (0 hunks)
  • docs/examples/case.less (0 hunks)
  • docs/examples/case.tsx (0 hunks)
  • docs/examples/click-nested.tsx (0 hunks)
  • docs/examples/clip.tsx (0 hunks)
  • docs/examples/container.tsx (0 hunks)
  • docs/examples/endless-scrolling.tsx (1 hunks)
  • docs/examples/group.tsx (1 hunks)
  • docs/examples/inside.tsx (0 hunks)
  • docs/examples/large-popup.tsx (0 hunks)
  • docs/examples/load-on-demand.tsx (1 hunks)
  • docs/examples/mobile.tsx (0 hunks)
  • docs/examples/nested.tsx (0 hunks)
  • docs/examples/no-virtual.tsx (1 hunks)
  • docs/examples/point.less (0 hunks)
  • docs/examples/point.tsx (0 hunks)
  • docs/examples/portal.tsx (0 hunks)
  • docs/examples/shadow.tsx (0 hunks)
  • docs/examples/simple.tsx (0 hunks)
  • docs/examples/static-scroll.tsx (0 hunks)
  • docs/examples/visible-fallback.tsx (0 hunks)
  • docs/index.md (1 hunks)
  • index.js (1 hunks)
  • jest.config.js (1 hunks)
  • now.json (0 hunks)
  • package.json (2 hunks)
  • src/List.tsx (1 hunks)
  • src/Popup/Arrow.tsx (0 hunks)
  • src/Popup/Mask.tsx (0 hunks)
  • src/Popup/PopupContent.tsx (0 hunks)
  • src/Popup/index.tsx (0 hunks)
  • src/context.ts (0 hunks)
  • src/hooks/index.ts (1 hunks)
  • src/hooks/useAction.ts (0 hunks)
  • src/hooks/useAlign.ts (0 hunks)
  • src/hooks/useFlattenRows.ts (1 hunks)
  • src/hooks/useGroupSegments.ts (1 hunks)
  • src/hooks/useOnEndReached.ts (1 hunks)
  • src/hooks/useStickyGroupHeader.tsx (1 hunks)
  • src/hooks/useWatch.ts (0 hunks)
  • src/hooks/useWinClick.ts (0 hunks)
  • src/index.ts (1 hunks)
  • src/index.tsx (0 hunks)
  • src/interface.ts (1 hunks)
  • src/mock.tsx (0 hunks)
  • src/util.ts (0 hunks)
  • tests/align.test.tsx (0 hunks)
  • tests/arrow.test.jsx (0 hunks)
  • tests/basic.test.jsx (0 hunks)
  • tests/flip-visibleFirst.test.tsx (0 hunks)
  • tests/flip.test.tsx (0 hunks)
  • tests/flipShift.test.tsx (0 hunks)
  • tests/hooks.test.tsx (1 hunks)
  • tests/listy.behavior.test.tsx (1 hunks)
  • tests/listy.test.tsx (1 hunks)
  • tests/mask.test.jsx (0 hunks)
  • tests/mobile.test.tsx (0 hunks)
  • tests/motion.test.jsx (0 hunks)
  • tests/onEndReached.test.tsx (1 hunks)
  • tests/perf.test.tsx (0 hunks)
  • tests/point.test.jsx (0 hunks)
  • tests/portal.test.jsx (0 hunks)
  • tests/rect.test.tsx (0 hunks)
  • tests/ref.test.tsx (0 hunks)
  • tests/shadow.test.tsx (0 hunks)
  • tests/util.tsx (0 hunks)
  • tsconfig.json (1 hunks)
💤 Files with no reviewable changes (64)
  • tests/motion.test.jsx
  • docs/examples/point.tsx
  • docs/demos/point.md
  • docs/examples/inside.tsx
  • docs/demos/simple.md
  • docs/examples/case.less
  • docs/examples/clip.tsx
  • tests/ref.test.tsx
  • docs/examples/container.tsx
  • docs/examples/visible-fallback.tsx
  • src/context.ts
  • docs/demos/static-scroll.md
  • tests/perf.test.tsx
  • tests/shadow.test.tsx
  • src/Popup/Mask.tsx
  • assets/index/Mobile.less
  • docs/demos/mobile.md
  • docs/examples/case.tsx
  • docs/examples/large-popup.tsx
  • docs/demos/nested.md
  • docs/demos/portal.md
  • tests/arrow.test.jsx
  • src/Popup/PopupContent.tsx
  • src/Popup/Arrow.tsx
  • docs/examples/body-overflow.tsx
  • docs/demos/click-nested.md
  • docs/examples/simple.tsx
  • tests/rect.test.tsx
  • tests/mask.test.jsx
  • docs/examples/shadow.tsx
  • src/hooks/useWinClick.ts
  • docs/demos/large-popup.md
  • docs/demos/container.md
  • src/hooks/useAction.ts
  • docs/examples/nested.tsx
  • src/util.ts
  • tests/flip-visibleFirst.test.tsx
  • src/mock.tsx
  • docs/examples/portal.tsx
  • src/index.tsx
  • docs/examples/mobile.tsx
  • docs/examples/point.less
  • assets/index/Mask.less
  • tests/flip.test.tsx
  • docs/demos/visible-fallback.md
  • HISTORY.md
  • tests/portal.test.jsx
  • docs/demos/inside.md
  • docs/examples/click-nested.tsx
  • docs/demos/body-overflow.md
  • docs/demos/shadow.md
  • docs/demos/clip.md
  • docs/demos/case.md
  • tests/mobile.test.tsx
  • tests/flipShift.test.tsx
  • src/hooks/useAlign.ts
  • src/hooks/useWatch.ts
  • tests/util.tsx
  • tests/basic.test.jsx
  • now.json
  • src/Popup/index.tsx
  • docs/examples/static-scroll.tsx
  • tests/point.test.jsx
  • tests/align.test.tsx
🧰 Additional context used
🧬 Code graph analysis (13)
docs/examples/no-virtual.tsx (2)
docs/examples/basic.tsx (1)
  • listRef (5-67)
src/interface.ts (1)
  • ListyRef (5-7)
src/hooks/useStickyGroupHeader.tsx (1)
src/interface.ts (1)
  • Group (9-12)
src/hooks/useGroupSegments.ts (1)
src/interface.ts (1)
  • Group (9-12)
docs/examples/load-on-demand.tsx (1)
docs/examples/endless-scrolling.tsx (1)
  • listRef (22-90)
src/hooks/useOnEndReached.ts (1)
src/hooks/index.ts (1)
  • useOnEndReached (10-10)
docs/examples/group.tsx (2)
docs/examples/basic.tsx (1)
  • listRef (5-67)
src/interface.ts (1)
  • ListyRef (5-7)
docs/examples/basic.tsx (1)
src/interface.ts (1)
  • ListyRef (5-7)
src/hooks/useFlattenRows.ts (2)
src/interface.ts (1)
  • Group (9-12)
src/hooks/useGroupSegments.ts (1)
  • GroupSegment (4-8)
src/interface.ts (1)
src/index.ts (2)
  • ListyRef (3-3)
  • ListyProps (3-3)
tests/hooks.test.tsx (1)
src/hooks/useStickyGroupHeader.tsx (2)
  • StickyHeaderParams (6-13)
  • useStickyGroupHeader (15-96)
src/List.tsx (5)
src/interface.ts (3)
  • ListyProps (19-30)
  • ListyRef (5-7)
  • GetKey (32-32)
src/hooks/useGroupSegments.ts (1)
  • useGroupSegments (10-52)
src/hooks/useFlattenRows.ts (2)
  • useFlattenRows (15-52)
  • Row (5-7)
src/hooks/useStickyGroupHeader.tsx (1)
  • useStickyGroupHeader (15-96)
src/hooks/useOnEndReached.ts (1)
  • useOnEndReached (9-34)
docs/examples/endless-scrolling.tsx (1)
docs/examples/load-on-demand.tsx (1)
  • listRef (22-90)
tests/listy.behavior.test.tsx (2)
src/hooks/useFlattenRows.ts (1)
  • FlattenRowsResult (9-13)
src/index.ts (2)
  • ListyProps (3-3)
  • ListyRef (3-3)
🪛 markdownlint-cli2 (0.18.1)
README.md

18-18: Link and image reference definitions should be needed
Unused link or image reference definition: "david-url"

(MD053, link-image-reference-definitions)


19-19: Link and image reference definitions should be needed
Unused link or image reference definition: "david-image"

(MD053, link-image-reference-definitions)


20-20: Link and image reference definitions should be needed
Unused link or image reference definition: "david-dev-url"

(MD053, link-image-reference-definitions)


21-21: Link and image reference definitions should be needed
Unused link or image reference definition: "david-dev-image"

(MD053, link-image-reference-definitions)

🔇 Additional comments (20)
.gitignore (1)

30-30: 忽略 pnpm-lock.yaml 的改动是合理的

与已有的 yarn.lock、package-lock.json 一致,将 pnpm-lock.yaml 加入忽略可以避免多包管理器锁文件混在一起提交,配置看起来没问题。

docs/demos/basic.md (1)

1-8: Basic 示例文档结构合理

Frontmatter 配置和 <code src="../examples/basic.tsx"> 的用法都很常规,能很好地把 demo 挂到 /demo 下的「Basic」页面,看起来无需额外调整。

jest.config.js (1)

3-5: Jest 别名与新的包入口保持一致

moduleNameMapper@rc-component/listy 映射到 <rootDir>/src/index.ts,这与包入口切换到 Listy 的重构方向一致,有利于测试直接按包名引入,配置本身看起来是正确的。建议在入口文件变动时同步更新这里,避免测试解析路径失配。

docs/index.md (1)

3-4: 首页文案已与 Listy 品牌对齐

将 hero 标题和描述更新为 rc-listy / React Listy Component 与整体从 Trigger 迁移到 Listy 的方向一致,命名和文案都对得上。

.dumirc.ts (1)

6-7: 配置更新符合重构目标

别名映射和主题名称已正确更新,与包重命名保持一致。

Also applies to: 12-12

tsconfig.json (1)

14-16: TypeScript 配置更新正确

路径别名已更新为新的包名称,并添加了测试所需的类型定义。这些更改与整体重构保持一致。

docs/demos/load-on-demand.md (1)

8-8: 示例文件已验证存在,代码变更有效

文件 docs/examples/load-on-demand.tsx 存在且正确被引用。

docs/demos/endless-scrolling.md (1)

8-8: 示例文件引用验证已完成

引用的示例文件 ../examples/endless-scrolling.tsx 存在且路径正确解析。验证已确认该文件位于 docs/examples/endless-scrolling.tsx,与相对路径引用一致。

src/index.ts (1)

1-5: 验证完成:所有模块依赖正确

验证结果确认:

  • src/List.tsx 存在并包含默认导出(ListyWithForwardRef
  • src/interface.ts 存在并正确导出 ListyRef(第5行)和 ListyProps(第19行)
  • 入口点的导入和重新导出语句均正确
docs/demos/no-virtual.md (1)

8-8: 示例文件验证成功,无需修改

引用的示例文件 ../examples/no-virtual.tsx 已存在,包含 160 行有效的 React/TypeScript 代码,相对路径引用正确。文件功能正常,无需任何调整。

index.js (1)

2-3: 审查评论不准确 - 不存在指控的破坏性变更

经过代码库验证,未发现任何证据表明默认导出曾经是 Trigger。查证结果:

  • src/index.ts 导出 Listy 组件(来自 ./List.tsx
  • src/List.tsx 定义组件名为 Listy
  • package.json 中无任何 rc-trigger 依赖项
  • 代码库中仅在测试文件注释中出现单词 "Trigger",并非导出相关
  • 组件始终命名为 Listy(与包名 @rc-component/listy 一致)

此包与 rc-trigger 是两个独立的组件库。无需进行所述的迁移或更新。

Likely an incorrect or invalid review comment.

docs/demos/group.md (1)

8-8: 验证完成:示例文件存在且路径正确

已确认文件 docs/examples/group.tsx 存在于预期位置。引用路径 ../examples/group.tsxdocs/demos/group.md 正确解析至该文件,无路径问题。

src/hooks/index.ts (1)

1-11: Hook 汇总导出实现合理

简单的 barrel 文件,按名称导出 4 个 hook,便于外部统一引入,目前看没有明显问题或循环依赖风险,可以保持这种结构。后续如果新增 hook,建议继续集中从此处导出以保持公共 API 一致性。

tests/hooks.test.tsx (1)

37-103: useGroupSegments 测试覆盖度不错

这里对连续分组、静态 group.key、无 group / 无 items 以及 length getter 异常行为都做了验证,能较好防止实现回退到基于 length 多次访问造成的边界问题,看起来没有明显遗漏。

tests/listy.behavior.test.tsx (1)

7-78: Listy 行为测试设计清晰,覆盖了核心交互

  • 自定义 rc-virtual-list mock 暴露内部状态与控制接口,使得对 scrollTo 转发、extraRenderdata 传递的断言非常直接。
  • renderList 中通过 hasOwnProperty 区分“未传 items”与“显式传 items: undefined”,很好地为“缺省 items 视为空数组”的行为建立测试基线。
  • 三个用例分别覆盖 ref.scrollTo 透传、缺省 items 处理以及非虚拟场景下 sticky 头部 className 的添加,基本锁定了对外 API 的核心行为。

整体看这里的结构和断言都很合理,不需要调整。

Also applies to: 80-157

docs/examples/basic.tsx (1)

5-67: 基础示例逻辑清晰,API 使用合理

  • items 构造、rowKey="id"group.key: item.type 一致,能很好演示分组与 header 渲染。
  • 通过 ref={listRef} + scrollTo({ key: 100, align: 'top' }) 展示了对外暴露的滚动控制能力。
  • itemRender 中用不同高度的样式说明了 Listy 在非均高场景下的表现,契合组件定位。

整体作为文档示例已经足够清晰,无需调整。

package.json (1)

2-4: 依赖版本验证已确认,所有包都可用且兼容

通过 npm view 检查的结果表明:

  • @rc-component/util@^1.3.1rc-virtual-list@^3.19.2classnames@^2.5.1react@^19.2.0react-dom@^19.2.0 的版本在 npm 上均存在
  • React devDependencies ^19.2.0 与 peerDependencies >=18.0.0 的版本兼容性正确
  • 无版本冲突或不兼容问题
assets/index.less (1)

1-25: Listy 分组头和粘性头样式设计合理

  • 统一使用 rc-listy 作为前缀,避免与旧 Trigger 样式冲突。
  • -group-header-sticky 使用 position: sticky-sticky-header 绝对定位并加 z-index: 1,能较好支撑分组头和全局悬浮头的场景。

这一块样式实现看起来没问题,可以直接使用。

tests/onEndReached.test.tsx (1)

8-211: onEndReached 行为测试覆盖很全面

  • 通过 mockScroll + scrollToBottom 抽象滚动细节,复用性不错。
  • “基本功能”“去重触发”“滚动高度增减后的再次触发”“动态加载”和 ref.scrollTo 等场景都被覆盖,能有效约束 useOnEndReached 的实现。

整体来看,这部分测试设计得很扎实,可以很好地防止回归。

README.md (1)

81-157: API 文档与类型定义基本一致,描述清晰

  • itemsrowKeyitemRenderheightitemHeightgroupstickyvirtualonEndReachedprefixCls 等 props 与 src/interface.ts 中的 ListyProps<T> 类型保持一致。
  • ListyRef.scrollTo 的签名也与实际使用场景(按 key/index 滚动并设置对齐方式和偏移)相符。

文档和类型目前是对齐的,便于使用者根据 README 直接推断组件能力。

Comment on lines +35 to 36
Include the default [styling](https://github.com/react-component/listy/blob/master/assets/index.less#L4:L11) and then:

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

默认样式链接的行号片段格式可能有误

链接目前为:

[styling](https://github.com/react-component/listy/blob/master/assets/index.less#L4:L11)

GitHub 对多行高亮通常使用 #L4-L11(中间连字符)而不是 #L4:L11。建议调整为:

[styling](https://github.com/react-component/listy/blob/master/assets/index.less#L4-L11)

避免用户点击后无法正确定位到样式定义。

🤖 Prompt for AI Agents
In README.md around lines 35-36 the GitHub file link fragment uses an incorrect
separator `#L4:L11`; update the fragment to use a hyphen so it reads `#L4-L11`
(i.e., change
`https://github.com/react-component/listy/blob/master/assets/index.less#L4:L11`
to
`https://github.com/react-component/listy/blob/master/assets/index.less#L4-L11`)
so clicking the link highlights the intended line range.

Comment on lines 37 to +57
```js
import React from 'react';
import ReactDOM from 'react-dom';
import Trigger from '@rc-component/trigger';

ReactDOM.render(
<Trigger
action={['click']}
popup={<span>popup</span>}
popupAlign={{
points: ['tl', 'bl'],
offset: [0, 3],
}}
>
<a href="#">hover</a>
</Trigger>,
container,
import ReactDOM from 'react-dom/client';
import Listy from '@rc-component/listy';

const items = Array.from({ length: 100 }, (_, index) => ({
id: index,
name: `Item ${index}`,
}));

const App = () => (
<Listy
items={items}
height={240}
itemHeight={32}
rowKey="id"
itemRender={(item) => <div>{item.name}</div>}
/>
);

ReactDOM.createRoot(container).render(<App />);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Usage 示例中缺少 container 定义,影响可复制性

当前示例:

import React from 'react';
import ReactDOM from 'react-dom/client';
import Listy from '@rc-component/listy';

// ...

const App = () => (/* ... */);

ReactDOM.createRoot(container).render(<App />);

这里的 container 未在示例中定义,直接复制会导致运行时报错。建议补充一行,例如:

-import ReactDOM from 'react-dom/client';
+import ReactDOM from 'react-dom/client';

+const container = document.getElementById('root')!;

或根据项目约定给出更完整的挂载示例,方便用户直接上手。

🤖 Prompt for AI Agents
In README.md around lines 37 to 57, the usage example calls
ReactDOM.createRoot(container) but never defines "container"; update the example
to define and obtain a mounting DOM element before calling createRoot — for
example, select an existing element by id (e.g., "root") or create a new div,
set an id, append it to document.body, and then pass that element as container
so the snippet runs copy-paste without error.

Comment on lines +4 to +7
interface UseOnEndReachedParams {
enabled: boolean;
onEndReached?: () => void;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

useOnEndReached 的 ref 类型和可选回调存在隐患

  • lastTriggeredScrollHeightRef 声明为 React.useRef<number>(null),在严格 TS 配置下 null 不能赋值给 number,会直接编译失败。
  • onEndReached 被声明为可选,但在滚动回调里无判空直接调用;虽然目前调用方用 enabled: !!onEndReached 规避了这个问题,但作为导出的 hook,一旦别处传入 enabled: true 且遗漏回调,会出现运行时异常。

建议在类型和实现上同时收紧/防御,类似:

-  const lastTriggeredScrollHeightRef = React.useRef<number>(null);
+  const lastTriggeredScrollHeightRef = React.useRef<number | null>(null);
@@
-    if (distanceToBottom <= 0) {
-      if (lastTriggeredScrollHeightRef.current !== scrollHeight) {
-        onEndReached();
-        lastTriggeredScrollHeightRef.current = scrollHeight;
-      }
-    }
+    if (distanceToBottom <= 0 && onEndReached) {
+      if (lastTriggeredScrollHeightRef.current !== scrollHeight) {
+        onEndReached();
+        lastTriggeredScrollHeightRef.current = scrollHeight;
+      }
+    }

这样既修复 TS 类型问题,又避免外部误用导致的空函数调用。

Also applies to: 12-31

🤖 Prompt for AI Agents
In src/hooks/useOnEndReached.ts around lines 4-7 (and similarly 12-31), the ref
is typed as React.useRef<number>(null) which fails under strict TS and the
optional onEndReached is invoked without a null check; change the ref type to
React.useRef<number | null>(null) (or initialize to 0 if semantically correct)
and update the scroll handler to defensively handle missing callback by either
checking if (onEndReached) before calling or using a safe default (const cb =
onEndReached ?? (() => {}); cb()), and consider tightening the hook API so
enabled cannot be true while onEndReached is undefined (validate arguments or
infer enabled from presence of callback).

Comment on lines 1 to 96
import * as React from 'react';
import Portal from '@rc-component/portal';
import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface';
import type { Group } from '../interface';

export interface StickyHeaderParams<T> {
enabled: boolean;
group: Group<T> | undefined;
headerRows: Array<{ groupKey: React.Key; rowIndex: number }>;
groupKeyToItems: Map<React.Key, T[]>;
containerRef: React.RefObject<HTMLDivElement>;
prefixCls: string;
}

export default function useStickyGroupHeader<T>(params: StickyHeaderParams<T>) {
const {
enabled,
group,
headerRows,
groupKeyToItems,
containerRef,
prefixCls,
} = params;

const lastHeaderIdxRef = React.useRef(0);

const extraRender = React.useCallback(
(info: ExtraRenderInfo) => {
const { start, virtual } = info;

if (!enabled || !headerRows.length || !virtual) {
lastHeaderIdxRef.current = 0;
return null;
}

const activeHeaderIdx = (() => {
// Fast path: reuse previously resolved header index if start still
// points within the same header block.
const cachedIdx = lastHeaderIdxRef.current;
const cachedRow = headerRows[cachedIdx];
const nextRow = headerRows[cachedIdx + 1];
if (
cachedRow &&
cachedRow.rowIndex <= start &&
(!nextRow || nextRow.rowIndex > start)
) {
return cachedIdx;
}

// Binary search to find the closest header whose rowIndex <= start.
let lo = 0;
let hi = headerRows.length - 1;
let candidate = 0;

while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
if (headerRows[mid].rowIndex <= start) {
candidate = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}

return candidate;
})();

lastHeaderIdxRef.current = activeHeaderIdx;

const currHeader = headerRows[activeHeaderIdx];
const groupItems = groupKeyToItems.get(currHeader.groupKey) || [];

const headerNode = (
<div className={`${prefixCls}-sticky-header`}>
{group.title(currHeader.groupKey, groupItems)}
</div>
);

return (
<Portal open getContainer={() => containerRef.current}>
{headerNode}
</Portal>
);
},
[
enabled,
group,
headerRows,
groupKeyToItems,
containerRef,
prefixCls,
],
);

return extraRender;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

group 为可选但被无条件使用,存在潜在运行时错误

  • StickyHeaderParamsgroup 明确定义为 Group<T> | undefined(第 8 行),但在构建 header 时第 75 行直接调用 group.title(...),当调用方未传 group 而却开启了 sticky 时会直接抛异常。这在类型层面已经暴露为“可空”,当前实现没对齐,属于需要修复的逻辑问题。

建议至少在使用前做空值防御,并在无法渲染时返回 null,类似:

-      const currHeader = headerRows[activeHeaderIdx];
-      const groupItems = groupKeyToItems.get(currHeader.groupKey) || [];
-
-      const headerNode = (
-        <div className={`${prefixCls}-sticky-header`}>
-          {group.title(currHeader.groupKey, groupItems)}
-        </div>
-      );
+      const currHeader = headerRows[activeHeaderIdx];
+
+      // group 在类型上是可选的,防御性处理避免运行时错误
+      if (!group) {
+        return null;
+      }
+
+      const groupItems = groupKeyToItems.get(currHeader.groupKey) || [];
+
+      const headerNode = (
+        <div className={`${prefixCls}-sticky-header`}>
+          {group.title(currHeader.groupKey, groupItems)}
+        </div>
+      );

另外两个可以考虑的点(推荐级而非必须):

  1. containerRef.current 在列表尚未挂载完成时可能为 null,此时 getContainer={() => containerRef.current} 的行为依赖 @rc-component/portal 的实现。建议:

    • 要么在前面和 group 一样做一次 if (!containerRef.current) return null; 的短路;
    • 要么在类型层保证 containerRef 只在已挂载容器上使用,避免“理论上可能为 null”的悬念。
  2. 当前二分查找逻辑隐含假设:headerRowsrowIndex 升序排列且覆盖所有 group header 行。这应该是 List 层已经保证的,但可以在 StickyHeaderParams 的注释中明确这一约束,方便后续维护者理解二分的前提。

总体算法(缓存命中 + 二分查找)设计本身是合理的,关键是把 group 的空值情况兜住,避免在分组关闭或误用时直接 crash。



🏁 Script executed:

#!/bin/bash

# 1. 查找 useStickyGroupHeader 的导入和使用
echo "=== 搜索 useStickyGroupHeader 的使用 ==="
rg -n "useStickyGroupHeader" --type=tsx --type=ts -B2 -A5

echo -e "\n=== 查找 Group 接口定义 ==="
rg -n "interface Group|type Group" --type=tsx --type=ts -B1 -A8

echo -e "\n=== 查找 StickyHeaderParams 的构造调用 ==="
rg -n "StickyHeaderParams" --type=tsx --type=ts -B3 -A3

echo -e "\n=== 检查是否有地方传入 undefined 作为 group ==="
rg -n "group.*undefined|{.*group.*:.*undefined" --type=tsx --type=ts -B2 -A2

Length of output: 349


🏁 Script executed:

#!/bin/bash

# 查找 useStickyGroupHeader 的导入和使用
echo "=== 搜索 useStickyGroupHeader 的使用位置 ==="
rg -n "useStickyGroupHeader" --type=tsx --type=ts -B3 -A10

echo -e "\n=== 查找 Group 接口定义(../interface) ==="
rg -n "interface Group|export.*Group" src/interface --type=tsx --type=ts -B1 -A8

echo -e "\n=== 检查 StickyHeaderParams 对象的构造和传参 ==="
rg -n "enabled|group|headerRows|groupKeyToItems" --type=tsx --type=ts | grep -E "(group.*:|{\s*group)" -B2 -A2 | head -50

echo -e "\n=== 验证 containerRef 的初始化状态 ==="
rg -n "useRef.*div|containerRef.*=" --type=tsx --type=ts -B2 -A2

Length of output: 258


🏁 Script executed:

#!/bin/bash

echo "=== 查找所有 .tsx 和 .ts 文件 ==="
fd -e tsx -e ts | head -20

echo -e "\n=== 搜索 useStickyGroupHeader 的使用和定义 ==="
rg "useStickyGroupHeader" -B2 -A8

echo -e "\n=== 查找 Group 接口定义 ==="
rg "interface Group|export.*Group" -B1 -A10

echo -e "\n=== 查找 StickyHeaderParams 的使用处 ==="
rg "StickyHeaderParams|group\s*:" -B2 -A4 | head -80

echo -e "\n=== 查看 src/interface 下的定义 ==="
fd -e tsx -e ts src/interface | xargs cat 2>/dev/null | head -100

Length of output: 10439


🏁 Script executed:

#!/bin/bash

echo "=== 查看 src/List.tsx 中完整的调用代码 ==="
rg -n "useStickyGroupHeader|enabled.*sticky|group" src/List.tsx -B5 -A15 | head -100

echo -e "\n=== 查看 containerRef 在 List.tsx 中的初始化 ==="
rg -n "useRef|containerRef" src/List.tsx -B2 -A2

echo -e "\n=== 查看测试中是否有 enabled=true 但 group=undefined 的用例 ==="
rg -n "enabled.*true|enabled.*false" tests/hooks.test.tsx -B3 -A3

Length of output: 3975


group 在类型上是可选但被直接使用,需要防御性处理以避免运行时错误

虽然 src/List.tsx 第 79 行通过 enabled: !!(sticky && group) 确保了当 enabled=true 时 group 存在,但这是隐含约束。在 useStickyGroupHeader.tsx 第 75 行直接调用 group.title(...) 的位置没有显式防御,一旦调用方不满足这个约束(例如未来的重构或其他调用点),就会抛异常。

建议在第 31 行之后、创建 headerNode 前添加防御性检查:

      lastHeaderIdxRef.current = activeHeaderIdx;

+     // group 在类型上是可选的,防御性处理避免运行时错误
+     if (!group) {
+       return null;
+     }

      const currHeader = headerRows[activeHeaderIdx];
      const groupItems = groupKeyToItems.get(currHeader.groupKey) || [];

其他两个建议(可选优化):

  1. containerRef.current 在组件挂载完成前可能为 null。建议在第 31 行的防御后增加:

    +   if (!containerRef.current) {
    +     return null;
    +   }
  2. 二分查找隐含假设 headerRowsrowIndex 升序排列。建议在函数开头添加注释说明这个前提条件,方便后续维护者理解。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import * as React from 'react';
import Portal from '@rc-component/portal';
import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface';
import type { Group } from '../interface';
export interface StickyHeaderParams<T> {
enabled: boolean;
group: Group<T> | undefined;
headerRows: Array<{ groupKey: React.Key; rowIndex: number }>;
groupKeyToItems: Map<React.Key, T[]>;
containerRef: React.RefObject<HTMLDivElement>;
prefixCls: string;
}
export default function useStickyGroupHeader<T>(params: StickyHeaderParams<T>) {
const {
enabled,
group,
headerRows,
groupKeyToItems,
containerRef,
prefixCls,
} = params;
const lastHeaderIdxRef = React.useRef(0);
const extraRender = React.useCallback(
(info: ExtraRenderInfo) => {
const { start, virtual } = info;
if (!enabled || !headerRows.length || !virtual) {
lastHeaderIdxRef.current = 0;
return null;
}
const activeHeaderIdx = (() => {
// Fast path: reuse previously resolved header index if start still
// points within the same header block.
const cachedIdx = lastHeaderIdxRef.current;
const cachedRow = headerRows[cachedIdx];
const nextRow = headerRows[cachedIdx + 1];
if (
cachedRow &&
cachedRow.rowIndex <= start &&
(!nextRow || nextRow.rowIndex > start)
) {
return cachedIdx;
}
// Binary search to find the closest header whose rowIndex <= start.
let lo = 0;
let hi = headerRows.length - 1;
let candidate = 0;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
if (headerRows[mid].rowIndex <= start) {
candidate = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return candidate;
})();
lastHeaderIdxRef.current = activeHeaderIdx;
const currHeader = headerRows[activeHeaderIdx];
const groupItems = groupKeyToItems.get(currHeader.groupKey) || [];
const headerNode = (
<div className={`${prefixCls}-sticky-header`}>
{group.title(currHeader.groupKey, groupItems)}
</div>
);
return (
<Portal open getContainer={() => containerRef.current}>
{headerNode}
</Portal>
);
},
[
enabled,
group,
headerRows,
groupKeyToItems,
containerRef,
prefixCls,
],
);
return extraRender;
}
import * as React from 'react';
import Portal from '@rc-component/portal';
import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface';
import type { Group } from '../interface';
export interface StickyHeaderParams<T> {
enabled: boolean;
group: Group<T> | undefined;
headerRows: Array<{ groupKey: React.Key; rowIndex: number }>;
groupKeyToItems: Map<React.Key, T[]>;
containerRef: React.RefObject<HTMLDivElement>;
prefixCls: string;
}
export default function useStickyGroupHeader<T>(params: StickyHeaderParams<T>) {
const {
enabled,
group,
headerRows,
groupKeyToItems,
containerRef,
prefixCls,
} = params;
const lastHeaderIdxRef = React.useRef(0);
const extraRender = React.useCallback(
(info: ExtraRenderInfo) => {
const { start, virtual } = info;
if (!enabled || !headerRows.length || !virtual) {
lastHeaderIdxRef.current = 0;
return null;
}
const activeHeaderIdx = (() => {
// Fast path: reuse previously resolved header index if start still
// points within the same header block.
const cachedIdx = lastHeaderIdxRef.current;
const cachedRow = headerRows[cachedIdx];
const nextRow = headerRows[cachedIdx + 1];
if (
cachedRow &&
cachedRow.rowIndex <= start &&
(!nextRow || nextRow.rowIndex > start)
) {
return cachedIdx;
}
// Binary search to find the closest header whose rowIndex <= start.
let lo = 0;
let hi = headerRows.length - 1;
let candidate = 0;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
if (headerRows[mid].rowIndex <= start) {
candidate = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return candidate;
})();
lastHeaderIdxRef.current = activeHeaderIdx;
// group 在类型上是可选的,防御性处理避免运行时错误
if (!group) {
return null;
}
const currHeader = headerRows[activeHeaderIdx];
const groupItems = groupKeyToItems.get(currHeader.groupKey) || [];
const headerNode = (
<div className={`${prefixCls}-sticky-header`}>
{group.title(currHeader.groupKey, groupItems)}
</div>
);
return (
<Portal open getContainer={() => containerRef.current}>
{headerNode}
</Portal>
);
},
[
enabled,
group,
headerRows,
groupKeyToItems,
containerRef,
prefixCls,
],
);
return extraRender;
}
🤖 Prompt for AI Agents
In src/hooks/useStickyGroupHeader.tsx lines 1-96: the hook uses optional `group`
directly at the headerNode creation (around line ~75) and assumes
`containerRef.current` is non-null and `headerRows` are sorted; add a defensive
guard that if enabled is true but `group` is falsy return null early (or skip
rendering the sticky header), also check `containerRef.current` before rendering
the Portal and return null if it is not mounted, and add a short comment near
the top stating that the binary search assumes `headerRows` is sorted ascending
by rowIndex so future maintainers know the precondition.

Comment on lines 87 to 97
const renderHeaderRow = React.useCallback(
(groupKey: React.Key) => {
const groupItems = groupKeyToItems.get(groupKey) || [];
const headerClassName = `${prefixCls}-group-header${
virtual ? '' : ` ${prefixCls}-group-header-sticky`
}`;

return <div className={headerClassName}>{group.title(groupKey, groupItems)}</div>;
},
[group, groupKeyToItems, prefixCls, virtual],
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

非虚拟场景下 sticky prop 被忽略,header 总是粘头

当前 renderHeaderRow 中 sticky 行为只取决于 virtual

const headerClassName = `${prefixCls}-group-header${
  virtual ? '' : ` ${prefixCls}-group-header-sticky`
}`;

这意味着:

  • virtual === false 时,无论 sticky 传 true 还是 false,header 都会带上 *-group-header-sticky class,从而总是粘在顶部。
  • sticky 在非虚拟列表中的语义实际上被忽略了,这与 props 名字预期不符。

建议把 sticky 纳入判断,让“非虚拟 + sticky=true”才加粘性 class,例如:

-      const headerClassName = `${prefixCls}-group-header${
-        virtual ? '' : ` ${prefixCls}-group-header-sticky`
-      }`;
+      const headerClassName = `${prefixCls}-group-header${
+        !virtual && sticky ? ` ${prefixCls}-group-header-sticky` : ''
+      }`;
@@
-    [group, groupKeyToItems, prefixCls, virtual],
+    [group, groupKeyToItems, prefixCls, virtual, sticky],

这样:

  • 虚拟列表仍通过 useStickyGroupHeader 的 Portal 实现粘头;
  • 非虚拟列表时,只有显式 sticky 才会启用 CSS 粘头行为,符合用户直觉。
🤖 Prompt for AI Agents
In src/List.tsx around lines 87 to 97, the group header class currently only
checks `virtual` so non-virtual lists always get the `*-group-header-sticky`
class and the `sticky` prop is ignored; change the condition so the sticky class
is added only when the list is non-virtual AND `sticky === true` (i.e.
`${!virtual && sticky ? ` ${prefixCls}-group-header-sticky` : ''}`), and add
`sticky` to the React.useCallback dependency array so the callback updates when
the prop changes; leave virtual behavior for portal-based sticky handling via
useStickyGroupHeader unchanged.

Copy link

@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 (1)
src/List.tsx (1)

90-104: 非虚拟场景下 sticky prop 被忽略,header 总是粘头

当前 renderHeaderRow 中 sticky 行为只取决于 virtual

const headerClassName = `${prefixCls}-group-header${
  virtual ? '' : ` ${prefixCls}-group-header-sticky`
}`;

这意味着:

  • virtual === false 时,无论 sticky 传 true 还是 false,header 都会带上 *-group-header-sticky class
  • sticky prop 的语义在非虚拟列表中被忽略了

建议将 sticky 纳入判断:

-      const headerClassName = `${prefixCls}-group-header${
-        virtual ? '' : ` ${prefixCls}-group-header-sticky`
-      }`;
+      const headerClassName = `${prefixCls}-group-header${
+        !virtual && sticky ? ` ${prefixCls}-group-header-sticky` : ''
+      }`;

同时更新依赖数组:

-    [group, groupKeyToItems, prefixCls, virtual],
+    [group, groupKeyToItems, prefixCls, virtual, sticky],
🧹 Nitpick comments (1)
docs/examples/no-virtual.tsx (1)

88-97: 可选优化:依赖数组中的 items 不必要

items 是模块级常量(定义在第 31 行),永远不会变化,因此将其包含在依赖数组中虽然无害但并非必需。

可应用以下 diff 移除不必要的依赖:

   const handleScrollToGroup = useCallback(
     (groupId: GroupId) => {
       const targetItem = items.find((item) => item.groupId === groupId);
       if (!targetItem) {
         return;
       }
       listRef.current?.scrollTo({ key: targetItem.id, align: 'top' });
     },
-    [items],
+    [],
   );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edb7851 and 37e1901.

📒 Files selected for processing (7)
  • docs/examples/group.tsx (1 hunks)
  • docs/examples/no-virtual.tsx (1 hunks)
  • src/List.tsx (1 hunks)
  • src/hooks/useFlattenRows.ts (1 hunks)
  • src/hooks/useGroupSegments.ts (1 hunks)
  • src/hooks/useStickyGroupHeader.tsx (1 hunks)
  • src/interface.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/hooks/useGroupSegments.ts
  • src/hooks/useStickyGroupHeader.tsx
  • src/hooks/useFlattenRows.ts
  • docs/examples/group.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
src/List.tsx (5)
src/interface.ts (2)
  • ListyProps (16-27)
  • ListyRef (5-7)
src/hooks/useGroupSegments.ts (1)
  • useGroupSegments (10-52)
src/hooks/useFlattenRows.ts (2)
  • useFlattenRows (15-52)
  • Row (5-7)
src/hooks/useStickyGroupHeader.tsx (1)
  • useStickyGroupHeader (15-92)
src/hooks/useOnEndReached.ts (1)
  • useOnEndReached (9-34)
docs/examples/no-virtual.tsx (3)
docs/examples/group.tsx (1)
  • listRef (5-71)
docs/examples/basic.tsx (1)
  • listRef (5-67)
src/interface.ts (1)
  • ListyRef (5-7)
src/interface.ts (1)
src/index.ts (2)
  • ListyRef (3-3)
  • ListyProps (3-3)
🔇 Additional comments (12)
docs/examples/no-virtual.tsx (4)

1-21: LGTM!

导入和分组元数据定义合理清晰。


23-67: LGTM!

类型定义和示例数据结构合理,为演示非虚拟分组列表提供了充足的测试数据。


69-87: LGTM!

ref 和 memoized 回调设置正确,空依赖数组符合预期(因为 GROUP_META 是常量)。


99-166: LGTM!

Listy 组件配置正确展示了非虚拟模式下的分组列表功能,包括:

  • 粘性头部(sticky)
  • 动态项高度
  • 基于元数据的分组头渲染
  • 编程式滚动控制
src/List.tsx (5)

1-27: LGTM!

导入和组件签名定义清晰,泛型类型参数使用恰当。


28-37: LGTM!

ref 设置和命令式 API 暴露正确,scrollTo 方法正确转发给底层的 VirtualList。


39-78: LGTM!

分组和扁平化逻辑实现正确:

  • getItemKey 正确处理函数和字符串类型的 rowKey
  • 使用自定义 hooks 进行分组和扁平化
  • groupKeyToItems 预计算优化了头部渲染性能

106-131: LGTM!

滚动处理和渲染逻辑正确:

  • 使用 useOnEndReached hook 处理触底回调
  • 正确配置 VirtualList 的各项属性
  • 子渲染函数正确区分 header 和 item 行

134-141: LGTM!

forwardRef 包装器的类型断言正确,这是处理泛型组件 ref 转发的标准模式。

src/interface.ts (3)

1-7: LGTM!

ListyRef 接口定义清晰,正确暴露了 rc-virtual-list 的 ScrollTo 类型以支持编程式滚动。


9-14: LGTM!

RowKey 和 Group 类型定义灵活且设计良好:

  • RowKey 支持属性名和函数两种方式
  • Group.key 支持静态和动态分组
  • Group.title 提供了自定义头部渲染的能力

16-29: LGTM!

ListyProps 接口定义完整且类型准确:

  • rowKey 正确标记为必需
  • 所有可选属性类型恰当
  • itemRender 提供了类型化的渲染函数
  • 重导出 GetKey 类型便于外部使用

Copy link

@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 (2)
README.md (2)

35-36: GitHub 链接行号片段格式有误(重复问题)。

此问题已在先前的评审中标记:链接使用了 #L4:L11 而非正确的 #L4-L11 格式。请参考之前的评审意见进行修正。


37-57: 示例代码中缺少 container 定义(重复问题)。

此问题已在先前的评审中标记:代码示例调用 ReactDOM.createRoot(container) 但未定义 container 变量。请参考之前的评审意见进行修正。

🧹 Nitpick comments (1)
README.md (1)

18-21: 可移除已废弃的 David DM 依赖徽章引用。

这些链接引用定义(david-urldavid-imagedavid-dev-urldavid-dev-image)未被使用,且 David DM 服务已于 2021 年关闭。建议删除这些过时的引用定义以保持文档整洁。

应用此差异移除未使用的引用:

-[david-url]: https://david-dm.org/react-component/listy
-[david-image]: https://david-dm.org/react-component/listy/status.svg?style=flat-square
-[david-dev-url]: https://david-dm.org/react-component/listy?type=dev
-[david-dev-image]: https://david-dm.org/react-component/listy/dev-status.svg?style=flat-square
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4d3faf and 60c2b2e.

📒 Files selected for processing (7)
  • README.md (4 hunks)
  • assets/index.less (1 hunks)
  • docs/examples/group.tsx (1 hunks)
  • docs/examples/no-virtual.tsx (1 hunks)
  • src/List.tsx (1 hunks)
  • src/interface.ts (1 hunks)
  • src/util/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/examples/group.tsx
  • docs/examples/no-virtual.tsx
  • src/List.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/util/index.ts (1)
src/interface.ts (2)
  • ListyScrollToConfig (7-13)
  • ScrollAlign (5-5)
src/interface.ts (1)
src/index.ts (2)
  • ListyRef (3-3)
  • ListyProps (3-3)
🪛 markdownlint-cli2 (0.18.1)
README.md

18-18: Link and image reference definitions should be needed
Unused link or image reference definition: "david-url"

(MD053, link-image-reference-definitions)


19-19: Link and image reference definitions should be needed
Unused link or image reference definition: "david-image"

(MD053, link-image-reference-definitions)


20-20: Link and image reference definitions should be needed
Unused link or image reference definition: "david-dev-url"

(MD053, link-image-reference-definitions)


21-21: Link and image reference definitions should be needed
Unused link or image reference definition: "david-dev-image"

(MD053, link-image-reference-definitions)

🔇 Additional comments (3)
assets/index.less (1)

1-25: LGTM!CSS 重构合理,支持粘性分组头部。

样式从 trigger 相关命名迁移到 listy 前缀,逻辑清晰。粘性定位(position: sticky)和绝对定位(position: absolute)的组合为不同场景提供了灵活性。背景色设置防止内容穿透也很合理。

src/interface.ts (1)

26-37: LGTM!ListyProps 接口设计合理,类型定义清晰。

必填属性 rowKeyitemRender 的类型定义准确,与文档说明一致。接口的泛型设计为 <T, K extends React.Key> 提供了良好的类型推断能力。

README.md (1)

154-158: 文档中的 groupKey 类型与代码实现不一致。

文档(第 156-157 行)声明 scrollTo 方法支持 groupKey: React.Key,但 src/interface.ts 第 10 行的实际类型定义为 groupKey: string。这种不一致会误导用户使用数字类型的分组键。

建议在修复 src/interface.ts 中的类型定义后,确保此文档描述与代码保持同步。

Comment on lines +7 to +13
export type ListyScrollToConfig =
| Parameters<ScrollTo>[0]
| {
groupKey: string;
align?: ScrollAlign;
offset?: number;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

groupKey 的类型应与 Group<T, K> 中的 K 保持一致。

当前 ListyScrollToConfig 中的 groupKey 被硬编码为 string(第 10 行),但 Group<T, K> 接口允许 K 为任意 React.Key 类型(string | number | bigint)。如果用户的分组使用数字键,则无法将数字 groupKey 传递给 scrollTo 方法,造成类型不匹配。

应用此差异以支持所有 React.Key 类型:

 export type ListyScrollToConfig =
   | Parameters<ScrollTo>[0]
   | {
-      groupKey: string;
+      groupKey: React.Key;
       align?: ScrollAlign;
       offset?: number;
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type ListyScrollToConfig =
| Parameters<ScrollTo>[0]
| {
groupKey: string;
align?: ScrollAlign;
offset?: number;
};
export type ListyScrollToConfig =
| Parameters<ScrollTo>[0]
| {
groupKey: React.Key;
align?: ScrollAlign;
offset?: number;
};
🤖 Prompt for AI Agents
In src/interface.ts around lines 7 to 13, the groupKey in ListyScrollToConfig is
hardcoded as string which mismatches Group<T, K> where K can be any React.Key;
change the groupKey type to React.Key (or the same generic K if this file is
generic) so it accepts string | number | bigint, and add the necessary
import/type-only reference to React at the top (e.g., import type React from
"react") or adjust the surrounding generics to propagate K so the scrollTo API
accepts the same key types as Group.

Comment on lines +3 to +7
export function isGroupScrollConfig(
config: ListyScrollToConfig,
): config is { groupKey: string; align?: ScrollAlign; offset?: number } {
return !!config && typeof config === 'object' && 'groupKey' in config;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

类型守卫应验证 groupKey 的类型,而非仅检查其存在性。

当前实现仅使用 'groupKey' in config 检查属性是否存在,但未验证 groupKey 的类型是否为 string。如果 config.groupKeyundefinednull 或其他类型,类型守卫仍会返回 true,导致类型断言不安全。

应用此差异修复类型检查:

 export function isGroupScrollConfig(
   config: ListyScrollToConfig,
 ): config is { groupKey: string; align?: ScrollAlign; offset?: number } {
-  return !!config && typeof config === 'object' && 'groupKey' in config;
+  return (
+    !!config &&
+    typeof config === 'object' &&
+    'groupKey' in config &&
+    typeof config.groupKey === 'string'
+  );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function isGroupScrollConfig(
config: ListyScrollToConfig,
): config is { groupKey: string; align?: ScrollAlign; offset?: number } {
return !!config && typeof config === 'object' && 'groupKey' in config;
}
export function isGroupScrollConfig(
config: ListyScrollToConfig,
): config is { groupKey: string; align?: ScrollAlign; offset?: number } {
return (
!!config &&
typeof config === 'object' &&
'groupKey' in config &&
typeof config.groupKey === 'string'
);
}
🤖 Prompt for AI Agents
In src/util/index.ts around lines 3 to 7, the type guard currently only checks
for the presence of groupKey using 'groupKey' in config which can be true for
non-string values; change the predicate to verify config is non-null object and
that typeof (config as any).groupKey === 'string' (or config.groupKey !==
undefined && typeof config.groupKey === 'string') so the guard only returns true
when groupKey is actually a string, preserving the existing optional
align/offset typing.

Copy link

@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)
src/hooks/useStickyGroupHeader.tsx (1)

97-110: 防御性检查缺失(与之前审查重复)

此代码段存在之前审查中已指出的问题:

  1. 第 102 行group.title() 在类型上 group 可为 undefined,虽然调用方通过 enabled: !!(sticky && group) 隐式保证了当 enabledtruegroup 存在,但 hook 本身未显式防御。若未来其他调用方不满足此约束,会导致运行时异常。

  2. 第 107 行containerRef.current 在组件挂载完成前可能为 null,Portal 的 getContainer 回调可能返回 null

建议按照之前审查中的建议,在第 31 行后、创建 headerNode 前添加防御性检查:

      lastHeaderIdxRef.current = activeHeaderIdx;

+     // group 在类型上是可选的,防御性处理避免运行时错误
+     if (!group) {
+       return null;
+     }
+
+     if (!containerRef.current) {
+       return null;
+     }

      const currHeader = headerRows[activeHeaderIdx];
🧹 Nitpick comments (1)
src/hooks/useStickyGroupHeader.tsx (1)

93-93: 缺少分号,建议补充以保持代码风格一致

第 93 行末尾缺少分号,虽然 JavaScript 的自动分号插入(ASI)机制会处理,但与文件中其他代码风格不一致,可能会被 linter 标记。

-     const activeHeaderIdx = resolveByScrollTop(scrollTop)
+     const activeHeaderIdx = resolveByScrollTop(scrollTop);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60c2b2e and c87871c.

📒 Files selected for processing (4)
  • src/List.tsx (1 hunks)
  • src/hooks/useStickyGroupHeader.tsx (1 hunks)
  • tests/hooks.test.tsx (1 hunks)
  • tests/listy.behavior.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/listy.behavior.test.tsx
  • src/List.tsx
  • tests/hooks.test.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/hooks/useStickyGroupHeader.tsx (1)
src/interface.ts (1)
  • Group (21-24)

Comment on lines +42 to +54
const getHolderScrollTop = () => {
const container = containerRef.current;
const holder =
container?.querySelector<HTMLDivElement>('.rc-virtual-list-holder') ||
listRef.current?.nativeElement?.querySelector?.(
'.rc-virtual-list-holder',
);
if (holder) {
return holder.scrollTop;
}
const infoScrollTop = listRef.current?.getScrollInfo?.().y;
return infoScrollTop;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

scrollTop 可能为 undefined,导致吸顶头部定位错误

getHolderScrollTop 在无法找到 holder 元素且 listRef.current 为 null 或 getScrollInfo 不可用时会返回 undefined(第 53 行)。此时第 93 行将 undefined 传给 resolveByScrollTop,该函数内部的比较运算(第 68、81 行)会因 undefined >= numbernumber <= undefined 均为 false 而导致二分查找失效,始终返回 candidate = 0,错误地显示第一个分组头部。

建议在第 92 行后添加防御性处理:

      const scrollTop = getHolderScrollTop();
+     if (scrollTop === undefined) {
+       return null;
+     }
      const activeHeaderIdx = resolveByScrollTop(scrollTop);

同时可为 getHolderScrollTopresolveByScrollTop 添加明确的类型标注以提升类型安全性。

Also applies to: 92-93

🤖 Prompt for AI Agents
In src/hooks/useStickyGroupHeader.tsx around lines 42 to 54 and usage at 92-93,
getHolderScrollTop can return undefined when holder or getScrollInfo is not
available, causing resolveByScrollTop to receive undefined and break the
binary-search logic; change getHolderScrollTop to always return a number
(default to 0) and add a defensive check before calling resolveByScrollTop so
you never pass undefined (e.g., compute scrollTop = getHolderScrollTop() ?? 0
and short-circuit or clamp negative values), and add explicit number return
types to getHolderScrollTop and resolveByScrollTop to improve type safety.

@codecov
Copy link

codecov bot commented Nov 17, 2025

Codecov Report

❌ Patch coverage is 98.70130% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.70%. Comparing base (41ce994) to head (77a440d).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/hooks/useStickyGroupHeader.tsx 96.15% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #17      +/-   ##
==========================================
+ Coverage   97.75%   98.70%   +0.94%     
==========================================
  Files          12        6       -6     
  Lines         801      154     -647     
  Branches      243       39     -204     
==========================================
- Hits          783      152     -631     
+ Misses         18        2      -16     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

return {
__esModule: true,
default: (items: any[], group: any, segments: any) =>
mockFlattenRows ?? actual.default(items, group, segments),

Check warning

Code scanning / CodeQL

Useless conditional Warning test

This use of variable 'mockFlattenRows' always evaluates to false.
@socket-security
Copy link

socket-security bot commented Nov 17, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​classnames@​2.3.41001003950100
Updated@​types/​jest@​30.0.0 ⏵ 29.5.14100 +110077 +181100
Addedcross-env@​7.0.310010010084100
Addedrc-virtual-list@​3.19.2991009289100

View full report

@aojunhao123 aojunhao123 merged commit 61c83f6 into react-component:master Nov 17, 2025
8 checks passed
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