Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -33,6 +33,8 @@ internal class SheetAnimationCoordinator(
private var lastScreenContainerBottomOffset: Int = 0
private val screenContainerRect = android.graphics.Rect()

private var lastBottomSystemBarInset: Int = 0

internal fun createSheetEnterAnimator(sheetAnimationContext: SheetDelegate.SheetAnimationContext): Animator {
val animatorSet = AnimatorSet()

Expand Down Expand Up @@ -224,6 +226,7 @@ internal class SheetAnimationCoordinator(

internal fun handleKeyboardInsetsProgress(insets: WindowInsetsCompat) {
lastKeyboardBottomOffset = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
lastBottomSystemBarInset = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
// Prioritize enter/exit animations over direct keyboard inset reactions.
// We store the latest keyboard offset in `lastKeyboardBottomOffset`
// so that it can always be respected when applying translations in `updateSheetTranslationY`.
Expand Down Expand Up @@ -280,10 +283,27 @@ internal class SheetAnimationCoordinator(
}

private fun updateSheetTranslationY(baseTranslationY: Float) {
val effectiveKeyboardHeight = maxOf(0, lastKeyboardBottomOffset - lastScreenContainerBottomOffset)
val bottomOffset = computeSheetOffsetYWithIMEPresent(effectiveKeyboardHeight).toFloat()
screen.translationY = baseTranslationY - calculateKeyboardShiftHidingBottomInset()
}

private fun calculateKeyboardShiftHidingBottomInset(): Float {
// Calculate how much of the keyboard physically intersects with our container.
val keyboardHeightInContainerBounds = maxOf(0, lastKeyboardBottomOffset - lastScreenContainerBottomOffset)

if (keyboardHeightInContainerBounds <= 0) {
return 0f
}

// Determine the maximum upward shift needed to keep the content above the keyboard.
val maxAllowedUpwardShift = computeSheetOffsetYWithIMEPresent(keyboardHeightInContainerBounds).toFloat()

// Determine how much of the navigation bar is located within our container's bounds.
val navigationBarHeightWithinContainer = maxOf(0, lastBottomSystemBarInset - lastScreenContainerBottomOffset)

screen.translationY = baseTranslationY - bottomOffset
// By subtracting the nav bar height from our shift, we let that empty padding slide behind
// the keyboard. This keeps the sheet's actual content flush against the keyboard's top edge
// without requiring size updates.
return maxOf(0f, maxAllowedUpwardShift - navigationBarHeightWithinContainer.toFloat())
}

private fun createSheetSlideInAnimator(): ValueAnimator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
Expand Down Expand Up @@ -373,20 +372,10 @@ class SheetDelegate(
isKeyboardVisible = false
}

val newBottomInset = if (!isImeVisible) systemBarsInsets.bottom else 0

// Note: We do not manipulate the top inset manually. Therefore, if SafeAreaView has top insets enabled,
// we must retain the top inset even if the formSheet does not currently overflow into the status bar.
// This is important because in some specific edge cases - for example, when the keyboard slides in -
// the formSheet might overlap the status bar. If we ignored the top inset and it suddenly became necessary,
// it would result in a noticeable visual content jump. To ensure consistency and avoid layout shifts,
// we always include the top inset upfront, which can be disabled from the application perspective.
return WindowInsetsCompat
.Builder(insets)
.setInsets(
WindowInsetsCompat.Type.systemBars(),
Insets.of(systemBarsInsets.left, systemBarsInsets.top, systemBarsInsets.right, newBottomInset),
).build()
// This listener runs at the decor-view level (via InsetsObserverProxy), so anything we
// return here propagates to the entire view hierarchy. We therefore return the insets
// unmodified and we should never consume insets globally.
return insets
}

private fun shouldDismissSheetInState(
Expand Down
86 changes: 86 additions & 0 deletions apps/src/tests/issue-tests/Test4244.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet, TextInput } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaView } from 'react-native-screens/experimental';
import { Colors } from '@apps/shared/styling';

const Stack = createNativeStackNavigator();

function MainScreen({ navigation }: any) {
return (
<View style={styles.container}>
<Text>Main Screen inside Stack</Text>
<Button
title="Open FormSheet Modal"
onPress={() => navigation.navigate('MyFormSheet')}
/>
</View>
);
}

function FormSheetScreen({ navigation }: any) {
const [text, setText] = useState('');

return (
<SafeAreaView edges={{ bottom: true }}>
<View style={[styles.container, { backgroundColor: Colors.NavyDark40 }]}>
<Text>FormSheet Modal</Text>
<TextInput
style={styles.input}
placeholder="Type something here..."
value={text}
onChangeText={setText}
/>
<Button title="Close" onPress={() => navigation.goBack()} />
</View>
</SafeAreaView>
);
}

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Main"
component={MainScreen}
options={{ title: 'Home' }}
/>
<Stack.Screen
name="MyFormSheet"
component={FormSheetScreen}
options={{
presentation: 'formSheet',
headerShown: false,
sheetAllowedDetents: 'fitToContents',
contentStyle: {
backgroundColor: Colors.RedDark100,
},
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 16,
paddingTop: 24,
paddingHorizontal: 24,
backgroundColor: Colors.White,
},
input: {
width: '80%',
height: 45,
borderColor: Colors.offBackground,
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 12,
backgroundColor: Colors.White,
},
});
Comment thread
Copilot marked this conversation as resolved.
1 change: 1 addition & 0 deletions apps/src/tests/issue-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export { default as Test4155 } from './Test4155';
export { default as Test4161 } from './Test4161';
export { default as Test4220 } from './Test4220';
export { default as Test4240 } from './Test4240';
export { default as Test4244 } from './Test4244';
export { default as TestScreenAnimation } from './TestScreenAnimation';
// The following test was meant to demo the "go back" gesture using Reanimated
// but the associated PR in react-navigation is currently put on hold
Expand Down
Loading