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
2 changes: 1 addition & 1 deletion packages/x/CNAME
Original file line number Diff line number Diff line change
@@ -1 +1 @@
x.ant.design
x.ant.design
13 changes: 13 additions & 0 deletions packages/x/components/file-card/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ describe('FileCard Component', () => {
const { container } = render(<FileCard name="test.png" type="image" loading />);
expect(container.querySelector('.ant-file-card-loading')).toBeTruthy();
});
it('loading usePercent', () => {
const { container } = render(
<FileCard
name="test.png"
type="image"
spinProps={{
percent: 50,
}}
loading
/>,
);
expect(container.querySelector('.ant-file-card-loading')).toBeTruthy();
});

it('should handle custom styles', () => {
const { container } = render(
Expand Down
74 changes: 73 additions & 1 deletion packages/x/components/sender/SlotTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
onFocus,
onBlur,
slotConfig,
maxLength,
skill,
...restProps
} = React.useContext(SenderContext);
Expand Down Expand Up @@ -590,10 +591,14 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
return;
}
}

onKeyDown?.(e as unknown as React.KeyboardEvent<HTMLTextAreaElement>);
};
};

// ============================ Input Event ============================


const onInternalFocus = (e: React.FocusEvent<HTMLDivElement>) => {
const selection = window.getSelection();
if (selection) {
Expand Down Expand Up @@ -626,6 +631,16 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
onBlur?.(e as unknown as React.FocusEvent<HTMLTextAreaElement>);
};

// 获取当前选中文本长度
const getSelectedTextLength = (): number => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
return range.toString().length;
}
return 0;
};

const onInternalInput = (e: React.FormEvent<HTMLDivElement>) => {
const newValue = getEditorValue();
removeSpecificBRs(editableRef?.current);
Expand All @@ -646,7 +661,21 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
}

if (text) {
insert([{ type: 'text', value: text.replace(/\n/g, '') }]);
const currentValue = getEditorValue().value;
const newText = text.replace(/\n/g, '');

// 检查最大长度限制
if (maxLength !== undefined) {
const selectedLength = getSelectedTextLength();
const remainingLength = maxLength - (currentValue.length - selectedLength);
if (remainingLength <= 0) {
return; // 已达到最大长度,不再插入
}
const truncatedText = newText.slice(0, remainingLength);
insert([{ type: 'text', value: truncatedText.replace(/\n/g, '') }]);
} else {
insert([{ type: 'text', value: newText.replace(/\n/g, '') }]);
}
}

onPaste?.(e as unknown as React.ClipboardEvent<HTMLTextAreaElement>);
Expand Down Expand Up @@ -704,6 +733,37 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
const editableDom = editableRef.current;
const selection = window.getSelection();
if (!editableDom || !selection) return;

// 检查最大长度限制
if (maxLength !== undefined) {
const currentValue = getEditorValue().value;
const selectedLength = getSelectedTextLength();
const remainingLength = maxLength - (currentValue.length - selectedLength);

if (remainingLength <= 0) {
return;
}

// 渐进式截断文本slot
let remainingChars = remainingLength;
slotConfig = slotConfig.map((item) => {
if (item.type === 'text' && remainingChars > 0) {
const textLength = (item.value || '').length;
if (textLength <= remainingChars) {
remainingChars -= textLength;
return item;
}
const truncated = (item.value || '').slice(0, remainingChars);
remainingChars = 0;
return { ...item, value: truncated };
}
if (item.type === 'text') {
return { ...item, value: '' };
}
return item;
});
}

const slotNode = getSlotListNode(slotConfig);
const { type, range: lastRage } = getInsertPosition(position);
let range: Range = document.createRange();
Expand Down Expand Up @@ -880,6 +940,18 @@ const SlotTextArea = React.forwardRef<SlotTextAreaRef>((_, ref) => {
onBlur={onInternalBlur}
onSelect={onInternalSelect}
onInput={onInternalInput}
onBeforeInput={(e) => {
if (maxLength !== undefined) {
const currentValue = getEditorValue().value;
const selectedLength = getSelectedTextLength();
const inputLength = (e as any).data?.length ?? 0;
const newLength = currentValue.length - selectedLength + inputLength;
if (newLength > maxLength) {
e.preventDefault();
return;
}
}
}}
{...(restProps as React.HTMLAttributes<HTMLDivElement>)}
/>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,15 @@ exports[`renders components/sender/demo/agent.tsx extend context correctly 1`] =
type="button"
>
<span
class="ant-btn-icon"
class="ant-sender-skill-tag-text"
>
Writing Assistant
</span>
<div
aria-label="Close skill"
class="ant-sender-skill-tag-close"
role="button"
tabindex="0"
>
<span
aria-label="api"
Expand All @@ -658,6 +666,44 @@ exports[`renders components/sender/demo/agent.tsx extend context correctly 1`] =
/>
</svg>
</span>
</div>
</div>
</span>
Please write an article about
<span
class="ant-sender-slot"
contenteditable="false"
data-slot-key="writing_type"
>
<span
class="ant-dropdown-trigger ant-sender-slot-select placeholder"
>
<span
class="ant-sender-slot-select-value"
data-placeholder="Please enter a topic"
/>
<span
class="ant-sender-slot-select-arrow"
>
<span
aria-label="caret-down"
class="anticon anticon-caret-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
/>
</svg>
</span>
</span>
</button>
<div
Expand Down
64 changes: 64 additions & 0 deletions packages/x/components/sender/demo/max-length.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Sender } from '@ant-design/x';
import { Typography } from 'antd';
import React from 'react';

const { Paragraph, Text } = Typography;

const App = () => {
const [value, setValue] = React.useState('');

return (
<div>
<Paragraph>Sender with Length Limit</Paragraph>
<Sender
value={value}
onChange={setValue}
maxLength={50}
showCount
placeholder="Please enter content, max 50 characters..."
onSubmit={(msg) => {
console.log('Submit:', msg);
setValue('');
}}
/>
<Paragraph style={{ marginTop: 40 }}>Custom Count Display</Paragraph>
<Sender
value={value}
onChange={setValue}
maxLength={50}
showCount={({ count, maxLength }) => (
<Text
style={{
position: 'absolute',
color: maxLength && count > maxLength * 0.8 ? 'red' : '#ccc',
fontSize: 10,
marginBottom: 4,
}}
>
{maxLength
? `Entered ${count} characters, limit ${maxLength} characters`
: `Entered ${count} characters`}
</Text>
)}
placeholder="Custom count display..."
onSubmit={(msg) => {
console.log('Submit:', msg);
setValue('');
}}
/>
<Paragraph style={{ marginTop: 40 }}>Count Only, No Length Limit</Paragraph>
<Sender
value={value}
onChange={setValue}
showCount
placeholder="Character count only, no length limit..."
onSubmit={(msg) => {
console.log('Submit:', msg);
setValue('');
}}
/>
</div>
);
};

export default App;
3 changes: 3 additions & 0 deletions packages/x/components/sender/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
<code src="./demo/footer.tsx">Custom Footer Content</code>
<code src="./demo/send-style.tsx">Style Adjustment</code>
<code src="./demo/paste-image.tsx">Paste Files</code>
<code src="./demo/max-length.tsx">Max Length</code>

## API

Expand All @@ -50,6 +51,8 @@ Common props ref:[Common props](/docs/react/common-props)
| header | Header panel | React.ReactNode \| false \| (oriNode: React.ReactNode, info: { components: ActionsComponents; }) => React.ReactNode \| false | false | - |
| prefix | Prefix content | React.ReactNode \| false \| (oriNode: React.ReactNode, info: { components: ActionsComponents; }) => React.ReactNode \| false | false | - |
| footer | Footer content | React.ReactNode \| false \| (oriNode: React.ReactNode, info: { components: ActionsComponents; }) => React.ReactNode \| false | false | - |
| maxLength | Maximum length of input content | number | - | - |
| showCount | Whether to display character count, supports custom rendering | boolean \| ((info: { value: string; count: number; maxLength?: number }) => React.ReactNode) | false | - |
| readOnly | Whether to make the input box read-only | boolean | false | - |
| rootClassName | Root element style class | string | - | - |
| styles | Semantic style definition | [See below](#semantic-dom) | - | - |
Expand Down
24 changes: 23 additions & 1 deletion packages/x/components/sender/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
placeholder,
onFocus,
onBlur,
showCount,
skill,
...restProps
} = props;
Expand Down Expand Up @@ -141,7 +142,7 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
const [innerValue, setInnerValue] = useMergedState(defaultValue || '', {
value,
});

const currentCount = innerValue.length;
const triggerValueChange: SenderProps['onChange'] = (nextValue, event, slotConfig) => {
if (slotConfig) {
setInnerValue(nextValue);
Expand Down Expand Up @@ -221,6 +222,26 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
: header || null;

// ============================ Footer ============================
const renderCount = () => {
if (!showCount) return null;

const countInfo = {
value: innerValue,
count: currentCount,
maxLength: restProps.maxLength,
};

if (typeof showCount === 'function') {
return showCount(countInfo);
}

return (
<div className={`${prefixCls}-count`}>
{restProps.maxLength ? `${currentCount}/${restProps.maxLength}` : currentCount}
</div>
);
};

const footerNode =
typeof footer === 'function'
? footer(actionNode, { components: sharedRenderComponents })
Expand Down Expand Up @@ -383,6 +404,7 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
)}
</ActionButtonContext.Provider>
</SenderContext.Provider>
{showCount && renderCount()}
</div>
);
});
Expand Down
3 changes: 3 additions & 0 deletions packages/x/components/sender/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
<code src="./demo/footer.tsx">自定义底部内容</code>
<code src="./demo/send-style.tsx">调整样式</code>
<code src="./demo/paste-image.tsx">黏贴文件</code>
<code src="./demo/max-length.tsx">长度限制</code>

## API

Expand All @@ -51,6 +52,8 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
| header | 头部面板 | React.ReactNode \| false \|(oriNode: React.ReactNode,info: { components: ActionsComponents;}) => React.ReactNode \| false; | false | - |
| prefix | 前缀内容 | React.ReactNode \| false \|(oriNode: React.ReactNode,info: { components: ActionsComponents;}) => React.ReactNode \| false; | false | - |
| footer | 底部内容 | React.ReactNode \| false \|(oriNode: React.ReactNode,info: { components: ActionsComponents;}) => React.ReactNode \| false; | false | - |
| maxLength | 输入内容最大长度 | number | - | - |
| showCount | 是否显示字符计数,支持自定义渲染 | boolean \| ((info: { value: string; count: number; maxLength?: number }) => React.ReactNode) | false | - |
| readOnly | 是否让输入框只读 | boolean | false | - |
| rootClassName | 根元素样式类 | string | - | - |
| styles | 语义化定义样式 | [见下](#semantic-dom) | - | - |
Expand Down
4 changes: 4 additions & 0 deletions packages/x/components/sender/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ export interface SenderProps
suffix?: BaseNode | NodeRender;
header?: BaseNode | NodeRender;
autoSize?: boolean | { minRows?: number; maxRows?: number };
maxLength?: number;
showCount?:
| boolean
| ((info: { value: string; count: number; maxLength?: number }) => React.ReactNode);
skill?: SkillType;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/x/components/sender/style/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ const genSenderStyle: GenerateStyle<SenderToken> = (token) => {
paddingBlockStart: paddingXXS,
boxSizing: 'border-box',
},
// ============================ Count ============================
[`${componentCls}-count`]: {
color: token.colorTextDescription,
fontSize: token.fontSizeSM,
lineHeight: token.lineHeightSM,
position: 'absolute',
bottom: -token.paddingXXS,
insetInlineEnd: token.paddingSM,
},
},
};
};
Expand Down