Skip to content

Commit 3c156b8

Browse files
authored
Merge branch 'main' into @maciekstosio/Fix-useHeaderHeight
2 parents b6fbfea + 4d044da commit 3c156b8

20 files changed

Lines changed: 233 additions & 52 deletions

android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ class ScreenStackHeaderSubview(
1010
) : FabricEnabledHeaderSubviewViewGroup(context) {
1111
private var reactWidth = 0
1212
private var reactHeight = 0
13+
14+
/**
15+
* Semantics: true iff we **believe** that SurfaceMountingManager has measured this view during mount item
16+
* execution. We recognize this case by checking measure mode in `onMeasure`. If Androidx
17+
* happens to use `EXACTLY` for both dimensions this property might convey invalid information.
18+
*/
19+
private var isReactSizeSet = false
20+
1321
var type = Type.RIGHT
1422

1523
val config: ScreenStackHeaderConfig?
@@ -22,9 +30,10 @@ class ScreenStackHeaderSubview(
2230
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY &&
2331
MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY
2432
) {
25-
// dimensions provided by react
33+
// dimensions provided by react (with high probability)
2634
reactWidth = MeasureSpec.getSize(widthMeasureSpec)
2735
reactHeight = MeasureSpec.getSize(heightMeasureSpec)
36+
isReactSizeSet = true
2837
val parent = parent
2938
if (parent != null) {
3039
forceLayout()
@@ -44,7 +53,15 @@ class ScreenStackHeaderSubview(
4453
if (changed && BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
4554
val width = r - l
4655
val height = b - t
47-
updateSubviewFrameState(width, height, l, t)
56+
57+
// When setting subviews via `setOptions` from `useEffect` hook in a component, the first
58+
// frame received might be computed by native layout & completely invalid (zero height).
59+
// RN layout is the source of subview **size** (not origin) & we need to avoid sending
60+
// this native size to ST. Doing otherwise might lead to problems.
61+
// See: https://github.com/software-mansion/react-native-screens/pull/2812
62+
if (isReactSizeSet) {
63+
updateSubviewFrameState(width, height, l, t)
64+
}
4865
}
4966
}
5067

android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ open class ScreenViewManager :
126126
when (presentation) {
127127
"push" -> Screen.StackPresentation.PUSH
128128
"formSheet" -> Screen.StackPresentation.FORM_SHEET
129-
"modal", "containedModal", "fullScreenModal" ->
129+
"modal", "containedModal", "fullScreenModal", "pageSheet" ->
130130
Screen.StackPresentation.MODAL
131131
"transparentModal", "containedTransparentModal" ->
132132
Screen.StackPresentation.TRANSPARENT_MODAL

apps/Example.tsx

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,34 @@ if (isTestSectionEnabled()) {
148148
});
149149
}
150150

151-
152151
const screens = Object.keys(SCREENS);
153152
const examples = screens.filter(name => SCREENS[name].type === 'example');
154153
const playgrounds = screens.filter(name => SCREENS[name].type === 'playground');
155-
const tests = isTestSectionEnabled() ? screens.filter(name => SCREENS[name].type === 'test') : [];
154+
const tests = isTestSectionEnabled()
155+
? screens
156+
.filter(name => SCREENS[name].type === 'test')
157+
.sort((name1, name2) => {
158+
const testNumber1 = Number(name1.substring(4));
159+
const testNumber2 = Number(name2.substring(4));
160+
161+
if (Number.isNaN(testNumber1) && Number.isNaN(testNumber2)) {
162+
return 0;
163+
} else if (Number.isNaN(testNumber1)) {
164+
return 1;
165+
} else if (Number.isNaN(testNumber2)) {
166+
return -1;
167+
} else {
168+
return testNumber1 - testNumber2;
169+
}
170+
})
171+
: [];
156172

157173
type RootStackParamList = {
158174
Main: undefined;
159175
Tests: undefined;
160176
} & {
161-
[P in keyof typeof SCREENS]: undefined;
162-
};
177+
[P in keyof typeof SCREENS]: undefined;
178+
};
163179

164180
const Stack = createNativeStackNavigator<RootStackParamList>();
165181

@@ -210,17 +226,19 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => {
210226
disabled={!isPlatformReady(name)}
211227
/>
212228
))}
229+
{isTestSectionEnabled() && (
230+
<ThemedText style={styles.label}>Tests</ThemedText>
231+
)}
213232
{isTestSectionEnabled() &&
214-
<ThemedText style={styles.label}>Tests</ThemedText>}
215-
{isTestSectionEnabled() && tests.map(name => (
216-
<ListItem
217-
key={name}
218-
testID={`root-screen-tests-${name}`}
219-
title={SCREENS[name].title}
220-
onPress={() => navigation.navigate(name)}
221-
disabled={false}
222-
/>
223-
))}
233+
tests.map(name => (
234+
<ListItem
235+
key={name}
236+
testID={`root-screen-tests-${name}`}
237+
title={SCREENS[name].title}
238+
onPress={() => navigation.navigate(name)}
239+
disabled={false}
240+
/>
241+
))}
224242
</ScrollView>
225243
);
226244
};
@@ -245,8 +263,9 @@ const ExampleApp = (): React.JSX.Element => {
245263
<Stack.Screen
246264
name="Main"
247265
options={{
248-
title: `${Platform.isTV ? '📺' : '📱'
249-
} React Native Screens Examples`,
266+
title: `${
267+
Platform.isTV ? '📺' : '📱'
268+
} React Native Screens Examples`,
250269
}}
251270
component={MainScreen}
252271
/>

apps/src/screens/Modals.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type StackParamList = {
1212
FullscreenModal: undefined;
1313
Alert: undefined;
1414
ContainedModal: undefined;
15+
PageSheet: undefined;
1516
};
1617

1718
interface MainScreenProps {
@@ -30,6 +31,10 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => (
3031
title="Open contained modal"
3132
onPress={() => navigation.navigate('ContainedModal')}
3233
/>
34+
<Button
35+
title="Open pageSheet"
36+
onPress={() => navigation.navigate('PageSheet')}
37+
/>
3338
<Button onPress={() => navigation.pop()} title="🔙 Back to Examples" />
3439
</View>
3540
);
@@ -50,6 +55,10 @@ const ModalScreen = ({ navigation }: ModalScreenProps): React.JSX.Element => (
5055
title="Open contained modal"
5156
onPress={() => navigation.navigate('ContainedModal')}
5257
/>
58+
<Button
59+
title="Open pageSheet"
60+
onPress={() => navigation.push('PageSheet')}
61+
/>
5362
<Button title="Go back" onPress={() => navigation.goBack()} />
5463
</View>
5564
);
@@ -81,6 +90,11 @@ const App = (): React.JSX.Element => (
8190
component={ModalScreen}
8291
options={{ presentation: 'containedModal' }}
8392
/>
93+
<Stack.Screen
94+
name="PageSheet"
95+
component={ModalScreen}
96+
options={{ presentation: 'pageSheet' }}
97+
/>
8498
<Stack.Screen
8599
name="Alert"
86100
component={Alert}

apps/src/screens/StackPresentation.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type StackParamList = {
1616
ContainedTransparentModal: undefined;
1717
FullScreenModal: undefined;
1818
FormSheet: { usesFormSheetPresentation?: boolean };
19+
PageSheet: undefined;
1920
};
2021

2122
interface MainScreenProps {
@@ -62,6 +63,11 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => {
6263
onPress={() => navigation.navigate('FormSheet')}
6364
testID="stack-presentation-form-sheet-button"
6465
/>
66+
<Button
67+
title="pageSheet"
68+
onPress={() => navigation.navigate('PageSheet')}
69+
testID="stack-presentation-page-sheet-button"
70+
/>
6571
<Button
6672
testID="stack-presentation-go-back-button"
6773
onPress={() => navigation.pop()}
@@ -123,6 +129,22 @@ const ModalScreen = ({ navigation }: ModalScreenProps): React.JSX.Element => (
123129
</View>
124130
);
125131

132+
interface PageSheetScreenProps {
133+
navigation: NativeStackNavigationProp<ParamListBase>;
134+
}
135+
136+
const PageSheetScreen = ({ navigation }: PageSheetScreenProps): React.JSX.Element => (
137+
<View style={styles.container}>
138+
<Choose />
139+
<Button
140+
testID="stack-presentation-page-sheet-screen-go-back-button"
141+
title="Go back"
142+
onPress={() => navigation.goBack()}
143+
/>
144+
</View>
145+
);
146+
147+
126148
interface FullScreenModalProps {
127149
navigation: NativeStackNavigationProp<ParamListBase>;
128150
}
@@ -204,6 +226,11 @@ const App = (): React.JSX.Element => (
204226
usesFormSheetPresentation: true
205227
}}
206228
/>
229+
<Stack.Screen
230+
name="PageSheet"
231+
component={PageSheetScreen}
232+
options={{ presentation: 'pageSheet' }}
233+
/>
207234
</Stack.Navigator>
208235
);
209236

apps/src/tests/Test2811.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { NavigationContainer } from '@react-navigation/native';
2+
import { NativeStackNavigationProp, createNativeStackNavigator } from '@react-navigation/native-stack';
3+
import React from 'react';
4+
import { Text, View } from 'react-native';
5+
6+
type StackRouteParamList = {
7+
Home: undefined;
8+
};
9+
10+
type StackRouteNavProps = {
11+
navigation: NativeStackNavigationProp<StackRouteParamList>;
12+
};
13+
14+
const Stack = createNativeStackNavigator<StackRouteParamList>();
15+
16+
function Home({ navigation }: StackRouteNavProps) {
17+
18+
React.useEffect(() => {
19+
const timerHandle = setTimeout(() => {
20+
navigation.setOptions({
21+
headerRight: () => <HeaderSubview size={20} />,
22+
});
23+
}, 1500);
24+
25+
return () => {
26+
clearTimeout(timerHandle);
27+
};
28+
}, [navigation]);
29+
30+
//React.useEffect(() => {
31+
// navigation.setOptions({
32+
// headerRight: () => <HeaderSubview size={20} />,
33+
// });
34+
//}, [navigation]);
35+
36+
return (
37+
<View style={{ flex: 1, backgroundColor: 'seagreen' }}>
38+
<Text>Home</Text>
39+
</View>
40+
);
41+
}
42+
43+
function HeaderSubview({ size }: { size?: number }) {
44+
const finalSize = size ?? 20;
45+
return (
46+
<View style={{ backgroundColor: 'crimson', width: finalSize, height: finalSize, maxHeight: finalSize }} />
47+
);
48+
}
49+
50+
export default function App() {
51+
return (
52+
<NavigationContainer>
53+
<Stack.Navigator>
54+
<Stack.Screen name="Home" component={Home} options={{
55+
//headerRight: () => <HeaderSubview size={20} />,
56+
}} />
57+
</Stack.Navigator>
58+
</NavigationContainer>
59+
);
60+
}

apps/src/tests/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,14 @@ export { default as Test2379 } from './Test2379';
119119
export { default as Test2395 } from './Test2395';
120120
export { default as Test2466 } from './Test2466';
121121
export { default as Test2552 } from './Test2552';
122+
export { default as Test2611 } from './Test2611';
122123
export { default as Test2631 } from './Test2631';
123124
export { default as Test2668 } from './Test2668';
124125
export { default as Test2675 } from './Test2675';
125126
export { default as Test2717 } from './Test2717';
126127
export { default as Test2767 } from './Test2767';
127128
export { default as Test2789 } from './Test2789';
128-
export { default as Test2611 } from './Test2611';
129+
export { default as Test2811 } from './Test2811';
129130
export { default as TestScreenAnimation } from './TestScreenAnimation';
130131
export { default as TestScreenAnimationV5 } from './TestScreenAnimationV5';
131132
export { default as TestHeader } from './TestHeader';
@@ -138,3 +139,4 @@ export { default as TestFormSheet } from './TestFormSheet';
138139
export { default as TestAndroidTransitions } from './TestAndroidTransitions';
139140
export { default as TestAnimation } from './TestAnimation';
140141

142+

common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,21 @@ class RNSScreenStackHeaderConfigComponentDescriptor final
3434
shadowNode.getState());
3535
auto stateData = state->getData();
3636

37-
if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
38-
layoutableShadowNode.setSize(stateData.frameSize);
3937
#ifdef ANDROID
38+
if (stateData.frameSize.width != 0) {
39+
layoutableShadowNode.setSize({stateData.frameSize.width, YGUndefined});
4040
layoutableShadowNode.setPadding({
4141
stateData.paddingStart,
4242
0,
4343
stateData.paddingEnd,
4444
0,
4545
});
46-
#endif // ANDROID
4746
}
47+
#else
48+
if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
49+
layoutableShadowNode.setSize(stateData.frameSize);
50+
}
51+
#endif // ANDROID
4852

4953
ConcreteComponentDescriptor::adopt(shadowNode);
5054
#if !defined(ANDROID) && !defined(NDEBUG)

guides/GUIDE_FOR_LIBRARY_AUTHORS.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ Defines how the method that should be used to present the given screen. It is a
242242
- `containedTransparentModal` – Explained below.
243243
- `fullScreenModal` – Explained below.
244244
- `formSheet` – Explained below.
245+
- `pageSheet` - Explained below.
245246

246247
Using `containedModal` and `containedTransparentModal` with other types of modals in one native stack navigator is not recommended and can result in a freeze or a crash of the application.
247248

@@ -253,13 +254,14 @@ For iOS:
253254
* on iOS 12 and earlier will use [`UIModalPresentationFullScreen`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationfullscreen?language=objc).
254255
- `fullScreenModal` will use [`UIModalPresentationFullScreen`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationfullscreen?language=objc)
255256
- `formSheet` will use [`UIModalPresentationFormSheet`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationformsheet?language=objc)
257+
- `pageSheet` will use [`UIModalPresentationPageSheet`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/pagesheet?language=objc)
256258
- `transparentModal` will use [`UIModalPresentationOverFullScreen`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationoverfullscreen?language=objc)
257259
- `containedModal` will use [`UIModalPresentationCurrentContext`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationcurrentcontext?language=objc)
258260
- `containedTransparentModal` will use [`UIModalPresentationOverCurrentContext`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationovercurrentcontext?language=objc)
259261

260262
For Android:
261263

262-
`modal`, `containedModal`, `fullScreenModal`, `formSheet` will use `Screen.StackPresentation.MODAL`.
264+
`modal`, `containedModal`, `fullScreenModal`, `formSheet`, `pageSheet` will use `Screen.StackPresentation.MODAL`.
263265

264266
`transparentModal`, `containedTransparentModal` will use `Screen.StackPresentation.TRANSPARENT_MODAL`.
265267

@@ -502,6 +504,8 @@ Allows for customizing font size to be used for back button title on iOS.
502504

503505
Whether the back button title should be visible. Defaults to `true`.
504506

507+
When set to `false` it works as a "kill switch": it enforces `backButtonDisplayMode=minimal` and ignores `backButtonDisplayMode`, `backTitleFontSize`, `backTitleFontFamily`, `disableBackButtonMenu`, and `backTitle` works only for back button menu.
508+
505509
### `blurEffect` (iOS only)
506510

507511
Blur effect to be applied to the header. Works with `backgroundColor`'s alpha < 1.
@@ -520,7 +524,9 @@ Boolean indicating whether to show the menu on longPress of iOS >= 14 back butto
520524

521525
### `backButtonDisplayMode` (iOS only)
522526

523-
Enum value indicating display mode of **default** back button. It works on iOS >= 14, and is used only when none of: `backTitleFontFamily`, `backTitleFontSize`, `disableBackButtonMenu` or `backTitle` is set. Otherwise, when the button is customized, under the hood we use iOS native `backButtonItem` which overrides `backButtonDisplayMode`. Read more [#2123](https://github.com/software-mansion/react-native-screens/pull/2123). Possible options:
527+
Enum value indicating display mode of back button. It is used only when none of: `backTitleFontFamily`, `backTitleFontSize`, `disableBackButtonMenu`, `backTitle` and `backTitleVisible=false` is set. The `backTitleVisible` forces `backButtonDisplayMode: minimal` and omits other values. Read more [#2800](https://github.com/software-mansion/react-native-screens/pull/2800). The other props, under the hood, customize `backButtonItem` which overrides `backButtonDisplayMode`. Read more [#2123](https://github.com/software-mansion/react-native-screens/pull/2123).
528+
529+
Possible options:
524530

525531
- `default` – show given back button previous controller title, system generic or just icon based on available space
526532
- `generic` – show given system generic or just icon based on available space

ios/RNSConvert.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ + (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
4949
return RNSScreenStackPresentationFullScreenModal;
5050
case FormSheet:
5151
return RNSScreenStackPresentationFormSheet;
52+
case PageSheet:
53+
return RNSScreenStackPresentationPageSheet;
5254
case ContainedModal:
5355
return RNSScreenStackPresentationContainedModal;
5456
case TransparentModal:

0 commit comments

Comments
 (0)