@@ -1030,7 +1030,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
10301030 ]
10311031 )
10321032
1033- /** Last observed selection; tells which edge of a range moved, and which way. */
1033+ /** Last selection reported by the DOM ; tells which edge of a range moved, and which way. */
10341034 const lastSelectionRef = useRef < { start : number ; end : number } > ( { start : 0 , end : 0 } )
10351035
10361036 /**
@@ -1045,55 +1045,53 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
10451045 const start = textarea . selectionStart ?? 0
10461046 const end = textarea . selectionEnd ?? 0
10471047 const prev = lastSelectionRef . current
1048+ // Always track the raw observed selection — never an intended write that
1049+ // may get superseded — so edge-movement inference stays true to the DOM.
1050+ lastSelectionRef . current = { start, end }
10481051
1049- // Deferred so in-flight click/drag processing can't override the write;
1050- // bails if the selection moved again first (a newer event supersedes it).
1051- const applySelection = ( nextStart : number , nextEnd : number ) => {
1052- const direction = textarea . selectionDirection ?? undefined
1053- setTimeout ( ( ) => {
1054- if ( textarea . selectionStart !== start || textarea . selectionEnd !== end ) return
1055- textarea . setSelectionRange ( nextStart , nextEnd , direction )
1056- } , 0 )
1057- }
1058-
1052+ let newStart = start
1053+ let newEnd = end
10591054 if ( start !== end ) {
10601055 const startRange = mentionTokensWithContext . findRangeContaining ( start )
10611056 const endRange = mentionTokensWithContext . findRangeContaining ( end )
10621057 // A lone moved edge (keyboard extend/shrink, drag) snaps in its direction
10631058 // of travel: growing absorbs the chip, shrinking releases it. Fresh
10641059 // selections (double-click, select-all) expand outward.
10651060 const singleEdgeMoved = ( start !== prev . start ) !== ( end !== prev . end )
1066- let newStart = startRange
1061+ newStart = startRange
10671062 ? singleEdgeMoved && start > prev . start
10681063 ? startRange . end
10691064 : startRange . start
10701065 : start
1071- const newEnd = endRange
1072- ? singleEdgeMoved && end < prev . end
1073- ? endRange . start
1074- : endRange . end
1075- : end
1066+ newEnd = endRange ? ( singleEdgeMoved && end < prev . end ? endRange . start : endRange . end ) : end
10761067 // A selection contained in one chip snaps both edges; don't let it invert.
10771068 if ( newStart > newEnd ) {
10781069 newStart = newEnd
10791070 }
1080- lastSelectionRef . current = { start : newStart , end : newEnd }
1081- if ( newStart !== start || newEnd !== end ) {
1082- applySelection ( newStart , newEnd )
1071+ } else {
1072+ const r = mentionTokensWithContext . findRangeContaining ( start )
1073+ if ( r ) {
1074+ const snapPos = start - r . start < r . end - start ? r . start : r . end
1075+ newStart = snapPos
1076+ newEnd = snapPos
10831077 }
1084- return
10851078 }
10861079
1087- const r = mentionTokensWithContext . findRangeContaining ( start )
1088- if ( r ) {
1089- const snapPos = start - r . start < r . end - start ? r . start : r . end
1090- lastSelectionRef . current = { start : snapPos , end : snapPos }
1091- applySelection ( snapPos , snapPos )
1080+ if ( newStart !== start || newEnd !== end ) {
1081+ // Deferred so in-flight click/drag processing can't override the write;
1082+ // bails if the selection moved again first (a newer event supersedes it).
1083+ // The write re-fires this handler, which then syncs the menus below.
1084+ const direction = textarea . selectionDirection ?? undefined
1085+ setTimeout ( ( ) => {
1086+ if ( textarea . selectionStart !== start || textarea . selectionEnd !== end ) return
1087+ textarea . setSelectionRange ( newStart , newEnd , direction )
1088+ } , 0 )
10921089 return
10931090 }
1094- lastSelectionRef . current = { start, end }
1095- syncMentionState ( textarea , textarea . value , start )
1096- syncSlashState ( textarea , textarea . value , start )
1091+
1092+ const focusPos = textarea . selectionDirection === 'backward' ? start : end
1093+ syncMentionState ( textarea , textarea . value , focusPos )
1094+ syncSlashState ( textarea , textarea . value , focusPos )
10971095 } , [ textareaRef , mentionTokensWithContext , syncMentionState , syncSlashState ] )
10981096
10991097 const handleInput = useCallback (
0 commit comments