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
49 changes: 49 additions & 0 deletions packages/x/components/sender/__tests__/slot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SenderRef>();
const { container } = render(
<Sender
ref={ref}
submitType="shiftEnter"
slotConfig={[
{ type: 'text', value: 'Text before slot' },
tagSlotConfig,
{ type: 'text', value: ' text after slot' },
]}
onChange={onChange}
/>,
);
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];
Expand Down
36 changes: 36 additions & 0 deletions packages/x/components/sender/components/SlotTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,42 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
return false;
}

// Chromium keeps a block wrapper after deleting the leading <br> 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;
Expand Down