Skip to content
Merged
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1924,6 +1924,93 @@ exports[`renders components/sender/demo/slot-filling.tsx correctly 1`] = `
</div>
`;

exports[`renders components/sender/demo/slot-format-result.tsx correctly 1`] = `
<div
class="ant-flex css-var-_R_0_ ant-flex-align-stretch ant-flex-vertical"
style="gap:16px"
>
<div
class="ant-flex css-var-_R_0_ ant-flex-wrap-wrap"
style="gap:8px"
>
<button
class="ant-btn css-var-_R_0_ ant-btn-default ant-btn-color-default ant-btn-variant-outlined"
type="button"
>
<span>
Get Value
</span>
</button>
</div>
<div
class="ant-sender css-var-_R_0_ ant-sender-main"
>
<div
class="ant-sender-content"
>
<div
class="ant-sender-input ant-sender-input-slot"
contenteditable="true"
data-placeholder="Enter to send"
role="textbox"
spellcheck="false"
style="min-height:52.7px;max-height:105.4px;overflow-y:auto"
tabindex="0"
value=""
/>
<div
id="ant-sender-slot-placeholders"
style="display:none"
/>
<div
class="ant-sender-actions-list"
>
<div
class="ant-sender-actions-list-presets ant-flex css-var-_R_0_"
>
<button
class="ant-btn css-var-_R_0_ ant-btn-circle ant-btn-primary ant-btn-color-primary ant-btn-variant-solid ant-btn-icon-only ant-sender-actions-btn ant-sender-actions-btn-disabled"
disabled=""
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="arrow-up"
class="anticon anticon-arrow-up"
role="img"
>
<svg
aria-hidden="true"
data-icon="arrow-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M868 545.5L536.1 163a31.96 31.96 0 00-48.3 0L156 545.5a7.97 7.97 0 006 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
<div>
<strong>
getValue() result:
</strong>
<!-- -->
(click "Get Value" to see formatted result)
</div>
</div>
`;

exports[`renders components/sender/demo/slot-with-suggestion.tsx correctly 1`] = `
<div
class="ant-flex css-var-_R_0_ ant-flex-align-stretch ant-flex-gap-middle ant-flex-vertical"
Expand Down
39 changes: 39 additions & 0 deletions packages/x/components/sender/__tests__/slot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ const customSlotConfig: SlotConfigType = {
formatResult: (v: any) => `[${v}]`,
};

const contentSlotConfigWithFormat: SlotConfigType = {
type: 'content',
key: 'content_fmt',
props: { defaultValue: 'Content Value', placeholder: 'Enter content' },
formatResult: (v: any) => `[formatted:${v}]`,
};

const errorTypeSlotConfig: any = {
type: 'error',
key: 'error1',
Expand Down Expand Up @@ -784,4 +791,36 @@ describe('Sender Slot Component', () => {
});
});
});
describe('content slot formatResult', () => {
it('should apply formatResult to content slot value in getValue()', () => {
const ref = createRef<SenderRef>();
render(
<Sender
ref={ref}
slotConfig={[
contentSlotConfigWithFormat,
{ type: 'text', value: ' ' },
{
type: 'input',
key: 'input_fmt',
props: { defaultValue: 'InputVal' },
formatResult: (v: any) => `[formatted:${v}]`,
},
]}
/>,
);
const fullValue = ref.current?.getValue();
// Both content and input slots should go through formatResult
expect(fullValue?.value).toContain('[formatted:Content Value]');
expect(fullValue?.value).toContain('[formatted:InputVal]');
});

it('should return raw value when content slot has no formatResult', () => {
const ref = createRef<SenderRef>();
render(<Sender ref={ref} slotConfig={[contentSlotConfigWithValue]} />);
const fullValue = ref.current?.getValue();
expect(fullValue?.value).toContain('Content Value');
expect(fullValue?.value).not.toContain('[formatted:');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,64 @@ describe('useSlotConfigState', () => {
rerender({ slotConfig: [] });
expect(result.current[0].get('x')).toBeDefined();
});

describe('getNodeTextValue – content type with formatResult', () => {
it('applies formatResult to content slot value', () => {
const contentConfig: SlotConfigType = {
type: 'content',
key: 'c1',
props: { defaultValue: 'hello' },
formatResult: (v: any) => `[${v}]`,
};
const { result } = renderHook(({ slotConfig }) => useSlotConfigState(slotConfig), {
initialProps: { slotConfig: [contentConfig] },
});

// Build a DOM element that mimics a content slot span
const span = document.createElement('span');
span.dataset.slotKey = 'c1';
span.innerText = 'hello';

const { getNodeTextValue } = result.current[1];
expect(getNodeTextValue(span)).toBe('[hello]');
});

it('returns raw textContent when content slot has no formatResult', () => {
const contentConfig: SlotConfigType = {
type: 'content',
key: 'c2',
props: { defaultValue: 'plain' },
};
const { result } = renderHook(({ slotConfig }) => useSlotConfigState(slotConfig), {
initialProps: { slotConfig: [contentConfig] },
});

const span = document.createElement('span');
span.dataset.slotKey = 'c2';
span.innerText = 'plain';

const { getNodeTextValue } = result.current[1];
expect(getNodeTextValue(span)).toBe('plain');
});

it('uses DOM textContent (not state) as the value for content type', () => {
const contentConfig: SlotConfigType = {
type: 'content',
key: 'c3',
props: { defaultValue: 'initial' },
formatResult: (v: any) => `<${v}>`,
};
const { result } = renderHook(({ slotConfig }) => useSlotConfigState(slotConfig), {
initialProps: { slotConfig: [contentConfig] },
});

// User edits the contenteditable area — DOM text differs from state
const span = document.createElement('span');
span.dataset.slotKey = 'c3';
span.innerText = 'user-edited';

const { getNodeTextValue } = result.current[1];
expect(getNodeTextValue(span)).toBe('<user-edited>');
});
});
});
7 changes: 7 additions & 0 deletions packages/x/components/sender/demo/slot-format-result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## zh-CN

`formatResult` 可对所有词槽类型(包含 `content`)的值进行格式化,统一控制最终输出。

## en-US

`formatResult` can format the value of all slot types (including `content`), providing unified control over the final output.
73 changes: 73 additions & 0 deletions packages/x/components/sender/demo/slot-format-result.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Sender, type SenderProps } from '@ant-design/x';
import { Button, Flex, GetRef, message } from 'antd';
import React, { useRef, useState } from 'react';

const slotConfig: SenderProps['slotConfig'] = [
{ type: 'text', value: 'Translate "' },
{
type: 'content',
key: 'text',
props: { defaultValue: 'Hello World', placeholder: 'Enter text' },
formatResult: (value: any) => `[${value}]`,
},
{ type: 'text', value: '" from ' },
{
type: 'select',
key: 'sourceLang',
props: {
defaultValue: 'English',
options: ['English', 'Chinese', 'Japanese'],
placeholder: 'Select language',
},
formatResult: (value: any) => `{${value}}`,
},
{ type: 'text', value: ' to ' },
{
type: 'select',
key: 'targetLang',
props: {
defaultValue: 'Chinese',
options: ['English', 'Chinese', 'Japanese'],
placeholder: 'Select language',
},
formatResult: (value: any) => `{${value}}`,
},
{ type: 'text', value: '.' },
];

const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const senderRef = useRef<GetRef<typeof Sender>>(null);
const [value, setValue] = useState<string>('');

return (
<Flex vertical gap={16}>
{contextHolder}
<Flex wrap gap={8}>
<Button
onClick={() => {
const val = senderRef.current?.getValue?.();
setValue(val?.value ?? '');
}}
>
Get Value
</Button>
</Flex>
<Sender
autoSize={{ minRows: 2, maxRows: 4 }}
placeholder="Enter to send"
onSubmit={(v) => {
setValue(v);
messageApi.open({ type: 'success', content: `Sent: ${v}` });
}}
slotConfig={slotConfig}
ref={senderRef}
/>
<div>
<strong>getValue() result:</strong> {value || '(click "Get Value" to see formatted result)'}
</div>
</Flex>
);
};

export default () => <App />;
7 changes: 5 additions & 2 deletions packages/x/components/sender/hooks/use-slot-config-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,13 @@ const useSlotConfigState = (
if (infoNodeType === 'nbsp') {
return ' ';
}
if (!slotConfig || slotConfig.type === 'content') {
if (!slotConfig) {
return textContent;
}
const slotValue = stateRef.current[slotKey] ?? '';
// content 类型的值来自 DOM 文本(用户可在 contenteditable 区域直接编辑),
// 其他类型的值来自 state。
const slotValue =
slotConfig.type === 'content' ? textContent : (stateRef.current[slotKey] ?? '');
return slotConfig.formatResult?.(slotValue) ?? slotValue;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/x/components/sender/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
<code src="./demo/basic.tsx">Basic Usage</code>
<code src="./demo/switch.tsx">Feature Toggle</code>
<code src="./demo/slot-filling.tsx">Slot Mode</code>
<code src="./demo/slot-format-result.tsx">Slot Format Result</code>
<code src="./demo/ref-action.tsx">Instance Methods</code>
<code src="./demo/submitType.tsx">Submit Methods</code>
<code src="./demo/speech.tsx">Voice Input</code>
Expand Down Expand Up @@ -120,7 +121,7 @@ type ActionsComponents = {
| --- | --- | --- | --- | --- |
| type | Node type, determines the rendering component type, required | 'text' \| 'input' \| 'select' \| 'tag' \| 'content' \| 'custom' | - | 2.0.0 |
| key | Unique identifier, can be omitted when type is text | string | - | - |
| formatResult | Format the final result | (value: any) => string | - | 2.0.0 |
| formatResult | Format the final result, applies to all slot types (including `content`) | (value: any) => string | - | 2.0.0 |

##### text node properties

Expand Down
3 changes: 2 additions & 1 deletion packages/x/components/sender/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
<code src="./demo/basic.tsx">基本用法</code>
<code src="./demo/switch.tsx">功能开关</code>
<code src="./demo/slot-filling.tsx">词槽模式</code>
<code src="./demo/slot-format-result.tsx">词槽格式化</code>
<code src="./demo/ref-action.tsx">实例方法</code>
<code src="./demo/submitType.tsx">提交方式</code>
<code src="./demo/speech.tsx">语音输入</code>
Expand Down Expand Up @@ -122,7 +123,7 @@ type ActionsComponents = {
| --- | --- | --- | --- | --- |
| type | 节点类型,决定渲染组件类型,必填 | 'text' \| 'input' \| 'select' \| 'tag' \| 'content' \| 'custom' | - | 2.0.0 |
| key | 唯一标识,type 为 text 时可省略 | string | - | - |
| formatResult | 格式化最终结果 | (value: any) => string | - | 2.0.0 |
| formatResult | 格式化最终结果,对所有词槽类型(含 `content`)生效 | (value: any) => string | - | 2.0.0 |

##### text 节点属性

Expand Down