Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions packages/x/components/actions/ActionsItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import pickAttrs from '@rc-component/util/lib/pickAttrs';
import { Tooltip } from 'antd';
import { Tooltip, type TooltipProps } from 'antd';
import { clsx } from 'clsx';
import React from 'react';
import useMobile from '../_util/hooks/use-mobile';
Expand Down Expand Up @@ -69,6 +69,11 @@ export interface ActionsItemProps extends Omit<React.HTMLAttributes<HTMLDivEleme
* @descEN Semantic structure styles
*/
styles?: Partial<Record<SemanticType, React.CSSProperties>>;
/**
* @desc 自定义操作项的 Tooltip,设为 false 时不渲染 Tooltip
* @descEN Tooltip for the action item, set to false to disable Tooltip
*/
tooltip?: string | TooltipProps | false;
}

const ActionsItem: React.FC<ActionsItemProps> = (props) => {
Expand All @@ -77,6 +82,7 @@ const ActionsItem: React.FC<ActionsItemProps> = (props) => {
defaultIcon,
runningIcon,
label,
tooltip,
className,
classNames = {},
styles = {},
Expand Down Expand Up @@ -138,7 +144,14 @@ const ActionsItem: React.FC<ActionsItemProps> = (props) => {
</div>
);

return isMobile ? innerNode : <Tooltip title={label}>{innerNode}</Tooltip>;
// ============================ Tooltip ============================
const tooltipEnabled = tooltip !== false;
const tooltipProps: TooltipProps =
typeof tooltip === 'string'
? { title: tooltip }
: (tooltip as TooltipProps) || { title: label };
Comment on lines +149 to +152

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.

medium

tooltip 传入一个 TooltipProps 对象(例如 { placement: 'bottom' })但未指定 title 时,当前的实现会导致 Tooltip 渲染时没有标题内容。

建议将传入的 TooltipProps 与默认的 title: label 进行合并,这样用户在仅自定义 Tooltip 属性(如位置、颜色等)时,依然能保留默认的 label 作为标题,避免出现空白 Tooltip 的问题。

Suggested change
const tooltipProps: TooltipProps =
typeof tooltip === 'string'
? { title: tooltip }
: (tooltip as TooltipProps) || { title: label };
const tooltipProps: TooltipProps =
typeof tooltip === 'string'
? { title: tooltip }
: { title: label, ...(tooltip || {}) };


return isMobile || !tooltipEnabled ? innerNode : <Tooltip {...tooltipProps}>{innerNode}</Tooltip>;
Comment on lines +147 to +154

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.

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

Suggested change
// ============================ 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.

};

export default ActionsItem;
15 changes: 13 additions & 2 deletions packages/x/components/actions/Item.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Tooltip } from 'antd';
import { Tooltip, type TooltipProps } from 'antd';
import { clsx } from 'clsx';
import React from 'react';
import useMobile from '../_util/hooks/use-mobile';
Expand Down Expand Up @@ -30,6 +30,17 @@ const Item: React.FC<ActionsItemProps> = (props) => {

const iconElement = <div className={`${prefixCls}-icon`}>{item?.icon}</div>;

// ============================ Tooltip ============================
const tooltipConfig = item.tooltip;
const tooltipEnabled = tooltipConfig !== false;
const tooltipProps: TooltipProps =
typeof tooltipConfig === 'string'
? { title: tooltipConfig }
: (tooltipConfig as TooltipProps) || { title: item.label };
Comment on lines +36 to +39

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.

medium

tooltipConfig 传入一个 TooltipProps 对象但未指定 title 时,当前的实现会导致 Tooltip 渲染时没有标题内容。

建议将传入的 TooltipProps 与默认的 title: item.label 进行合并,这样用户在仅自定义 Tooltip 属性(如位置、颜色等)时,依然能保留默认的 item.label 作为标题,避免出现空白 Tooltip 的问题。

Suggested change
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 || {}) };

Comment on lines +36 to +39

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.

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

Suggested change
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.


const wrappedIcon =
isMobile || !tooltipEnabled ? iconElement : <Tooltip {...tooltipProps}>{iconElement}</Tooltip>;

return (
<div
className={clsx(`${prefixCls}-item`, classNames.item, {
Expand All @@ -50,7 +61,7 @@ const Item: React.FC<ActionsItemProps> = (props) => {
}}
key={itemKey}
>
{isMobile ? iconElement : <Tooltip title={item.label}>{iconElement}</Tooltip>}
{wrappedIcon}
</div>
);
};
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,98 @@ exports[`renders components/actions/demo/sub.tsx correctly 1`] = `
</div>
`;

exports[`renders components/actions/demo/tooltip.tsx correctly 1`] = `
<div
class="ant-actions css-var-_R_0_"
>
<div
class="ant-actions-list ant-actions-variant-borderless"
>
<div
class="ant-actions-item"
>
<div
class="ant-actions-icon"
>
<span
aria-label="redo"
class="anticon anticon-redo"
role="img"
>
<svg
aria-hidden="true"
data-icon="redo"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M758.2 839.1C851.8 765.9 912 651.9 912 523.9 912 303 733.5 124.3 512.6 124 291.4 123.7 112 302.8 112 523.9c0 125.2 57.5 236.9 147.6 310.2 3.5 2.8 8.6 2.2 11.4-1.3l39.4-50.5c2.7-3.4 2.1-8.3-1.2-11.1-8.1-6.6-15.9-13.7-23.4-21.2a318.64 318.64 0 01-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5a318.64 318.64 0 01-68.6 101.7c-9.3 9.3-19.1 18-29.3 26L668.2 724a8 8 0 00-14.1 3l-39.6 162.2c-1.2 5 2.6 9.9 7.7 9.9l167 .8c6.7 0 10.5-7.7 6.3-12.9l-37.3-47.9z"
/>
</svg>
</span>
</div>
</div>
<div
class="ant-actions-item"
>
<div
class="ant-actions-icon"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="img"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
</div>
</div>
<div
class="ant-actions-item"
>
<div
class="ant-actions-icon"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="img"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
`;

exports[`renders components/actions/demo/variant.tsx correctly 1`] = `
<div
class="ant-flex css-var-_R_0_ ant-flex-align-stretch ant-flex-gap-middle ant-flex-vertical"
Expand Down
45 changes: 44 additions & 1 deletion packages/x/components/actions/__tests__/action-item.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,54 @@
import React from 'react';
import { render } from '../../../tests/utils';
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
import ActionsItem from '../ActionsItem';

describe('Actions.Item', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('renders with no status', () => {
const { getByText } = render(<ActionsItem defaultIcon="default-icon" />);
expect(getByText('default-icon')).toBeTruthy();
render(<ActionsItem defaultIcon="default-icon" status={'xxx' as any} />);
});

it('renders default tooltip with label', async () => {
const { container } = render(
<ActionsItem defaultIcon={<span>icon</span>} label="Action Label" />,
);
const trigger = container.querySelector('.ant-actions-button-item')!;
fireEvent.mouseEnter(trigger);
await waitFakeTimer();
const tooltip = document.querySelector('.ant-tooltip');
expect(tooltip).toBeInTheDocument();
expect(tooltip?.textContent).toBe('Action Label');
});

it('renders custom tooltip string', async () => {
const { container } = render(
<ActionsItem defaultIcon={<span>icon</span>} label="Action Label" tooltip="Custom Tooltip" />,
);
const trigger = container.querySelector('.ant-actions-button-item')!;
fireEvent.mouseEnter(trigger);
await waitFakeTimer();
const tooltip = document.querySelector('.ant-tooltip');
expect(tooltip).toBeInTheDocument();
expect(tooltip?.textContent).toBe('Custom Tooltip');
});

it('does not render tooltip when tooltip is false', async () => {
const { container } = render(
<ActionsItem defaultIcon={<span>icon</span>} label="Action Label" tooltip={false} />,
);
const trigger = container.querySelector('.ant-actions-button-item')!;
fireEvent.mouseEnter(trigger);
await waitFakeTimer();
const tooltip = document.querySelector('.ant-tooltip');
expect(tooltip).not.toBeInTheDocument();
});
});
98 changes: 97 additions & 1 deletion packages/x/components/actions/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ActionsProps } from '@ant-design/x';
import React from 'react';
import { fireEvent, render } from '../../../tests/utils';
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';

import { findItem } from '../ActionsMenu';
import Actions from '../index';
Expand Down Expand Up @@ -83,6 +83,102 @@ describe('Actions Component', () => {
});
});

describe('Actions tooltip', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('renders default tooltip with label', async () => {
const { container } = render(
<Actions
items={[
{
key: '1',
label: 'Action 1',
icon: <span>icon1</span>,
},
]}
/>,
);
const trigger = container.querySelector('.ant-actions-icon')!;
fireEvent.mouseEnter(trigger);
await waitFakeTimer();
const tooltip = document.querySelector('.ant-tooltip');
expect(tooltip).toBeInTheDocument();
expect(tooltip?.textContent).toBe('Action 1');
});

it('renders custom tooltip string', async () => {
const { container } = render(
<Actions
items={[
{
key: '1',
label: 'Action 1',
icon: <span>icon1</span>,
tooltip: 'Custom Tooltip Text',
},
]}
/>,
);
const trigger = container.querySelector('.ant-actions-icon')!;
fireEvent.mouseEnter(trigger);
await waitFakeTimer();
const tooltip = document.querySelector('.ant-tooltip');
expect(tooltip).toBeInTheDocument();
expect(tooltip?.textContent).toBe('Custom Tooltip Text');
});

it('renders custom tooltip props', async () => {
const { container } = render(
<Actions
items={[
{
key: '1',
label: 'Action 1',
icon: <span>icon1</span>,
tooltip: {
title: 'Custom Title',
placement: 'bottom',
color: 'blue',
},
},
]}
/>,
);
const trigger = container.querySelector('.ant-actions-icon')!;
fireEvent.mouseEnter(trigger);
await waitFakeTimer();
const tooltip = document.querySelector('.ant-tooltip');
expect(tooltip).toBeInTheDocument();
expect(tooltip?.textContent).toBe('Custom Title');
});

it('does not render tooltip when tooltip is false', async () => {
const { container } = render(
<Actions
items={[
{
key: '1',
label: 'Action 1',
icon: <span>icon1</span>,
tooltip: false,
},
]}
/>,
);
const trigger = container.querySelector('.ant-actions-icon')!;
fireEvent.mouseEnter(trigger);
await waitFakeTimer();
const tooltip = document.querySelector('.ant-tooltip');
expect(tooltip).not.toBeInTheDocument();
});
});

describe('Actions.Menu findItem function', () => {
const items = [
{ key: '1', label: 'Action 1' },
Expand Down
7 changes: 7 additions & 0 deletions packages/x/components/actions/demo/tooltip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## zh-CN

自定义每个操作项的 Tooltip,支持传入字符串、TooltipProps 或 `false` 关闭。

## en-US

Customize the Tooltip of each action item, supports string, TooltipProps, or `false` to disable.
39 changes: 39 additions & 0 deletions packages/x/components/actions/demo/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { EditOutlined, RedoOutlined } from '@ant-design/icons';
import { Actions } from '@ant-design/x';
import React from 'react';

const App: React.FC = () => {
return (
<Actions
items={[
{
key: 'retry',
icon: <RedoOutlined />,
label: 'Retry',
// Custom tooltip text
tooltip: 'Click to retry',
},
{
key: 'edit',
icon: <EditOutlined />,
label: 'Edit',
// Custom tooltip props (e.g. placement, color)
tooltip: {
title: 'Edit content',
placement: 'bottom',
color: 'blue',
},
},
{
key: 'share',
icon: <EditOutlined />,
label: 'Share',
// Disable tooltip
tooltip: false,
},
]}
/>
);
};

export default App;
Loading
Loading