Skip to content

Commit 5efd60f

Browse files
thomasvoclaude
andcommitted
fix: use measureInWindow for correct KAV positioning in modals
KeyboardAvoidingView uses onLayout to get the view's frame, but onLayout returns parent-relative coordinates. In pageSheet modals, the parent's y=0 doesn't correspond to screen y=0, causing the keyboard avoidance calculation to use incorrect absolute positions. This commit fixes two issues: 1. Use measureInWindow instead of onLayout to get absolute screen coordinates. This ensures frame.y reflects the actual screen position, not the parent-relative position. 2. Compensate for the gap below pageSheet modals. In pageSheet presentation, the modal doesn't extend to the screen bottom, but the keyboard covers this gap. The overlap calculation now treats the view as extending to the screen bottom when it detects a modal-like configuration (gap > 0, gap < keyboard height, view > 50% of screen). A combinedRef is used to maintain both the internal ref (needed for measureInWindow) and the forwarded ref from the consumer. Fixes #867 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d4f1a4c commit 5efd60f

1 file changed

Lines changed: 45 additions & 5 deletions

File tree

src/components/KeyboardAvoidingView/index.tsx

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ const KeyboardAvoidingView = forwardRef<
9292
ref,
9393
) => {
9494
const initialFrame = useSharedValue<LayoutRectangle | null>(null);
95+
const internalRef = React.useRef<View>(null);
9596
const frame = useDerivedValue(() => initialFrame.value || defaultLayout);
9697

9798
const { translate, padding } = useTranslateAnimation();
@@ -103,8 +104,19 @@ const KeyboardAvoidingView = forwardRef<
103104

104105
const keyboardY =
105106
screenHeight - keyboard.heightWhenOpened.value - keyboardVerticalOffset;
106-
107-
return Math.max(frame.value.y + frame.value.height - keyboardY, 0);
107+
const viewBottom = frame.value.y + frame.value.height;
108+
// In pageSheet modals, the view doesn't extend to the screen bottom.
109+
// The keyboard covers the gap below the view, so we treat the view as
110+
// extending to the screen bottom for the overlap calculation. This only
111+
// applies to large views (>50% of screen) to avoid affecting small views.
112+
const gap = screenHeight - viewBottom;
113+
const isModalLikeView =
114+
gap > 0 &&
115+
gap < keyboard.heightWhenOpened.value &&
116+
frame.value.height > screenHeight * 0.5;
117+
const effectiveBottom = isModalLikeView ? screenHeight : viewBottom;
118+
119+
return Math.max(effectiveBottom - keyboardY, 0);
108120
}, [screenHeight, keyboardVerticalOffset]);
109121
const interpolateToRelativeKeyboardHeight = useCallback(
110122
(value: number) => {
@@ -132,7 +144,25 @@ const KeyboardAvoidingView = forwardRef<
132144
);
133145
const onLayout = useCallback<NonNullable<ViewProps["onLayout"]>>(
134146
(e) => {
135-
runOnUI(onLayoutWorklet)(e.nativeEvent.layout);
147+
const layout = e.nativeEvent.layout;
148+
// Use measureInWindow to get absolute screen coordinates instead of
149+
// parent-relative coordinates from onLayout. This fixes keyboard avoidance
150+
// in modals where the parent view's y=0 doesn't correspond to screen y=0.
151+
const viewRef = internalRef.current;
152+
153+
if (viewRef && viewRef.measureInWindow) {
154+
viewRef.measureInWindow((x, y, _width, _height) => {
155+
runOnUI(onLayoutWorklet)({
156+
x,
157+
y,
158+
width: layout.width,
159+
height: layout.height,
160+
});
161+
});
162+
} else {
163+
runOnUI(onLayoutWorklet)(layout);
164+
}
165+
136166
onLayoutProps?.(e);
137167
},
138168
[onLayoutProps],
@@ -177,6 +207,16 @@ const KeyboardAvoidingView = forwardRef<
177207
return {};
178208
}
179209
}, [behavior, enabled, interpolateToRelativeKeyboardHeight]);
210+
const combinedRef = useCallback(
211+
(node: View | null) => {
212+
(internalRef as React.MutableRefObject<View | null>).current = node;
213+
214+
if (typeof ref === "function") ref(node);
215+
else if (ref)
216+
(ref as React.MutableRefObject<View | null>).current = node;
217+
},
218+
[ref],
219+
);
180220
const isPositionBehavior = behavior === "position";
181221
const containerStyle = isPositionBehavior ? contentContainerStyle : style;
182222
const combinedStyles = useMemo(
@@ -186,15 +226,15 @@ const KeyboardAvoidingView = forwardRef<
186226

187227
if (isPositionBehavior) {
188228
return (
189-
<View ref={ref} style={style} onLayout={onLayout} {...props}>
229+
<View ref={combinedRef} style={style} onLayout={onLayout} {...props}>
190230
<Reanimated.View style={combinedStyles}>{children}</Reanimated.View>
191231
</View>
192232
);
193233
}
194234

195235
return (
196236
<Reanimated.View
197-
ref={ref}
237+
ref={combinedRef}
198238
style={combinedStyles}
199239
onLayout={onLayout}
200240
{...props}

0 commit comments

Comments
 (0)