Skip to content

Commit 2dee70f

Browse files
alanleedevfacebook-github-bot
authored andcommitted
Re-emit keyboardDidShow on IME height change (#56486)
Summary: Follow-up to #55855 (D100437445), which replaced the legacy height-based IME detector in `ReactRootView.CustomGlobalLayoutListener` with a visibility-only detector (`WindowInsetsCompat.Type.ime()`). The legacy code also re-emitted `keyboardDidShow` whenever the IME *height* changed while still visible (e.g., user toggles the emoji panel or predictive bar). Removing that left these JS consumers reading stale `endCoordinates`: - `KeyboardAvoidingView` — caches `_keyboardEvent`, no longer resizes on IME height changes. - `ScrollView` — caches `_keyboardMetrics`; focused `TextInput` auto-scroll uses stale `screenY`/`height`. - `Keyboard.metrics()` — public API returning the cached payload from the last `keyboardDidShow`. Fix: track the last reported IME height and re-emit `keyboardDidShow` when the keyboard is visible AND (it just appeared OR the height changed), restoring pre-D100437445 semantics. The hide branch is gated on `mKeyboardIsVisible` so it still fires exactly once per cycle. Changelog: [Android][Fixed] KeyboardAvoidingView and TextInput auto-scroll not responding to IME height changes (e.g., when toggling emoji panel or predictive bar) Differential Revision: D101385688
1 parent 4aa375b commit 2dee70f

2 files changed

Lines changed: 106 additions & 14 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,7 @@ private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLay
928928
private final Rect mVisibleViewArea;
929929

930930
private boolean mKeyboardIsVisible = false;
931+
private int mKeyboardHeight = 0;
931932
private int mDeviceRotation = 0;
932933

933934
/* package */ CustomGlobalLayoutListener() {
@@ -954,13 +955,17 @@ private void checkForKeyboardEvents() {
954955
}
955956

956957
boolean keyboardIsVisible = rootInsets.isVisible(WindowInsetsCompat.Type.ime());
957-
if (keyboardIsVisible != mKeyboardIsVisible) {
958-
mKeyboardIsVisible = keyboardIsVisible;
959-
Insets barInsets = rootInsets.getInsets(WindowInsetsCompat.Type.systemBars());
958+
Insets barInsets = rootInsets.getInsets(WindowInsetsCompat.Type.systemBars());
960959

961-
if (keyboardIsVisible) {
962-
Insets imeInsets = rootInsets.getInsets(WindowInsetsCompat.Type.ime());
963-
int height = imeInsets.bottom - barInsets.bottom;
960+
if (keyboardIsVisible) {
961+
Insets imeInsets = rootInsets.getInsets(WindowInsetsCompat.Type.ime());
962+
int height = imeInsets.bottom - barInsets.bottom;
963+
964+
// Re-emit on height change while keyboard stays visible (e.g., emoji
965+
// panel toggle); JS consumers cache endCoordinates from keyboardDidShow.
966+
if (!mKeyboardIsVisible || height != mKeyboardHeight) {
967+
mKeyboardIsVisible = true;
968+
mKeyboardHeight = height;
964969

965970
ViewGroup.LayoutParams rootLayoutParams = getRootView().getLayoutParams();
966971
Assertions.assertCondition(rootLayoutParams instanceof WindowManager.LayoutParams);
@@ -978,15 +983,18 @@ private void checkForKeyboardEvents() {
978983
PixelUtil.toDIPFromPixel(mVisibleViewArea.left),
979984
PixelUtil.toDIPFromPixel(mVisibleViewArea.width()),
980985
PixelUtil.toDIPFromPixel(height)));
981-
} else {
982-
sendEvent(
983-
"keyboardDidHide",
984-
createKeyboardEventPayload(
985-
PixelUtil.toDIPFromPixel(mVisibleViewArea.bottom + barInsets.bottom),
986-
0,
987-
PixelUtil.toDIPFromPixel(mVisibleViewArea.width()),
988-
0));
989986
}
987+
} else if (mKeyboardIsVisible) {
988+
mKeyboardIsVisible = false;
989+
mKeyboardHeight = 0;
990+
991+
sendEvent(
992+
"keyboardDidHide",
993+
createKeyboardEventPayload(
994+
PixelUtil.toDIPFromPixel(mVisibleViewArea.bottom + barInsets.bottom),
995+
0,
996+
PixelUtil.toDIPFromPixel(mVisibleViewArea.width()),
997+
0));
990998
}
991999
}
9921000

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/RootViewTest.kt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99

1010
package com.facebook.react
1111

12+
import android.annotation.SuppressLint
1213
import android.app.Activity
1314
import android.graphics.Insets
1415
import android.graphics.Rect
16+
import android.view.ViewGroup
1517
import android.view.WindowInsets
1618
import android.view.WindowManager
1719
import com.facebook.react.bridge.Arguments
@@ -107,4 +109,86 @@ class RootViewTest {
107109
params.putString("easing", "keyboard")
108110
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidShow", params)
109111
}
112+
113+
// Regression test for the keyboard re-emit behavior. Without the
114+
// height-change re-emit in `checkForKeyboardEvents`, JS consumers that
115+
// cache `endCoordinates` (KeyboardAvoidingView, ScrollView, Keyboard.metrics)
116+
// observe stale geometry when the IME height changes (e.g., emoji panel
117+
// toggle) without a visibility transition.
118+
@SuppressLint("NewApi", "DeprecatedClass")
119+
@Test
120+
fun testCheckForKeyboardEventsReEmitsOnHeightChange() {
121+
val instanceManager: ReactInstanceManager = mock()
122+
val activity = Robolectric.buildActivity(Activity::class.java).create().get()
123+
whenever(instanceManager.currentReactContext).thenReturn(reactContext)
124+
125+
val imeBottom = intArrayOf(370)
126+
val imeVisible = booleanArrayOf(true)
127+
128+
val rootView: ReactRootView =
129+
object : ReactRootView(activity) {
130+
override fun getWindowVisibleDisplayFrame(outRect: Rect) {
131+
outRect.set(0, 0, 370, 100)
132+
}
133+
134+
override fun getRootWindowInsets(): WindowInsets =
135+
WindowInsets.Builder()
136+
.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, imeBottom[0]))
137+
.setVisible(WindowInsets.Type.ime(), imeVisible[0])
138+
.build()
139+
140+
override fun getLayoutParams(): ViewGroup.LayoutParams = WindowManager.LayoutParams()
141+
}
142+
143+
rootView.startReactApplication(instanceManager, "")
144+
145+
// 1) Initial show — keyboardDidShow fires once with height=370.
146+
rootView.simulateCheckForKeyboardForTesting()
147+
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidShow", showParams(370.0))
148+
149+
// 2) Idempotent layout pass with same height — must NOT re-emit.
150+
rootView.simulateCheckForKeyboardForTesting()
151+
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidShow", showParams(370.0))
152+
153+
// 3) IME height grows (e.g., emoji panel) — must re-emit with new height.
154+
// This is the case the regression silently dropped.
155+
imeBottom[0] = 420
156+
rootView.simulateCheckForKeyboardForTesting()
157+
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidShow", showParams(420.0))
158+
159+
// 4) Hide — keyboardDidHide fires once.
160+
imeVisible[0] = false
161+
rootView.simulateCheckForKeyboardForTesting()
162+
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidHide", hideParams())
163+
164+
// 5) Idempotent layout pass with keyboard still hidden — must NOT re-emit.
165+
rootView.simulateCheckForKeyboardForTesting()
166+
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidHide", hideParams())
167+
}
168+
169+
private fun showParams(keyboardHeight: Double): com.facebook.react.bridge.WritableMap {
170+
val params = Arguments.createMap()
171+
val endCoordinates = Arguments.createMap()
172+
params.putDouble("duration", 0.0)
173+
endCoordinates.putDouble("width", 370.0)
174+
endCoordinates.putDouble("screenX", 0.0)
175+
endCoordinates.putDouble("height", keyboardHeight)
176+
endCoordinates.putDouble("screenY", 100.0)
177+
params.putMap("endCoordinates", endCoordinates)
178+
params.putString("easing", "keyboard")
179+
return params
180+
}
181+
182+
private fun hideParams(): com.facebook.react.bridge.WritableMap {
183+
val params = Arguments.createMap()
184+
val endCoordinates = Arguments.createMap()
185+
params.putDouble("duration", 0.0)
186+
endCoordinates.putDouble("width", 370.0)
187+
endCoordinates.putDouble("screenX", 0.0)
188+
endCoordinates.putDouble("height", 0.0)
189+
endCoordinates.putDouble("screenY", 100.0)
190+
params.putMap("endCoordinates", endCoordinates)
191+
params.putString("easing", "keyboard")
192+
return params
193+
}
110194
}

0 commit comments

Comments
 (0)