Skip to content

Commit 4ddaaa9

Browse files
committed
fix: prevent textarea selection reset upon text insertion
1 parent cf9c418 commit 4ddaaa9

File tree

1 file changed

+17
-24
lines changed

1 file changed

+17
-24
lines changed

src/components/TextareaComposer/TextareaComposer.tsx

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
TextareaHTMLAttributes,
66
UIEventHandler,
77
} from 'react';
8-
import React, { useCallback, useEffect, useRef, useState } from 'react';
8+
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
99
import Textarea from 'react-textarea-autosize';
1010
import { useMessageComposer } from '../MessageInput';
1111
import type {
@@ -200,7 +200,6 @@ export const TextareaComposer = ({
200200
event.preventDefault();
201201
}
202202
handleSubmit();
203-
textareaRef.current.selectionEnd = 0;
204203
}
205204
},
206205
[
@@ -225,7 +224,7 @@ export const TextareaComposer = ({
225224
[onScroll, textComposer],
226225
);
227226

228-
const setSelectionDebounced = useCallback(
227+
const setSelection = useCallback(
229228
(e: SyntheticEvent<HTMLTextAreaElement>) => {
230229
onSelect?.(e);
231230
textComposer.setSelection({
@@ -236,17 +235,6 @@ export const TextareaComposer = ({
236235
[onSelect, textComposer],
237236
);
238237

239-
useEffect(() => {
240-
// FIXME: find the real reason for cursor being set to the end on each change
241-
// This is a workaround to prevent the cursor from jumping
242-
// to the end of the textarea when the user is typing
243-
// at the position that is not at the end of the textarea value.
244-
if (textareaRef.current && !isComposing) {
245-
textareaRef.current.selectionStart = selection.start;
246-
textareaRef.current.selectionEnd = selection.end;
247-
}
248-
}, [text, textareaRef, selection.start, selection.end, isComposing]);
249-
250238
useEffect(() => {
251239
if (textComposer.suggestions) {
252240
setFocusedItemIndex(0);
@@ -259,18 +247,22 @@ export const TextareaComposer = ({
259247
textareaRef.current.focus();
260248
}, [attachments, focus, quotedMessage, textareaRef]);
261249

262-
useEffect(() => {
250+
useLayoutEffect(() => {
263251
/**
264-
* The textarea value has to be overridden outside the render cycle so that the events like compositionend can be triggered.
265-
* If we have overridden the value during the component rendering, the compositionend event would not be triggered, and
266-
* it would not be possible to type composed characters (ô).
267-
* On the other hand, just removing the value override via prop (value={text}) would not allow us to change the text based on
268-
* middleware results (e.g. replace characters with emojis)
252+
* It is important to perform set text and after that the range
253+
* to prevent cursor reset to the end of the textarea if doing it in separate effects.
269254
*/
270255
const textarea = textareaRef.current;
271-
if (!textarea) return;
272-
textarea.value = text;
273-
}, [textareaRef, text]);
256+
if (!textarea || isComposing) return;
257+
258+
const length = textarea.value.length;
259+
const start = Math.max(0, Math.min(selection.start, length));
260+
const end = Math.max(start, Math.min(selection.end, length));
261+
262+
if (textarea.selectionStart === start && textarea.selectionEnd === end) return;
263+
264+
textarea.setSelectionRange(start, end, 'forward');
265+
}, [text, selection.start, selection.end, isComposing, textareaRef]);
274266

275267
return (
276268
<div
@@ -303,11 +295,12 @@ export const TextareaComposer = ({
303295
onKeyDown={keyDownHandler}
304296
onPaste={onPaste}
305297
onScroll={scrollHandler}
306-
onSelect={setSelectionDebounced}
298+
onSelect={setSelection}
307299
placeholder={placeholder || t('Type your message')}
308300
ref={(ref) => {
309301
textareaRef.current = ref;
310302
}}
303+
value={text}
311304
/>
312305
{/* todo: X document the layout change for the accessibility purpose (tabIndex) */}
313306
{!isComposing && (

0 commit comments

Comments
 (0)