Skip to content

fix(sender): trigger onClose when removing skill via Backspace - #1984

Open
wenzeyu8888-rgb wants to merge 2 commits into
ant-design:mainfrom
wenzeyu8888-rgb:fix/sender-backspace-skill-onclose-1955
Open

fix(sender): trigger onClose when removing skill via Backspace#1984
wenzeyu8888-rgb wants to merge 2 commits into
ant-design:mainfrom
wenzeyu8888-rgb:fix/sender-backspace-skill-onclose-1955

Conversation

@wenzeyu8888-rgb

@wenzeyu8888-rgb wenzeyu8888-rgb commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📝 Problem

Closes #1955
Related: #1938

In the Sender component, when removing an inserted skill tag via the Backspace key, the closable.onClose callback was not fired. External code could not detect skill removal via keyboard.

🔍 Root Cause

In SlotTextArea.tsx, the handleDeleteOperation function handles Backspace deletion. The skillKey branch only called removeSkill() without firing closable.onClose, which was inconsistent with the click-close path in Skill.tsx that fires both removeSkill() and config.onClose?.(event).

✅ Fix

Fire closable.onClose after removeSkill() in the keyboard deletion branch, matching the click-close behavior.

🧪 Test

Added a unit test that:

  1. Renders a Sender with a skill that has closable.onClose callback
  2. Simulates Backspace key deletion
  3. Asserts onClose is called exactly once

All existing tests continue to pass.

Summary by CodeRabbit

  • 新功能

    • 优化流式 Markdown 渲染,使多个自定义组件的加载状态彼此隔离,互不影响。
    • 新增多组件流式加载演示,支持逐步渲染与重新开始。
  • 问题修复

    • 通过退格键移除可关闭技能时,现会正确触发关闭回调。
  • 文档

    • 补充多组件加载隔离的流式渲染示例。

温清 added 2 commits July 16, 2026 21:58
…#1949

Add unit tests verifying streamStatus is isolated per component instance:
- Renderer-level: different/same tag names, interleaved, nested, self-closing
- Full pipeline: Parser + DOMPurify + html-react-parser
- XMarkdown component: streaming loading/done transitions

Add demo showing multiple custom components with independent loading states.

Closes ant-design#1949
In handleDeleteOperation, the Backspace key deletion branch for skill
nodes only called removeSkill() without firing the closable.onClose
callback. This was inconsistent with the click-close path in Skill.tsx
which fires both removeSkill() and config.onClose?.(event).

Fix: fire closable.onClose after removeSkill() in the keyboard
deletion branch to match the click-close behavior.

Closes ant-design#1955
@dosubot dosubot Bot added the bug Something isn't working label Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

新增 XMarkdown 多组件流式加载隔离测试与演示文档,确保各组件实例独立维护 streamStatus。同时修复 Sender 通过 Backspace 删除 skill 时未触发 onClose 的问题,并补充测试。

Changes

Markdown 流式加载隔离

Layer / File(s) Summary
流式状态隔离验证
packages/x-markdown/src/XMarkdown/__tests__/loading-isolation.test.tsx
覆盖 Renderer、Parser 与 XMarkdown 组件级场景,验证不同组件实例的 loading/done 状态相互隔离。
多组件加载演示与文档
packages/x/docs/x-markdown/demo/streaming/multi-component-loading.tsx, packages/x/docs/x-markdown/streaming.en-US.md, packages/x/docs/x-markdown/streaming.zh-CN.md
新增 DataCardInfoPanel 流式演示,并将示例加入中英文文档。

Sender Skill 删除回调

Layer / File(s) Summary
Backspace 删除 Skill 回调
packages/x/components/sender/components/SlotTextArea.tsx, packages/x/components/sender/__tests__/slot.test.tsx
通过 Backspace 删除 skill 时触发 closable.onClose,测试验证回调调用一次。

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant XMarkdown
  participant Parser
  participant Renderer
  participant CustomComponents
  XMarkdown->>Parser: 解析流式 Markdown
  Parser->>Renderer: 传递解析后的 HTML
  Renderer->>CustomComponents: 为各组件实例传递 streamStatus
  CustomComponents-->>XMarkdown: 渲染 loading 或 done 状态
Loading

Possibly related PRs

  • ant-design/x#1982:同样涉及 XMarkdown 流式 streamStatus 隔离测试、演示与文档更新。

Suggested reviewers: div627

Poem

小兔挥笔写新章,
流光组件各自忙。
DataCard 戴好骨架帽,
InfoPanel 等待月亮。
Backspace 轻轻一敲,
onClose 唤醒回响。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning PR 还包含 x-markdown 的测试、演示和文档更新,这些与 Sender 的 Backspace/onClose 修复无关。 将 x-markdown 相关改动拆分到独立 PR,或从本次变更中移除,仅保留 Sender 修复与对应测试。
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了本次 PR 的主要变更:通过 Backspace 删除 skill 时触发 onClose。
Linked Issues check ✅ Passed 代码修改和新增测试都覆盖了 #1955 的要求:Backspace 删除 skill 时补发 onClose 并断言回调触发一次。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 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.

@wenzeyu8888-rgb

Copy link
Copy Markdown
Contributor Author

#1955

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces loading isolation tests for XMarkdown to ensure stream status is isolated per component instance, adds a multi-component loading demo, and triggers the onClose callback when a skill is removed via Backspace in SlotTextArea. Feedback was provided to improve test robustness by refactoring manual spy cleanup to lifecycle hooks, using safer assertions instead of toBeDefined(), and addressing an unsafe event type cast in SlotTextArea.

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.

it('should mark only the unclosed instance as loading (different tag names)', () => {
const components = { 'comp-a': MockComponent, 'comp-b': MockComponent };
const renderer = new Renderer({ components });
const spy = jest.spyOn(React, 'createElement');

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

Creating and restoring the spy manually inside each test case can lead to mock leakage. If any assertion (expect) fails, the test execution will halt, and spy.mockRestore() will never be called. This pollutes the global React.createElement for subsequent tests, potentially causing cascading failures.\n\nIt is highly recommended to refactor this by moving the spy setup and teardown to beforeEach and afterEach hooks at the describe block level. This ensures that the mock is always cleaned up properly, even if a test fails, and reduces code duplication across all test cases in this file.

Comment on lines +545 to +546
expect(ref.current).toBeDefined();
expect(skillDom).toBeDefined();

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

Using toBeDefined() to assert the presence of ref.current and skillDom is unsafe because null is considered a defined value in JavaScript/Jest. If ref.current is null or skillDom is not found (returning null), these assertions will still pass.\n\nUse not.toBeNull() or toBeTruthy() to ensure that these values are actually present and not null.

      expect(ref.current).not.toBeNull();\n      expect(skillDom).not.toBeNull();

removeSkill();
// Fire onClose callback to match click-close behavior (Skill.tsx)
const closableConfig = typeof skill?.closable === 'boolean' ? {} : skill?.closable;
closableConfig?.onClose?.(e as unknown as React.MouseEvent<HTMLDivElement>);

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

Casting a KeyboardEvent or ClipboardEvent directly to a MouseEvent (e as unknown as React.MouseEvent<HTMLDivElement>) is unsafe. If the consumer's onClose callback accesses mouse-specific properties (such as clientX, clientY, or button), it will result in undefined values or potential runtime errors.\n\nConsider typing the event parameter in onClose more broadly (e.g., allowing React.SyntheticEvent or union types) if possible, or document this behavior clearly so consumers know that onClose can be triggered by keyboard events with a different event payload.

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

🤖 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/sender/components/SlotTextArea.tsx`:
- Around line 526-530: 在 Backspace 删除处理逻辑中,基于 closableConfig.disabled 检查 skill
是否禁用;禁用时阻止默认行为并立即返回,不执行 removeSkill() 或 onClose?.( ),同时保留启用状态下现有的删除与回调行为。

In `@packages/x/docs/x-markdown/demo/streaming/multi-component-loading.tsx`:
- Line 77: Update the Component B info-panel text to say “loads independently”
instead of “downloads independently,” preserving the rest of the message and the
streaming demo 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: a1f9b807-b6a1-4dd8-b58c-84406392f56e

📥 Commits

Reviewing files that changed from the base of the PR and between 17db20b and 794a77b.

📒 Files selected for processing (6)
  • packages/x-markdown/src/XMarkdown/__tests__/loading-isolation.test.tsx
  • packages/x/components/sender/__tests__/slot.test.tsx
  • packages/x/components/sender/components/SlotTextArea.tsx
  • packages/x/docs/x-markdown/demo/streaming/multi-component-loading.tsx
  • packages/x/docs/x-markdown/streaming.en-US.md
  • packages/x/docs/x-markdown/streaming.zh-CN.md

Comment on lines 526 to +530
e.preventDefault();
removeSkill();
// Fire onClose callback to match click-close behavior (Skill.tsx)
const closableConfig = typeof skill?.closable === 'boolean' ? {} : skill?.closable;
closableConfig?.onClose?.(e as unknown as React.MouseEvent<HTMLDivElement>);

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

Backspace 删除前需要遵守 closable.disabled

点击关闭路径在 Skill.tsx 中会阻止禁用 skill 的删除,但这里无条件执行 removeSkill()onClose。因此 closable={{ disabled: true }} 的 skill 仍可通过 Backspace 被删除。

建议在删除前读取 closableConfig.disabled,禁用时阻止默认行为并直接返回。

建议修复
           if (skillKey) {
             e.preventDefault();
-            removeSkill();
             const closableConfig = typeof skill?.closable === 'boolean' ? {} : skill?.closable;
+            if (closableConfig?.disabled) {
+              return true;
+            }
+            removeSkill();
             closableConfig?.onClose?.(e as unknown as React.MouseEvent<HTMLDivElement>);
             return true;
           }
📝 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
e.preventDefault();
removeSkill();
// Fire onClose callback to match click-close behavior (Skill.tsx)
const closableConfig = typeof skill?.closable === 'boolean' ? {} : skill?.closable;
closableConfig?.onClose?.(e as unknown as React.MouseEvent<HTMLDivElement>);
e.preventDefault();
const closableConfig = typeof skill?.closable === 'boolean' ? {} : skill?.closable;
if (closableConfig?.disabled) {
return true;
}
removeSkill();
// Fire onClose callback to match click-close behavior (Skill.tsx)
closableConfig?.onClose?.(e as unknown as React.MouseEvent<HTMLDivElement>);
🤖 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/sender/components/SlotTextArea.tsx` around lines 526 -
530, 在 Backspace 删除处理逻辑中,基于 closableConfig.disabled 检查 skill
是否禁用;禁用时阻止默认行为并立即返回,不执行 removeSkill() 或 onClose?.( ),同时保留启用状态下现有的删除与回调行为。


Some text between components.

<info-panel>Component B: This info panel downloads independently.</info-panel>

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

downloads independently 修正为 loads independently

当前文案误指“独立下载”,与组件独立加载状态的演示含义不符。

建议修改
-<info-panel>Component B: This info panel downloads independently.</info-panel>
+<info-panel>Component B: This info panel loads independently.</info-panel>
📝 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
<info-panel>Component B: This info panel downloads independently.</info-panel>
<info-panel>Component B: This info panel loads independently.</info-panel>
🤖 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/docs/x-markdown/demo/streaming/multi-component-loading.tsx` at
line 77, Update the Component B info-panel text to say “loads independently”
instead of “downloads independently,” preserving the rest of the message and the
streaming demo behavior.

@kimteayon kimteayon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

审核结论

不建议当前状态合并,有两个需要修复的行为问题:

  1. [P1] Backspace 未遵守 closable.disabled
    [SlotTextArea.tsx:525](https://github.com/ant-design/x/pull/1984/files/794a77ba745dace4ec9e722e4435e713bfab93f9#diff-1227930f6b2e9e6ced1d046c7de3ce83cb7fd0e546d8499ef112edce86b97d57R525) 无条件调用 removeSkill()onClose,而点击关闭路径会在 disabled 时直接返回。
    我补充临时回归用例验证:设置 closable={{ disabled: true, onClose }} 后按 Backspace,回调仍被调用 1 次。应在删除前检查 disabled,同时阻止浏览器默认删除行为。

  2. [P2] onClose 的事件类型契约被破坏
    [SlotTextArea.tsx:530](https://github.com/ant-design/x/pull/1984/files/794a77ba745dace4ec9e722e4435e713bfab93f9#diff-1227930f6b2e9e6ced1d046c7de3ce83cb7fd0e546d8499ef112edce86b97d57R530) 把 KeyboardEvent 强制转换成 MouseEvent,但公开 API 仍声明为 React.MouseEventHandler。实际回调收到的是 type: "keydown",不存在可靠的 buttonclientX 等鼠标属性。应把接口、Skill.tsx 和文档统一调整为 React.SyntheticEvent<HTMLDivElement>,或显式定义鼠标/键盘事件联合类型。

链接中的测试文件还有两个非阻塞问题:

验证结果:PR head 的 loading isolation 测试 13/13 通过,TypeScript 和 Biome 通过;disabled 回归用例稳定失败。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Good First Issue] Sender 用 Backspace 删除 skill 时未触发 onClose

2 participants