Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.reactnativekeyboardcontroller

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.reactnativekeyboardcontroller.modules.KeyboardControllerModuleImpl

Expand Down Expand Up @@ -33,6 +34,13 @@ class KeyboardControllerModule(
module.setFocusTo(direction)
}

override fun windowPosition(
viewTag: Double,
promise: Promise,
) {
module.windowPosition(viewTag, promise)
}

override fun addListener(eventName: String?) {
// Required for RN built-in Event Emitter Calls.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import android.os.Build
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
import com.reactnativekeyboardcontroller.interactive.KeyboardAnimationController
Expand Down Expand Up @@ -77,6 +79,28 @@ class KeyboardControllerModuleImpl(
}
}

fun windowPosition(
viewTag: Double,
promise: Promise,
) {
UiThreadUtil.runOnUiThread {
val view = mReactContext.currentActivity?.findViewById<View>(viewTag.toInt())
if (view == null) {
promise.reject("E_VIEW_NOT_FOUND", "Could not find view for tag")
return@runOnUiThread
}
val location = IntArray(2)
view.getLocationInWindow(location)
val density = mReactContext.resources.displayMetrics.density
Comment thread
kirillzyusko marked this conversation as resolved.
Outdated
val map = Arguments.createMap()
map.putDouble("x", location[0].toDouble() / density)
map.putDouble("y", location[1].toDouble() / density)
map.putDouble("width", view.width.toDouble() / density)
map.putDouble("height", view.height.toDouble() / density)
Comment thread
kirillzyusko marked this conversation as resolved.
Outdated
promise.resolve(map)
}
}

private fun setSoftInputMode(mode: Int) {
UiThreadUtil.runOnUiThread {
if (getCurrentMode() != mode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.reactnativekeyboardcontroller

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
Expand Down Expand Up @@ -40,6 +41,14 @@ class KeyboardControllerModule(
module.setFocusTo(direction)
}

@ReactMethod
fun windowPosition(
viewTag: Double,
promise: Promise,
) {
module.windowPosition(viewTag, promise)
}

@Suppress("detekt:UnusedParameter")
@ReactMethod
fun addListener(eventName: String?) {
Expand Down
51 changes: 51 additions & 0 deletions ios/KeyboardControllerModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,57 @@ - (void)setFocusTo:(NSString *)direction
[ViewHierarchyNavigator setFocusToDirection:direction];
}

#ifdef RCT_NEW_ARCH_ENABLED
- (void)windowPosition:(double)viewTag
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
#else
RCT_EXPORT_METHOD(windowPosition
: (nonnull NSNumber *)viewTag resolve
: (RCTPromiseResolveBlock)resolve reject
: (RCTPromiseRejectBlock)reject)
#endif
{
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger tag;
#ifdef RCT_NEW_ARCH_ENABLED
tag = (NSInteger)viewTag;
#else
tag = viewTag.integerValue;
#endif
UIWindow *window = nil;
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive &&
[scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
for (UIWindow *w in windowScene.windows) {
if (w.isKeyWindow) {
window = w;
break;
}
}
if (window)
break;
}
}
UIView *view = [window viewWithTag:tag];
Comment thread
kirillzyusko marked this conversation as resolved.
Outdated
if (!view || !view.superview) {
reject(@"E_VIEW_NOT_FOUND", @"Could not find view for tag", nil);
return;
}
// Use UIKit coordinate conversion to get true window coordinates.
// This bypasses Fabric's shadow tree which returns surface-relative
// coordinates for views inside Modals (RN bug #52450).
CGRect windowFrame = [view.superview convertRect:view.frame toView:nil];
resolve(@{
@"x" : @(windowFrame.origin.x),
@"y" : @(windowFrame.origin.y),
@"width" : @(windowFrame.size.width),
@"height" : @(windowFrame.size.height),
});
});
}

+ (KeyboardController *)shared
{
return shared;
Expand Down
1 change: 1 addition & 0 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const KeyboardControllerNative: KeyboardControllerNativeModule = {
preload: NOOP,
dismiss: NOOP,
setFocusTo: NOOP,
windowPosition: () => Promise.resolve({ x: 0, y: 0, width: 0, height: 0 }),
Comment thread
kirillzyusko marked this conversation as resolved.
Outdated
addListener: NOOP,
removeListeners: NOOP,
};
Expand Down
44 changes: 38 additions & 6 deletions src/components/KeyboardAvoidingView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import Reanimated, {
useSharedValue,
} from "react-native-reanimated";

import { KeyboardControllerNative } from "../../bindings";
import { useWindowDimensions } from "../../hooks";
import { findNodeHandle } from "../../utils/findNodeHandle";
import useCombinedRef from "../hooks/useCombinedRef";

import { useKeyboardAnimation, useTranslateAnimation } from "./hooks";
Expand Down Expand Up @@ -121,23 +123,53 @@ const KeyboardAvoidingView = forwardRef<
if (
keyboard.isClosed.value ||
initialFrame.value === null ||
behavior !== "height"
// When automaticOffset is enabled, always preserve the pre-keyboard
// frame to avoid iOS modal keyboard adjustment shrinking the frame.
// Without automaticOffset, only preserve for "height" behavior
// (existing behavior for backward compatibility).
(!automaticOffset && behavior !== "height")
) {
// eslint-disable-next-line react-compiler/react-compiler
initialFrame.value = layout;
}
},
[behavior],
[behavior, automaticOffset],
);
const onLayout = useCallback<NonNullable<ViewProps["onLayout"]>>(
(e) => {
const layout = e.nativeEvent.layout;

if (automaticOffset) {
// ref is always set here — onLayout only fires after mount
internalRef.current?.measureInWindow((x, y) => {
runOnUI(onLayoutWorklet)({ ...layout, x, y });
});
const node = internalRef.current;
const tag = node ? findNodeHandle(node) : null;

if (tag !== null) {
// Use native windowPosition to get true screen-absolute coordinates.
// This bypasses Fabric's measureInWindow which returns
// surface-relative coordinates inside Modals (RN bug #52450).
KeyboardControllerNative.windowPosition(tag)
.then((position) => {
runOnUI(onLayoutWorklet)({
...layout,
x: position.x,
y: position.y,
});
})
.catch(() => {
// windowPosition failed (e.g. view unmounted or tag not found).
// Fall back to measureInWindow which returns correct absolute
// coordinates on Paper architecture.
if (node) {
node.measureInWindow((x, y) => {
runOnUI(onLayoutWorklet)({ ...layout, x, y });
});
} else {
runOnUI(onLayoutWorklet)(layout);
}
});
} else {
runOnUI(onLayoutWorklet)(layout);
}
} else {
runOnUI(onLayoutWorklet)(layout);
}
Expand Down
1 change: 1 addition & 0 deletions src/specs/NativeKeyboardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Spec extends TurboModule {
preload(): void;
dismiss(keepFocus: boolean, animated: boolean): void;
setFocusTo(direction: string): void;
windowPosition(viewTag: number): Promise<object>;

// event emitter
addListener: (eventName: string) => void;
Expand Down
7 changes: 7 additions & 0 deletions src/types/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ export type KeyboardControllerModule = {
*/
state: () => KeyboardEventData;
};
export type WindowPositionResult = {
x: number;
y: number;
width: number;
height: number;
};
export type KeyboardControllerNativeModule = {
// android only
setDefaultMode: () => void;
Expand All @@ -118,6 +124,7 @@ export type KeyboardControllerNativeModule = {
// all platforms
dismiss: (keepFocus: boolean, animated: boolean) => void;
setFocusTo: (direction: Direction) => void;
windowPosition: (viewTag: number) => Promise<WindowPositionResult>;
Comment thread
kirillzyusko marked this conversation as resolved.
Outdated
// native event module stuff
addListener: (eventName: string) => void;
removeListeners: (count: number) => void;
Expand Down
Loading