From 1af8f10a85760983166e07826747e010cd85a4fd Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Tue, 27 Oct 2026 23:24:38 -0700 Subject: [PATCH 1/3] Add auto-expanding multiline TextInput example to RNTester (#59416) Summary: Add a new RNTester example in TextInputSharedExamples to reproduce iOS and Android issues with auto-expanding multiline text fields. There is already an existing example, but an empty, uncontrolled variant is needed to reproduce certain issues. An existing iOS-only auto-expanding example is removed as well since it duplicates functionality. Changelog: [Internal] Differential Revision: D127048669 --- .../examples/TextInput/TextInputExample.ios.js | 17 ----------------- .../TextInput/TextInputSharedExamples.js | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js index a6bcda4ec182..b768aff875ed 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -778,23 +778,6 @@ const textInputExamples: Array = [ ); }, }, - { - title: 'Auto-expanding', - render: function (): React.Node { - return ( - - - - ); - }, - }, { title: 'TextInput maxLength', render: function (): React.Node { diff --git a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js index 795ea66911ea..60df3a252499 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js @@ -1192,6 +1192,20 @@ module.exports = [ ); }, }, + { + title: 'Auto-expanding (uncontrolled)', + render: function (): React.Node { + return ( + + + + ); + }, + }, { title: 'Drag and drop', render: function (): React.Node { From 29039a720fcf52e055f056e44a0b2ad705f33fd8 Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Tue, 27 Oct 2026 23:24:38 -0700 Subject: [PATCH 2/3] Fix uncontrolled multiline TextInput not expanding on iOS (#59417) Summary: Fixes an issue where on iOS, multiline expanding TextInputs do not expand if the input starts off as empty and uncontrolled. ### Root Cause When a multiline TextInput on iOS is provided a `value` and `onChange()` prop, as shown in the [React Native docs](https://reactnative.dev/docs/textinput), the input grows as expected. However, uncontrolled inputs do not grow past the first initial line. On mount, `BaseTextInputShadowNode::updateStateIfNeeded()` writes state only if the text changes, so the first state write is skipped for empty, uncontrolled inputs. As a result, `TextAttributes` keeps the default `fontSizeMultiplier = NaN` which is carried over since iOS copies the previous state for subsequent keystrokes. During measurement, `attributedStringBoxToMeasure()` sees that the current multiplier doesn't match the stored multiplier of NaN and falls back to `props.text` which is empty for uncontrolled inputs. This causes the input to stay one-line tall. ### This PR Modify `BaseTextInputShadowNode::updateStateIfNeeded()` to update the state if base text attributes differ. On mount, the default font multiplier of NaN now differs from the real multiplier, so a state update occurs and the text field grows normally. Changelog: [iOS][Fixed] - Fix multiline TextInput not growing with its content when uncontrolled and initially empty Reviewed By: vzaidman Differential Revision: D127048714 --- .../renderer/components/textinput/BaseTextInputShadowNode.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h index 42cf11b0695f..fe0caaec6097 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h @@ -157,7 +157,9 @@ class BaseTextInputShadowNode const auto &stateData = BaseShadowNode::getStateData(); const auto &reactTreeAttributedString = getAttributedString(layoutContext); - if (stateData.reactTreeAttributedString.isContentEqual(reactTreeAttributedString)) { + if (stateData.reactTreeAttributedString.isContentEqual(reactTreeAttributedString) && + stateData.reactTreeAttributedString.getBaseTextAttributes() == + reactTreeAttributedString.getBaseTextAttributes()) { return; } From 735add058ffc813dbb752a9a37d09d589a8b43ec Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Tue, 27 Oct 2026 23:24:38 -0700 Subject: [PATCH 3/3] Fix content jump for growing multiline TextInputs on Android (#59419) Summary: Fixes the issue described in [#46813](https://github.com/react/react-native/issues/46813), where multiline TextInputs on Android experience a content jump when adding and removing lines that change the input's height. ### Root Cause ReactEditText hardcodes `isLayoutRequested() = false` which causes Android to immediately scroll the caret to the newly typed character instead of waiting until the next layout. When adding a new line to an auto-expanding input, Android scrolls to the new line before Fabric's asynchronous state update triggers a new layout. This manifests as a glitchy content jump since the current line briefly scrolls to the newly added line before being reset by the incoming layout. In React Native, text is centered vertically in TextInputs by default. When removing a line from an auto-expanding input, Android immediately recenters the text in the remaining space before Fabric's asynchronous state update occurs, causing another glitchy content jump since the current line briefly shifts from its current position due to recentering before being reset by the incoming layout. One workaround for this is to set the `textAlignVertical` CSS style in the opposite direction of the growth direction, such as `textAlignVertical: 'top'` for a text field that grows downwards. ### This PR Update `ReactTextInputTextWatcher` to trigger an `unstable_Immediate` synchronous state update when a multiline text field's line count changes. This ensures that a layout arrives in the next frame, avoiding the existing race issues where Android scrolls or recenters before the next layout. Gated behind `fixMultilineContentJumpAndroid`. ### Limitations This fix depends on synchronous state updates, which could have performance concerns. This is mitigated by limiting synchronous updates only to when the input expands or shrinks, with all other updates remaining async. One potential alternative is to use scroll deferral to fix the content jump when adding new lines. By conditionally setting `isLayoutRequested() = true` in ReactEditText, the content jump can be avoided by delaying the scroll in cases when we can guarantee a layout is pending. However, this means ReactEditText needs to predict when Fabric guarantees a layout, which means hardcoding assumptions about Fabric measurement that break if the measurement code changes. Alternatively, ReactEditText would need a working `requestLayout()`, but this goes against existing architectural patterns (see `ReactViewGroup.requestLayout()`) where UIManager is meant to handle laying out children. Even then, would only fix the content jump when adding new lines and not when removing lines. Changelog: [Android]Fixed] - Fix content jump for expanding multiline TextInputs on Android Differential Revision: D127048758 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 ++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../textinput/ReactTextInputTextWatcher.kt | 49 ++++++++-- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 92 +++++++++++-------- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 ++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 11 +++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 21 files changed, 214 insertions(+), 64 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 7fcdaaeec16d..1d585e43bf40 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -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<> */ /** @@ -396,6 +396,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = accessor.fixMappingOfEventPrioritiesBetweenFabricAndReact() + /** + * Flushes TextInput state updates synchronously on Android when a multiline input changes line count, so the new height mounts in the same frame instead of the content jumping. Pairs with enableFollowUpMountItemDispatchAndroid for updates that happen during mount dispatch. + */ + @JvmStatic + public fun fixMultilineContentJumpAndroid(): Boolean = accessor.fixMultilineContentJumpAndroid() + /** * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 6e0da4d9d0e4..70f50c066da5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -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<> */ /** @@ -81,6 +81,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null private var fixBorderlessRippleAndroidCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null + private var fixMultilineContentJumpAndroidCache: Boolean? = null private var fixTextLayoutOnPixelDensityChangeCache: Boolean? = null private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null @@ -667,6 +668,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun fixMultilineContentJumpAndroid(): Boolean { + var cached = fixMultilineContentJumpAndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.fixMultilineContentJumpAndroid() + fixMultilineContentJumpAndroidCache = cached + } + return cached + } + override fun fixTextLayoutOnPixelDensityChange(): Boolean { var cached = fixTextLayoutOnPixelDensityChangeCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 2268d03f3674..729752671bf2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -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<<6d86d4adbc261c9271b5bed72bd12dcf>> + * @generated SignedSource<<3462cafdf5b22443a12396abe0c84307>> */ /** @@ -150,6 +150,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean + @DoNotStrip @JvmStatic public external fun fixMultilineContentJumpAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun fixTextLayoutOnPixelDensityChange(): Boolean @DoNotStrip @JvmStatic public external fun fixYogaFlexBasisFitContentInMainAxis(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index defafe46f6b4..b45b25004229 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -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<<179b4a67b13046830313cca7338c924e>> */ /** @@ -145,6 +145,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = false + override fun fixMultilineContentJumpAndroid(): Boolean = false + override fun fixTextLayoutOnPixelDensityChange(): Boolean = false override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 53af4b4edab0..f5c6a20b0c0f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -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<<704cb0047cf8240b635b1d632b128505>> */ /** @@ -85,6 +85,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null private var fixBorderlessRippleAndroidCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null + private var fixMultilineContentJumpAndroidCache: Boolean? = null private var fixTextLayoutOnPixelDensityChangeCache: Boolean? = null private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null @@ -732,6 +733,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun fixMultilineContentJumpAndroid(): Boolean { + var cached = fixMultilineContentJumpAndroidCache + if (cached == null) { + cached = currentProvider.fixMultilineContentJumpAndroid() + accessedFeatureFlags.add("fixMultilineContentJumpAndroid") + fixMultilineContentJumpAndroidCache = cached + } + return cached + } + override fun fixTextLayoutOnPixelDensityChange(): Boolean { var cached = fixTextLayoutOnPixelDensityChangeCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 8fa2f4269464..d9784837d74e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -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<> */ /** @@ -145,6 +145,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean + @DoNotStrip public fun fixMultilineContentJumpAndroid(): Boolean + @DoNotStrip public fun fixTextLayoutOnPixelDensityChange(): Boolean @DoNotStrip public fun fixYogaFlexBasisFitContentInMainAxis(): Boolean 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 e94f841ffb99..c996a9c72498 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 @@ -12,9 +12,13 @@ import android.text.TextWatcher import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.uimanager.StateWrapper.UpdateMode import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.EventDispatcher +@OptIn(UnstableReactNativeAPI::class) internal class ReactTextInputTextWatcher( reactContext: ReactContext, private val editText: ReactEditText, @@ -22,10 +26,13 @@ internal class ReactTextInputTextWatcher( private val eventDispatcher: EventDispatcher? = UIManagerHelper.getEventDispatcher(reactContext) private val surfaceId = UIManagerHelper.getSurfaceId(reactContext) private var previousText: String? = null + private var previousLineCount: Int? = null + private var pendingEventCount: Int? = null override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { // Incoming charSequence gets mutated before onTextChanged() is invoked previousText = s.toString() + previousLineCount = editText.layout?.lineCount } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { @@ -46,13 +53,13 @@ internal class ReactTextInputTextWatcher( return } - val stateWrapper = editText.stateWrapper - - if (stateWrapper != null) { - val newStateData: WritableMap = WritableNativeMap() - newStateData.putInt("mostRecentEventCount", editText.incrementAndGetEventCounter()) - newStateData.putInt("opaqueCacheId", editText.id) - stateWrapper.updateState(newStateData) + if (editText.stateWrapper != null) { + val eventCount = editText.incrementAndGetEventCounter() + if (shouldUpdateSynchronously) { + pendingEventCount = eventCount + } else { + updateState(eventCount) + } } // The event that contains the event counter and updates it must be sent first. @@ -68,5 +75,31 @@ internal class ReactTextInputTextWatcher( ) } - override fun afterTextChanged(s: Editable) = Unit + override fun afterTextChanged(s: Editable) { + // Update here rather than in on TextChanged since ReactEditText updates the cached spannable + // only after every TextWatcher has seen onTextChanged + pendingEventCount?.let { eventCount -> + // A synchronous update flushes all enqueued state updates. Mounting a queued text update + // calls maybeSetTextFromState() which re-runs this callback, so clear pendingEventCount first + pendingEventCount = null + updateState(eventCount, UpdateMode.unstable_Immediate) + } + } + + private fun updateState(eventCount: Int, updateMode: UpdateMode = UpdateMode.Asynchronous) { + val stateWrapper = editText.stateWrapper ?: return + val newStateData: WritableMap = WritableNativeMap() + newStateData.putInt("mostRecentEventCount", eventCount) + newStateData.putInt("opaqueCacheId", editText.id) + stateWrapper.updateState(newStateData, updateMode) + } + + private val shouldUpdateSynchronously: Boolean + get() { + if (!ReactNativeFeatureFlags.fixMultilineContentJumpAndroid()) { + return false + } + val lineCount = editText.layout?.lineCount + return editText.isMultiline && lineCount != null && lineCount != previousLineCount + } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 23fd0978a766..3e58bc10cc9e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -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<<944781e7a547e601f0637869a3a3ec5e>> + * @generated SignedSource<<9700d8eed3620386e6a3ce0866fe6557>> */ /** @@ -405,6 +405,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool fixMultilineContentJumpAndroid() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fixMultilineContentJumpAndroid"); + return method(javaProvider_); + } + bool fixTextLayoutOnPixelDensityChange() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fixTextLayoutOnPixelDensityChange"); @@ -930,6 +936,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabri return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool JReactNativeFeatureFlagsCxxInterop::fixMultilineContentJumpAndroid( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::fixMultilineContentJumpAndroid(); +} + bool JReactNativeFeatureFlagsCxxInterop::fixTextLayoutOnPixelDensityChange( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange(); @@ -1324,6 +1335,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "fixMappingOfEventPrioritiesBetweenFabricAndReact", JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabricAndReact), + makeNativeMethod( + "fixMultilineContentJumpAndroid", + JReactNativeFeatureFlagsCxxInterop::fixMultilineContentJumpAndroid), makeNativeMethod( "fixTextLayoutOnPixelDensityChange", JReactNativeFeatureFlagsCxxInterop::fixTextLayoutOnPixelDensityChange), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index ae774f80c82b..6512112d115f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -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<> */ /** @@ -213,6 +213,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool fixMappingOfEventPrioritiesBetweenFabricAndReact( facebook::jni::alias_ref); + static bool fixMultilineContentJumpAndroid( + facebook::jni::alias_ref); + static bool fixTextLayoutOnPixelDensityChange( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index ac8ddb96a112..3805215ecaa5 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -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<<56e127471f63e47798ec619e6fa56738>> + * @generated SignedSource<<8ecd065eb7d2b9f4beecd17450ede5bb>> */ /** @@ -270,6 +270,10 @@ bool ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact() return getAccessor().fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool ReactNativeFeatureFlags::fixMultilineContentJumpAndroid() { + return getAccessor().fixMultilineContentJumpAndroid(); +} + bool ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange() { return getAccessor().fixTextLayoutOnPixelDensityChange(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 0e5b3f2013fb..964a2bec569d 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -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<<6887b671526bc8a3b0678e3e45b4e3b4>> + * @generated SignedSource<<9f5eb778c4a7077447b2eb94fdf7a94e>> */ /** @@ -346,6 +346,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); + /** + * Flushes TextInput state updates synchronously on Android when a multiline input changes line count, so the new height mounts in the same frame instead of the content jumping. Pairs with enableFollowUpMountItemDispatchAndroid for updates that happen during mount dispatch. + */ + RN_EXPORT static bool fixMultilineContentJumpAndroid(); + /** * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7640cea6418a..79847d6fc021 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -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<<5d9712bd7e62ba3a29b084ba4c60bec5>> + * @generated SignedSource<> */ /** @@ -1127,6 +1127,24 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::fixMultilineContentJumpAndroid() { + auto flagValue = fixMultilineContentJumpAndroid_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(61, "fixMultilineContentJumpAndroid"); + + flagValue = currentProvider_->fixMultilineContentJumpAndroid(); + fixMultilineContentJumpAndroid_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { auto flagValue = fixTextLayoutOnPixelDensityChange_.load(); @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "fixTextLayoutOnPixelDensityChange"); + markFlagAsAccessed(62, "fixTextLayoutOnPixelDensityChange"); flagValue = currentProvider_->fixTextLayoutOnPixelDensityChange(); fixTextLayoutOnPixelDensityChange_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(63, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(64, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "fuseboxEnabledRelease"); + markFlagAsAccessed(65, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(66, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(67, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxWebSocketEventsEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(68, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::keepTimersRunningWhileHostPausedAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "keepTimersRunningWhileHostPausedAndroid"); + markFlagAsAccessed(69, "keepTimersRunningWhileHostPausedAndroid"); flagValue = currentProvider_->keepTimersRunningWhileHostPausedAndroid(); keepTimersRunningWhileHostPausedAndroid_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::optimizedAnimatedPropUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "optimizedAnimatedPropUpdates"); + markFlagAsAccessed(70, "optimizedAnimatedPropUpdates"); flagValue = currentProvider_->optimizedAnimatedPropUpdates(); optimizedAnimatedPropUpdates_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(71, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "perfIssuesEnabled"); + markFlagAsAccessed(72, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "perfMonitorV2Enabled"); + markFlagAsAccessed(73, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1352,7 +1370,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "preparedTextCacheSize"); + markFlagAsAccessed(74, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(75, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "redBoxV2Android"); + markFlagAsAccessed(76, "redBoxV2Android"); flagValue = currentProvider_->redBoxV2Android(); redBoxV2Android_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "redBoxV2IOS"); + markFlagAsAccessed(77, "redBoxV2IOS"); flagValue = currentProvider_->redBoxV2IOS(); redBoxV2IOS_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(78, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(79, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(80, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "skipUnchangedChildrenInDifferentiator"); + markFlagAsAccessed(81, "skipUnchangedChildrenInDifferentiator"); flagValue = currentProvider_->skipUnchangedChildrenInDifferentiator(); skipUnchangedChildrenInDifferentiator_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(82, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(83, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(85, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(86, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useFabricInterop"); + markFlagAsAccessed(87, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(88, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useNestedScrollViewAndroid"); + markFlagAsAccessed(89, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useSharedAnimatedBackend"); + markFlagAsAccessed(90, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1658,7 +1676,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(91, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "useTurboModuleInterop"); + markFlagAsAccessed(92, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(93, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1712,7 +1730,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "viewCullingOutsetRatio"); + markFlagAsAccessed(94, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1730,7 +1748,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(94, "viewTransitionEnabled"); + markFlagAsAccessed(95, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1748,7 +1766,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(95, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(96, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1766,7 +1784,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(96, "virtualViewPrerenderRatio"); + markFlagAsAccessed(97, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 7a9bc59bc57e..b9ab26363816 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -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<<17f41335dce7e5b45e7af2878c47d69b>> + * @generated SignedSource<> */ /** @@ -95,6 +95,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableVirtualViewContainerStateExperimental(); bool fixBorderlessRippleAndroid(); bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); + bool fixMultilineContentJumpAndroid(); bool fixTextLayoutOnPixelDensityChange(); bool fixYogaFlexBasisFitContentInMainAxis(); bool fuseboxAssertSingleHostState(); @@ -142,7 +143,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 97> accessedFeatureFlags_; + std::array, 98> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -205,6 +206,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableVirtualViewContainerStateExperimental_; std::atomic> fixBorderlessRippleAndroid_; std::atomic> fixMappingOfEventPrioritiesBetweenFabricAndReact_; + std::atomic> fixMultilineContentJumpAndroid_; std::atomic> fixTextLayoutOnPixelDensityChange_; std::atomic> fixYogaFlexBasisFitContentInMainAxis_; std::atomic> fuseboxAssertSingleHostState_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 600aab1882cc..71b3d830b5d1 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -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<<35e8175277baa31d135d802ef5a5e6f1>> + * @generated SignedSource<<757ae6653133e54a98ba7b39279d0f49>> */ /** @@ -273,6 +273,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool fixMultilineContentJumpAndroid() override { + return false; + } + bool fixTextLayoutOnPixelDensityChange() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 88878807cfec..6fd951824e5b 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -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<> */ /** @@ -596,6 +596,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } + bool fixMultilineContentJumpAndroid() override { + auto value = values_["fixMultilineContentJumpAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fixMultilineContentJumpAndroid(); + } + bool fixTextLayoutOnPixelDensityChange() override { auto value = values_["fixTextLayoutOnPixelDensityChange"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 664922287009..dd2dd0451939 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -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<<7413e958441d0502dc1266ff87e9da85>> + * @generated SignedSource<<325098fd6813390c7c46476ce2db848a>> */ /** @@ -88,6 +88,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableVirtualViewContainerStateExperimental() = 0; virtual bool fixBorderlessRippleAndroid() = 0; virtual bool fixMappingOfEventPrioritiesBetweenFabricAndReact() = 0; + virtual bool fixMultilineContentJumpAndroid() = 0; virtual bool fixTextLayoutOnPixelDensityChange() = 0; virtual bool fixYogaFlexBasisFitContentInMainAxis() = 0; virtual bool fuseboxAssertSingleHostState() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index ca8a80f1adbf..8a2b48d5a4c3 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -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<<8ef2355892a233c00b025c5254291fb8>> */ /** @@ -349,6 +349,11 @@ bool NativeReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndR return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool NativeReactNativeFeatureFlags::fixMultilineContentJumpAndroid( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::fixMultilineContentJumpAndroid(); +} + bool NativeReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7413766e285a..746224669840 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -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<<90f271ea47f91e6a84979209b7b3d816>> + * @generated SignedSource<<50fceb077fb769b57d6c30400785cb61>> */ /** @@ -160,6 +160,8 @@ class NativeReactNativeFeatureFlags bool fixMappingOfEventPrioritiesBetweenFabricAndReact(jsi::Runtime& runtime); + bool fixMultilineContentJumpAndroid(jsi::Runtime& runtime); + bool fixTextLayoutOnPixelDensityChange(jsi::Runtime& runtime); bool fixYogaFlexBasisFitContentInMainAxis(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 612c196ff22d..462f4cc9e365 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -707,6 +707,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + fixMultilineContentJumpAndroid: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-22', + description: + 'Flushes TextInput state updates synchronously on Android when a multiline input changes line count, so the new height mounts in the same frame instead of the content jumping. Pairs with enableFollowUpMountItemDispatchAndroid for updates that happen during mount dispatch.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, fixTextLayoutOnPixelDensityChange: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index e5a8120cff62..489ce8f96a8e 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -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<<674f975884ae746e067e614523f2bfc3>> + * @generated SignedSource<<4dc2b5a48504059b3cb99331bd0ed107>> * @flow strict * @noformat */ @@ -110,6 +110,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enableVirtualViewContainerStateExperimental: Getter, fixBorderlessRippleAndroid: Getter, fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter, + fixMultilineContentJumpAndroid: Getter, fixTextLayoutOnPixelDensityChange: Getter, fixYogaFlexBasisFitContentInMainAxis: Getter, fuseboxAssertSingleHostState: Getter, @@ -466,6 +467,10 @@ export const fixBorderlessRippleAndroid: Getter = createNativeFlagGette * Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React. */ export const fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter = createNativeFlagGetter('fixMappingOfEventPrioritiesBetweenFabricAndReact', false); +/** + * Flushes TextInput state updates synchronously on Android when a multiline input changes line count, so the new height mounts in the same frame instead of the content jumping. Pairs with enableFollowUpMountItemDispatchAndroid for updates that happen during mount dispatch. + */ +export const fixMultilineContentJumpAndroid: Getter = createNativeFlagGetter('fixMultilineContentJumpAndroid', false); /** * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 6fc752bba2a9..bfa4b4ed905e 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -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<<2eb3f4a3dca013c51ad2913944855010>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -86,6 +86,7 @@ export interface Spec extends TurboModule { readonly enableVirtualViewContainerStateExperimental?: () => boolean; readonly fixBorderlessRippleAndroid?: () => boolean; readonly fixMappingOfEventPrioritiesBetweenFabricAndReact?: () => boolean; + readonly fixMultilineContentJumpAndroid?: () => boolean; readonly fixTextLayoutOnPixelDensityChange?: () => boolean; readonly fixYogaFlexBasisFitContentInMainAxis?: () => boolean; readonly fuseboxAssertSingleHostState?: () => boolean;