Skip to content

Conversation

cactuser-Lu
Copy link
Contributor

@cactuser-Lu cactuser-Lu commented Aug 25, 2025

对于以下代码,当0、1列为固定列,且使用 onCell 进行表格列合并时,存在一个问题:

//第0列
onCell: (_, index) => ({
  colSpan: index === 1 ?0 : 1,
}),
//第1列
onCell: (_, index) => ({
  colSpan: index === 1 ?2 : 1,
}),
  1. 当 columnIndex: 0 的单元格因为 colSpan 返回 0 而不被渲染时,浏览器会顺延地让 columnIndex: 1 且 colspan="2" 的 占据第 0 列和第 1 列的空间。所以 的宽度是 width[0] + width[1],这是符合预期的。
  2. 偏移不正确:useStickyOffsets在计算固定列的 left 偏移时,没有排除掉那些因 colSpan 返回 0 而在视觉上被“隐藏”的列,导致偏移量计算错误,这里的left计算为width[0] ,而实际应该为0,出现重叠或错位。

为此,我在bodyrow中动态去除了 colSpan=0列的宽度,修正了相关方法

Summary by CodeRabbit

  • 新功能

    • 表格按行“粘性偏移”增强:按行动态计算固定列偏移,按需跳过 colSpan=0 列,并使用预计算的单元格属性缓存以提升渲染稳定性。
  • 修复

    • 解决 colSpan=0 导致的固定列错位与遮挡问题,滚动与定位更稳定,兼容可展开行场景。
  • 变更

    • 行级信息包含列宽;粘性相关接口接受可选行上下文与缓存参数以支持按行计算。
  • 测试

    • 新增回归测试,覆盖固定列、合并单元格及可展开行交互。

Copy link

vercel bot commented Aug 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
table Ready Ready Preview Comment Aug 26, 2025 3:26am

Copy link

coderabbitai bot commented Aug 25, 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.

Walkthrough

引入按行粘性偏移支持:useStickyOffsets 增加可选 rowContextuseRowInfo 暴露 colWidthsBodyRow 计算 hasColSpanZero、构建 cellPropsCache 并将 rowStickyOffsetshasColSpanZero 与缓存传入 getCellPropsgetCellProps 签名相应扩展,兼容无粘性场景。

Changes

Cohort / File(s) Change Summary
Sticky 偏移计算扩展
src/hooks/useStickyOffsets.ts
增加可选 rowContext 参数;按行从 column.onCell(rowContext.record, rowContext.rowIndex) 获取 colSpan,当 colSpan === 0 时跳过固定列宽度累加;依赖项加入 rowContext;更新 JSDoc 与签名。
行上下文补充列宽
src/hooks/useRowInfo.tsx
从 TableContext 读取并返回 colWidths(在返回值与类型中加入 colWidths),未改变其他行为。
Body 行集成 sticky 与缓存
src/Body/BodyRow.tsx
引入 useStickyOffsetsgetCellFixedInfo;从 rowInfo 解构 colWidths;用 useMemo 计算 hasColSpanZero 并构建 cellPropsCache(预计算每列 onCell);按需传入 {record,rowIndex}useStickyOffsets;扩展 getCellProps 签名以接收 rowStickyOffsetshasColSpanZerocachedCellProps;在固定列且存在按行 colSpan 情况时通过 getCellFixedInfo 重新计算 fixedInfo;使用缓存避免重复调用 column.onCell
测试:colSpan 与固定列回归
tests/FixedColumn.spec.tsx
新增回归测试覆盖 colSpan=0 与固定列交互场景(含可展开行),验证渲染单元格数量、合并行为与计算的 sticky left 偏移。

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant BR as BodyRow
  participant RI as useRowInfo
  participant US as useStickyOffsets
  participant GCP as getCellProps
  participant GFI as getCellFixedInfo

  BR->>RI: 读取 flattenColumns、colWidths、record、index 等
  BR->>BR: 遍历列,调用 column.onCell 构建 cellPropsCache,并计算 hasColSpanZero
  alt hasColSpanZero
    BR->>US: useStickyOffsets(colWidths, flattenColumns, {record,rowIndex})
  else
    BR->>US: useStickyOffsets(colWidths, flattenColumns)
  end
  BR->>GCP: getCellProps(..., rowStickyOffsets, hasColSpanZero, cachedCellProps)
  note over GCP,GFI: 若列为 fixed 且 hasColSpanZero 且 提供 rowStickyOffsets
  GCP->>GFI: getCellFixedInfo(colIndex, colIndex, flattenColumns, rowStickyOffsets)
  GFI-->>GCP: 返回固定列位置信息
  GCP-->>BR: 返回带 fixed/offset 的 cell props
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–75 minutes

Possibly related PRs

Suggested reviewers

  • afc163
  • zombieJ

Poem

我是小兔在跑道,
列宽跳跃算得巧,
固定随行不迷路,
缓存轻敲少重复,
代码跳跃庆成功 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e317908 and 08a1a65.

📒 Files selected for processing (4)
  • src/Body/BodyRow.tsx (7 hunks)
  • src/hooks/useRowInfo.tsx (2 hunks)
  • src/hooks/useStickyOffsets.ts (3 hunks)
  • tests/FixedColumn.spec.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/hooks/useStickyOffsets.ts
  • tests/FixedColumn.spec.tsx
  • src/Body/BodyRow.tsx
  • src/hooks/useRowInfo.tsx
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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.

Summary of Changes

Hello @cactuser-Lu, 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!

此PR旨在修复表格组件中,当固定列与onCell方法结合使用进行单元格合并时,由于colSpan返回0导致宽度计算偏移不正确的问题。这会导致固定列出现重叠或错位。通过在BodyRow组件中动态调整useStickyOffsets的计算逻辑,确保colSpan为0的列的宽度被正确排除,从而修正了固定列的偏移量。

Highlights

  • 表格行处理: 在BodyRow组件中引入新的逻辑,以检测行中是否存在colSpan为0的单元格,并根据此信息动态调整固定列的偏移量计算。
  • 粘性偏移量计算: 修改useStickyOffsets钩子,使其能够根据单元格的onCell属性动态获取colSpan值,并在计算固定列偏移时排除colSpan为0的列的宽度。
  • 单元格属性处理: 更新了单元格属性获取函数getCellProps,使其在计算固定列的fixedInfo时,能够利用新的行级粘性偏移量信息。
  • 行信息钩子: 调整了useRowInfo钩子,以传递列宽信息,支持BodyRow中更精确的宽度计算。
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 in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

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 issue 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

您好,感谢您对修复表格 onCell 方法导致固定列宽度计算偏移问题的贡献。整体的修复思路是正确的,通过在行级别动态计算 sticky 偏移量来解决 colSpan=0 带来的布局问题。我发现有两处可以改进的地方:一处是 useStickyOffsets 钩子中的逻辑可能引入新问题,另一处是 BodyRow 中存在性能优化的空间。请查看我的具体建议。

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

🧹 Nitpick comments (6)
src/hooks/useRowInfo.tsx (1)

109-124: 避免返回整个 context,减小不必要的重渲染面

当前直接 ...context 会把 TableContext 的全部字段都放进返回对象,这会让依赖 useRowInfo 的组件在 context 其它无关字段变化时也产生重渲染。建议仅返回 Pick 中声明的键,保持“最小暴露面”。不影响功能,但可作为一次小的性能与可维护性优化。

可以考虑如下精简(仅示意主要键,按需补全),保持类型与实际使用一致:

-  return {
-    ...context,
+  return {
+    prefixCls: context.prefixCls,
+    fixedInfoList: context.fixedInfoList,
+    flattenColumns: context.flattenColumns,
+    colWidths: context.colWidths,
+    expandableType: context.expandableType,
+    expandRowByClick: context.expandRowByClick,
+    onTriggerExpand: onInternalTriggerExpand,
+    rowClassName: context.rowClassName,
+    expandedRowClassName: context.expandedRowClassName,
+    indentSize: context.indentSize,
+    expandIcon: context.expandIcon,
+    expandedRowRender: context.expandedRowRender,
+    expandIconColumnIndex: context.expandIconColumnIndex,
+    expandedKeys: context.expandedKeys,
+    childrenColumnName: context.childrenColumnName,
+    onRow: context.onRow,
     columnsKey,
     nestExpandable,
     expanded,
     hasNestChildren,
     record,
-    onTriggerExpand: onInternalTriggerExpand,
     rowSupportExpand,
     expandable: mergedExpandable,
     rowProps: {
       ...rowProps,
       className: classNames(computeRowClassName, rowProps?.className),
       onClick,
     },
   };
src/hooks/useStickyOffsets.ts (1)

23-37: 仅在“对应侧”累加宽度,避免无意义的宽度累加;同时不要在无 rowContext 时读取列级 colSpan

  • 目前 total += ... 的判定是 column.fixed && colSpan !== 0,这会在计算 start(左固定)时也把“右固定列”的宽度累加进 start 的中间态值;虽然最终不会被取用(右固定位使用 end),但会平白增加计算噪音,且不利于日后维护。
  • rowContext 不存在时,使用 column.colSpan ?? 1 作为回退可能会误将“列定义层面的 colSpan(更偏向表头语义)”带入“行内单元格”逻辑,改变既有行为。建议在无 rowContext 时统一按 1 处理。

建议如下精确到“固定方向”的实现,同时移除列级 colSpan 回退(保持无 rowContext 时的行为与历史一致):

-    const getOffsets = (startIndex: number, endIndex: number, offset: number) => {
+    const getOffsets = (
+      startIndex: number,
+      endIndex: number,
+      offset: number,
+      side: 'start' | 'end',
+    ) => {
       const offsets: number[] = [];
       let total = 0;

       for (let i = startIndex; i !== endIndex; i += offset) {
         const column = flattenColumns[i];

         offsets.push(total);

-        let colSpan = 1;
-        if (rowContext) {
-          const cellProps = column.onCell?.(rowContext.record, rowContext.rowIndex) || {};
-          colSpan = cellProps.colSpan ?? 1;
-        } else {
-          colSpan = column.colSpan ?? 1;
-        }
+        // 仅逐行模式下解析 colSpan;否则默认 1
+        const colSpan =
+          rowContext
+            ? (column.onCell?.(rowContext.record, rowContext.rowIndex)?.colSpan ?? 1)
+            : 1;

-        if (column.fixed && colSpan !== 0) {
+        const isFixedStart = column.fixed === true || column.fixed === 'left';
+        const isFixedEnd = column.fixed === 'right';
+        const shouldCount = side === 'start' ? isFixedStart : isFixedEnd;
+        if (shouldCount && colSpan !== 0) {
           total += colWidths[i] || 0;
         }
       }

       return offsets;
     };

-    const startOffsets = getOffsets(0, columnCount, 1);
-    const endOffsets = getOffsets(columnCount - 1, -1, -1).reverse();
+    const startOffsets = getOffsets(0, columnCount, 1, 'start');
+    const endOffsets = getOffsets(columnCount - 1, -1, -1, 'end').reverse();

这样可以:

  • 排除与当前侧无关的固定列宽度,降低心智负担;
  • 避免将“列定义层面的 colSpan”误用于 Body 的单元格宽度折算。

请用右固定列(例如最后两列 fixed: 'right')的场景跑一下回归,确认 end 偏移与历史版本一致;同时验证本 PR 提到的 0/1 列左固定 + colSpan=0/2 的复现用例在该实现下仍然正确。

src/Body/BodyRow.tsx (4)

70-72: 当前条件将所有 fixed 单元都走“逐行偏移”路径;建议仅在需要时启用

这里判断为 column.fixed && rowStickyOffsets,而上层始终会调用 useStickyOffsets(...),因此 rowStickyOffsets 永远为真。这样在“没有 colSpan=0”的常规场景中也会放弃使用既有的 fixedInfoList[colIndex]。为尽量减少无关行为差异,建议仅在本行确实存在 colSpan=0 时才传入 rowStickyOffsets,否则传 undefined 让代码走旧路径。

对应改动建议在调用处(见下条评论)按需传参。


162-168: hasColSpanZero 会对全列调用一次 onCell,后续每个单元格还会再调一次 onCell(在 getCellProps 中),存在重复计算

在逐行开启 sticky 的场景下,onCell 可能被调用两次(检测 + 取 additional props)。可以考虑做一个轻量缓存(例如 Map<colIndex, cellProps>)在本行生命周期内复用,以减少自定义 onCell 的开销。这是性能微优化,不影响本次修复的正确性。

示意实现(不要求本 PR 内必改):

+  const cellPropsCache = React.useMemo(() => {
+    const m = new Map<number, any>();
+    flattenColumns.forEach((col, i) => {
+      const props = col.onCell?.(record, index) || {};
+      m.set(i, props);
+    });
+    return m;
+  }, [flattenColumns, record, index]);
+
-  const hasColSpanZero = React.useMemo(() => {
-    return flattenColumns.some(col => {
-      const cellProps = col.onCell?.(record, index) || {};
-      return (cellProps.colSpan ?? 1) === 0;
-    });
-  }, [flattenColumns, record, index]);
+  const hasColSpanZero = React.useMemo(
+    () => Array.from(cellPropsCache.values()).some(p => (p.colSpan ?? 1) === 0),
+    [cellPropsCache],
+  );

随后在 getCellProps 调用处优先读取 cellPropsCache.get(colIndex) 并作为 fallback 传入,避免重复求值。


169-174: useStickyOffsets 无条件执行没问题,但可减少对新逻辑的“误用”

Hook 必须在同一顺序调用,当前写法是正确的。但结合上一条建议,你可以只在 hasColSpanZero 为真时将返回值传递下去(否则传 undefined),以避免正常行也走逐行路径带来潜在行为差异。


263-266: 关于阴影与可视宽度:逐行 sticky 下的阴影偏移可能仍受 widths(未排除 colSpan=0)影响

getCellFixedInfo 中计算阴影偏移(offsetFixedStartShadow/offsetFixedEndShadow)依赖 stickyOffsets.widths。当前我们仅在 start/end 偏移处排除了 colSpan=0 的列宽,widths 仍是原始 colWidths。在左/右固定列前存在被合并(colSpan=0)但非固定的列时,阴影长度可能略有偏差。建议在真实用例下观察,如有视觉差异,可考虑在 fixUtil 中对阴影偏移的计算也引入逐行 colSpan 的判定(可后续 PR 跟进)。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a7b68af and bc3e1f5.

📒 Files selected for processing (3)
  • src/Body/BodyRow.tsx (6 hunks)
  • src/hooks/useRowInfo.tsx (2 hunks)
  • src/hooks/useStickyOffsets.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。
📚 Learning: 2024-11-08T12:53:09.293Z
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。

Applied to files:

  • src/Body/BodyRow.tsx
🧬 Code graph analysis (2)
src/hooks/useStickyOffsets.ts (1)
src/interface.ts (1)
  • ColumnType (118-133)
src/Body/BodyRow.tsx (1)
src/utils/fixUtil.ts (1)
  • getCellFixedInfo (33-102)
🔇 Additional comments (5)
src/hooks/useRowInfo.tsx (1)

18-19: 补充透出 colWidths 的方向正确,契合本 PR 的修复思路

useRowInfo 现在将 colWidths 从 TableContext 取出并作为返回值透出,配合 BodyRow 的逐行 sticky 计算是必须的。依赖键也同步加入了 colWidths,一致性良好。无功能性风险点。

Also applies to: 45-46

src/hooks/useStickyOffsets.ts (2)

6-9: JSDoc 较为清晰,参数扩展说明到位

新增 rowContext 参数的目的描述清晰,有助于理解“逐行”场景下的 colSpan 参与计算的时机。


50-53: 依赖项选择基本合理,但仍建议关注 record 引用稳定性

useMemo 依赖包含了 rowContext.recordrowIndex,可以正确触发逐行重算。请确认上层传入的 record 引用在数据未变更时尽量保持稳定,以减少不必要的重算。若无法保证,可考虑在上层进行浅比较或稳定化处理。

src/Body/BodyRow.tsx (2)

48-49: getCellProps 新增 rowStickyOffsets 入参的接口扩展是必要的

为逐行 colSpan=0 的场景重新计算 fixedInfo 提供了入口,接口设计符合最小改动原则。


211-220: 仅在检测到本行存在 colSpan=0 时传入 rowStickyOffsets,以保持非粘滞场景的旧行为

这样能最大限度降低回归风险,且与修复目标更吻合。

[ suggest_optional_refactor ]

         const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps(
           rowInfo,
           column,
           colIndex,
           indent,
           index,
           rowKeys,
           expandedRowInfo?.offset,
-          rowStickyOffsets,
+          hasColSpanZero ? rowStickyOffsets : undefined,
         );

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/Body/BodyRow.tsx (1)

220-222: 仅在存在 colSpan=0 时传入 rowStickyOffsets 并重算 fixedInfo,已落实之前建议

此变更响应了先前建议“仅在必要时重算 fixedInfo”,避免对每一固定列在每一行都进行重复计算,性能更可控。

🧹 Nitpick comments (4)
src/Body/BodyRow.tsx (4)

153-154: 首次渲染/测量阶段 colWidths 可能为空的防御性考量

当列宽尚未测量完成时,colWidths 可能长度为 0 或与 flattenColumns.length 不一致。虽然 useStickyOffsets 往往内部会兜底,但为避免边界闪动或不必要的重算,建议在开发环境下做一次长度一致性告警,便于排查异常来源(列增删或合并导致的错配)。

可以考虑在开发模式加入轻量校验(示意,非必须):

if (process.env.NODE_ENV !== 'production' && colWidths && colWidths.length !== flattenColumns.length) {
  // eslint-disable-next-line no-console
  console.warn('[BodyRow] colWidths length mismatch with flattenColumns:', colWidths.length, flattenColumns.length);
}

163-169: 避免对同一行重复调用多次 onCell:可缓存以降低开销并规避潜在副作用

这里用 some(col => col.onCell?.(record, index)) 来探测 colSpan===0,稍后在 getCellProps 内又会再次调用 column.onCell(record, index) 生成 additionalCellProps。在大表或虚拟滚动场景下,双次调用既有性能成本,也放大了 onCell 的潜在副作用(虽然我们期望它是纯函数)。

建议缓存一次 onCell 结果并在两处复用:

可以在此处引入缓存(示例 diff,仅涉及当前代码块;配套在调用点处需要读取该缓存,见下方“额外改动”):

-  const hasColSpanZero = React.useMemo(() => {
-    return flattenColumns.some(col => {
-      const cellProps = col.onCell?.(record, index) || {};
-      return (cellProps.colSpan ?? 1) === 0;
-    });
-  }, [flattenColumns, record, index]);
+  const onCellPropsByColIndex = React.useMemo(
+    () => flattenColumns.map(col => col.onCell?.(record, index) || {}),
+    [flattenColumns, record, index],
+  );
+  const hasColSpanZero = React.useMemo(
+    () => onCellPropsByColIndex.some(p => (p.colSpan ?? 1) === 0),
+    [onCellPropsByColIndex],
+  );

额外改动(在本块之外):在调用 getCellProps 时,可将预计算的 onCellPropsByColIndex[colIndex] 作为入参传入,避免二次调用 onCell。如有需要,我可以补充完整的最小改动方案。


212-219: (可选)调用参数序列可读性改进

getCellProps 参数较长,末尾两个与“逐行 colSpan 修正”强相关。为了降低误传/错序风险,可将它们合并为一个 options 对象传入(例如 { expandedRowOffset, rowStickyOffsets, hasColSpanZero }),从而提升可读性与可维护性。此为可选建议。


114-120: 建议补充用例覆盖“首两列固定 + 第0列 colSpan=0 + 第1列 colSpan=2”的回归测试

为防止未来回归,建议在 e2e 或可视化单测中补充用例,断言:

  • 第1列固定单元格的 left 偏移为 0(而非 width[0]
  • 第1列单元格宽度为 width[0]+width[1]
  • 滚动与展开/收起状态切换后偏移保持正确

如需要,我可以基于现有测试框架补充最小可复现用例。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bc3e1f5 and 00f42d7.

📒 Files selected for processing (2)
  • src/Body/BodyRow.tsx (6 hunks)
  • src/hooks/useStickyOffsets.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useStickyOffsets.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。
📚 Learning: 2024-11-08T12:53:09.293Z
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。

Applied to files:

  • src/Body/BodyRow.tsx
🧬 Code graph analysis (1)
src/Body/BodyRow.tsx (1)
src/utils/fixUtil.ts (1)
  • getCellFixedInfo (33-102)
🔇 Additional comments (4)
src/Body/BodyRow.tsx (4)

11-12: 引入逐行 sticky 偏移计算相关依赖 ✅

新增 useStickyOffsetsgetCellFixedInfo 的 import 与本次修复方向一致,能支撑“按行动态修正偏移”的方案。无循环依赖迹象。


56-57: 解构引入 flattenColumns 用于重算 fixedInfo,方向正确

flattenColumnsgetCellProps 中用于 getCellFixedInfo 的重算,能与“逐行 stickyOffsets”配合;命名与上下文一致。


170-175: 逐行 sticky 偏移计算触发条件与参数选择合理

仅当 hasColSpanZero 为真时才传入 rowContext,避免了对全量行的额外计算压力;以 colWidths + flattenColumns + rowContext 为依赖可确保在列宽变更或合并规则变化时正确更新。


48-50: 无需更新:getCellProps 新增的可选参数不会引入 Breaking Change
经全量检索,仓库中仅存在以下调用点:

  • src/Body/BodyRow.tsx 内部对自身声明的调用
  • src/VirtualTable/VirtualCell.tsx 两处调用(仅传递前三个必需参数)

新增的 rowStickyOffsets?hasColSpanZero? 都是可选且位于参数尾部,现有调用无需传参即可正常工作,故不影响兼容性。

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

Caution

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

⚠️ Outside diff range comments (1)
src/Body/BodyRow.tsx (1)

94-112: 严重:不要原地修改缓存的 onCell 结果对象,rowSpan 会在重渲染中累加并污染 useMemo 缓存

当前 additionalCellProps 直接引用了 cellPropsCache[colIndex]。后续对 additionalCellProps.rowSpan 的赋值会“脏写”缓存对象,导致:

  • 下一次渲染读取到已经被修改过的 rowSpan,从而再次累加,出现行高异常;
  • cellPropsCache 的 useMemo 在依赖不变时仍持有被污染的对象,产生隐性状态泄漏。

建议对 cachedCellProps 进行浅拷贝后再修改,且以未拷贝的基值作为计算基线:

-  const additionalCellProps = cachedCellProps || column.onCell?.(record, index) || {};
+  const baseCellProps = cachedCellProps ?? column.onCell?.(record, index) ?? {};
+  const additionalCellProps = { ...baseCellProps };

   // Expandable row has offset
   if (expandedRowOffset) {
-    const { rowSpan = 1 } = additionalCellProps;
+    const baseRowSpan = baseCellProps.rowSpan ?? 1;

     // For expandable row with rowSpan,
     // We should increase the rowSpan if the row is expanded
-    if (expandable && rowSpan && colIndex < expandedRowOffset) {
-      let currentRowSpan = rowSpan;
+    if (expandable && baseRowSpan && colIndex < expandedRowOffset) {
+      let currentRowSpan = baseRowSpan;

-      for (let i = index; i < index + rowSpan; i += 1) {
+      for (let i = index; i < index + baseRowSpan; i += 1) {
         const rowKey = rowKeys[i];
         if (expandedKeys.has(rowKey)) {
           currentRowSpan += 1;
         }
       }
       additionalCellProps.rowSpan = currentRowSpan;
     }
   }
♻️ Duplicate comments (1)
src/Body/BodyRow.tsx (1)

70-74: 修复点确认:仅在存在 colSpan=0 的行上按需重算 fixedInfo,正确排除被隐藏列宽度

  • 判断条件 column.fixed && hasColSpanZero && rowStickyOffsets 精准,避免无谓重算并修复偏移错误。
  • 这也响应了先前评审中“仅在 hasColSpanZero 时重算”的建议。做得好。
🧹 Nitpick comments (5)
src/Body/BodyRow.tsx (5)

11-12: 类型解耦建议:导出函数签名不应依赖 hook 的 ReturnType

getCellProps 形参类型写成 ReturnType<typeof useStickyOffsets> 会把实现细节(hook)泄露到公共 API,并造成 d.ts 可读性与稳定性下降。建议使用框架内已有的结构化类型 StickyOffsets(fixUtil.ts 已在使用),仅做类型导入:

  • 优点:API 稳定、避免循环依赖、文档友好。

示例(按你们的类型定义落点调整 import 路径即可):

+ import type { StickyOffsets } from '../interface';
  export function getCellProps<RecordType>(
     ...
-    rowStickyOffsets?: ReturnType<typeof useStickyOffsets>,
+    rowStickyOffsets?: StickyOffsets,
     ...
  ) { ... }

Also applies to: 48-49


50-51: 把 cachedCellProps 精确标注为 onCell 的返回类型,去除 any

当前 cachedCellProps?: Record<string, any> 过于宽泛。可直接从列定义上推导返回类型,既减少 any 扩散,也便于 IDE 补全与约束。

-  cachedCellProps?: Record<string, any>,
+  cachedCellProps?: ReturnType<NonNullable<ColumnType<RecordType>['onCell']>>,

配合上一个评论中的“浅拷贝”修正,可完全避免对未知结构的原地写入。

Also applies to: 94-95


168-176: 减少不必要的粘性偏移重算:对 rowContext 做引用稳定化

hasColSpanZero 为 true 时,每次渲染都会创建新的 { record, rowIndex } 对象,可能导致 useStickyOffsets 下游依赖频繁失效。可以用 useMemo 稳定其引用。

-  const rowStickyOffsets = useStickyOffsets(
-    colWidths,
-    flattenColumns,
-    hasColSpanZero ? { record, rowIndex: index } : undefined,
-  );
+  const rowContext = React.useMemo(
+    () => (hasColSpanZero ? { record, rowIndex: index } : undefined),
+    [hasColSpanZero, record, index],
+  );
+  const rowStickyOffsets = useStickyOffsets(colWidths, flattenColumns, rowContext);

268-269: 可选:ExpandedRow 的 colSpan 在存在 colSpan=0 时可改为“可见列数”以避免空白错位

当前退化到 flattenColumns.length。当本行有若干列 colSpan=0 被隐藏时,展开行可能占用过多列数产生多余空白。可以用 cellPropsCache 计算可见列数量:

+  // 统计本行可见列数(排除 colSpan=0)
+  const visibleColCount = React.useMemo(
+    () => cellPropsCache.filter(cp => ((cp.colSpan ?? 1) !== 0)).length,
+    [cellPropsCache],
+  );
...
-        colSpan={expandedRowInfo ? expandedRowInfo.colSpan : flattenColumns.length}
+        colSpan={expandedRowInfo ? expandedRowInfo.colSpan : visibleColCount}

此改动会改变展开行在合并列场景下的占位逻辑,请在 demo 中确认视觉符合预期。


126-283: 补充回归测试:覆盖“固定列 + colSpan=0/2 合并”的宽度与偏移

建议新增用例,确保本修复不被后续改动回归:

  • 左侧固定第 0、1 列;
  • 第 0 列 onCell:在 index === 1 时返回 { colSpan: 0 };
  • 第 1 列 onCell:在 index === 1 时返回 { colSpan: 2 };
  • 断言:第 1 列在该行的 left 偏移不包含第 0 列宽度(应为 0),且合并单元格宽度为 width[0] + width[1];
  • 覆盖展开行存在/不存在两种情况。

如需,我可以基于现有测试框架起草测试代码。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 00f42d7 and 0447cc5.

📒 Files selected for processing (1)
  • src/Body/BodyRow.tsx (7 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。
📚 Learning: 2024-11-08T12:53:09.293Z
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。

Applied to files:

  • src/Body/BodyRow.tsx
🧬 Code graph analysis (1)
src/Body/BodyRow.tsx (1)
src/utils/fixUtil.ts (1)
  • getCellFixedInfo (33-102)
🔇 Additional comments (2)
src/Body/BodyRow.tsx (2)

40-51: 确认无需关注向后兼容性变更:getCellProps 仅作内部使用,可安全添加可选参数

  • 仓库内仅有两处调用
    • src/VirtualTable/VirtualCell.tsx(第 58–62 行)
    • src/Body/BodyRow.tsx(第 212–216 行)
  • 类型声明文件中无任何 .d.ts 暴露 getCellProps

新增的三个参数均为末尾可选参数,不会影响现有调用,符合 JavaScript/TypeScript 的向后兼容性原则,无需进一步操作。


164-167: 确认 Cell 组件内部无 onCell 调用,可放心使用预计算

经脚本验证,src/Footer/Cell.tsx 中未检测到任何 onCell(onCell 引用,说明 <Cell> 内部不会再次调用 column.onCellcellPropsCache 能有效避免重复执行复杂逻辑,无需额外修改。

  • 验证位置:src/Footer/Cell.tsx
  • 验证结果:未发现 onCell 调用或引用

@afc163 afc163 requested a review from Copilot August 26, 2025 02:04
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes an issue with sticky column offset calculation when using onCell method for table column merging. When columns have colSpan: 0, they are visually hidden but their widths were still being included in sticky offset calculations, causing misalignment and overlap of fixed columns.

  • Adds row context support to useStickyOffsets hook for dynamic colSpan calculation
  • Implements per-row sticky offset calculation in BodyRow component
  • Caches cell properties to avoid redundant onCell calls

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/hooks/useStickyOffsets.ts Enhanced to accept row context and exclude colSpan=0 columns from offset calculations
src/hooks/useRowInfo.tsx Added colWidths to the exported row information
src/Body/BodyRow.tsx Implemented row-level sticky offset calculation with colSpan awareness

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +164 to +165
const cellPropsCache = React.useMemo(() => {
return flattenColumns.map(col => col.onCell?.(record, index) || {});
Copy link
Preview

Copilot AI Aug 26, 2025

Choose a reason for hiding this comment

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

The cellPropsCache is recalculated on every render when record or index changes. For large tables with many columns, this could be expensive. Consider memoizing individual cell props or using a more granular dependency array.

Suggested change
const cellPropsCache = React.useMemo(() => {
return flattenColumns.map(col => col.onCell?.(record, index) || {});
return flattenColumns.map((col, colIdx) =>
React.useMemo(
() => col.onCell?.(record, index) || {},
[col, col.onCell, record, index]
)
);

Copilot uses AI. Check for mistakes.

Comment on lines +172 to +176
const rowStickyOffsets = useStickyOffsets(
colWidths,
flattenColumns,
hasColSpanZero ? { record, rowIndex: index } : undefined,
);
Copy link
Preview

Copilot AI Aug 26, 2025

Choose a reason for hiding this comment

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

The rowStickyOffsets hook is called on every render even when hasColSpanZero is false. Consider conditionally calling this hook only when needed to avoid unnecessary calculations.

Suggested change
const rowStickyOffsets = useStickyOffsets(
colWidths,
flattenColumns,
hasColSpanZero ? { record, rowIndex: index } : undefined,
);
const rowStickyOffsets = hasColSpanZero
? useStickyOffsets(
colWidths,
flattenColumns,
{ record, rowIndex: index },
)
: undefined;

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

条件性调用 Hook违反规则

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

🧹 Nitpick comments (3)
tests/FixedColumn.spec.tsx (1)

427-472: 补强断言:展开行场景下继续验证合并单元格与子行结构

建议在“可展开行”用例中补充对父行合并单元格的断言,以及子行不受影响的基础断言,使回归测试覆盖更完整。

       const parentRow = allRows[1];
       const parentCells = parentRow.querySelectorAll('.rc-table-cell');
       expect(parentCells).toHaveLength(2);
+      // 父行第一个单元格应保持合并
+      expect(parentCells[0]).toHaveAttribute('colSpan', '2');

       const childRow = allRows[2];
       const childCells = childRow.querySelectorAll('.rc-table-cell');
       expect(childCells).toHaveLength(3);
+      // 子行不应受父行合并影响
+      expect(childCells[0]).not.toHaveAttribute('colSpan', '0');
src/Body/BodyRow.tsx (2)

48-51: 为 cachedCellProps 提供更精确的类型,减少 any 滥用

cachedCellProps?: Record<string, any> 过于宽泛。考虑收紧到表格单元格可接受的 HTML 属性并显式包含行/列合并字段,有助于类型提示和重构安全。

-  cachedCellProps?: Record<string, any>,
+  cachedCellProps?: React.TdHTMLAttributes<HTMLTableCellElement> & {
+    colSpan?: number;
+    rowSpan?: number;
+  },

70-75: 仅在需要时重算 fixedInfo 的策略正确;进一步兼容 colSpan>1 的固定阴影/层级计算

当前在存在 colSpan=0 且提供了 rowStickyOffsets 时重算 fixedInfo,能避免把隐藏列宽度计入偏移,方向正确。不过当当前单元格自身存在 colSpan>1 时,getCellFixedInfocolEnd 仍然使用 colIndex,可能导致阴影/层级计算(如 zIndex 与 shadow 判定)与实际跨列范围不完全一致。建议利用已传入的 cachedCellProps 推导 colEnd

-  if (column.fixed && hasColSpanZero && rowStickyOffsets) {
-    fixedInfo = getCellFixedInfo(colIndex, colIndex, flattenColumns, rowStickyOffsets);
-  }
+  if (column.fixed && hasColSpanZero && rowStickyOffsets) {
+    const span = Math.max(1, Number(cachedCellProps?.colSpan ?? 1));
+    const colEnd = Math.min(flattenColumns.length - 1, colIndex + span - 1);
+    fixedInfo = getCellFixedInfo(colIndex, colEnd, flattenColumns, rowStickyOffsets);
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0447cc5 and dd6db3e.

⛔ Files ignored due to path filters (1)
  • tests/__snapshots__/FixedColumn.spec.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • src/Body/BodyRow.tsx (7 hunks)
  • tests/FixedColumn.spec.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。
📚 Learning: 2024-11-08T12:53:09.293Z
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。

Applied to files:

  • src/Body/BodyRow.tsx
🧬 Code graph analysis (2)
tests/FixedColumn.spec.tsx (1)
src/interface.ts (1)
  • ColumnsType (135-138)
src/Body/BodyRow.tsx (1)
src/utils/fixUtil.ts (1)
  • getCellFixedInfo (33-102)
🔇 Additional comments (4)
tests/FixedColumn.spec.tsx (1)

348-381: 新增的测试列定义覆盖了关键场景,设计合理

  • 固定左侧的第 0/1 列分别返回 colSpan=0 与 colSpan=2(仅第 1 行),能稳定复现并验证粘性偏移的计算修复点。
  • 列宽分别为 100/120/150,便于在断言中确认 left/阴影计算是否正确。
src/Body/BodyRow.tsx (3)

164-171: 按行预取 onCell 与 hasColSpanZero 的判定方式合理

提前计算 cellPropsCache 并据此判断是否存在 colSpan=0,从而在需要时开启“按行” sticky 偏移,达到了最小化重算的目的。同时避免在渲染 Cell 时重复调用 onCell。LGTM。


172-177: 正确使用 Hook:始终调用 useStickyOffsets,仅在需要时传入 rowContext

未采用条件调用 Hook 的做法,符合 Hooks 规则;通过传入/省略 rowContext 控制行为,既保证类型与时序安全,也避免无效重算。赞。


214-225: 参数穿透清晰且向后兼容

rowStickyOffsetshasColSpanZero 与缓存的 cellProps 透传给 getCellProps,旧签名保持兼容(新增均为可选参数)。这与修复目标强相关,风险可控。

Comment on lines +419 to +424
const hasFixedLeftClass = mergedCell.classList.contains('rc-table-cell-fix-left');

if (hasFixedLeftClass) {
const cellStyle = window.getComputedStyle(mergedCell);
expect(cellStyle.left).toBe('0px');
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

让断言更强:固定列类名应为必然条件,且使用逻辑方向属性以避免环境差异

当前以“如果包含 rc-table-cell-fix-left 再断言 left”为条件,可能在类名缺失时静默通过,掩盖回归。并且使用 left 在开启逻辑方向属性(inset-inline-start)的实现或 RTL 下可能不稳定。建议:

  • 显式断言单元格具有固定左类名;
  • 使用 computedStyle 同时兼容 leftinset-inline-start

[建议变更如下]

-      const hasFixedLeftClass = mergedCell.classList.contains('rc-table-cell-fix-left');
-
-      if (hasFixedLeftClass) {
-        const cellStyle = window.getComputedStyle(mergedCell);
-        expect(cellStyle.left).toBe('0px');
-      }
+      // 固定左列应当存在
+      expect(mergedCell).toHaveClass('rc-table-cell-fix-left');
+      // 兼容传统属性与逻辑方向属性
+      const cellStyle = window.getComputedStyle(mergedCell);
+      const left = cellStyle.left;
+      const insetInlineStart = cellStyle.getPropertyValue('inset-inline-start');
+      expect(left === '0px' || insetInlineStart === '0px').toBeTruthy();
📝 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
const hasFixedLeftClass = mergedCell.classList.contains('rc-table-cell-fix-left');
if (hasFixedLeftClass) {
const cellStyle = window.getComputedStyle(mergedCell);
expect(cellStyle.left).toBe('0px');
}
// 固定左列应当存在
expect(mergedCell).toHaveClass('rc-table-cell-fix-left');
// 兼容传统属性与逻辑方向属性
const cellStyle = window.getComputedStyle(mergedCell);
const left = cellStyle.left;
const insetInlineStart = cellStyle.getPropertyValue('inset-inline-start');
expect(left === '0px' || insetInlineStart === '0px').toBeTruthy();
🤖 Prompt for AI Agents
In tests/FixedColumn.spec.tsx around lines 419 to 424, the test currently
conditionally asserts the computed left when the cell has the
'rc-table-cell-fix-left' class which can silently pass if the class is missing
and uses the physical 'left' property which is unstable in RTL or when logical
inset-inline-start is used; change this to explicitly assert that
mergedCell.classList contains 'rc-table-cell-fix-left' (fail the test if
missing), then read computedStyle and assert that either computedStyle.left ===
'0px' or computedStyle.getPropertyValue('inset-inline-start') === '0px' (i.e.,
require at least one to be '0px') so the test is both strict about the class
presence and robust across logical/physical CSS directions.

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/hooks/useStickyOffsets.ts (1)

27-31: 移除了回退到 column.colSpan 的逻辑,修正了过去的误用

当 rowContext 不存在时默认 colSpan=1,不再错误地使用 column.colSpan 参与“行级”偏移计算,避免把表头或列级 colSpan 施加到所有行。和过往 review 建议一致,赞。

🧹 Nitpick comments (4)
src/hooks/useStickyOffsets.ts (4)

6-9: 文档补充建议:明确 rowContext 的使用时机与性能影响

建议在 JSDoc 中说明:仅当该“行”存在动态 colSpan=0(或其它动态合并)时才传入 rowContext;否则请传入 undefined 以避免不必要的 onCell 计算与 useMemo 重算。可加一行“当未提供 rowContext 时,行为与变更前保持一致”。


18-42: 使左右两侧的累计更显式:按方向过滤 fixed=left/right(健壮性提升)

当前实现用 column.fixed 的真值在 start/end 两次遍历中都做累加。假设列顺序始终为 [left][normal][right] 时是等价的,但可读性与对未来改动的鲁棒性较弱。建议按方向显式区分,仅在 start 时累加 fixed==='left',在 end 时累加 fixed==='right'。功能等价但意图更清晰,减少后续维护风险。

建议变更如下:

-    const getOffsets = (startIndex: number, endIndex: number, offset: number) => {
+    const getOffsets = (
+      startIndex: number,
+      endIndex: number,
+      step: number,
+      side: 'start' | 'end',
+    ) => {
       const offsets: number[] = [];
       let total = 0;

-      for (let i = startIndex; i !== endIndex; i += offset) {
+      for (let i = startIndex; i !== endIndex; i += step) {
         const column = flattenColumns[i];

         offsets.push(total);

-        let colSpan = 1;
-        if (rowContext) {
-          const cellProps = column.onCell?.(rowContext.record, rowContext.rowIndex) || {};
-          colSpan = cellProps.colSpan ?? 1;
-        }
+        let colSpan = 1;
+        if (rowContext) {
+          const cellProps = column.onCell?.(rowContext.record, rowContext.rowIndex) || {};
+          colSpan = cellProps.colSpan ?? 1;
+        }

-        if (column.fixed && colSpan !== 0) {
+        const isLeft = column.fixed === 'left';
+        const isRight = column.fixed === 'right';
+        if ((side === 'start' ? isLeft : isRight) && colSpan !== 0) {
           total += colWidths[i] || 0;
         }
       }

       return offsets;
     };
@@
-    const startOffsets = getOffsets(0, columnCount, 1);
-    const endOffsets = getOffsets(columnCount - 1, -1, -1).reverse();
+    const startOffsets = getOffsets(0, columnCount, 1, 'start');
+    const endOffsets = getOffsets(columnCount - 1, -1, -1, 'end').reverse();

16-36: 减少 onCell 的重复调用:预计算本行的 colSpan 映射

当前在 start 与 end 两次遍历中都会调用一次 column.onCell,导致每列每行计算两次。可在 useMemo 内(rowContext 存在时)预先生成 colSpanAt 数组,循环内直接索引,避免重复函数调用,尤其大列数/大数据量时更稳。

   const stickyOffsets: StickyOffsets = useMemo(() => {
     const columnCount = flattenColumns.length;
 
+    // 仅当按行上下文存在时,预先计算每列在该行的 colSpan
+    const colSpanAt: number[] | undefined = rowContext
+      ? flattenColumns.map(col => {
+          const cellProps = col.onCell?.(rowContext.record, rowContext.rowIndex) || {};
+          return cellProps.colSpan ?? 1;
+        })
+      : undefined;
+
     const getOffsets = (startIndex: number, endIndex: number, offset: number) => {
       const offsets: number[] = [];
       let total = 0;
 
       for (let i = startIndex; i !== endIndex; i += offset) {
         const column = flattenColumns[i];
 
         offsets.push(total);
 
-        let colSpan = 1;
-        if (rowContext) {
-          const cellProps = column.onCell?.(rowContext.record, rowContext.rowIndex) || {};
-          colSpan = cellProps.colSpan ?? 1;
-        }
+        const colSpan = colSpanAt ? colSpanAt[i] : 1;
 
         if (column.fixed && colSpan !== 0) {
           total += colWidths[i] || 0;
         }
       }

备注:若采纳上条“按方向过滤”的建议,请将本段与之合并应用。


49-49: 依赖数组建议去“对象壳”,仅依赖原子字段,降低无效重算

依赖 rowContext(对象)会因调用方每次创建新对象而频繁失效。建议改为依赖 rowContext?.rowIndex 与 rowContext?.record(引用),以减少无意义的 useMemo 失效;当 record 引用变化时仍会正确重算。

-  }, [colWidths, flattenColumns, rowContext]);
+  }, [colWidths, flattenColumns, rowContext?.rowIndex, rowContext?.record]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dd6db3e and e317908.

📒 Files selected for processing (1)
  • src/hooks/useStickyOffsets.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。
📚 Learning: 2024-11-08T12:53:09.293Z
Learnt from: bbb169
PR: react-component/table#1202
File: src/Table.tsx:903-904
Timestamp: 2024-11-08T12:53:09.293Z
Learning: 在 `src/Table.tsx` 文件的 React 组件 `Table` 中,即使 `bodyScrollLeft` 频繁更新,也需要在 `TableContextValue` 的 `useMemo` 依赖数组中包含 `bodyScrollLeft` 和 `headerCellRefs`,因为每次滚动时重新计算 `TableContextValue` 是解决该问题所必须的。

Applied to files:

  • src/hooks/useStickyOffsets.ts
🧬 Code graph analysis (1)
src/hooks/useStickyOffsets.ts (2)
src/interface.ts (1)
  • ColumnType (118-133)
src/index.ts (1)
  • ColumnType (25-25)
🔇 Additional comments (2)
src/hooks/useStickyOffsets.ts (2)

23-24: 局部变量 column 提升可读性

把 flattenColumns[i] 提前赋值为 column 简化了多次访问,阅读性更好。


13-14: 签名扩展校验通过
脚本检查结果显示:

  • 只有在 src/Body/BodyRow.tsx(第172–176行)按行场景下才会传入第三个 rowContext 参数,且严格使用 { record, rowIndex }
  • 其余所有调用(例如 src/Table.tsx:384)均为两参调用,不涉及第三参。

无需调整,闭环。

Copy link

codecov bot commented Aug 26, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.11%. Comparing base (b94558d) to head (e317908).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1315      +/-   ##
==========================================
- Coverage   96.18%   96.11%   -0.08%     
==========================================
  Files          57       57              
  Lines        3408     3448      +40     
  Branches      620      630      +10     
==========================================
+ Hits         3278     3314      +36     
- Misses        125      129       +4     
  Partials        5        5              

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

@afc163
Copy link
Member

afc163 commented Aug 26, 2025

有问题重现么?

@cactuser-Lu
Copy link
Contributor Author

有问题重现么?

这是一份复现的代码链接(demo),第1行0、1列合并后偏移位置不对

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.

2 participants