Skip to content

Commit eb2dfbb

Browse files
thomasttvothomasvoclaudekirillzyusko
authored
fix: handle broken measureInWindow (#1355)
## 📜 Description Fixed broken `measureInWindow` measurements in react-native. ## 💡 Motivation and Context The `measureInWindow` works unreliably in Fabric + formSheet Modal (facebook/react-native#56062) or on Android with edge-to-edge enabled (facebook/react-native#56056) Upstream fixes are available, but the problem is that it will require at least RN 0.85+ to work properly. So in this PR we add internal function that is capable of measuring given view. In future this functionality can be removed, but for now this is critical to have it bundled within a package. Fixes #1356 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### JS - added `viewPositionInWindow` to `types`; - added `viewPositionInWindow` to codegen; - added `viewPositionInWindow` to bindings/module; - use `viewPositionInWindow` instead of `measureInWindow`; - added `viewPositionInWindow` to mocks; ### iOS - implement `viewPositionInWindow`; - make `activeWindow` objc available; ### Android - implement `viewPositionInWindow`; - add `uiManager` and `eventDispatcher` to `ReactContext` extensions; ## 🤔 How Has This Been Tested? Tested manually on iPhone 17 Pro (iOS 26.2, simulator) and Pixel 9 Pro (API 35, emulator). ## 📸 Screenshots (if appropriate): ### Android #### Fabric |Before|After| |-------|-----| |<video src="https://github.com/user-attachments/assets/afb99e51-043f-459c-90e3-7e1eb1e184ed">|<video src="https://github.com/user-attachments/assets/8ea0d593-f7bd-47a5-9343-e2fd85f4af45">| #### Paper |Before|After| |-------|-----| |<video src="https://github.com/user-attachments/assets/702c7d88-755b-45f0-b39f-fe4aaa60ab2e">|<video src="https://github.com/user-attachments/assets/f5346bbd-cc85-4a40-9cc5-9422931b04af">| ### iOS #### Fabric |Before|After| |-------|-----| |<video src="https://github.com/user-attachments/assets/49fb9ac6-15b8-4842-939c-4ec06a4e0c42">|<video src="https://github.com/user-attachments/assets/b559a52a-0a91-48bc-911a-7125ce96e013">| #### Paper |Before|After| |-------|-----| |<video src="https://github.com/user-attachments/assets/c4d234a7-122a-4023-8d86-fad71ed043dc">|<video src="https://github.com/user-attachments/assets/d5d48487-16ad-40c2-b6c6-fea85b563e20">| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed --------- Co-authored-by: thomasvo <thomas.vo@openspace.ai> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: kirillzyusko <zyusko.kirik@gmail.com>
1 parent f5dca3c commit eb2dfbb

13 files changed

Lines changed: 136 additions & 15 deletions

File tree

FabricExample/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3184,7 +3184,7 @@ SPEC CHECKSUMS:
31843184
RNCMaskedView: 63268de1986a098b5f4d1fb5b1bc1e97fade0aee
31853185
RNGestureHandler: 4f7cc97a71d4fe0fcba38c94acdd969f5f17c91c
31863186
RNReactNativeHapticFeedback: 63aa39dbd6ef56e9de688210c5761d4007927a7e
3187-
RNReanimated: f2cdef8c5ec70e440498b949f9af3ea39f70fcec
3187+
RNReanimated: aabfea4a99566babeedbd3e33eb234b9756932bd
31883188
RNScreens: 74985ca8e102294a60cec7513fa84c936fa0b20b
31893189
RNWorklets: ab7740c2a152f77ff76a40d52be860d8f128fab1
31903190
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748

android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.reactnativekeyboardcontroller
22

3+
import com.facebook.react.bridge.Promise
34
import com.facebook.react.bridge.ReactApplicationContext
45
import com.reactnativekeyboardcontroller.modules.KeyboardControllerModuleImpl
56

@@ -33,6 +34,13 @@ class KeyboardControllerModule(
3334
module.setFocusTo(direction)
3435
}
3536

37+
override fun viewPositionInWindow(
38+
viewTag: Double,
39+
promise: Promise,
40+
) {
41+
module.viewPositionInWindow(viewTag, promise)
42+
}
43+
3644
override fun addListener(eventName: String?) {
3745
// Required for RN built-in Event Emitter Calls.
3846
}

android/src/main/java/com/reactnativekeyboardcontroller/extensions/ReactContext.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ package com.reactnativekeyboardcontroller.extensions
33
import android.view.View
44
import android.view.ViewGroup
55
import com.facebook.react.bridge.ReactContext
6+
import com.facebook.react.uimanager.UIManagerHelper
7+
import com.facebook.react.uimanager.common.UIManagerType
8+
import com.facebook.react.uimanager.events.EventDispatcher
9+
import com.reactnativekeyboardcontroller.BuildConfig
10+
11+
private val archType = if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) UIManagerType.FABRIC else UIManagerType.DEFAULT
12+
13+
val ReactContext.uiManager
14+
get() = UIManagerHelper.getUIManager(this, archType)
15+
16+
val ReactContext.eventDispatcher: EventDispatcher?
17+
get() = UIManagerHelper.getEventDispatcher(this, archType)
618

719
val ReactContext.rootView: View?
820
get() =

android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ import android.view.ViewGroup
44
import android.view.WindowManager
55
import androidx.core.view.ViewCompat
66
import com.facebook.react.uimanager.ThemedReactContext
7-
import com.facebook.react.uimanager.UIManagerHelper
8-
import com.facebook.react.uimanager.common.UIManagerType
97
import com.facebook.react.uimanager.events.Event
108
import com.facebook.react.uimanager.events.EventDispatcherListener
119
import com.facebook.react.views.modal.ReactModalHostView
1210
import com.facebook.react.views.view.ReactViewGroup
13-
import com.reactnativekeyboardcontroller.BuildConfig
1411
import com.reactnativekeyboardcontroller.constants.Keyboard
12+
import com.reactnativekeyboardcontroller.extensions.eventDispatcher
1513
import com.reactnativekeyboardcontroller.extensions.removeSelf
14+
import com.reactnativekeyboardcontroller.extensions.uiManager
1615
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
1716
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallbackConfig
1817
import com.reactnativekeyboardcontroller.log.Logger
@@ -26,9 +25,8 @@ class ModalAttachedWatcher(
2625
private val config: KeyboardAnimationCallbackConfig,
2726
private var callback: () -> KeyboardAnimationCallback?,
2827
) : EventDispatcherListener {
29-
private val archType = if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) UIManagerType.FABRIC else UIManagerType.DEFAULT
30-
private val uiManager = UIManagerHelper.getUIManager(reactContext.reactApplicationContext, archType)
31-
private val eventDispatcher = UIManagerHelper.getEventDispatcher(reactContext.reactApplicationContext, archType)
28+
private val uiManager = reactContext.uiManager
29+
private val eventDispatcher = reactContext.eventDispatcher
3230

3331
override fun onEventDispatch(event: Event<*>) {
3432
if (event.eventName != MODAL_SHOW_EVENT) {

android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ import android.os.Build
55
import android.view.View
66
import android.view.WindowManager
77
import android.view.inputmethod.InputMethodManager
8+
import com.facebook.react.bridge.Arguments
9+
import com.facebook.react.bridge.Promise
810
import com.facebook.react.bridge.ReactApplicationContext
911
import com.facebook.react.bridge.UiThreadUtil
12+
import com.reactnativekeyboardcontroller.extensions.dp
13+
import com.reactnativekeyboardcontroller.extensions.screenLocation
14+
import com.reactnativekeyboardcontroller.extensions.uiManager
1015
import com.reactnativekeyboardcontroller.interactive.KeyboardAnimationController
1116
import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder
1217
import com.reactnativekeyboardcontroller.traversal.ViewHierarchyNavigator
1318

1419
class KeyboardControllerModuleImpl(
1520
private val mReactContext: ReactApplicationContext,
1621
) {
22+
private val uiManager = mReactContext.uiManager
1723
private val controller = KeyboardAnimationController()
1824
private val mDefaultMode: Int = getCurrentMode()
1925

@@ -77,6 +83,26 @@ class KeyboardControllerModuleImpl(
7783
}
7884
}
7985

86+
fun viewPositionInWindow(
87+
viewTag: Double,
88+
promise: Promise,
89+
) {
90+
UiThreadUtil.runOnUiThread {
91+
val view = uiManager?.resolveView(viewTag.toInt())
92+
if (view == null) {
93+
promise.reject("E_VIEW_NOT_FOUND", "Could not find view for tag")
94+
return@runOnUiThread
95+
}
96+
val location = view.screenLocation
97+
val map = Arguments.createMap()
98+
map.putDouble("x", location[0].toFloat().dp)
99+
map.putDouble("y", location[1].toFloat().dp)
100+
map.putDouble("width", view.width.toFloat().dp)
101+
map.putDouble("height", view.height.toFloat().dp)
102+
promise.resolve(map)
103+
}
104+
}
105+
80106
private fun setSoftInputMode(mode: Int) {
81107
UiThreadUtil.runOnUiThread {
82108
if (getCurrentMode() != mode) {

android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.reactnativekeyboardcontroller
22

3+
import com.facebook.react.bridge.Promise
34
import com.facebook.react.bridge.ReactApplicationContext
45
import com.facebook.react.bridge.ReactContextBaseJavaModule
56
import com.facebook.react.bridge.ReactMethod
@@ -40,6 +41,14 @@ class KeyboardControllerModule(
4041
module.setFocusTo(direction)
4142
}
4243

44+
@ReactMethod
45+
fun viewPositionInWindow(
46+
viewTag: Double,
47+
promise: Promise,
48+
) {
49+
module.viewPositionInWindow(viewTag, promise)
50+
}
51+
4352
@Suppress("detekt:UnusedParameter")
4453
@ReactMethod
4554
fun addListener(eventName: String?) {

ios/KeyboardControllerModule.mm

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
#endif
2424

2525
#import <React/RCTEventDispatcherProtocol.h>
26+
#ifndef RCT_NEW_ARCH_ENABLED
27+
#import <React/RCTUIManager.h>
28+
#endif
2629

2730
#ifdef RCT_NEW_ARCH_ENABLED
2831
@interface KeyboardController () <NativeKeyboardControllerSpec>
@@ -95,6 +98,39 @@ - (void)setFocusTo:(NSString *)direction
9598
[ViewHierarchyNavigator setFocusToDirection:direction];
9699
}
97100

101+
#ifdef RCT_NEW_ARCH_ENABLED
102+
- (void)viewPositionInWindow:(double)viewTag
103+
resolve:(RCTPromiseResolveBlock)resolve
104+
reject:(RCTPromiseRejectBlock)reject
105+
#else
106+
RCT_EXPORT_METHOD(viewPositionInWindow
107+
: (nonnull NSNumber *)viewTag resolve
108+
: (RCTPromiseResolveBlock)resolve reject
109+
: (RCTPromiseRejectBlock)reject)
110+
#endif
111+
{
112+
dispatch_async(dispatch_get_main_queue(), ^{
113+
UIView *view = nil;
114+
#ifdef RCT_NEW_ARCH_ENABLED
115+
NSInteger tag = (NSInteger)viewTag;
116+
view = [UIApplication.sharedApplication.activeWindow viewWithTag:tag];
117+
#else
118+
view = [self.bridge.uiManager viewForReactTag:viewTag];
119+
#endif
120+
if (!view || !view.superview) {
121+
reject(@"E_VIEW_NOT_FOUND", @"Could not find view for tag", nil);
122+
return;
123+
}
124+
CGRect frame = [view.superview convertRect:view.frame toView:nil];
125+
resolve(@{
126+
@"x" : @(frame.origin.x),
127+
@"y" : @(frame.origin.y),
128+
@"width" : @(frame.size.width),
129+
@"height" : @(frame.size.height),
130+
});
131+
});
132+
}
133+
98134
+ (KeyboardController *)shared
99135
{
100136
return shared;

ios/extensions/UIApplication.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99
import UIKit
1010

1111
public extension UIApplication {
12-
var activeWindow: UIWindow? {
12+
@objc var activeWindow: UIWindow? {
1313
if #available(iOS 13.0, *) {
1414
for scene in connectedScenes {
1515
if scene.activationState == .foregroundActive,

jest/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ const mock = {
8787
setFocusTo: jest.fn(),
8888
isVisible: jest.fn().mockReturnValue(false),
8989
state: jest.fn().mockReturnValue(lastKeyboardEvent),
90+
viewPositionInWindow: jest
91+
.fn()
92+
.mockReturnValue(Promise.resolve({ x: 0, y: 0, width: 0, height: 0 })),
9093
},
9194
AndroidSoftInputModes: {
9295
SOFT_INPUT_ADJUST_NOTHING: 48,

src/bindings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export const KeyboardControllerNative: KeyboardControllerNativeModule = {
2222
preload: NOOP,
2323
dismiss: NOOP,
2424
setFocusTo: NOOP,
25+
viewPositionInWindow: () =>
26+
Promise.resolve({ x: 0, y: 0, width: 0, height: 0 }),
2527
addListener: NOOP,
2628
removeListeners: NOOP,
2729
};

0 commit comments

Comments
 (0)