mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-30 11:08:45 +08:00
fix textarea maxlength issue (#33910)
* fix: textarea maxlength * refactor: delete some useless code * refactor: fix ci * refactor and fix test case fall * update snapshot * add testcase * refactor type defined
This commit is contained in:
parent
9eed463be3
commit
38b4a03c56
@ -17,6 +17,26 @@ function fixEmojiLength(value: string, maxLength: number) {
|
||||
return [...(value || '')].slice(0, maxLength).join('');
|
||||
}
|
||||
|
||||
function setTriggerValue(
|
||||
isCursorInEnd: boolean,
|
||||
preValue: string,
|
||||
triggerValue: string,
|
||||
maxLength: number,
|
||||
) {
|
||||
let newTriggerValue = triggerValue;
|
||||
if (isCursorInEnd) {
|
||||
// 光标在尾部,直接截断
|
||||
newTriggerValue = fixEmojiLength(triggerValue, maxLength!);
|
||||
} else if (
|
||||
[...(preValue || '')].length < triggerValue.length &&
|
||||
[...(triggerValue || '')].length > maxLength!
|
||||
) {
|
||||
// 光标在中间,如果最后的值超过最大值,则采用原先的值
|
||||
newTriggerValue = preValue;
|
||||
}
|
||||
return newTriggerValue;
|
||||
}
|
||||
|
||||
export interface TextAreaProps extends RcTextAreaProps {
|
||||
allowClear?: boolean;
|
||||
bordered?: boolean;
|
||||
@ -54,6 +74,8 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
const clearableInputRef = React.useRef<ClearableLabeledInput>(null);
|
||||
|
||||
const [compositing, setCompositing] = React.useState(false);
|
||||
const oldCompositionValueRef = React.useRef<string>();
|
||||
const oldSelectionStartRef = React.useRef<number>(0);
|
||||
|
||||
const [value, setValue] = useMergedState(props.defaultValue, {
|
||||
value: props.value,
|
||||
@ -73,6 +95,10 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
|
||||
const onInternalCompositionStart: React.CompositionEventHandler<HTMLTextAreaElement> = e => {
|
||||
setCompositing(true);
|
||||
// 拼音输入前保存一份旧值
|
||||
oldCompositionValueRef.current = value as string;
|
||||
// 保存旧的光标位置
|
||||
oldSelectionStartRef.current = e.currentTarget.selectionStart;
|
||||
onCompositionStart?.(e);
|
||||
};
|
||||
|
||||
@ -81,9 +107,16 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
|
||||
let triggerValue = e.currentTarget.value;
|
||||
if (hasMaxLength) {
|
||||
triggerValue = fixEmojiLength(triggerValue, maxLength!);
|
||||
const isCursorInEnd =
|
||||
oldSelectionStartRef.current >= maxLength! + 1 ||
|
||||
oldSelectionStartRef.current === oldCompositionValueRef.current?.length;
|
||||
triggerValue = setTriggerValue(
|
||||
isCursorInEnd,
|
||||
oldCompositionValueRef.current as string,
|
||||
triggerValue,
|
||||
maxLength!,
|
||||
);
|
||||
}
|
||||
|
||||
// Patch composition onChange when value changed
|
||||
if (triggerValue !== value) {
|
||||
handleSetValue(triggerValue);
|
||||
@ -96,9 +129,13 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
let triggerValue = e.target.value;
|
||||
if (!compositing && hasMaxLength) {
|
||||
triggerValue = fixEmojiLength(triggerValue, maxLength!);
|
||||
// 1. 复制粘贴超过maxlength的情况 2.未超过maxlength的情况
|
||||
const isCursorInEnd =
|
||||
e.target.selectionStart >= maxLength! + 1 ||
|
||||
e.target.selectionStart === triggerValue.length ||
|
||||
!e.target.selectionStart;
|
||||
triggerValue = setTriggerValue(isCursorInEnd, value as string, triggerValue, maxLength!);
|
||||
}
|
||||
|
||||
handleSetValue(triggerValue);
|
||||
resolveOnChange(e.currentTarget, e, onChange, triggerValue);
|
||||
};
|
||||
|
@ -9530,10 +9530,19 @@ Array [
|
||||
`;
|
||||
|
||||
exports[`renders ./components/input/demo/textarea.md extend context correctly 1`] = `
|
||||
<textarea
|
||||
class="ant-input"
|
||||
rows="4"
|
||||
/>
|
||||
Array [
|
||||
<textarea
|
||||
class="ant-input"
|
||||
rows="4"
|
||||
/>,
|
||||
<br />,
|
||||
<br />,
|
||||
<textarea
|
||||
class="ant-input"
|
||||
placeholder="maxLength is 6"
|
||||
rows="4"
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/input/demo/textarea-resize.md extend context correctly 1`] = `
|
||||
|
@ -3304,10 +3304,19 @@ Array [
|
||||
`;
|
||||
|
||||
exports[`renders ./components/input/demo/textarea.md correctly 1`] = `
|
||||
<textarea
|
||||
class="ant-input"
|
||||
rows="4"
|
||||
/>
|
||||
Array [
|
||||
<textarea
|
||||
class="ant-input"
|
||||
rows="4"
|
||||
/>,
|
||||
<br />,
|
||||
<br />,
|
||||
<textarea
|
||||
class="ant-input"
|
||||
placeholder="maxLength is 6"
|
||||
rows="4"
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/input/demo/textarea-resize.md correctly 1`] = `
|
||||
|
@ -110,6 +110,64 @@ describe('TextArea', () => {
|
||||
expect.objectContaining({ target: expect.objectContaining({ value: '竹' }) }),
|
||||
);
|
||||
});
|
||||
|
||||
// 字符输入
|
||||
it('should not cut off string when cursor position is not at the end', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mount(<TextArea maxLength={6} defaultValue="123456" onChange={onChange} />);
|
||||
wrapper
|
||||
.find('textarea')
|
||||
.simulate('change', { target: { selectionStart: 1, value: 'w123456' } });
|
||||
wrapper
|
||||
.find('textarea')
|
||||
.simulate('change', { target: { selectionStart: 3, value: '123w456' } });
|
||||
expect(wrapper.find('textarea').at(0).getDOMNode().value).toBe('123456');
|
||||
});
|
||||
|
||||
// 拼音输入
|
||||
// 1. 光标位于最后,且当前字符数未达到6个,若选中的字符 + 原字符的长度超过6个,则将最终的字符按照maxlength截断
|
||||
it('when the input method is pinyin and the cursor is at the end, should use maxLength to crop', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mount(<TextArea maxLength={6} defaultValue="1234" onChange={onChange} />);
|
||||
wrapper.find('textarea').instance().value = '1234'; // enzyme not support change `currentTarget`
|
||||
wrapper.find('textarea').instance().selectionStart = 4;
|
||||
wrapper.find('textarea').simulate('compositionStart');
|
||||
|
||||
wrapper
|
||||
.find('textarea')
|
||||
.simulate('change', { target: { selectionStart: 9, value: '1234z z z' } });
|
||||
wrapper
|
||||
.find('textarea')
|
||||
.simulate('change', { target: { selectionStart: 7, value: '1234组织者' } });
|
||||
|
||||
wrapper.find('textarea').instance().value = '1234组织者';
|
||||
wrapper.find('textarea').instance().selectionStart = 7;
|
||||
wrapper.find('textarea').simulate('compositionEnd');
|
||||
|
||||
expect(wrapper.find('textarea').at(0).getDOMNode().value).toBe('1234组织');
|
||||
});
|
||||
|
||||
// 2. 光标位于中间或开头,且当前字符数未达到6个,若选中的字符 + 原字符的长度超过6个,则显示原有字符
|
||||
it('when the input method is Pinyin and the cursor is in the middle, should display the original string', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mount(<TextArea maxLength={6} defaultValue="1234" onChange={onChange} />);
|
||||
wrapper.find('textarea').instance().value = '1234'; // enzyme not support change `currentTarget`
|
||||
wrapper.find('textarea').instance().selectionStart = 2;
|
||||
wrapper.find('textarea').simulate('compositionStart');
|
||||
|
||||
wrapper
|
||||
.find('textarea')
|
||||
.simulate('change', { target: { selectionStart: 2, value: '12z z z34' } });
|
||||
wrapper
|
||||
.find('textarea')
|
||||
.simulate('change', { target: { selectionStart: 5, value: '12组织者34' } });
|
||||
|
||||
wrapper.find('textarea').instance().value = '12组织者34';
|
||||
wrapper.find('textarea').instance().selectionStart = 5;
|
||||
wrapper.find('textarea').simulate('compositionEnd');
|
||||
|
||||
expect(wrapper.find('textarea').at(0).getDOMNode().value).toBe('1234');
|
||||
});
|
||||
});
|
||||
|
||||
it('when prop value not in this.props, resizeTextarea should be called', async () => {
|
||||
|
@ -18,5 +18,13 @@ import { Input } from 'antd';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
ReactDOM.render(<TextArea rows={4} />, mountNode);
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<TextArea rows={4} />
|
||||
<br />
|
||||
<br />
|
||||
<TextArea rows={4} placeholder="maxLength is 6" maxLength={6} />
|
||||
</>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
||||
|
Loading…
Reference in New Issue
Block a user