From 916ba5e9bca70fbbf821ed8b97d2f05a3d714237 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Fri, 12 Jun 2026 08:05:19 -0700 Subject: [PATCH] Fix TextInput placeholder stuck on multiple lines Summary: On Android, when a `TextInput`'s `multiline` prop changes from `false` to `true` and back to `false`, the placeholder stayed rendered across multiple lines instead of returning to a single line. `ReactEditText.setInputType` only forced the view out of single-line mode when `multiline` was enabled; it never restored single-line mode when `multiline` was disabled. `TextView.setInputType` re-applies its single-line layout (max lines, horizontal scrolling) only when its internal single-line flag actually changes, and because we toggle that flag off whenever multiline is on, it can be stale so the reset is skipped. On top of that, under the new architecture the view is not re-measured when only the input type changes while the measured size is unchanged. The placeholder is then rebuilt at draw time at the view's physical width and stays wrapped. This change restores single-line mode when `multiline` is turned off (skipping secure-text fields so their password transformation method is preserved) and forces a re-measure on the multiline-to-single-line transition, so the placeholder is laid out on a single line again. Changelog: [Android][Fixed] - Fix `TextInput` placeholder staying on multiple lines after `multiline` is toggled from `true` back to `false` Reviewed By: javache Differential Revision: D108370869 --- .../react/views/textinput/ReactEditText.kt | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index aae65314509b..0cccc0bb7ec1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -140,6 +140,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private var selectTextOnFocus = false private var placeholder: String? = null private var overflow = Overflow.VISIBLE + private var wasMultiline = false public var stateWrapper: StateWrapper? = null internal var disableTextDiffing: Boolean = false @@ -544,14 +545,41 @@ public open class ReactEditText public constructor(context: Context) : AppCompat super.setTypeface(tf) /** - * If set forces multiline on input, because of a restriction on Android source that enables - * multiline only for inputs of type Text and Multiline on method + * Keep the single-line state in sync with the multiline input type flag. + * + * When multiline is on we must force [isSingleLine] off, because of a restriction on Android + * source that enables multiline only for inputs of type Text and Multiline on method * [android.widget.TextView.isMultilineInputType]} Source: * [TextView.java](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java) + * + * When multiline is off we must force [isSingleLine] back on. [TextView.setInputType] only + * re-applies the single-line layout (maxLines, horizontal scrolling) when its internal + * single-line flag actually changes; because we force it off above whenever multiline is on, + * that flag can be stale and the reset is skipped, leaving the placeholder/hint wrapped across + * multiple lines after multiline is toggled back off. Setting it explicitly guarantees the + * reset. We skip secure text so we don't replace its password transformation method with the + * single-line one. */ if (isMultiline) { isSingleLine = false + } else if (!isSecureText) { + isSingleLine = true + } + + // Restoring the single-line input type above is not enough on its own when multiline is toggled + // off: under Fabric the view is not re-measured while its measured size is unchanged, so the + // placeholder/hint is rebuilt at draw time (which lays the hint out at the view's physical + // width) and stays wrapped across multiple lines. Forcing a re-measure at the current bounds + // rebuilds the hint as a single line, matching the initial mount. + if (wasMultiline && !isMultiline && isLaidOut && width > 0 && height > 0) { + forceLayout() + measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY), + ) + layout(left, top, right, bottom) } + wasMultiline = isMultiline // We override the KeyListener so that all keys on the soft input keyboard as well as hardware // keyboards work. Some KeyListeners like DigitsKeyListener will display the keyboard but not