diff --git a/native/components/selectable-text-input.react.js b/native/components/selectable-text-input.react.js --- a/native/components/selectable-text-input.react.js +++ b/native/components/selectable-text-input.react.js @@ -2,6 +2,7 @@ import * as React from 'react'; +import type { SelectionChangeEvent } from '../types/react-native'; import ClearableTextInput from './clearable-text-input.react'; import type { SelectableTextInputProps, @@ -12,7 +13,20 @@ props, ref, ): React.Node { - const { clearableTextInputRef, onUpdateSyncedSelectionData, ...rest } = props; + const { + clearableTextInputRef, + onUpdateSyncedSelectionData, + onSelectionChange, + selection, + ...rest + } = props; + + // React Native doesn't handle controlled selection well, so we only set the + // selection prop when we need to mutate the selection + // https://github.com/facebook/react-native/issues/29063 + const [controlSelection, setControlSelection] = React.useState( + false, + ); const clearableTextInputRefCallback = React.useCallback( (clearableTextInput: ?React.ElementRef) => { @@ -21,17 +35,45 @@ [clearableTextInputRef], ); - const prepareForSelectionMutation = React.useCallback(() => {}, []); + const prepareForSelectionMutation = React.useCallback( + () => setControlSelection(true), + [], + ); const ourRef = React.useMemo( () => ({ prepareForSelectionMutation, }), [prepareForSelectionMutation], ); - React.useImperativeHandle(ref, () => ourRef, [ourRef]); - return ; + // - It's important for us to keep text and selection state in sync, since + // upstream code in ChatInputBar processes this data during render to + // generate a list of @-mention suggestions + // - On Android, text change events precede selection events, and each leads + // to a separate React render cycle + // - To prevent render cycles where the data isn't in sync, we defer text + // change events until the corresponding selection event comes in + const onSelectionChangeOverride = React.useCallback( + (event: SelectionChangeEvent) => { + setControlSelection(false); + onSelectionChange?.(event); + onUpdateSyncedSelectionData({ + text: props.value, + selection: event.nativeEvent.selection, + }); + }, + [onUpdateSyncedSelectionData, props.value, onSelectionChange], + ); + + return ( + + ); }); const MemoizedSelectableTextInput: React.AbstractComponent<