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:
chenyizhongx 2022-02-14 11:27:52 +08:00 committed by GitHub
parent 9eed463be3
commit 38b4a03c56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 13 deletions

View File

@ -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);
};

View File

@ -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`] = `

View File

@ -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`] = `

View File

@ -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 () => {

View File

@ -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,
);
```