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
3 changes: 3 additions & 0 deletions packages/x/components/sender/Sender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
header,
onPaste,
onPasteFile,
pasteFilter,
autoSize = { maxRows: 8 },
placeholder,
onFocus,
Expand Down Expand Up @@ -226,6 +227,7 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
onKeyDown,
onPaste,
onPasteFile,
pasteFilter,
disabled,
readOnly,
submitType,
Expand All @@ -249,6 +251,7 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
onKeyDown,
onPaste,
onPasteFile,
pasteFilter,
disabled,
readOnly,
submitType,
Expand Down
48 changes: 48 additions & 0 deletions packages/x/components/sender/__tests__/slot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,54 @@ describe('Sender Slot Component', () => {

expect(onPaste).toHaveBeenCalled();
});
it('should call pasteFilter with the raw pasted text and use its result', () => {
document.execCommand = jest.fn();
// pasteFilter replaces the default getCleanedText: it receives the raw
// pasted text (line breaks intact) and its return value is what's inserted.
const pasteFilter = jest.fn((text: string) => `<<${text}>>`);
const slotConfig = [textSlotConfig];
const { container } = render(<Sender slotConfig={slotConfig} pasteFilter={pasteFilter} />);

const inputArea = container.querySelector('[role="textbox"]') as HTMLElement;

fireEvent.paste(inputArea, {
clipboardData: {
// multi-line raw text: newlines are NOT stripped before pasteFilter.
getData: () => 'pasted\nmultiline\n',
files: [],
},
});

expect(pasteFilter).toHaveBeenCalledTimes(1);
// Input to pasteFilter is the raw pasted text, with newlines preserved.
expect(pasteFilter.mock.calls[0][0]).toBe('pasted\nmultiline\n');
// The filter's return value is what gets inserted.
expect(document.execCommand).toHaveBeenCalledWith(
'insertText',
false,
'<<pasted\nmultiline\n>>',
);
});
it('should apply pasteFilter that preserves line breaks', () => {
document.execCommand = jest.fn();
// Returning the text as-is keeps the original line breaks in the
// inserted content, which the default getCleanedText would have stripped.
const pasteFilter = jest.fn((text: string) => text);
const slotConfig = [textSlotConfig];
const { container } = render(<Sender slotConfig={slotConfig} pasteFilter={pasteFilter} />);

const inputArea = container.querySelector('[role="textbox"]') as HTMLElement;

fireEvent.paste(inputArea, {
clipboardData: {
getData: () => 'a\nb\nc',
files: [],
},
});

expect(pasteFilter).toHaveBeenCalledTimes(1);
expect(document.execCommand).toHaveBeenCalledWith('insertText', false, 'a\nb\nc');
});
it('should handle select all keyboard shortcut', () => {
const onKeyDown = jest.fn();
const onSubmit = jest.fn();
Expand Down
3 changes: 2 additions & 1 deletion packages/x/components/sender/components/SlotTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
onKeyDown,
onPaste,
onPasteFile,
pasteFilter,
disabled,
readOnly,
submitType = 'enter',
Expand Down Expand Up @@ -647,7 +648,7 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
}
if (text) {
let success = false;
const cleanedText = getCleanedText(text);
const cleanedText = pasteFilter ? pasteFilter(text) : getCleanedText(text);
try {
success = document.execCommand('insertText', false, cleanedText);
} catch (err) {
Expand Down
7 changes: 7 additions & 0 deletions packages/x/components/sender/demo/paste-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## zh-CN

通过 `pasteFilter` 自定义 slot 模式下粘贴文本的清洗逻辑,默认清洗会移除换行,传 `pasteFilter={(text) => text}` 即可保留原始排版。

## en-US

Customize how pasted text is cleaned in slot mode with `pasteFilter`. The default cleaning strips line breaks; pass `pasteFilter={(text) => text}` to preserve the original formatting.
31 changes: 31 additions & 0 deletions packages/x/components/sender/demo/paste-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Sender, type SenderProps } from '@ant-design/x';
import { Flex, Typography } from 'antd';
import React from 'react';

type SlotConfig = SenderProps['slotConfig'];

// A simple slot config so pasting lands inside the editable slot area.
const slotConfig: SlotConfig = [
{ type: 'text', value: 'Paste a code snippet or structured text: ' },
];

const App: React.FC = () => {
return (
<Flex vertical gap={16}>
<div>
<Typography.Text type="secondary">
默认情况下,slot 模式粘贴会移除换行。使用 <code>pasteFilter</code> 可保留原始排版。
</Typography.Text>
</div>
<Sender
slotConfig={slotConfig}
autoSize={{ minRows: 3, maxRows: 6 }}
placeholder="Paste multi-line text here"
// Return the raw pasted text as-is so line breaks survive the paste.
pasteFilter={(text) => text}
/>
</Flex>
);
};

export default () => <App />;
8 changes: 8 additions & 0 deletions packages/x/components/sender/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ export interface SenderProps
onKeyDown?: (event: React.KeyboardEvent) => void | false;
onPaste?: React.ClipboardEventHandler<HTMLElement>;
onPasteFile?: (files: FileList) => void;
/**
* Custom paste text filter. When provided, it replaces the default
* `getCleanedText` cleaning (which strips line breaks) and receives the raw
* pasted text. Should return the final text to insert. Useful to preserve
* formatting (e.g. line breaks); e.g. `pasteFilter={(text) => text}` keeps
* the original text as-is. Slot mode only.
*/
pasteFilter?: (text: string) => string;
components?: SenderComponents;
classNames?: Partial<Record<SemanticType, string>>;
styles?: Partial<Record<SemanticType, React.CSSProperties>>;
Expand Down