Skip to content

Commit abe2aec

Browse files
authored
fix: fix multiline input issue (#808)
1 parent e8f9a4e commit abe2aec

3 files changed

Lines changed: 134 additions & 160 deletions

File tree

docs/content.en/docs/release-notes/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Information about release notes of Coco Server is provided here.
3636
- fix: fix selection issue after renaming #800
3737
- fix: fix shortcut issue in windows context menu #804
3838
- fix: panic caused by "state() called before manage()" #806
39+
- fix: fix multiline input issue #808
3940

4041
### ✈️ Improvements
4142

@@ -59,7 +60,6 @@ Information about release notes of Coco Server is provided here.
5960
- refactor: clean up unsupported characters from query string in Win Search #802
6061
- chore: display backtrace in panic log #805
6162

62-
6363
## 0.6.0 (2025-06-29)
6464

6565
### ❌ Breaking changes

src/components/Search/AutoResizeTextarea.tsx

Lines changed: 64 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { useBoolean, useDebounceFn } from "ahooks";
1+
import { useBoolean } from "ahooks";
22
import {
3-
useRef,
43
useImperativeHandle,
54
forwardRef,
65
KeyboardEvent,
7-
useEffect,
86
useCallback,
7+
ChangeEvent,
8+
useRef,
9+
useEffect,
910
} from "react";
1011
import { useTranslation } from "react-i18next";
1112

12-
const LINE_HEIGHT = 24; // 1.5rem
13-
const MAX_FIRST_LINE_WIDTH = 470; // Width in pixels for first line
14-
const MAX_HEIGHT = 240; // 15rem
13+
const MAX_HEIGHT = 240;
1514

1615
interface AutoResizeTextareaProps {
1716
isChatMode: boolean;
@@ -21,6 +20,7 @@ interface AutoResizeTextareaProps {
2120
chatPlaceholder?: string;
2221
lineCount?: number;
2322
onLineCountChange?: (lineCount: number) => void;
23+
firstLineMaxWidth: number;
2424
}
2525

2626
// Forward ref to allow parent to interact with this component
@@ -35,87 +35,15 @@ const AutoResizeTextarea = forwardRef<
3535
setInput,
3636
handleKeyDown,
3737
chatPlaceholder,
38-
lineCount = 1,
3938
onLineCountChange,
39+
firstLineMaxWidth,
4040
},
4141
ref
4242
) => {
4343
const { t } = useTranslation();
44-
const textareaRef = useRef<HTMLTextAreaElement>(null);
4544
const [isComposition, { setTrue, setFalse }] = useBoolean();
46-
47-
// Memoize resize logic
48-
const { run: debouncedResize } = useDebounceFn(
49-
() => {
50-
const textarea = textareaRef.current;
51-
if (!textarea) return;
52-
if (typeof window === "undefined" || typeof document === "undefined")
53-
return;
54-
55-
// Reset height to auto to get the correct scrollHeight
56-
textarea.style.height = "auto";
57-
58-
// Create a hidden span to measure first line width
59-
const span = document.createElement("span");
60-
span.style.visibility = "hidden";
61-
span.style.position = "absolute";
62-
span.style.whiteSpace = "pre";
63-
span.style.font = window.getComputedStyle(textarea).font;
64-
65-
// Get first line content
66-
const content = textarea.value;
67-
const firstLineEnd =
68-
content.indexOf("\n") === -1 ? content.length : content.indexOf("\n");
69-
span.textContent = content.slice(0, firstLineEnd);
70-
document.body.appendChild(span);
71-
72-
// Calculate lines based on first line width
73-
const firstLineWidth = span.offsetWidth;
74-
document.body.removeChild(span);
75-
76-
// Start with 1 line
77-
let lines = 1;
78-
79-
// Add a line if first line exceeds max width
80-
if (firstLineWidth > MAX_FIRST_LINE_WIDTH) {
81-
lines += 1;
82-
}
83-
84-
// Add lines based on scrollHeight for remaining content
85-
const scrollHeight = textarea.scrollHeight;
86-
const remainingLines = Math.floor(
87-
(scrollHeight - LINE_HEIGHT) / LINE_HEIGHT
88-
);
89-
lines += Math.max(0, remainingLines);
90-
91-
// Calculate final height
92-
const newHeight = Math.min(lines * LINE_HEIGHT, MAX_HEIGHT);
93-
94-
// Only update if height actually changed
95-
if (textarea.style.height !== `${newHeight}px`) {
96-
textarea.style.height = `${newHeight}px`;
97-
onLineCountChange?.(lines);
98-
}
99-
},
100-
{ wait: 100 }
101-
);
102-
103-
// Handle input changes and initial setup
104-
useEffect(() => {
105-
if (textareaRef.current) {
106-
debouncedResize();
107-
}
108-
}, [input, debouncedResize]);
109-
110-
useEffect(() => {
111-
if (textareaRef.current) {
112-
requestAnimationFrame(() => {
113-
// Set cursor position to end
114-
const length = textareaRef.current?.value.length || 0;
115-
textareaRef.current?.setSelectionRange(length, length);
116-
});
117-
}
118-
}, [lineCount]);
45+
const textareaRef = useRef<HTMLTextAreaElement>(null);
46+
const calcRef = useRef<HTMLDivElement>(null);
11947

12048
// Expose methods to the parent via ref
12149
useImperativeHandle(ref, () => ({
@@ -135,40 +63,67 @@ const AutoResizeTextarea = forwardRef<
13563
handleKeyDown?.(event);
13664
};
13765

66+
useEffect(() => {
67+
const textarea = textareaRef.current;
68+
69+
if (!textarea || !calcRef.current) return;
70+
71+
if (!calcRef.current) return;
72+
73+
textarea.style.height = "auto";
74+
75+
const computedStyle = getComputedStyle(textarea);
76+
const lineHeight = parseInt(computedStyle.lineHeight);
77+
let height = lineHeight;
78+
let minHeight = lineHeight;
79+
80+
if (calcRef.current?.offsetWidth >= firstLineMaxWidth - 32) {
81+
minHeight = lineHeight * 2;
82+
height = Math.min(
83+
Math.max(minHeight, textarea.scrollHeight),
84+
MAX_HEIGHT
85+
);
86+
}
87+
88+
textarea.style.height = `${height}px`;
89+
textarea.style.minHeight = `${minHeight}px`;
90+
91+
onLineCountChange?.(height / lineHeight);
92+
}, [input, firstLineMaxWidth]);
93+
13894
const handleChange = useCallback(
139-
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
140-
setInput(e.target.value);
95+
(event: ChangeEvent<HTMLTextAreaElement>) => {
96+
setInput(event.currentTarget.value);
14197
},
14298
[setInput]
14399
);
144100

145101
return (
146-
<textarea
147-
ref={textareaRef}
148-
id={isChatMode ? "chat-textarea" : "search-textarea"}
149-
autoFocus
150-
autoComplete="off"
151-
autoCapitalize="none"
152-
spellCheck="false"
153-
className="text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar"
154-
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
155-
aria-label={t("search.textarea.ariaLabel")}
156-
value={input}
157-
onChange={handleChange}
158-
onKeyDown={handleKeyPress}
159-
onCompositionStart={setTrue}
160-
onCompositionEnd={() => {
161-
setTimeout(setFalse, 0);
162-
}}
163-
rows={1}
164-
style={{
165-
resize: "none", // Prevent manual resize
166-
overflow: "auto",
167-
minHeight: "1.5rem",
168-
maxHeight: "13.5rem", // Limit height to 9 rows (9 * 1.5 line-height)
169-
lineHeight: "1.5rem", // Line height to match row height
170-
}}
171-
/>
102+
<>
103+
<textarea
104+
ref={textareaRef}
105+
id={isChatMode ? "chat-textarea" : "search-textarea"}
106+
autoFocus
107+
autoComplete="off"
108+
autoCapitalize="none"
109+
spellCheck="false"
110+
className="text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto"
111+
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
112+
aria-label={t("search.textarea.ariaLabel")}
113+
value={input}
114+
onChange={handleChange}
115+
onKeyDown={handleKeyPress}
116+
onCompositionStart={setTrue}
117+
onCompositionEnd={() => {
118+
setTimeout(setFalse, 0);
119+
}}
120+
rows={1}
121+
/>
122+
123+
<div ref={calcRef} className="absolute whitespace-nowrap -z-10">
124+
{input}
125+
</div>
126+
</>
172127
);
173128
}
174129
);

0 commit comments

Comments
 (0)