Skip to content

fix(sender): handle slot backspace after dynamic insert - #1967

Open
cc-hearts wants to merge 10 commits into
ant-design:mainfrom
cc-hearts:fix/sender-slot-backspace
Open

fix(sender): handle slot backspace after dynamic insert#1967
cc-hearts wants to merge 10 commits into
ant-design:mainfrom
cc-hearts:fix/sender-slot-backspace

Conversation

@cc-hearts

@cc-hearts cc-hearts commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🤔 这个变动的性质是?

  • 🆕 新特性提交
  • 🐞 Bug 修复
  • 📝 站点、文档改进
  • 📽️ 演示代码改进
  • 💄 组件样式/交互改进
  • 🤖 TypeScript 定义更新
  • 📦 包体积优化
  • ⚡️ 性能优化
  • ⭐️ 功能增强
  • 🌐 国际化改进
  • 🛠 重构
  • 🎨 代码风格优化
  • ✅ 测试用例
  • 🔀 分支合并
  • ⏩ 工作流程
  • ⌨️ 无障碍改进
  • ❓ 其他改动(是关于什么的改动?)

🔗 相关 Issue

复现地址:https://codesandbox.io/p/sandbox/zhi-neng-ti-shu-ru-antd-6-1-1-forked-9jn7t7

💡 需求背景和解决方案

当 Sender 使用 slotConfig 并通过 insert 动态插入词槽后,输入区域末尾可能出现空文本节点。此时光标位于 Sender 根 contenteditable 节点末尾,按 Backspace 无法命中前一个词槽节点,导致动态插入后的词槽无法正常删除。

复现步骤:

  1. 打开上面的 CodeSandbox 复现地址。
  2. 在 Sender 输入框中输入 @,触发动态插入词槽。
  3. 将光标放在输入框末尾,按 Backspace。
  4. 修复前:末尾词槽没有被删除;修复后:会跳过空文本节点并删除前一个词槽。

本次修复在处理 Backspace 时支持从 Sender 根节点的 childNodes 中向前查找前一个 slot/skill 节点,并且只跳过空文本节点,避免跨过普通文本误删更早的词槽。

📝 更新日志

语言 更新描述
🇺🇸 英文 Fix Sender slot deletion when empty text nodes exist before the cursor.
🇨🇳 中文 修复 Sender 光标前存在空文本节点时无法删除前一个词槽的问题。

✅ 验证

  • npm test --workspace packages/x -- sender/__tests__/slot.test.tsx --runInBand
  • npm exec biome check components/sender/components/SlotTextArea.tsx components/sender/__tests__/slot.test.tsx

Summary by CodeRabbit

  • Bug Fixes
    • 优化退格键删除逻辑:当光标前为仅包含零宽字符/空文本时,能够准确触发并删除对应的 slot 或 skill,正确移除后续相关 DOM;光标前为非空文本时不再误触发。
    • 清理导出/读取内容中的零宽字符,避免 \u200B/\uFEFF 污染值字符串。
  • Tests
    • 扩展退格键场景用例:覆盖空/不可见文本与跨节点选择等情况,校验回调数据一致性及 slot/skill 节点移除效果。

@dosubot dosubot Bot added bug Something isn't working javascript Pull requests that update Javascript code labels Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 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

Walkthrough

调整 SlotTextArea 的零宽字符解析与 Backspace 删除路径,使其根据光标位置跳过空文本并定位最近的 slot 或 skill;同时新增覆盖相关场景的行为测试。

Changes

Backspace 删除 slot/skill 逻辑

Layer / File(s) Summary
文本清洗与 Backspace 删除流程
packages/x/components/sender/components/SlotTextArea.tsx
清理文本节点中的零宽字符,并根据光标位置回溯空文本节点,定位 slot 或 skill 后调用对应删除逻辑。
Backspace 行为测试用例
packages/x/components/sender/__tests__/slot.test.tsx
新增非塌陷选择、空/不可见文本、普通文本及 skill 删除场景的测试,并校验 onChange 与 DOM 变化。

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SlotTextArea
  participant DOMSelection
  participant SlotStore

  User->>SlotTextArea: 按下 Backspace
  SlotTextArea->>DOMSelection: 读取光标位置
  SlotTextArea->>SlotTextArea: 清理零宽字符并回溯节点
  alt 找到 slot
    SlotTextArea->>SlotStore: removeSlot(slotKey, event)
  else 找到 skill
    SlotTextArea->>SlotStore: removeSkill()
  end
Loading

Poem

兔兔轻敲退格键,
零宽字符悄悄散;
slot、skill 找得准,
测试守住每一关。 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了 Sender 中动态插入 slot 后的 Backspace 删除修复,且与本次变更一致。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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.

@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 improves the backspace deletion behavior for slots and skills in the SlotTextArea component by skipping empty text nodes when searching for the previous element to delete, and adds corresponding unit tests. The review feedback suggests enhancing the isEmptyTextNode helper to also treat zero-width spaces (such as \u200B and \uFEFF) as empty, which prevents backspace deletion from getting stuck on invisible characters commonly inserted by browsers in contenteditable fields.

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.

Comment thread packages/x/components/sender/components/SlotTextArea.tsx Outdated

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

🧹 Nitpick comments (1)
packages/x/components/sender/components/SlotTextArea.tsx (1)

514-567: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Backspace 查找逻辑正确,可选:合并两个重复的遍历分支。

两个分支(anchorNode === editableRef.current 的索引回溯与非根节点的 previousSibling 回溯)共享完全相同的「跳过空文本节点 / 遇到 slot/skill 命中 / 遇到普通节点停止」规则。由于 childNodes[i-1] === childNodes[i].previousSibling,可统一为单一的 previousSibling 遍历,避免后续两处逻辑各自演进而产生分歧。

♻️ 合并遍历逻辑
       const isEmptyTextNode = (node: Node) => node.nodeType === Node.TEXT_NODE && !node.textContent;
 
       let previousNode: HTMLElement | null = null;
 
-      if (anchorNode === editableRef.current) {
-        for (let i = focusOffset - 1; i >= 0; i -= 1) {
-          const childNode = editableRef.current.childNodes[i];
-          if (childNode && isSlotOrSkill(childNode)) {
-            previousNode = childNode;
-            break;
-          }
-          if (childNode && !isEmptyTextNode(childNode)) {
-            break;
-          }
-        }
-      } else {
-        let currentNode: Node | null = anchorNode.previousSibling;
-
-        while (currentNode) {
-          if (isSlotOrSkill(currentNode)) {
-            previousNode = currentNode;
-            break;
-          }
-          if (!isEmptyTextNode(currentNode)) {
-            break;
-          }
-          currentNode = currentNode.previousSibling;
-        }
-      }
+      let currentNode: Node | null =
+        anchorNode === editableRef.current
+          ? editableRef.current.childNodes[focusOffset - 1] ?? null
+          : anchorNode.previousSibling;
+
+      while (currentNode) {
+        if (isSlotOrSkill(currentNode)) {
+          previousNode = currentNode;
+          break;
+        }
+        if (!isEmptyTextNode(currentNode)) {
+          break;
+        }
+        currentNode = currentNode.previousSibling;
+      }
🤖 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 514 -
567, The backspace lookup in SlotTextArea duplicates the same slot/skill
traversal logic in two branches: one using editableRef.current childNodes
indexing and one using previousSibling walking. Refactor this into a single
shared traversal path, using the existing isSlotOrSkill and isEmptyTextNode
checks, so both anchorNode cases resolve previousNode with the same
stop/continue rules and won’t drift apart later.
🤖 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.

Nitpick comments:
In `@packages/x/components/sender/components/SlotTextArea.tsx`:
- Around line 514-567: The backspace lookup in SlotTextArea duplicates the same
slot/skill traversal logic in two branches: one using editableRef.current
childNodes indexing and one using previousSibling walking. Refactor this into a
single shared traversal path, using the existing isSlotOrSkill and
isEmptyTextNode checks, so both anchorNode cases resolve previousNode with the
same stop/continue rules and won’t drift apart later.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 83776de4-a2d9-4ef9-b01f-5573ed943670

📥 Commits

Reviewing files that changed from the base of the PR and between 04a3b32 and 5742fce.

📒 Files selected for processing (2)
  • packages/x/components/sender/__tests__/slot.test.tsx
  • packages/x/components/sender/components/SlotTextArea.tsx

@cc-hearts

Copy link
Copy Markdown
Contributor Author

复现步骤如:
Kapture 2026-07-08 at 12 21 52

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 8 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
antdx-array-push 2.11MB 8 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: antdx-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
antdx.min.js 8 bytes 2.11MB 0.0%

@cc-hearts
cc-hearts force-pushed the fix/sender-slot-backspace branch from 5742fce to 02f8eea Compare July 8, 2026 04:27
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.11%. Comparing base (13afbf4) to head (9111765).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1967      +/-   ##
==========================================
+ Coverage   97.06%   97.11%   +0.04%     
==========================================
  Files         159      159              
  Lines        5766     5788      +22     
  Branches     1712     1733      +21     
==========================================
+ Hits         5597     5621      +24     
+ Misses        167      165       -2     
  Partials        2        2              

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

@cc-hearts
cc-hearts force-pushed the fix/sender-slot-backspace branch from 02f8eea to 0a03fe0 Compare July 8, 2026 15:37

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

发现 2 个需要修改的问题:

1、非折叠选区可能误删 slot" body="这里将 anchorNodefocusOffset 配对使用,但非折叠 Selection 中 focusOffset 属于 focusNode。例如 DOM 为 [slot, Text(\"ab\")],选区从根节点 offset 1 到文本 offset 1 时,Backspace 会取到 childNodes[0] 并删除 slot,而不是删除选中的 a。回溯前应要求 range.collapsed,并使用同一端点的 node/offset。" file="/private/tmp/x-pr1967-review/packages/x/components/sender/components/SlotTextArea.tsx" start=514 end=517 priority=1}

2、零宽节点仍会污染 Sender 值" body="这些节点被判定为空并跳过,但没有从 DOM 或序列化结果中清理。删除 slot/skill 后,triggerValueChange() 会通过 getEditorValue() 原样返回 \\u200B\\uFEFF,同时把它们作为 text slot;因此视觉上清空的 Sender 仍可能有非空 value。新增测试只检查 slot 列表,遗漏了 onChange 第一参数和 getValue()。应在删除前移除已跳过节点,或统一在值序列化时过滤。" file="/private/tmp/x-pr1967-review/packages/x/components/sender/components/SlotTextArea.tsx" start=524 end=525 priority=2}

@cc-hearts

Copy link
Copy Markdown
Contributor Author

我尝试修复了一下,请review下

@cc-hearts
cc-hearts requested a review from kimteayon July 21, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants