From 8a7ce5582f051384791cf0058b175d84dbcd488b Mon Sep 17 00:00:00 2001 From: Alan Lee Date: Mon, 29 Dec 2025 15:13:46 -0800 Subject: [PATCH] Add selection to TextInput onChange event Summary: This change adds `selection` data to the `TextInput.onChange` event for both iOS and Android platforms. ## Why On the web, text input elements provide `selectionStart` and `selectionEnd` properties that are always accessible during input events. This is a W3C DOM standard that enables common use cases like: - mention and tagging systems (insert at cursor position) - Autocomplete/typeahead (replace text at cursor) - Input masking (format text while maintaining cursor position) - Rich text editing features React Native's `onChange` event previously included `selection` on iOS (Fabric), but this was removed in D76253999 to unify with Android (which never had it). This change restores and extends this capability to both platforms, better aligning React Native with web standards. ## What Changed 1. **Flow Types**: Added optional `selection?: Selection` to `TextInputChangeEventData` 2. **iOS/macOS (C++)**: Enable selection in `onChange` event via `TextInputEventEmitter` 3. **Android (Kotlin)**: Added selection data to `ReactTextChangedEvent` 4. **RNTester**: Updated examples to verify the change ## Changelog [General][Added] - TextInput onChange event now includes selection data on iOS and Android Differential Revision: D89898092 --- .../Libraries/Components/TextInput/TextInput.flow.js | 1 + .../react/views/textinput/ReactTextChangedEvent.kt | 8 ++++++++ .../react/views/textinput/ReactTextInputTextWatcher.kt | 2 ++ .../components/textinput/TextInputEventEmitter.cpp | 2 +- packages/react-native/ReactNativeApi.d.ts | 9 +++++---- .../js/examples/TextInput/TextInputSharedExamples.js | 9 ++++++++- 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index 8438a0eac10..5b3d3dbdf7f 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -29,6 +29,7 @@ type TextInputChangeEventData = $ReadOnly<{ eventCount: number, target: number, text: string, + selection?: Selection, }>; export type TextInputChangeEvent = diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.kt index b4e4a7caad8..f4c0daabb00 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.kt @@ -17,6 +17,8 @@ internal class ReactTextChangedEvent( viewId: Int, private val text: String, private val eventCount: Int, + private val selectionStart: Int, + private val selectionEnd: Int, ) : Event(surfaceId, viewId) { override fun getEventName(): String = EVENT_NAME @@ -25,6 +27,12 @@ internal class ReactTextChangedEvent( putString("text", text) putInt("eventCount", eventCount) putInt("target", viewTag) + val selectionData = + Arguments.createMap().apply { + putInt("start", selectionStart) + putInt("end", selectionEnd) + } + putMap("selection", selectionData) } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputTextWatcher.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputTextWatcher.kt index 887251ccbbc..e6e13a00955 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputTextWatcher.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputTextWatcher.kt @@ -63,6 +63,8 @@ internal class ReactTextInputTextWatcher( editText.id, s.toString(), editText.incrementAndGetEventCounter(), + editText.selectionStart, + editText.selectionEnd, ) ) } diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp index a9bc219f8bf..909c4521bc9 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp @@ -140,7 +140,7 @@ void TextInputEventEmitter::onBlur(const Metrics& textInputMetrics) const { } void TextInputEventEmitter::onChange(const Metrics& textInputMetrics) const { - dispatchTextInputEvent("change", textInputMetrics); + dispatchTextInputEvent("change", textInputMetrics, true); } void TextInputEventEmitter::onContentSizeChange( diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 1663252c3ed..cca5cb350ae 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<249ca589a73aaef41a965bcf3a2c0208>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -5277,6 +5277,7 @@ declare type TextInputChangeEvent = NativeSyntheticEvent declare type TextInputChangeEventData = { readonly eventCount: number + readonly selection?: Selection readonly target: number readonly text: string } @@ -6189,15 +6190,15 @@ export { TaskProvider, // 266dedf2 Text, // e55ac2e2 TextContentType, // 239b3ecc - TextInput, // cf7a3331 + TextInput, // 2e89b91d TextInputAndroidProps, // 3f09ce49 - TextInputChangeEvent, // 380cbe93 + TextInputChangeEvent, // 6821f629 TextInputContentSizeChangeEvent, // 5fba3f54 TextInputEndEditingEvent, // 8c22fac3 TextInputFocusEvent, // c36e977c TextInputIOSProps, // 0d05a855 TextInputKeyPressEvent, // 967178c2 - TextInputProps, // a817a7f7 + TextInputProps, // c75f0362 TextInputSelectionChangeEvent, // a1a7622f TextInputSubmitEditingEvent, // 48d903af TextLayoutEvent, // 45b0a8d7 diff --git a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js index 2de5275b9dd..2e789412f98 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js @@ -393,7 +393,14 @@ class TextEventsExample extends React.Component<{...}, $FlowFixMe> { onFocus={() => this.updateText('onFocus')} onBlur={() => this.updateText('onBlur')} onChange={event => - this.updateText('onChange text: ' + event.nativeEvent.text) + this.updateText( + 'onChange text: ' + + event.nativeEvent.text + + ', selection: ' + + (event.nativeEvent.selection != null + ? JSON.stringify(event.nativeEvent.selection) + : 'undefined'), + ) } onContentSizeChange={event => this.updateText(