From 5ad4fe8ea8f4f90a4a19f808d16397fdd84b958b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 2 Jul 2024 13:10:51 +0200 Subject: [PATCH 1/5] feat: include information about text range changes in onChange --- .../Components/TextInput/TextInput.d.ts | 3 +++ .../TextInput/RCTTextInputComponentView.mm | 14 ++++++++++++++ .../views/textinput/ReactTextChangedEvent.java | 16 +++++++++++++--- .../views/textinput/ReactTextInputManager.java | 7 ++++++- .../iostextinput/TextInputEventEmitter.cpp | 3 +++ .../iostextinput/TextInputEventEmitter.h | 3 +++ 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts index 0aa50fe80fc277..94ca9f1aafa9e5 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -462,6 +462,9 @@ export interface TextInputKeyPressEventData { export interface TextInputChangeEventData extends TargetedEvent { eventCount: number; text: string; + before: number; + start: number; + count: number; } /** diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 51914b7448e837..0c29d84c0bd3f0 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -57,6 +57,13 @@ @implementation RCTTextInputComponentView { */ BOOL _comingFromJS; BOOL _didMoveToWindow; + + /** + * Keep track of the range of the text that is being changed. + */ + NSInteger _changeStart; + NSInteger _changeBefore; + NSInteger _changeCount; } #pragma mark - UIView overrides @@ -328,6 +335,10 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range { const auto &props = static_cast(*_props); + _changeStart = range.location; + _changeBefore = range.length; + _changeCount = text.length; + if (!_backedTextInputView.textWasPasted) { if (_eventEmitter) { const auto &textInputEventEmitter = static_cast(*_eventEmitter); @@ -576,6 +587,9 @@ - (void)handleInputAccessoryDoneButton .text = RCTStringFromNSString(_backedTextInputView.attributedText.string), .selectionRange = [self _selectionRange], .eventCount = static_cast(_mostRecentEventCount), + .start = static_cast(_changeStart), + .count = static_cast(_changeCount), + .before = static_cast(_changeBefore), .contentOffset = RCTPointFromCGPoint(_backedTextInputView.contentOffset), .contentInset = RCTEdgeInsetsFromUIEdgeInsets(_backedTextInputView.contentInset), .contentSize = RCTSizeFromCGSize(_backedTextInputView.contentSize), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java index 4540b90abc2b4b..c44a11f1b56887 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java @@ -23,16 +23,23 @@ public class ReactTextChangedEvent extends Event { private String mText; private int mEventCount; + // See https://developer.android.com/reference/android/text/TextWatcher#onTextChanged(java.lang.CharSequence,%20int,%20int,%20int) + private int mStart; + private int mCount; + private int mBefore; @Deprecated - public ReactTextChangedEvent(int viewId, String text, int eventCount) { - this(ViewUtil.NO_SURFACE_ID, viewId, text, eventCount); + public ReactTextChangedEvent(int viewId, String text, int eventCount, int start, int count, int before) { + this(ViewUtil.NO_SURFACE_ID, viewId, text, eventCount, start, count, before); } - public ReactTextChangedEvent(int surfaceId, int viewId, String text, int eventCount) { + public ReactTextChangedEvent(int surfaceId, int viewId, String text, int eventCount, int start, int count, int before) { super(surfaceId, viewId); mText = text; mEventCount = eventCount; + mStart = start; + mCount = count; + mBefore = before; } @Override @@ -47,6 +54,9 @@ protected WritableMap getEventData() { eventData.putString("text", mText); eventData.putInt("eventCount", mEventCount); eventData.putInt("target", getViewTag()); + eventData.putInt("start", mStart); + eventData.putInt("count", mCount); + eventData.putInt("before", mBefore); return eventData; } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 983a0ae0afd0b3..6929f843e9c5db 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -1118,7 +1118,12 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { mSurfaceId, mEditText.getId(), s.toString(), - mEditText.incrementAndGetEventCounter())); + mEditText.incrementAndGetEventCounter(), + start, + count, + before, + ) + ); } @Override diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputEventEmitter.cpp index c2262b0326793c..cd02237b3dab1b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputEventEmitter.cpp @@ -31,6 +31,9 @@ static jsi::Value textInputMetricsPayload( textInputMetrics.selectionRange.location + textInputMetrics.selectionRange.length); payload.setProperty(runtime, "selection", selection); + payload.setProperty(runtime, "start", textInputMetrics.start); + payload.setProperty(runtime, "count", textInputMetrics.count); + payload.setProperty(runtime, "before", textInputMetrics.before); } return payload; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputEventEmitter.h index 9182dd3d2edccb..c6e7b8e2bb2703 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputEventEmitter.h @@ -19,6 +19,9 @@ class TextInputEventEmitter : public ViewEventEmitter { struct Metrics { std::string text; AttributedString::Range selectionRange; + int count; + int start; + int before; // ScrollView-like metrics Size contentSize; Point contentOffset; From c4d09be085a66bec5317d00c9867143d6346a13c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 2 Jul 2024 13:33:34 +0200 Subject: [PATCH 2/5] removed erroneous comma --- .../facebook/react/views/textinput/ReactTextInputManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 6929f843e9c5db..394c7069c088f3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -1121,7 +1121,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { mEditText.incrementAndGetEventCounter(), start, count, - before, + before ) ); } From d5ddbe8c2f3d6128d613e38303041bd997063db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 2 Jul 2024 15:34:05 +0200 Subject: [PATCH 3/5] add example code --- .../TextInput/TextInputSharedExamples.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js index 5309967ea5e3d3..a8dcbaf24eabcb 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js @@ -813,6 +813,32 @@ function MultilineStyledTextInput({ ); } +function PartialUpdatesTextInput() { + const [value, setValue] = useState(''); + + const onChange = ({nativeEvent}) => { + console.log('onChange', nativeEvent); + setValue((previousValue) => { + const {count, start, before, text: fullNewText} = nativeEvent; + // This method is called to notify you that, within fullNewText, the "count" characters beginning at "start" have just + // replaced old text that had length "before". + const newText = fullNewText.substring(start, start + count); + // Replace newText in the original text: + const updatedText = previousValue.substring(0, start) + newText + previousValue.substring(start + before); + + return updatedText; + }); + }; + + return ( + + ); +} + module.exports = ([ { title: 'Auto-focus', @@ -1121,4 +1147,13 @@ module.exports = ([ ); }, }, + { + title: 'Text input with partial updates in onChange', + name: 'partialUpdates', + render: function (): React.Node { + return ( + + ); + }, + }, ]: Array); From 5bcdda84e490f4355a2c801cb118c18edf43a7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 2 Jul 2024 16:14:00 +0200 Subject: [PATCH 4/5] add flow types --- .../TextInput/AndroidTextInputNativeComponent.js | 9 ++++++++- .../Components/TextInput/TextInput.flow.js | 3 +++ .../Libraries/Components/TextInput/TextInput.js | 3 +++ .../__snapshots__/public-api-test.js.snap | 15 ++++++++++++++- .../examples/TextInput/TextInputSharedExamples.js | 3 ++- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js index a77e5b42f715af..e86997e7b399b7 100644 --- a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -359,7 +359,14 @@ export type NativeProps = $ReadOnly<{| * TODO: differentiate between onChange and onChangeText */ onChange?: ?BubblingEventHandler< - $ReadOnly<{|target: Int32, eventCount: Int32, text: string|}>, + $ReadOnly<{| + target: Int32, + eventCount: Int32, + text: string, + start: Int32, + count: Int32, + before: Int32, + |}>, >, /** diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index 638acd7c7925a2..71e90cec983b11 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -31,6 +31,9 @@ export type ChangeEvent = SyntheticEvent< eventCount: number, target: number, text: string, + start: number, + count: number, + before: number, |}>, >; diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 8b827ae1cc6969..28e0609c15589a 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -69,6 +69,9 @@ export type ChangeEvent = SyntheticEvent< eventCount: number, target: number, text: string, + start: number, + count: number, + before: number, |}>, >; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 2ea05e0ad302ed..b18bbfea3efc62 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -2270,7 +2270,14 @@ export type NativeProps = $ReadOnly<{| onBlur?: ?BubblingEventHandler<$ReadOnly<{| target: Int32 |}>>, onFocus?: ?BubblingEventHandler<$ReadOnly<{| target: Int32 |}>>, onChange?: ?BubblingEventHandler< - $ReadOnly<{| target: Int32, eventCount: Int32, text: string |}>, + $ReadOnly<{| + target: Int32, + eventCount: Int32, + text: string, + start: Int32, + count: Int32, + before: Int32, + |}>, >, onChangeText?: ?BubblingEventHandler< $ReadOnly<{| target: Int32, eventCount: Int32, text: string |}>, @@ -2423,6 +2430,9 @@ export type ChangeEvent = SyntheticEvent< eventCount: number, target: number, text: string, + start: number, + count: number, + before: number, |}>, >; export type TextInputEvent = SyntheticEvent< @@ -2768,6 +2778,9 @@ export type ChangeEvent = SyntheticEvent< eventCount: number, target: number, text: string, + start: number, + count: number, + before: number, |}>, >; export type TextInputEvent = SyntheticEvent< diff --git a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js index a8dcbaf24eabcb..35e4370096b256 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js @@ -11,6 +11,7 @@ 'use strict'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; +import type {ChangeEvent} from 'react-native/Libraries/Components/TextInput/TextInput'; import type {TextStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; import RNTesterButton from '../../components/RNTesterButton'; @@ -816,7 +817,7 @@ function MultilineStyledTextInput({ function PartialUpdatesTextInput() { const [value, setValue] = useState(''); - const onChange = ({nativeEvent}) => { + const onChange = ({nativeEvent}: ChangeEvent) => { console.log('onChange', nativeEvent); setValue((previousValue) => { const {count, start, before, text: fullNewText} = nativeEvent; From 6792f46ce8314412a6d9ef8976de61d97b8a3f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 2 Jul 2024 16:57:04 +0200 Subject: [PATCH 5/5] run prettier --- .../js/examples/TextInput/TextInputSharedExamples.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js index 35e4370096b256..fa143205f25b52 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js @@ -819,13 +819,16 @@ function PartialUpdatesTextInput() { const onChange = ({nativeEvent}: ChangeEvent) => { console.log('onChange', nativeEvent); - setValue((previousValue) => { + setValue(previousValue => { const {count, start, before, text: fullNewText} = nativeEvent; // This method is called to notify you that, within fullNewText, the "count" characters beginning at "start" have just // replaced old text that had length "before". const newText = fullNewText.substring(start, start + count); // Replace newText in the original text: - const updatedText = previousValue.substring(0, start) + newText + previousValue.substring(start + before); + const updatedText = + previousValue.substring(0, start) + + newText + + previousValue.substring(start + before); return updatedText; }); @@ -1152,9 +1155,7 @@ module.exports = ([ title: 'Text input with partial updates in onChange', name: 'partialUpdates', render: function (): React.Node { - return ( - - ); + return ; }, }, ]: Array);