From 9914a9be155fcef918779bad2d43bfbcff490433 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Wed, 26 Jul 2023 18:34:22 +0300 Subject: [PATCH 1/4] Issue#11068 [Android] Fix issue of duplicating characters when replacing letters to lowercase or uppercase in TextInput --- .../react/views/textinput/ReactEditText.java | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index ecc6165f7244bf..b05f996b622f02 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -676,6 +676,8 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { // try to update state if the wrapper is available. Temporarily disable // to prevent an infinite loop. getText().replace(0, length(), spannableStringBuilder); + + attachCompositeSpansToTextFrom(spannableStringBuilder); } mDisableTextDiffing = false; @@ -688,18 +690,24 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { } /** - * Remove and/or add {@link Spanned.SPAN_EXCLUSIVE_EXCLUSIVE} spans, since they should only exist - * as long as the text they cover is the same. All other spans will remain the same, since they - * will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes + * Remove and/or add {@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE} spans, since they should only exist + * as long as the text they cover is the same unless they are {@link Spanned#SPAN_COMPOSING}. + * All other spans will remain the same, since they will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes * them. + * Keep copy of {@link Spanned#SPAN_COMPOSING} Spans in {@param spannableStringBuilder}, because they are important for + * keyboard suggestions. Without keeping these Spans, suggestions default to be put after the current selection position, + * possibly resulting in letter duplication (ex. Samsung Keyboard). */ private void manageSpans(SpannableStringBuilder spannableStringBuilder) { Object[] spans = getText().getSpans(0, length(), Object.class); + boolean shouldKeepComposingSpans = length() == spannableStringBuilder.length(); for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) { Object span = spans[spanIdx]; int spanFlags = getText().getSpanFlags(span); boolean isExclusiveExclusive = (spanFlags & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; + boolean isComposing = + (spanFlags & Spanned.SPAN_COMPOSING) == Spanned.SPAN_COMPOSING; // Remove all styling spans we might have previously set if (span instanceof ReactSpan) { @@ -714,6 +722,12 @@ private void manageSpans(SpannableStringBuilder spannableStringBuilder) { final int spanStart = getText().getSpanStart(span); final int spanEnd = getText().getSpanEnd(span); + // We keep a copy of Composing spans + if (shouldKeepComposingSpans && isComposing) { + spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags); + continue; + } + // Make sure the span is removed from existing text, otherwise the spans we set will be // ignored or it will cover text that has changed. getText().removeSpan(span); @@ -841,6 +855,34 @@ private void addSpansFromStyleAttributes(SpannableStringBuilder workingText) { } } + /** + * Attaches the {@link Spanned#SPAN_COMPOSING} from {@param spannableStringBuilder} to {@link ReactEditText#getText} + * if they are the same length. + * + * See {@link ReactEditText#manageSpans} for more details. + * Also https://github.com/facebook/react-native/issues/11068 + */ + private void attachCompositeSpansToTextFrom(SpannableStringBuilder spannableStringBuilder) { + Editable text = getText(); + if (text == null || text.length() != spannableStringBuilder.length()) { + return; + } + Object[] spans = spannableStringBuilder.getSpans(0, length(), Object.class); + for (Object span : spans) { + int spanFlags = spannableStringBuilder.getSpanFlags(span); + boolean isComposing = (spanFlags & Spanned.SPAN_COMPOSING) == Spanned.SPAN_COMPOSING; + + if (!isComposing) { + continue; + } + + final int spanStart = spannableStringBuilder.getSpanStart(span); + final int spanEnd = spannableStringBuilder.getSpanEnd(span); + + text.setSpan(span, spanStart, spanEnd, spanFlags); + } + } + private static boolean sameTextForSpan( final Editable oldText, final SpannableStringBuilder newText, From 7aeb500ebdc1507e8d3d22a4a5201b8a1782f286 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jul 2023 10:20:47 +0300 Subject: [PATCH 2/4] Issue#11068 [Android] Add TextInput Example for LowerCase rewrite --- .../TextInput/TextInputSharedExamples.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js index c05948b0639800..6f3f8aed7c3d98 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js @@ -135,6 +135,30 @@ class RewriteExample extends React.Component<$FlowFixMeProps, any> { } } +class RewriteToLowerCaseExample extends React.Component<$FlowFixMeProps, any> { + constructor(props: any | void) { + super(props); + this.state = {text: ''}; + } + render(): React.Node { + return ( + + { + text = text.toLowerCase(); + this.setState({text}); + }} + style={styles.default} + value={this.state.text} + /> + + ); + } +} + class RewriteExampleInvalidCharacters extends React.Component< $FlowFixMeProps, any, @@ -854,6 +878,13 @@ module.exports = ([ return ; }, }, + { + name: 'lowerCase', + title: 'Live Re-Write to LowerCase', + render: function (): React.Node { + return ; + }, + }, { title: 'Live Re-Write (no spaces allowed)', render: function (): React.Node { From 904a018e7afd0143a941afedf57a5e6c0bed4436 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Fri, 4 Aug 2023 12:58:17 +0300 Subject: [PATCH 3/4] Issue#11068 PR#38649 [Android] Feature Flag the keeping of Composing Spans --- .../react/config/ReactFeatureFlags.java | 3 ++ .../react/views/textinput/ReactEditText.java | 32 ++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 242cdf5661a516..e1de280a776add 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -166,6 +166,9 @@ public class ReactFeatureFlags { /** Enables Stable API for TurboModule (removal of ReactModule, ReactModuleInfoProvider). */ public static boolean enableTurboModuleStableAPI = false; + /** Enable keeping Composing Spans on Text input change if the new text has the same length. */ + public static boolean enableComposingSpanRestorationOnSameLength = false; + /** * When enabled, it uses the modern fork of RuntimeScheduler that allows scheduling tasks with * priorities from any thread. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index b05f996b622f02..3b36facbcc66bd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -8,6 +8,7 @@ package com.facebook.react.views.textinput; import static com.facebook.react.uimanager.UIManagerHelper.getReactContext; +import static com.facebook.react.config.ReactFeatureFlags.enableComposingSpanRestorationOnSameLength; import android.content.Context; import android.graphics.Color; @@ -47,6 +48,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.StateWrapper; import com.facebook.react.uimanager.UIManagerModule; @@ -694,20 +696,20 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { * as long as the text they cover is the same unless they are {@link Spanned#SPAN_COMPOSING}. * All other spans will remain the same, since they will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes * them. - * Keep copy of {@link Spanned#SPAN_COMPOSING} Spans in {@param spannableStringBuilder}, because they are important for + * When {@link ReactFeatureFlags#enableComposingSpanRestorationOnSameLength} is enabled, + * keep copy of {@link Spanned#SPAN_COMPOSING} Spans in {@param spannableStringBuilder}, because they are important for * keyboard suggestions. Without keeping these Spans, suggestions default to be put after the current selection position, * possibly resulting in letter duplication (ex. Samsung Keyboard). */ private void manageSpans(SpannableStringBuilder spannableStringBuilder) { Object[] spans = getText().getSpans(0, length(), Object.class); - boolean shouldKeepComposingSpans = length() == spannableStringBuilder.length(); + boolean shouldKeepComposingSpans = enableComposingSpanRestorationOnSameLength + && length() == spannableStringBuilder.length(); for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) { Object span = spans[spanIdx]; int spanFlags = getText().getSpanFlags(span); boolean isExclusiveExclusive = (spanFlags & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; - boolean isComposing = - (spanFlags & Spanned.SPAN_COMPOSING) == Spanned.SPAN_COMPOSING; // Remove all styling spans we might have previously set if (span instanceof ReactSpan) { @@ -722,10 +724,13 @@ private void manageSpans(SpannableStringBuilder spannableStringBuilder) { final int spanStart = getText().getSpanStart(span); final int spanEnd = getText().getSpanEnd(span); - // We keep a copy of Composing spans - if (shouldKeepComposingSpans && isComposing) { - spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags); - continue; + if (shouldKeepComposingSpans) { + // We keep a copy of Composing spans + boolean isComposing = (spanFlags & Spanned.SPAN_COMPOSING) == Spanned.SPAN_COMPOSING; + if (isComposing) { + spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags); + continue; + } } // Make sure the span is removed from existing text, otherwise the spans we set will be @@ -856,13 +861,18 @@ private void addSpansFromStyleAttributes(SpannableStringBuilder workingText) { } /** - * Attaches the {@link Spanned#SPAN_COMPOSING} from {@param spannableStringBuilder} to {@link ReactEditText#getText} - * if they are the same length. + * When {@link ReactFeatureFlags#enableComposingSpanRestorationOnSameLength} is enabled, this + * function attaches the {@link Spanned#SPAN_COMPOSING} from {@param spannableStringBuilder} to + * {@link ReactEditText#getText} if they are the same length. * * See {@link ReactEditText#manageSpans} for more details. - * Also https://github.com/facebook/react-native/issues/11068 + * Also this GitHub issue */ private void attachCompositeSpansToTextFrom(SpannableStringBuilder spannableStringBuilder) { + if (!enableComposingSpanRestorationOnSameLength) { + return; + } + Editable text = getText(); if (text == null || text.length() != spannableStringBuilder.length()) { return; From 2114952e4532f6b92da092dcf819245969128531 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Fri, 4 Aug 2023 13:23:05 +0300 Subject: [PATCH 4/4] Issue#11068 PR#38649 [Android] Enable enableComposingSpanRestorationOnSameLength by default --- .../main/java/com/facebook/react/config/ReactFeatureFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index e1de280a776add..beb5bbd371cafa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -167,7 +167,7 @@ public class ReactFeatureFlags { public static boolean enableTurboModuleStableAPI = false; /** Enable keeping Composing Spans on Text input change if the new text has the same length. */ - public static boolean enableComposingSpanRestorationOnSameLength = false; + public static boolean enableComposingSpanRestorationOnSameLength = true; /** * When enabled, it uses the modern fork of RuntimeScheduler that allows scheduling tasks with