diff --git a/packages/x/components/sender/__tests__/slot.test.tsx b/packages/x/components/sender/__tests__/slot.test.tsx index 1dfa46c1c..5d418b69c 100644 --- a/packages/x/components/sender/__tests__/slot.test.tsx +++ b/packages/x/components/sender/__tests__/slot.test.tsx @@ -691,6 +691,55 @@ describe('Sender Slot Component', () => { setupDOMMocks(customSelectionMock, customRangeMock); fireEvent.keyDown(dom, { key: 'Backspace' }); }); + it('should remove a newline immediately before a slot', () => { + const onChange = jest.fn(); + const ref = createRef(); + const { container } = render( + , + ); + const dom = ref.current?.inputElement as HTMLElement; + const slotDom = container.querySelector('[data-slot-key="tag1"]') as HTMLElement; + const trailingText = Array.from(dom.childNodes).find( + (node) => node.nodeType === Node.TEXT_NODE && node.textContent === ' text after slot', + ) as Text; + const nextLine = document.createElement('div'); + + nextLine.append(document.createElement('br'), slotDom, trailingText); + dom.append(nextLine); + onChange.mockClear(); + + setupDOMMocks( + { + rangeCount: 1, + focusOffset: 0, + anchorNode: nextLine, + anchorOffset: 0, + focusNode: nextLine, + isCollapsed: true, + }, + { + startContainer: nextLine, + endContainer: nextLine, + startOffset: 0, + endOffset: 0, + collapsed: true, + }, + ); + + expect(fireEvent.keyDown(dom, { key: 'Backspace' })).toBe(false); + expect(nextLine).not.toBeInTheDocument(); + expect(slotDom.parentElement).toBe(dom); + expect(onChange).toHaveBeenCalledTimes(1); + }); it('should handle backspace key in content slot', () => { const onSubmit = jest.fn(); const slotConfig = [contentSlotConfigWithValue]; diff --git a/packages/x/components/sender/components/SlotTextArea.tsx b/packages/x/components/sender/components/SlotTextArea.tsx index 47963b01d..23e152c46 100644 --- a/packages/x/components/sender/components/SlotTextArea.tsx +++ b/packages/x/components/sender/components/SlotTextArea.tsx @@ -493,6 +493,42 @@ const SlotTextArea = React.forwardRef((_, ref) => { return false; } + // Chromium keeps a block wrapper after deleting the leading
of a line that starts + // with a non-editable slot. Merge that line explicitly so Backspace removes the newline. + if ( + operationType === 'backspace' && + range?.collapsed && + focusOffset === 0 && + anchorNode.nodeType === Node.ELEMENT_NODE + ) { + const editableDom = editableRef.current; + const line = anchorNode as HTMLElement; + const lineBreak = line.firstChild; + const firstContentNode = lineBreak?.nextSibling; + const firstContentInfo = + firstContentNode?.nodeType === Node.ELEMENT_NODE + ? getNodeInfo(firstContentNode as HTMLElement) + : null; + + if ( + line.parentElement === editableDom && + line.previousSibling && + lineBreak?.nodeName === 'BR' && + firstContentInfo?.slotKey + ) { + const lineIndex = Array.from(editableDom.childNodes).indexOf(line); + e.preventDefault(); + lineBreak.remove(); + while (line.firstChild) { + editableDom.insertBefore(line.firstChild, line); + } + line.remove(); + setCursorPosition(editableDom, editableDom, lineIndex); + triggerValueChange(e as unknown as EventType); + return true; + } + } + // 处理文本节点中的slot删除 if (anchorNode.nodeType === Node.TEXT_NODE && range) { const parentElement = anchorNode.parentNode as HTMLElement;