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
36 changes: 36 additions & 0 deletions packages/x/components/sender/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,40 @@ describe('Sender Component', () => {
expect(container.querySelector('.ant-sender')).toBeTruthy();
});
});
describe('show Count', () => {
it('should render current character count with max count when maxLength is set', () => {
const maxCountValue = 10;

const { container } = render(<Sender lengthLimit={{ maxLength: maxCountValue }} />);
const textLength = container.querySelector('.sender-text-length');
expect(textLength).toHaveTextContent('0');

const maxCount = container.querySelector('.sender-text-max-length');
expect(maxCount).toHaveTextContent(maxCountValue.toString());
});

it('should render count in red and error message and disable action button when showCount is true and maxLength is set with text length greater than max length', () => {
const maxCountValue = 10;

const { container } = render(<Sender lengthLimit={{ maxLength: maxCountValue }} />);
const textLength = container.querySelector('.sender-text-length');

const maxCount = container.querySelector('.sender-text-max-length');

const textarea = container.querySelector('textarea')!;
fireEvent.input(textarea, { target: { value: 'Updated longer text' } });

expect(textLength).toHaveTextContent('19');
expect(textLength).toHaveStyle({ color: '#ff4d4f' });
expect(maxCount).toHaveTextContent(maxCountValue.toString());
expect(maxCount).toHaveStyle({ color: '#ff4d4f' });

const errorMessage = container.querySelector('.sender-text-max-length-error');
expect(errorMessage).toBeVisible();
expect(errorMessage).toHaveStyle({ color: '#ff4d4f' });

const sendButton = container.querySelector('.ant-sender-actions-btn button');
expect(sendButton).toBeDisabled();
});
});
});
7 changes: 7 additions & 0 deletions packages/x/components/sender/demo/length-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## zh-CN

限制文本输入长度,并提供可自定义的错误提示信息

## en-US

Limit the text length input with customizable error message
32 changes: 32 additions & 0 deletions packages/x/components/sender/demo/length-limit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Sender } from '@ant-design/x';
import { App } from 'antd';
import React, { useState } from 'react';

const Demo: React.FC = () => {
const [value, setValue] = useState<string>('Try for longer text');

const { message } = App.useApp();

return (
<Sender
value={value}
onChange={(v) => {
setValue(v);
}}
onSubmit={() => {
setValue('');
message.success('Send message successfully!');
}}
autoSize={{ minRows: 4, maxRows: 6 }}
lengthLimit={{
maxLength: 100,
}}
/>
);
};

export default () => (
<App>
<Demo />
</App>
);
2 changes: 2 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/length-limit.tsx">Text Length Limit</code>

## API

Expand All @@ -40,6 +41,7 @@ Common props ref:[Common props](/docs/react/common-props)

| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| lengthLimit | Set maximum length of input with customizable error message | { maxLength: number; exceedMessage?: string } | - | - |
| allowSpeech | Whether to allow voice input | boolean \| SpeechConfig | false | - |
| classNames | Style class names | [See below](#semantic-dom) | - | - |
| components | Custom components | Record<'input', ComponentType> | - | - |
Expand Down
45 changes: 38 additions & 7 deletions packages/x/components/sender/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Flex } from 'antd';
import { Flex, Typography } from 'antd';
import classnames from 'classnames';
import { useMergedState } from 'rc-util';
import pickAttrs from 'rc-util/lib/pickAttrs';
Expand Down Expand Up @@ -80,6 +80,7 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
onFocus,
onBlur,
skill,
lengthLimit,
...restProps
} = props;

Expand Down Expand Up @@ -220,6 +221,12 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
? header(actionNode, { components: sharedRenderComponents })
: header || null;

// ============================ Input ============================
const exceedLengthIsTrue =
lengthLimit !== undefined &&
lengthLimit.maxLength > 0 &&
(innerValue.length || 0) > lengthLimit.maxLength;

// ============================ Footer ============================
const footerNode =
typeof footer === 'function'
Expand All @@ -230,7 +237,7 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
const actionsButtonContextProps = {
prefixCls: actionBtnCls,
onSend: triggerSend,
onSendDisabled: !innerValue,
onSendDisabled: !innerValue || exceedLengthIsTrue,
onClear: triggerClear,
onClearDisabled: !innerValue,
onCancel,
Expand Down Expand Up @@ -346,11 +353,35 @@ const ForwardSender = React.forwardRef<SenderRef, SenderProps>((props, ref) => {
)}

{/* Input */}
{isSlotMode ? (
<SlotTextArea ref={inputRef as React.Ref<SlotTextAreaRef>} />
) : (
<TextArea ref={inputRef as React.Ref<TextAreaRef>} />
)}
<Flex orientation="vertical" flex={1} style={{ width: '100%' }}>
{isSlotMode ? (
<SlotTextArea ref={inputRef as React.Ref<SlotTextAreaRef>} />
) : (
<TextArea ref={inputRef as React.Ref<TextAreaRef>} />
)}
{lengthLimit !== undefined && (
<Flex justify="flex-start" gap={4}>
<Typography.Text type={exceedLengthIsTrue ? 'danger' : 'secondary'}>
<small>
<span className={`sender-text-length`}>{innerValue.length || '0'}</span>
{' / '}
<span className={`sender-text-max-length`}>
{lengthLimit.maxLength.toString()}
</span>
</small>
</Typography.Text>
{exceedLengthIsTrue && (
<Typography.Text type={'danger'}>
<small>
<span className="sender-text-max-length-error">
{lengthLimit.exceedMessage ?? 'Exceeded maximum length'}
</span>
</small>
</Typography.Text>
)}
</Flex>
)}
</Flex>

{/* Action List */}
{suffixNode && (
Expand Down
2 changes: 2 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/length-limit.tsx">限制文本输入长度</code>

## API

Expand All @@ -41,6 +42,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA

| 属性 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| lengthLimit | 用于设置输入的最大长度,并支持自定义错误提示 | { maxLength: number; exceedMessage?: string } | - | - |
| allowSpeech | 是否允许语音输入 | boolean \| SpeechConfig | false | - |
| classNames | 样式类名 | [见下](#semantic-dom) | - | - |
| components | 自定义组件 | Record<'input', ComponentType> | - | - |
Expand Down
1 change: 1 addition & 0 deletions packages/x/components/sender/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export interface SenderProps
header?: BaseNode | NodeRender;
autoSize?: boolean | { minRows?: number; maxRows?: number };
skill?: SkillType;
lengthLimit?: { maxLength: number; exceedMessage?: string };
}

export type SenderRef = Omit<TextAreaRef, 'nativeElement'> &
Expand Down