feat(Actions): 支持自定义每个操作项的 Tooltip - #1977
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (9)
📝 WalkthroughWalkthroughActions 现在支持为每个操作项配置字符串或 TooltipProps,也可使用 ChangesActions Tooltip 配置
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Actions
participant ActionsItem
participant Item
participant Tooltip
Actions->>ActionsItem: 传入 tooltip 配置
Actions->>Item: 传递 item.tooltip
ActionsItem->>Tooltip: 启用时包裹 innerNode
Item->>Tooltip: 启用时包裹 iconElement
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a custom tooltip feature for action items, allowing configurations via string, TooltipProps, or disabling them entirely. It also improves the stability of the Mermaid component's streaming render by replacing throttle with debounce, utilizing useId for stable IDs, and handling incomplete syntax. The reviewer feedback highlights a bug where the debounced renderDiagram function is recreated on every render, suggesting it be cached using React.useMemo. Additionally, the reviewer recommends merging custom TooltipProps with the default title in both ActionsItem and Item components to prevent empty tooltips when only non-title properties are customized.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const renderDiagram = debounce(async () => { | ||
| if (!children || !containerRef.current || renderType === RenderType.Code) return; | ||
|
|
||
| try { | ||
| const isValid = await mermaid.parse(children, { suppressErrors: true }); | ||
| if (!isValid) throw new Error('Invalid Mermaid syntax'); | ||
| if (!isValid) return; // 流式渲染过程中语法不完整时静默跳过,不报错 | ||
|
|
||
| const { svg } = await mermaid.render(id, children); | ||
| containerRef.current.innerHTML = svg; | ||
| // 确保渲染完成后容器仍然存在(防止异步过程中组件已卸载) | ||
| if (containerRef.current) { | ||
| containerRef.current.innerHTML = svg; | ||
| } | ||
| } catch (error) { | ||
| warning(false, 'Mermaid', `Render failed: ${error}`); | ||
| } | ||
| }, 100); | ||
| }, 200); |
There was a problem hiding this comment.
在当前的实现中,renderDiagram 防抖函数是在组件函数体内部直接声明的。这意味着每次组件重新渲染(例如由于 scale、position、isDragging 等状态更新)时,都会重新创建一个全新的 debounce 函数。
这会导致以下问题:
- 防抖失效:每次渲染都会生成新的防抖函数和定时器,无法正确合并多次连续的渲染请求。
- 性能开销:频繁创建和销毁防抖函数。
- 潜在的竞态条件:如果之前的定时器未被清理,可能会在之后触发并使用旧的闭包数据渲染。
建议使用 React.useMemo 将 renderDiagram 缓存起来,并将 children 和 id 作为参数传递给它,以确保防抖函数在组件生命周期内保持稳定,且不会产生闭包陈旧数据的问题。
const renderDiagram = React.useMemo(
() =>
debounce(async (currentChildren: string, currentId: string) => {
if (!currentChildren || !containerRef.current) return;
try {
const isValid = await mermaid.parse(currentChildren, { suppressErrors: true });
if (!isValid) return; // 流式渲染过程中语法不完整时静默跳过,不报错
const { svg } = await mermaid.render(currentId, currentChildren);
// 确保渲染完成后容器仍然存在(防止异步过程中组件已卸载)
if (containerRef.current) {
containerRef.current.innerHTML = svg;
}
} catch (error) {
warning(false, 'Mermaid', `Render failed: ${error}`);
}
}, 200),
[],
);
| const tooltipProps: TooltipProps = | ||
| typeof tooltip === 'string' | ||
| ? { title: tooltip } | ||
| : (tooltip as TooltipProps) || { title: label }; |
There was a problem hiding this comment.
当 tooltip 传入一个 TooltipProps 对象(例如 { placement: 'bottom' })但未指定 title 时,当前的实现会导致 Tooltip 渲染时没有标题内容。
建议将传入的 TooltipProps 与默认的 title: label 进行合并,这样用户在仅自定义 Tooltip 属性(如位置、颜色等)时,依然能保留默认的 label 作为标题,避免出现空白 Tooltip 的问题。
| const tooltipProps: TooltipProps = | |
| typeof tooltip === 'string' | |
| ? { title: tooltip } | |
| : (tooltip as TooltipProps) || { title: label }; | |
| const tooltipProps: TooltipProps = | |
| typeof tooltip === 'string' | |
| ? { title: tooltip } | |
| : { title: label, ...(tooltip || {}) }; |
| const tooltipProps: TooltipProps = | ||
| typeof tooltipConfig === 'string' | ||
| ? { title: tooltipConfig } | ||
| : (tooltipConfig as TooltipProps) || { title: item.label }; |
There was a problem hiding this comment.
当 tooltipConfig 传入一个 TooltipProps 对象但未指定 title 时,当前的实现会导致 Tooltip 渲染时没有标题内容。
建议将传入的 TooltipProps 与默认的 title: item.label 进行合并,这样用户在仅自定义 Tooltip 属性(如位置、颜色等)时,依然能保留默认的 item.label 作为标题,避免出现空白 Tooltip 的问题。
| const tooltipProps: TooltipProps = | |
| typeof tooltipConfig === 'string' | |
| ? { title: tooltipConfig } | |
| : (tooltipConfig as TooltipProps) || { title: item.label }; | |
| const tooltipProps: TooltipProps = | |
| typeof tooltipConfig === 'string' | |
| ? { title: tooltipConfig } | |
| : { title: item.label, ...(tooltipConfig || {}) }; |
0c85e63 to
42a7e68
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@packages/x/components/actions/ActionsItem.tsx`:
- Around line 147-154: Update the tooltipProps construction in ActionsItem.tsx
to merge object-based tooltip configuration with the default { title: label },
while preserving explicit string titles and tooltip:false behavior. Apply the
same merge in Item.tsx so custom options without a title consistently fall back
to label.
In `@packages/x/components/actions/Item.tsx`:
- Around line 36-39: Update the tooltipProps assignment in Item so object-valued
tooltipConfig is merged with a fallback title of item.label, while preserving
any explicitly provided title and other TooltipProps. Keep the existing string
configuration behavior unchanged.
In `@packages/x/components/mermaid/__tests__/index.test.tsx`:
- Around line 1234-1256: Update the Mermaid rerender test around mockRender to
wait for the call count to increase after each rerender before recording the
latest call’s ID. In the unmount/cancellation test around mockParse and
mockRender, advance fake timers beyond 200ms after unmounting, then assert
neither mock was executed.
In `@packages/x/components/mermaid/Mermaid.tsx`:
- Around line 105-116: Update renderDiagram in Mermaid.tsx to track a
monotonically increasing render request identifier, invalidate the active
request during cleanup, and verify the identifier after both mermaid.parse and
mermaid.render before writing to containerRef.current.innerHTML. Ensure only the
latest asynchronous render can update the container, while preserving the
existing debounce cancellation and validation behavior.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c6417cb0-e8da-4687-9213-9515cc0d19e6
⛔ Files ignored due to path filters (2)
packages/x/components/actions/__tests__/__snapshots__/demo-extend.test.ts.snapis excluded by!**/*.snappackages/x/components/actions/__tests__/__snapshots__/demo.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (11)
packages/x/components/actions/ActionsItem.tsxpackages/x/components/actions/Item.tsxpackages/x/components/actions/__tests__/action-item.test.tsxpackages/x/components/actions/__tests__/index.test.tsxpackages/x/components/actions/demo/tooltip.mdpackages/x/components/actions/demo/tooltip.tsxpackages/x/components/actions/index.en-US.mdpackages/x/components/actions/index.zh-CN.mdpackages/x/components/actions/interface.tspackages/x/components/mermaid/Mermaid.tsxpackages/x/components/mermaid/__tests__/index.test.tsx
| // ============================ Tooltip ============================ | ||
| const tooltipEnabled = tooltip !== false; | ||
| const tooltipProps: TooltipProps = | ||
| typeof tooltip === 'string' | ||
| ? { title: tooltip } | ||
| : (tooltip as TooltipProps) || { title: label }; | ||
|
|
||
| return isMobile || !tooltipEnabled ? innerNode : <Tooltip {...tooltipProps}>{innerNode}</Tooltip>; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
修复 Tooltip 对象配置时丢失默认 title 的问题。
当用户仅通过 tooltip 传递自定义配置(如 tooltip={{ placement: 'bottom' }})而未显式声明 title 时,当前的逻辑会导致 title 为空,从而导致 Tooltip 无法正常显示默认的 label。
建议将其与 { title: label } 进行合并,以确保具有默认标题回退。同时,请注意 packages/x/components/actions/Item.tsx 中也存在相同的逻辑,请一并修复以保持行为的一致性。
🐛 修复建议
// ============================ Tooltip ============================
const tooltipEnabled = tooltip !== false;
const tooltipProps: TooltipProps =
typeof tooltip === 'string'
? { title: tooltip }
- : (tooltip as TooltipProps) || { title: label };
+ : { title: label, ...(tooltip as TooltipProps) };
return isMobile || !tooltipEnabled ? innerNode : <Tooltip {...tooltipProps}>{innerNode}</Tooltip>;📝 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.
| // ============================ Tooltip ============================ | |
| const tooltipEnabled = tooltip !== false; | |
| const tooltipProps: TooltipProps = | |
| typeof tooltip === 'string' | |
| ? { title: tooltip } | |
| : (tooltip as TooltipProps) || { title: label }; | |
| return isMobile || !tooltipEnabled ? innerNode : <Tooltip {...tooltipProps}>{innerNode}</Tooltip>; | |
| // ============================ Tooltip ============================ | |
| const tooltipEnabled = tooltip !== false; | |
| const tooltipProps: TooltipProps = | |
| typeof tooltip === 'string' | |
| ? { title: tooltip } | |
| : { title: label, ...(tooltip as TooltipProps) }; | |
| return isMobile || !tooltipEnabled ? innerNode : <Tooltip {...tooltipProps}>{innerNode}</Tooltip>; |
🤖 Prompt for 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.
In `@packages/x/components/actions/ActionsItem.tsx` around lines 147 - 154, Update
the tooltipProps construction in ActionsItem.tsx to merge object-based tooltip
configuration with the default { title: label }, while preserving explicit
string titles and tooltip:false behavior. Apply the same merge in Item.tsx so
custom options without a title consistently fall back to label.
| const tooltipProps: TooltipProps = | ||
| typeof tooltipConfig === 'string' | ||
| ? { title: tooltipConfig } | ||
| : (tooltipConfig as TooltipProps) || { title: item.label }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
合并 TooltipProps 以避免丢失默认的 title。
当 tooltipConfig 是一个对象(例如仅用于配置位置 { placement: 'bottom' })且未显式提供 title 属性时,当前的逻辑会直接将其作为 tooltipProps 返回,从而丢失默认的 item.label。此时 Tooltip 可能会渲染出空内容或不显示。
建议在传入对象配置时,保留 item.label 作为后备的 title 进行合并:
💡 建议的修复
- const tooltipProps: TooltipProps =
- typeof tooltipConfig === 'string'
- ? { title: tooltipConfig }
- : (tooltipConfig as TooltipProps) || { title: item.label };
+ const tooltipProps: TooltipProps =
+ typeof tooltipConfig === 'string'
+ ? { title: tooltipConfig }
+ : { title: item.label, ...(typeof tooltipConfig === 'object' ? tooltipConfig : {}) };📝 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.
| const tooltipProps: TooltipProps = | |
| typeof tooltipConfig === 'string' | |
| ? { title: tooltipConfig } | |
| : (tooltipConfig as TooltipProps) || { title: item.label }; | |
| const tooltipProps: TooltipProps = | |
| typeof tooltipConfig === 'string' | |
| ? { title: tooltipConfig } | |
| : { title: item.label, ...(typeof tooltipConfig === 'object' ? tooltipConfig : {}) }; |
🤖 Prompt for 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.
In `@packages/x/components/actions/Item.tsx` around lines 36 - 39, Update the
tooltipProps assignment in Item so object-valued tooltipConfig is merged with a
fallback title of item.label, while preserving any explicitly provided title and
other TooltipProps. Keep the existing string configuration behavior unchanged.
| it('should not use uuid++ that changes every render', async () => { | ||
| const { rerender } = render(<Mermaid>{mermaidContent}</Mermaid>); | ||
|
|
||
| await waitFor(() => { | ||
| expect(mockRender).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| // Record id from first render | ||
| const ids: string[] = [mockRender.mock.calls[0][0]]; | ||
|
|
||
| // Multiple re-renders simulating streaming | ||
| for (let i = 0; i < 5; i++) { | ||
| rerender(<Mermaid>{`${mermaidContent} ${i}`}</Mermaid>); | ||
| await waitFor(() => { | ||
| expect(mockRender).toHaveBeenCalled(); | ||
| }); | ||
| const lastCall = mockRender.mock.calls[mockRender.mock.calls.length - 1]; | ||
| ids.push(lastCall[0]); | ||
| } | ||
|
|
||
| // All ids should be identical | ||
| const uniqueIds = new Set(ids); | ||
| expect(uniqueIds.size).toBe(1); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
让回归测试实际等待并验证防抖结果。
Line 1247 只断言“至少调用过一次”,因此后续循环可能重复读取第一次调用;Line 1307 也无法证明挂起任务已取消。请等待调用次数递增,并在卸载后推进超过 200ms 的假计时器,断言 mockParse/mockRender 未执行。
Also applies to: 1303-1308
🤖 Prompt for 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.
In `@packages/x/components/mermaid/__tests__/index.test.tsx` around lines 1234 -
1256, Update the Mermaid rerender test around mockRender to wait for the call
count to increase after each rerender before recording the latest call’s ID. In
the unmount/cancellation test around mockParse and mockRender, advance fake
timers beyond 200ms after unmounting, then assert neither mock was executed.
| const renderDiagram = debounce(async () => { | ||
| if (!children || !containerRef.current || renderType === RenderType.Code) return; | ||
|
|
||
| try { | ||
| const isValid = await mermaid.parse(children, { suppressErrors: true }); | ||
| if (!isValid) throw new Error('Invalid Mermaid syntax'); | ||
| if (!isValid) return; // 流式渲染过程中语法不完整时静默跳过,不报错 | ||
|
|
||
| const { svg } = await mermaid.render(id, children); | ||
| containerRef.current.innerHTML = svg; | ||
| // 确保渲染完成后容器仍然存在(防止异步过程中组件已卸载) | ||
| if (containerRef.current) { | ||
| containerRef.current.innerHTML = svg; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the target file and inspect the relevant region with line numbers.
FILE="packages/x/components/mermaid/Mermaid.tsx"
wc -l "$FILE"
sed -n '1,220p' "$FILE" | cat -nRepository: ant-design/x
Length of output: 8571
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether lodash is available in the sandbox and inspect the package dependency.
python3 - <<'PY'
import json, pathlib
pkg = json.loads(pathlib.Path('package.json').read_text())
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
print('lodash' in deps, deps.get('lodash'))
PY
node - <<'JS'
try {
const lodashPath = require.resolve('lodash');
console.log('lodash-resolve:', lodashPath);
} catch (e) {
console.log('lodash-resolve: NOT_FOUND');
}
JSRepository: ant-design/x
Length of output: 187
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the effect cleanup region and search for any request-token or stale-result guard.
sed -n '104,132p' packages/x/components/mermaid/Mermaid.tsx | cat -n
printf '\n----\n'
rg -n "request|token|latest|stale|abort|cancel\\(" packages/x/components/mermaid/Mermaid.tsxRepository: ant-design/x
Length of output: 1327
阻止旧的异步渲染覆盖新图表。
renderDiagram.cancel() 只能取消尚未执行的防抖任务;一旦 mermaid.parse/render 已开始,后续更新仍可能在更晚完成后把旧 SVG 写回 innerHTML。加一个递增请求序号(或内容快照),并在 parse、render 后只允许最新请求写入;清理时把当前请求失效。
🤖 Prompt for 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.
In `@packages/x/components/mermaid/Mermaid.tsx` around lines 105 - 116, Update
renderDiagram in Mermaid.tsx to track a monotonically increasing render request
identifier, invalidate the active request during cleanup, and verify the
identifier after both mermaid.parse and mermaid.render before writing to
containerRef.current.innerHTML. Ensure only the latest asynchronous render can
update the container, while preserving the existing debounce cancellation and
validation behavior.
42a7e68 to
5a78cb0
Compare
Bundle ReportChanges will increase total bundle size by 8 bytes (0.0%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: antdx-array-pushAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1977 +/- ##
=======================================
Coverage 97.08% 97.08%
=======================================
Files 159 159
Lines 5755 5764 +9
Branches 1694 1719 +25
=======================================
+ Hits 5587 5596 +9
Misses 166 166
Partials 2 2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Summary
Resolves #1952. Adds support for customizing the Tooltip of each action item in the Actions component.
Changes
ItemTypenow accepts an optionaltooltipfield of typestring | TooltipProps | falsetooltipis a string → used as the Tooltip titletooltipis aTooltipPropsobject → spread as Tooltip props (solves Allow customizing the Tooltip of an Action. #1834'sgetPopupContainerneed)tooltipisfalse→ no Tooltip renderedtooltipis undefined → existing behavior (title = label)Actions(viaitems) and standaloneActions.Itemare supportedChecklist
Summary by CodeRabbit
新功能
tooltip: false关闭提示,并在移动端自动保持无 Tooltip 展示。文档
测试