Skip to content

Commit fc04d13

Browse files
authored
fix: prevent textarea selection reset upon text insertion (#2814)
1 parent cf9c418 commit fc04d13

File tree

3 files changed

+23
-30
lines changed

3 files changed

+23
-30
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
"emoji-mart": "^5.4.0",
143143
"react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0",
144144
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0",
145-
"stream-chat": "^9.17.0"
145+
"stream-chat": "^9.19.0"
146146
},
147147
"peerDependenciesMeta": {
148148
"@breezystack/lamejs": {
@@ -236,7 +236,7 @@
236236
"react": "^19.0.0",
237237
"react-dom": "^19.0.0",
238238
"semantic-release": "^24.2.3",
239-
"stream-chat": "^9.17.0",
239+
"stream-chat": "^9.19.0",
240240
"ts-jest": "^29.2.5",
241241
"typescript": "^5.4.5",
242242
"typescript-eslint": "^8.17.0"

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 && (

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12042,10 +12042,10 @@ [email protected]:
1204212042
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
1204312043
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
1204412044

12045-
stream-chat@^9.17.0:
12046-
version "9.17.0"
12047-
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.17.0.tgz#540cf1ea03b08a394d6140696aae8528e9ba9ce2"
12048-
integrity sha512-ys6K73wIVWs5+qsfPJ9wumEUtgbMXYVbH1dhmAZ1oYtQ01dY/avsvt25PYDakVjKeyrnT+y8T/xEzfeF/WDJsg==
12045+
stream-chat@^9.19.0:
12046+
version "9.19.0"
12047+
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.19.0.tgz#8a2055be0f7c073ee8ca10cbc40af7d36648c476"
12048+
integrity sha512-ooRLubHPWxVr8Ws3fZvR30BFhVNM1xcrEgRnGGBxNINYYH/Wq+uc6AWONYIeu+n8crwRp3NSrtNfGWpWhLSy7Q==
1204912049
dependencies:
1205012050
"@types/jsonwebtoken" "^9.0.8"
1205112051
"@types/ws" "^8.5.14"

0 commit comments

Comments
 (0)