Skip to content

Commit bf1729f

Browse files
authored
refactor(input): merge textarea into rc-input and deduplicate shared logic (#163)
* feat: migrate textarea implementation into rc-input * refactor(input): extract shared count display state for input and textarea * fix: lint * fix: test case * fix(input): avoid restoring stale selection after rerender * refactor(input): export textarea separately from rc-input
1 parent c1e9676 commit bf1729f

29 files changed

+2351
-77
lines changed

README.md

Lines changed: 77 additions & 26 deletions
Large diffs are not rendered by default.

assets/index.less

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,25 @@
3232
}
3333
}
3434
}
35+
36+
@textarea-prefix-cls: rc-textarea;
37+
38+
.rc-textarea-affix-wrapper {
39+
display: inline-block;
40+
box-sizing: border-box;
41+
42+
textarea {
43+
box-sizing: border-box;
44+
width: 100%;
45+
height: 100%;
46+
padding: 0;
47+
border: 1px solid #1677ff;
48+
}
49+
}
50+
51+
.@{textarea-prefix-cls}-out-of-range {
52+
&,
53+
& textarea {
54+
color: red;
55+
}
56+
}

docs/demo/textarea.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
title: TextArea
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
## Basic
9+
10+
<code src="../examples/textarea-basic.tsx"></code>
11+
12+
## Auto Size
13+
14+
<code src="../examples/textarea-auto-size.tsx"></code>
15+
16+
## Allow Clear
17+
18+
<code src="../examples/textarea-allow-clear.tsx"></code>
19+
20+
## Show Count
21+
22+
<code src="../examples/textarea-show-count.tsx"></code>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* eslint-disable no-console */
2+
import Input from '@rc-component/input';
3+
import React, { useState, type ChangeEvent } from 'react';
4+
5+
const TextArea = Input.TextArea;
6+
7+
export default function App() {
8+
const [value, setValue] = useState('hello\nworld');
9+
10+
const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
11+
const {
12+
target: { value: currentValue },
13+
} = e;
14+
setValue(currentValue);
15+
};
16+
17+
return (
18+
<div>
19+
<p>Uncontrolled</p>
20+
<TextArea autoSize allowClear />
21+
<p>controlled</p>
22+
<TextArea value={value} onChange={onChange} allowClear />
23+
</div>
24+
);
25+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* eslint-disable no-console */
2+
import Input, { type TextAreaProps } from '@rc-component/input';
3+
import React, { useState, type ChangeEvent } from 'react';
4+
5+
const TextArea = Input.TextArea;
6+
7+
export default function App() {
8+
const [value, setValue] = useState('hello\nworld');
9+
10+
const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
11+
const {
12+
target: { value: currentValue },
13+
} = e;
14+
setValue(currentValue);
15+
};
16+
17+
const onResize: TextAreaProps['onResize'] = ({ width, height }) => {
18+
console.log(`size is changed, width:${width} height:${height}`);
19+
};
20+
21+
return (
22+
<div>
23+
<p>when set to true</p>
24+
<TextArea
25+
autoSize
26+
onResize={onResize}
27+
value={value}
28+
onChange={onChange}
29+
/>
30+
<p>when set to object of minRows and maxRows</p>
31+
<TextArea
32+
autoSize={{ minRows: 5, maxRows: 15 }}
33+
onResize={onResize}
34+
value={value}
35+
onChange={onChange}
36+
/>
37+
</div>
38+
);
39+
}

docs/examples/textarea-basic.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* eslint-disable no-console */
2+
import Input, { type TextAreaProps } from '@rc-component/input';
3+
import React, { useState, type ChangeEvent, type KeyboardEvent } from 'react';
4+
5+
const TextArea = Input.TextArea;
6+
7+
export default function App() {
8+
const [value, setValue] = useState('');
9+
10+
const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
11+
const {
12+
target: { value: currentValue },
13+
} = e;
14+
console.log(e.target.value);
15+
setValue(currentValue);
16+
};
17+
18+
const onResize: TextAreaProps['onResize'] = ({ width, height }) => {
19+
console.log(`size is changed, width:${width} height:${height}`);
20+
};
21+
22+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
23+
const onPressEnter = (e: KeyboardEvent<HTMLTextAreaElement>) => {
24+
console.log(`enter key is pressed`);
25+
};
26+
27+
return (
28+
<div>
29+
<TextArea
30+
prefixCls="custom-textarea"
31+
onPressEnter={onPressEnter}
32+
onResize={onResize}
33+
value={value}
34+
onChange={onChange}
35+
autoFocus
36+
onFocus={() => console.log('focus')}
37+
/>
38+
</div>
39+
);
40+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* eslint-disable no-console */
2+
import Input from '@rc-component/input';
3+
import React, { useState, type ChangeEvent } from 'react';
4+
import '../../assets/index.less';
5+
6+
const TextArea = Input.TextArea;
7+
8+
export default function App() {
9+
const [value, setValue] = useState('hello\nworld');
10+
11+
const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
12+
const {
13+
target: { value: currentValue },
14+
} = e;
15+
setValue(currentValue);
16+
};
17+
18+
return (
19+
<div>
20+
<p>Uncontrolled</p>
21+
<TextArea autoSize showCount />
22+
<p>controlled</p>
23+
<TextArea value={value} onChange={onChange} showCount maxLength={100} />
24+
<p>with height</p>
25+
<TextArea
26+
value={value}
27+
onChange={onChange}
28+
showCount
29+
style={{ height: 200, width: '100%', resize: 'vertical' }}
30+
/>
31+
<hr />
32+
<p>Count.exceedFormatter</p>
33+
<TextArea
34+
defaultValue="👨‍👨‍👧‍👦"
35+
count={{
36+
show: true,
37+
max: 5,
38+
}}
39+
/>
40+
<TextArea
41+
defaultValue="🔥"
42+
count={{
43+
show: true,
44+
max: 5,
45+
exceedFormatter: (val, { max }) => {
46+
const segments = [...new Intl.Segmenter().segment(val)];
47+
48+
return segments
49+
.filter((seg) => seg.index + seg.segment.length <= max)
50+
.map((seg) => seg.segment)
51+
.join('');
52+
},
53+
}}
54+
/>
55+
</div>
56+
);
57+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"prepare": "husky install"
4545
},
4646
"dependencies": {
47+
"@rc-component/resize-observer": "^1.1.1",
4748
"@rc-component/util": "^1.4.0",
4849
"clsx": "^2.1.1"
4950
},

src/Input.tsx

Lines changed: 18 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { clsx } from 'clsx';
2-
import useControlledState from '@rc-component/util/lib/hooks/useControlledState';
32
import omit from '@rc-component/util/lib/omit';
43
import React, {
54
forwardRef,
@@ -11,6 +10,9 @@ import React, {
1110
import type { HolderRef } from './BaseInput';
1211
import BaseInput from './BaseInput';
1312
import useCount from './hooks/useCount';
13+
import useCountDisplay from './hooks/useCountDisplay';
14+
import useCountExceed from './hooks/useCountExceed';
15+
import useMergedValue from './hooks/useMergedValue';
1416
import type { ChangeEventInfo, InputProps, InputRef } from './interface';
1517
import { resolveOnChange } from './utils/commonUtils';
1618
import {
@@ -58,21 +60,21 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
5860
};
5961

6062
// ====================== Value =======================
61-
const [value, setValue] = useControlledState(props.defaultValue, props.value);
62-
const formatValue =
63-
value === undefined || value === null ? '' : String(value);
64-
65-
// =================== Select Range ===================
66-
const [selection, setSelection] = useState<
67-
[start: number, end: number] | null
68-
>(null);
63+
const { setValue, formatValue } = useMergedValue(
64+
props.defaultValue,
65+
props.value,
66+
);
6967

70-
// ====================== Count =======================
7168
const countConfig = useCount(count, showCount);
72-
const mergedMax = countConfig.max || maxLength;
73-
const valueLength = countConfig.strategy(formatValue);
74-
75-
const isOutOfRange = !!mergedMax && valueLength > mergedMax;
69+
const { isOutOfRange, dataCount } = useCountDisplay({
70+
countConfig,
71+
value: formatValue,
72+
maxLength,
73+
});
74+
const getExceedValue = useCountExceed({
75+
countConfig,
76+
getTarget: () => inputRef.current,
77+
});
7678

7779
// ======================= Ref ========================
7880
useImperativeHandle(ref, () => ({
@@ -108,25 +110,9 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
108110
currentValue: string,
109111
info: ChangeEventInfo,
110112
) => {
111-
let cutValue = currentValue;
112-
113-
if (
114-
!compositionRef.current &&
115-
countConfig.exceedFormatter &&
116-
countConfig.max &&
117-
countConfig.strategy(currentValue) > countConfig.max
118-
) {
119-
cutValue = countConfig.exceedFormatter(currentValue, {
120-
max: countConfig.max,
121-
});
113+
const cutValue = getExceedValue(currentValue, compositionRef.current);
122114

123-
if (currentValue !== cutValue) {
124-
setSelection([
125-
inputRef.current?.selectionStart || 0,
126-
inputRef.current?.selectionEnd || 0,
127-
]);
128-
}
129-
} else if (info.source === 'compositionEnd') {
115+
if (info.source === 'compositionEnd' && currentValue === cutValue) {
130116
// Avoid triggering twice
131117
// https://github.com/ant-design/ant-design/issues/46587
132118
return;
@@ -138,12 +124,6 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
138124
}
139125
};
140126

141-
useEffect(() => {
142-
if (selection) {
143-
inputRef.current?.setSelectionRange(...selection);
144-
}
145-
}, [selection]);
146-
147127
const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
148128
triggerChange(e, e.target.value, {
149129
source: 'change',
@@ -260,17 +240,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
260240

261241
const getSuffix = () => {
262242
// Max length value
263-
const hasMaxLength = Number(mergedMax) > 0;
264-
265243
if (suffix || countConfig.show) {
266-
const dataCount = countConfig.showFormatter
267-
? countConfig.showFormatter({
268-
value: formatValue,
269-
count: valueLength,
270-
maxLength: mergedMax,
271-
})
272-
: `${valueLength}${hasMaxLength ? ` / ${mergedMax}` : ''}`;
273-
274244
return (
275245
<>
276246
{countConfig.show && (

0 commit comments

Comments
 (0)