@@ -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' ;
99import Textarea from 'react-textarea-autosize' ;
1010import { useMessageComposer } from '../MessageInput' ;
1111import 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