Skip to content

feat: support expand all control in expandable column header - #1505

Open
nikzanda wants to merge 12 commits into
react-component:masterfrom
nikzanda:feat/expand-all-header
Open

feat: support expand all control in expandable column header#1505
nikzanda wants to merge 12 commits into
react-component:masterfrom
nikzanda:feat/expand-all-header

Conversation

@nikzanda

@nikzanda nikzanda commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an opt-in expand/collapse-all control to the expandable column header.

This is intended to support the use case described in ant-design/ant-design#8791.

The implementation is ready for maintainer review. The corresponding Ant Design integration will follow once the API is accepted and released.

API

  • Add expandable.showExpandAll to enable the header control.
  • Add expandable.onExpandAll to observe expand/collapse-all actions.
  • Add a unified components.ExpandIcon for both row expansion and the expand-all control.
  • Deprecate expandable.expandIcon, retaining it as a row-only fallback for backward compatibility.
  • Allow expandable.columnTitle to use object-based render props, exposing the generated expand-all control as expandIcon.

components.ExpandIcon receives:

  • type: 'row' | 'all'
  • expanded
  • expandable
  • onClick
  • prefixCls
  • record for row controls only

expandable.columnTitle follows the object-based render props convention used by Ant Design column titles:

columnTitle={({ expandIcon }) => (
  <div>
    {expandIcon}
    <span>Details</span>
  </div>
)}

Static ReactNode titles keep their existing behavior.

Behavior

  • The feature is disabled by default.
  • Only rows allowed by rowExpandable are affected.
  • Controlled and uncontrolled expandedRowKeys are supported.
  • Existing unrelated expanded keys are preserved.
  • components.ExpandIcon takes precedence over the deprecated expandable.expandIcon fallback.
  • The unified component supports both expandedRowRender and nested/tree row controls.
  • The expand-all header control is currently limited to expandedRowRender tables.
  • expandable.columnTitle receives { expandIcon } when used as a render function.
  • The default expand-all control is keyboard accessible.

Summary by CodeRabbit

新功能

  • 新增“全部展开/收起”控件,支持一键操作所有可展开行。
  • 支持自定义行展开图标、展开全部图标和展开列标题。
  • 新增展开状态回调,并支持按条件筛选可展开行。
  • expandIcon 标记为弃用,建议使用 components.ExpandIcon

文档

  • 更新中英文文档,补充相关配置项、回调及迁移说明。

测试

  • 增加全部展开、收起、自定义图标及条件筛选等场景测试。

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

新增展开全部行功能。该功能支持默认或自定义图标、批量展开与收起、可展开行过滤、受控状态同步和自定义展开列表头。README 文档同步更新。

Changes

展开全部行支持

Layer / File(s) Summary
公开配置与图标渲染
src/interface.ts, src/utils/expandUtil.tsx, src/index.ts, assets/index.less
新增 ExpandIcon 类型、展开全部配置和默认图标渲染。旧版 expandable.expandIcon 保留为行图标回退。
展开状态与展开列
src/hooks/useExpand.ts, src/hooks/useColumns/index.tsx, src/Table.tsx, src/context/TableContext.tsx, src/hooks/useRowInfo.tsx, src/Body/BodyRow.tsx
useExpand 管理可展开行、整体状态和批量处理器。Table 与上下文向展开列和行传递 ExpandIconexpandAllInfo
集成、验证与文档
tests/ExpandRow.spec.jsx, README.md, README.zh-CN.md
测试覆盖自定义图标、可访问性、不可展开状态、批量操作、受控状态和标题渲染。中英文文档同步更新。

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Table
  participant useExpand
  participant useColumns
  participant ExpandIcon
  Table->>useExpand: 获取展开全部状态和处理器
  useExpand-->>Table: 返回 expandAllInfo
  Table->>useColumns: 传递 ExpandIcon 和 expandAllInfo
  useColumns->>ExpandIcon: 渲染 type="all" 图标
  ExpandIcon->>useExpand: 触发展开或收起全部行
  useExpand-->>Table: 更新 expandedRowKeys
Loading

Poem

兔子轻点展开钮,
行列一齐向前跑。
再点一次全收起,
可展开行听指令。
测试守住每一步。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了在可展开列标题中新增展开全部控件这一主要变更。
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@nikzanda
nikzanda force-pushed the feat/expand-all-header branch from 22bede1 to ef33496 Compare July 31, 2026 10:18
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.09%. Comparing base (60ab7b7) to head (779d287).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1505      +/-   ##
==========================================
+ Coverage   99.05%   99.09%   +0.03%     
==========================================
  Files          45       45              
  Lines        1376     1429      +53     
  Branches      413      433      +20     
==========================================
+ Hits         1363     1416      +53     
  Misses         13       13              

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

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

@nikzanda
nikzanda marked this pull request as ready for review July 31, 2026 10:20
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/expandUtil.tsx`:
- Around line 57-64: Update the expand-all control in the rendered component
from a span to a native button with type="button", preserving its className and
onClick behavior. Add an aria-label that reflects the current expanded state and
bind aria-expanded to expanded. Extend the interaction tests to verify Tab focus
and activation via Enter and Space.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3313343f-3c8c-4313-babe-050aa6c7b4e7

📥 Commits

Reviewing files that changed from the base of the PR and between 08188d3 and ef33496.

📒 Files selected for processing (8)
  • README.md
  • README.zh-CN.md
  • src/Table.tsx
  • src/hooks/useColumns/index.tsx
  • src/hooks/useExpand.ts
  • src/interface.ts
  • src/utils/expandUtil.tsx
  • tests/ExpandRow.spec.jsx

Comment thread src/utils/expandUtil.tsx
@nikzanda

Copy link
Copy Markdown
Contributor Author

@afc163 @zombieJ, the implementation is now complete and this PR is ready for review.

It includes the expand-all API, controlled and uncontrolled behavior, rowExpandable filtering, keyboard accessibility, documentation, and regression tests. All modified and coverable lines are covered by tests, and CodeRabbit reports no remaining actionable comments.

When you have time, could you please review the proposed API and implementation? I’m happy to make any adjustments needed before proceeding with the Ant Design integration for ant-design/ant-design#8791. Thanks!

Comment thread src/hooks/useColumns/index.tsx Outdated
@nikzanda
nikzanda requested a review from zombieJ August 5, 2026 10:58
Comment thread src/interface.ts Outdated
@nikzanda
nikzanda requested a review from zombieJ August 7, 2026 08:20
@nikzanda

Copy link
Copy Markdown
Contributor Author

@zombieJ, gentle follow-up on this PR.

I’ve addressed your latest feedback by changing expandable.columnTitle to use object-based render props:

columnTitle={({ expandIcon }) => (
  <div>{expandIcon}</div>
)}

The typings, documentation, and regression tests have also been updated. The test suite, type checks, lint, and coverage checks are passing.

When you have time, could you please take another look and let me know whether the API now matches what you had in mind? I’m happy to make any further adjustments. Thanks!

Comment thread src/context/TableContext.tsx Outdated
expandedRowRender: ExpandedRowRender<RecordType>;
forceRender: boolean;
expandIcon: RenderExpandIcon<RecordType>;
ExpandIcon?: ExpandIconComponent<RecordType>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Table Context give the getComponent ability. No need additional pass the ExpandIcon.
Or we just merge the expandIcon & ExpandIcon with mergedExpandIcon to handle both components def or legacy expandIcon def.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, @zombieJ. I followed the second approach and merged the component and legacy definitions into the internal mergedExpandIcon.

The resolution order is now:

  1. components.ExpandIcon
  2. expandable.expandIcon as the deprecated row-only fallback
  3. the default expand icon

TableContext now exposes only the resolved expandIcon; the additional ExpandIcon field and its row-level propagation have been removed. Both expanded-row tables and nested/virtual rows consume the same resolved renderer.

The raw components.ExpandIcon is still used directly only for the header control with type="all", since the legacy expandable.expandIcon API applies only to row icons.

@nikzanda
nikzanda requested a review from zombieJ August 13, 2026 08:25
Comment thread src/hooks/useExpand.ts Outdated
}, [expandableType, getRowKey, mergedData, rowExpandable, showExpandAll]);

const allExpanded =
expandableRows.length > 0 && expandableRows.every(({ key }) => mergedExpandedKeys.has(key));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will cause perf cost when large data.
And above rowExpandable maybe not stable object in parent render:

<VirtualTable
  data={source.filter(...)}
  expandable={{
    showExpandAll: true,
    rowExpandable: record => ...
  }}
/>

We can move the allExpanded into memo also and remove rowExpandable from the deps(in most case it's safe).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated as suggested:

  • memoized allExpanded separately;
  • intentionally removed rowExpandable from the expandableRows dependencies;
  • added a regression test covering unstable inline callbacks and data changes.

Comment thread src/hooks/useExpand.ts Outdated
[getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange],
);

const onTriggerExpandAll: React.MouseEventHandler<HTMLElement> = React.useCallback(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

change to rc util useEvent which will not need deps

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

@nikzanda
nikzanda requested a review from zombieJ August 14, 2026 10:09

@nrps9909 nrps9909 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I reviewed the current head, 779d2877ac82309d4ea24554989982e66f757f35, and found one correctness blocker in the new expand-all state.

expandableRows intentionally omits rowExpandable from its dependency list. That avoids rescanning for an equivalent inline callback, but it also keeps the old eligible-row set when the callback's result legitimately changes while data keeps the same identity. Row rendering calls the latest predicate, so the row controls and the expand-all control then disagree: the header can report itself expanded and emit keys for a row that is now non-expandable, while the newly eligible row is ignored.

I reproduced this on the exact head by extending the existing test with the same stable sampleData:

const { container, rerender } = render(renderTable(({ key }) => key === 0));
rerender(renderTable(({ key }) => key === 1));
fireEvent.click(container.querySelector('.expand-all-icon'));

expect(onExpandAll).toHaveBeenLastCalledWith(true, [sampleData[1]]);
expect(onExpandedRowsChange).toHaveBeenLastCalledWith([1]);

The test fails because both callbacks receive row/key 0, from the previous predicate. This is a realistic pattern when an inline rowExpandable closes over component state or permissions. The current regression test only swaps in a new callback with identical key === 1 behavior, so it cannot catch the stale-semantics case.

Please make the cached eligible-row set invalidate when rowExpandable semantics change (including the callback in the memo dependencies is the direct correctness fix), and change/add a test where the replacement predicate selects a different row. If preserving the large-data optimization requires another structure, it still needs to keep the header state, callback records, and row-level rowExpandable result synchronized.

Validation on this head: the focused ExpandRow suite passed 49/49 before the probe; the full suite passed 21 files / 236 tests; tsc, lint (existing warnings only), compile, and git diff --check passed.

AI assistance disclosure: Codex was used to trace the memoized eligibility path, inspect all current review threads, and draft/run the focused regression probe. I verified the failure and all reported commands against the exact head above.

@nikzanda

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review, @nrps9909.

I reproduced the stale eligibility case with stable data and a semantically different rowExpandable callback. I agree that the current regression test does not cover this scenario, because it replaces the callback with an equivalent predicate and later changes the data identity.

The dependency was intentionally omitted following @zombieJ's earlier performance feedback, since inline callbacks could otherwise trigger a full-data scan on every parent render.

@zombieJ, could you confirm which trade-off you prefer here? The direct correctness fix would be to restore rowExpandable in the expandableRows dependencies and add the proposed regression test. This would reintroduce the O(n) scan whenever the callback identity changes.

I also noticed that useColumns already omits rowExpandable from its memo dependencies: useRowInfo sees the latest predicate, while the expand-column renderer may retain the previous closure. If we prioritize correctness, should we update both memoized paths so the row controls and expand-all state remain fully synchronized?

My preference is to preserve correctness and let consumers stabilize equivalent predicates with useCallback, but I would like to align the implementation with your intended performance contract before changing it.

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.

3 participants