Skip to content

Commit 3accd66

Browse files
t0maborokkafar
andauthored
fix(iOS, FormSheet): Allow handling dynamic content size in FormSheet since 0.82 with synchronous updates enabled (#3454)
## Description Follow-up to 4605692 where I introduced a regression causing content flickering. The regression occurs when the content is allowed to dynamically adjust to the screen size (e.g., using `flex: 1` in styles). The issue is caused by the asynchronous nature of the ShadowNode state update for the screen, which leads to its size being updated with a delay relative to the detent change transition. It's important to note that this issue does not occur when the update is sent **synchronously**. Therefore, I'd like to propose allowing `flex` in screen styles when a set of conditions are met: - The formSheet does not have `fitToContents` defined in `sheetDetents`. - React Native version is at least 0.82 (the one which introduced synchronous state updates). - The feature flag `featureFlags.experiment.synchronousScreenUpdatesEnabled` in `react-native-screens` is enabled (for now, the default value remains disabled). Fixes #2522 ## Changes - Added conditional style with `flex: 1` for FormSheet ## Screenshots / GIFs Here you can add screenshots / GIFs documenting your change. You can add before / after section if you're changing some behavior. ### Before https://github.com/user-attachments/assets/fcfe7e32-5d3c-4536-83ba-01851a555120 ### After https://github.com/user-attachments/assets/79b864e2-f04f-4a37-9bde-693282b6f131 ## Test code and steps to reproduce Test2522 - covering 4 cases: {flex, fixed height} x {detents from 0.0 to 1.0, fitToContents} ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes --------- Co-authored-by: Kacper Kafara <[email protected]>
1 parent 548e8ea commit 3accd66

File tree

3 files changed

+177
-1
lines changed

3 files changed

+177
-1
lines changed

apps/src/tests/Test2522.tsx

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import React, { useEffect } from 'react';
2+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
3+
import type {
4+
NativeStackNavigationOptions,
5+
NativeStackNavigationProp,
6+
} from '@react-navigation/native-stack';
7+
import { NavigationContainer } from '@react-navigation/native';
8+
import { Button, ScrollView, StyleSheet, Text, View } from 'react-native';
9+
import { Spacer } from '../shared';
10+
import Colors from '../shared/styling/Colors';
11+
import { featureFlags } from 'react-native-screens';
12+
13+
// For keeping the reference to the original value from the global scope,
14+
// because we need to override it for this specific example.
15+
let originalSynchronousScreenUpdatesFlagEnabled: boolean;
16+
17+
type StackParamList = {
18+
Main: undefined;
19+
FormSheetWithFlex: undefined;
20+
FormSheetWithFixedHeight: undefined;
21+
FormSheetWithFlexAndFitToContents: undefined;
22+
FormSheetWithFixedHeightAndFitToContents: undefined;
23+
};
24+
25+
const Stack = createNativeStackNavigator<StackParamList>();
26+
27+
type MainProps = {
28+
navigation: NativeStackNavigationProp<StackParamList, 'Main'>;
29+
};
30+
31+
interface Example {
32+
title: string;
33+
screen: keyof StackParamList;
34+
}
35+
36+
const EXAMPLES: Example[] = [
37+
{ title: 'flex:1', screen: 'FormSheetWithFlex' },
38+
{ title: 'height:fixed', screen: 'FormSheetWithFixedHeight' },
39+
{
40+
title: 'flex:1 & fitToContents',
41+
screen: 'FormSheetWithFlexAndFitToContents',
42+
},
43+
{
44+
title: 'height:fixed & fitToContents',
45+
screen: 'FormSheetWithFixedHeightAndFitToContents',
46+
},
47+
];
48+
49+
function Main({ navigation }: MainProps) {
50+
return (
51+
<View style={{ flex: 1, padding: 16 }}>
52+
<Text style={styles.text}>
53+
Supported since RN 0.82 with synchronousScreenUpdatesEnabled flag
54+
enabled
55+
</Text>
56+
<ScrollView>
57+
{EXAMPLES.map(({ title, screen }) => (
58+
<View key={screen} style={{ marginVertical: 4 }}>
59+
<Button title={title} onPress={() => navigation.navigate(screen)} />
60+
</View>
61+
))}
62+
</ScrollView>
63+
</View>
64+
);
65+
}
66+
67+
const formSheetBaseOptions: NativeStackNavigationOptions = {
68+
presentation: 'formSheet',
69+
animation: 'slide_from_bottom',
70+
headerShown: false,
71+
contentStyle: {
72+
backgroundColor: Colors.GreenLight100,
73+
},
74+
};
75+
76+
function FormSheetWithFlex() {
77+
return (
78+
<View
79+
style={{
80+
flex: 1,
81+
justifyContent: 'space-between',
82+
alignItems: 'center',
83+
backgroundColor: Colors.RedDark100,
84+
}}>
85+
<Text style={styles.text}>Start</Text>
86+
<Text style={styles.text}>End</Text>
87+
</View>
88+
);
89+
}
90+
91+
function FormSheetWithFixedHeight() {
92+
return (
93+
<View style={{ backgroundColor: Colors.RedDark100 }}>
94+
<Text style={styles.text}>Start</Text>
95+
<Spacer space={100} />
96+
<Text style={styles.text}>End</Text>
97+
</View>
98+
);
99+
}
100+
101+
export default function App() {
102+
useEffect(() => {
103+
originalSynchronousScreenUpdatesFlagEnabled =
104+
featureFlags.experiment.synchronousScreenUpdatesEnabled;
105+
featureFlags.experiment.synchronousScreenUpdatesEnabled = true;
106+
107+
return () => {
108+
// Note: It signals an error that the flag value has changed, but this is intentional
109+
featureFlags.experiment.synchronousScreenUpdatesEnabled =
110+
originalSynchronousScreenUpdatesFlagEnabled;
111+
};
112+
}, []);
113+
114+
return (
115+
<NavigationContainer>
116+
<Stack.Navigator>
117+
<Stack.Screen
118+
name="Main"
119+
options={{ title: 'Main' }}
120+
children={({ navigation }) => <Main navigation={navigation} />}
121+
/>
122+
<Stack.Screen
123+
name="FormSheetWithFlex"
124+
component={FormSheetWithFlex}
125+
options={{
126+
...formSheetBaseOptions,
127+
sheetAllowedDetents: [0.3, 0.5, 0.8],
128+
}}
129+
/>
130+
<Stack.Screen
131+
name="FormSheetWithFixedHeight"
132+
component={FormSheetWithFixedHeight}
133+
options={{
134+
...formSheetBaseOptions,
135+
sheetAllowedDetents: [0.3, 0.5, 0.8],
136+
}}
137+
/>
138+
<Stack.Screen
139+
name="FormSheetWithFlexAndFitToContents"
140+
component={FormSheetWithFlex}
141+
options={{
142+
...formSheetBaseOptions,
143+
sheetAllowedDetents: 'fitToContents',
144+
}}
145+
/>
146+
<Stack.Screen
147+
name="FormSheetWithFixedHeightAndFitToContents"
148+
component={FormSheetWithFixedHeight}
149+
options={{
150+
...formSheetBaseOptions,
151+
sheetAllowedDetents: 'fitToContents',
152+
}}
153+
/>
154+
</Stack.Navigator>
155+
</NavigationContainer>
156+
);
157+
}
158+
159+
const styles = StyleSheet.create({
160+
text: {
161+
fontSize: 20,
162+
margin: 8,
163+
textAlign: 'center',
164+
},
165+
});

apps/src/tests/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export { default as Test2332 } from './Test2332';
119119
export { default as Test2379 } from './Test2379';
120120
export { default as Test2395 } from './Test2395';
121121
export { default as Test2466 } from './Test2466';
122+
export { default as Test2522 } from './Test2522';
122123
export { default as Test2538 } from './Test2538';
123124
export { default as Test2543 } from './Test2543'; // [E2E created](iOS): issue related to iOS formSheet initial detent
124125
export { default as Test2552 } from './Test2552';

src/components/ScreenStackItem.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { RNSScreensRefContext } from '../contexts';
2121
import { FooterComponent } from './ScreenFooter';
2222
import { SafeAreaViewProps } from './safe-area/SafeAreaView.types';
2323
import SafeAreaView from './safe-area/SafeAreaView';
24+
import { featureFlags } from '../flags';
2425

2526
type Props = Omit<
2627
ScreenProps,
@@ -208,13 +209,22 @@ function getPositioningStyle(
208209
presentation: StackPresentationTypes,
209210
) {
210211
const isIOS = Platform.OS === 'ios';
212+
const rnMinorVersion = Platform.constants.reactNativeVersion.minor;
211213

212214
if (presentation !== 'formSheet') {
213215
return styles.container;
214216
}
215217

216218
if (isIOS) {
217-
return styles.absoluteWithNoBottom;
219+
if (
220+
allowedDetents !== 'fitToContents' &&
221+
rnMinorVersion >= 82 &&
222+
featureFlags.experiment.synchronousScreenUpdatesEnabled
223+
) {
224+
return styles.container;
225+
} else {
226+
return styles.absoluteWithNoBottom;
227+
}
218228
}
219229

220230
// Other platforms, tested reliably only on Android

0 commit comments

Comments
 (0)