Skip to content

Commit e7518c4

Browse files
committed
fix(bottom-sheet-native): improve toggle animations and initial render
1 parent 3800099 commit e7518c4

2 files changed

Lines changed: 176 additions & 65 deletions

File tree

packages/pluggableWidgets/bottom-sheet-native/src/components/CustomModalSheet.tsx

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReactElement, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
2-
import { Dimensions, LayoutChangeEvent, Modal, Pressable } from "react-native";
2+
import { Animated, Dimensions, LayoutChangeEvent, Modal, Pressable, View } from "react-native";
33
import BottomSheet, {
44
BottomSheetBackdrop,
55
BottomSheetBackdropProps,
@@ -8,6 +8,20 @@ import BottomSheet, {
88
import { EditableValue, ValueStatus } from "mendix";
99
import { BottomSheetStyle } from "../ui/Styles";
1010

11+
const BACKDROP_FADE_IN_DURATION = 200;
12+
const BACKDROP_FADE_OUT_DURATION = 150;
13+
// Delay before animating sheet open to ensure Modal and BottomSheet are fully laid out
14+
const SHEET_ANIMATION_DELAY = 50;
15+
16+
// Styles for off-screen measurement container
17+
const MEASUREMENT_CONTAINER_STYLE = {
18+
position: "absolute" as const,
19+
opacity: 0,
20+
top: -10000, // Position far off-screen to avoid any layout interference
21+
left: 0,
22+
pointerEvents: "none" as const
23+
};
24+
1125
interface CustomModalSheetProps {
1226
triggerAttribute?: EditableValue<boolean>;
1327
content?: ReactNode;
@@ -18,6 +32,8 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement =>
1832
const bottomSheetRef = useRef<BottomSheet>(null);
1933
const [contentHeight, setContentHeight] = useState(0);
2034
const [currentStatus, setCurrentStatus] = useState(false);
35+
const [isMeasured, setIsMeasured] = useState(false);
36+
const backdropOpacity = useRef(new Animated.Value(0)).current;
2137

2238
const isAvailable = props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available;
2339

@@ -31,6 +47,7 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement =>
3147
const layoutHeight = event.nativeEvent.layout.height;
3248
if (layoutHeight > 0 && layoutHeight !== contentHeight) {
3349
setContentHeight(layoutHeight);
50+
setIsMeasured(true);
3451
}
3552
},
3653
[contentHeight]
@@ -42,17 +59,29 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement =>
4259

4360
const renderBackdrop = useCallback(
4461
(backdropProps: BottomSheetBackdropProps) => (
45-
<Pressable style={{ flex: 1 }} onPress={close}>
46-
<BottomSheetBackdrop
47-
{...backdropProps}
48-
pressBehavior={"close"}
49-
opacity={0.3}
50-
appearsOnIndex={0}
51-
disappearsOnIndex={-1}
52-
/>
53-
</Pressable>
62+
<Animated.View
63+
style={{
64+
position: "absolute",
65+
top: 0,
66+
left: 0,
67+
right: 0,
68+
bottom: 0,
69+
backgroundColor: "rgba(0, 0, 0, 0.3)",
70+
opacity: backdropOpacity
71+
}}
72+
>
73+
<Pressable style={{ flex: 1 }} onPress={close}>
74+
<BottomSheetBackdrop
75+
{...backdropProps}
76+
pressBehavior={"close"}
77+
opacity={0}
78+
appearsOnIndex={0}
79+
disappearsOnIndex={-1}
80+
/>
81+
</Pressable>
82+
</Animated.View>
5483
),
55-
[close]
84+
[close, backdropOpacity]
5685
);
5786

5887
const snapPoints = useMemo(() => {
@@ -89,38 +118,62 @@ export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement =>
89118
const shouldBeOpen = props.triggerAttribute?.value === true;
90119

91120
if (shouldBeOpen && !currentStatus) {
121+
setCurrentStatus(true);
92122
requestAnimationFrame(() => {
93-
setCurrentStatus(true);
123+
// Fade in backdrop - this helps smooth the transition as the sheet opens, reducing the perception of any initial stuttering.
124+
Animated.timing(backdropOpacity, {
125+
toValue: 1,
126+
duration: BACKDROP_FADE_IN_DURATION,
127+
useNativeDriver: true
128+
}).start();
129+
// Delay animation to ensure Modal and BottomSheet are fully mounted and laid out
130+
setTimeout(() => {
131+
bottomSheetRef.current?.snapToIndex(0);
132+
}, SHEET_ANIMATION_DELAY);
94133
});
95134
} else if (!shouldBeOpen && currentStatus) {
96135
bottomSheetRef.current?.close();
97-
setCurrentStatus(false);
136+
Animated.timing(backdropOpacity, {
137+
toValue: 0,
138+
duration: BACKDROP_FADE_OUT_DURATION,
139+
useNativeDriver: true
140+
}).start(() => {
141+
setCurrentStatus(false);
142+
});
98143
}
99-
}, [props.triggerAttribute?.value, currentStatus, isAvailable]);
144+
}, [props.triggerAttribute?.value, currentStatus, isAvailable, backdropOpacity]);
100145

101146
return (
102-
<Modal onRequestClose={close} transparent visible={!!isOpen}>
103-
<BottomSheet
104-
ref={bottomSheetRef}
105-
index={isOpen ? 0 : -1}
106-
snapPoints={snapPoints}
107-
onClose={() => handleSheetChanges(-1)}
108-
onChange={handleSheetChanges}
109-
backdropComponent={renderBackdrop}
110-
style={[props.styles.modal]}
111-
backgroundStyle={props.styles.container}
112-
enablePanDownToClose={false}
113-
handleComponent={null}
114-
handleStyle={{ display: "none" }}
115-
>
116-
<BottomSheetScrollView
117-
style={[{ flex: 1 }]}
118-
contentContainerStyle={{ paddingBottom: 16 }}
147+
<>
148+
{/* Off-screen measurement - measure content before showing Modal to prevent sudden jumps in layout */}
149+
{!isMeasured && (
150+
<View
151+
style={[MEASUREMENT_CONTAINER_STYLE, { width: Dimensions.get("screen").width }]}
119152
onLayout={onContentLayoutHandler}
120153
>
121154
{props.content}
122-
</BottomSheetScrollView>
123-
</BottomSheet>
124-
</Modal>
155+
</View>
156+
)}
157+
158+
<Modal onRequestClose={close} transparent visible={isOpen && isMeasured}>
159+
<BottomSheet
160+
ref={bottomSheetRef}
161+
index={-1}
162+
snapPoints={snapPoints}
163+
onClose={() => handleSheetChanges(-1)}
164+
onChange={handleSheetChanges}
165+
backdropComponent={renderBackdrop}
166+
style={[props.styles.modal]}
167+
backgroundStyle={props.styles.container}
168+
enablePanDownToClose={false}
169+
handleComponent={null}
170+
handleStyle={{ display: "none" }}
171+
>
172+
<BottomSheetScrollView style={[{ flex: 1 }]} contentContainerStyle={{ paddingBottom: 16 }}>
173+
{props.content}
174+
</BottomSheetScrollView>
175+
</BottomSheet>
176+
</Modal>
177+
</>
125178
);
126179
};

packages/pluggableWidgets/bottom-sheet-native/src/components/NativeBottomSheet.tsx

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
22
import {
33
ActionSheetIOS,
4+
Animated,
45
Appearance,
56
Dimensions,
67
LayoutChangeEvent,
@@ -22,6 +23,20 @@ import { ItemsBasicType } from "../../typings/BottomSheetProps";
2223
import { BottomSheetStyle, ModalItemContainerStyle } from "../ui/Styles";
2324
import { executeAction } from "@mendix/piw-utils-internal";
2425

26+
const BACKDROP_FADE_IN_DURATION = 200;
27+
const BACKDROP_FADE_OUT_DURATION = 150;
28+
// Delay before animating sheet open to ensure Modal and BottomSheet are fully laid out
29+
const SHEET_ANIMATION_DELAY = 50;
30+
31+
// Styles for off-screen measurement container
32+
const MEASUREMENT_CONTAINER_STYLE = {
33+
position: "absolute" as const,
34+
opacity: 0,
35+
top: -10000, // Position far off-screen to avoid any layout interference
36+
left: 0,
37+
pointerEvents: "none" as const
38+
};
39+
2540
interface NativeBottomSheetProps {
2641
name: string;
2742
triggerAttribute?: EditableValue<boolean>;
@@ -35,6 +50,8 @@ let lastIndexRef = -1;
3550
export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement => {
3651
const bottomSheetRef = useRef<BottomSheet>(null);
3752
const [contentHeight, setContentHeight] = useState(0);
53+
const [isMeasured, setIsMeasured] = useState(false);
54+
const backdropOpacity = useRef(new Animated.Value(0)).current;
3855

3956
const isAvailable = props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available;
4057
const isOpen =
@@ -45,12 +62,28 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement =
4562
const manageBottomSheet = useCallback(() => {
4663
if (props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available) {
4764
if (props.triggerAttribute.value) {
48-
bottomSheetRef.current?.snapToIndex(0);
65+
requestAnimationFrame(() => {
66+
// Fade in backdrop - this helps smooth the transition as the sheet opens, reducing the perception of any initial stuttering.
67+
Animated.timing(backdropOpacity, {
68+
toValue: 1,
69+
duration: BACKDROP_FADE_IN_DURATION,
70+
useNativeDriver: true
71+
}).start();
72+
// Delay animation to ensure Modal and BottomSheet are fully mounted and laid out
73+
setTimeout(() => {
74+
bottomSheetRef.current?.snapToIndex(0);
75+
}, SHEET_ANIMATION_DELAY);
76+
});
4977
} else {
5078
bottomSheetRef.current?.close();
79+
Animated.timing(backdropOpacity, {
80+
toValue: 0,
81+
duration: BACKDROP_FADE_OUT_DURATION,
82+
useNativeDriver: true
83+
}).start();
5184
}
5285
}
53-
}, [props.triggerAttribute]);
86+
}, [props.triggerAttribute, backdropOpacity]);
5487

5588
useEffect(() => {
5689
manageBottomSheet();
@@ -91,24 +124,37 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement =
91124

92125
const renderBackdrop = useCallback(
93126
(backdropProps: BottomSheetBackdropProps) => (
94-
<Pressable style={{ flex: 1 }} onPress={close}>
95-
<BottomSheetBackdrop
96-
{...backdropProps}
97-
pressBehavior="close"
98-
opacity={0.3}
99-
appearsOnIndex={0}
100-
disappearsOnIndex={-1}
101-
/>
102-
</Pressable>
127+
<Animated.View
128+
style={{
129+
position: "absolute",
130+
top: 0,
131+
left: 0,
132+
right: 0,
133+
bottom: 0,
134+
backgroundColor: "rgba(0, 0, 0, 0.3)",
135+
opacity: backdropOpacity
136+
}}
137+
>
138+
<Pressable style={{ flex: 1 }} onPress={close}>
139+
<BottomSheetBackdrop
140+
{...backdropProps}
141+
pressBehavior="close"
142+
opacity={0}
143+
appearsOnIndex={0}
144+
disappearsOnIndex={-1}
145+
/>
146+
</Pressable>
147+
</Animated.View>
103148
),
104-
[close]
149+
[close, backdropOpacity]
105150
);
106151

107152
const onLayoutHandler = useCallback(
108153
(event: LayoutChangeEvent) => {
109154
const height = event.nativeEvent.layout.height;
110155
if (height > 0 && height !== contentHeight) {
111156
setContentHeight(height);
157+
setIsMeasured(true);
112158
}
113159
},
114160
[contentHeight]
@@ -206,26 +252,38 @@ export const NativeBottomSheet = (props: NativeBottomSheetProps): ReactElement =
206252
}
207253

208254
return (
209-
<Modal onRequestClose={close} transparent visible={isOpen}>
210-
<BottomSheet
211-
ref={bottomSheetRef}
212-
index={isOpen && contentHeight > 0 ? 0 : -1}
213-
snapPoints={snapPoints}
214-
enablePanDownToClose
215-
animateOnMount={false}
216-
onClose={() => handleSheetChanges(-1)}
217-
onChange={handleSheetChanges}
218-
style={getContainerStyle()}
219-
backdropComponent={renderBackdrop}
220-
backgroundStyle={props.styles.container}
221-
handleComponent={null}
222-
handleStyle={{ display: "none" }}
223-
>
224-
<BottomSheetScrollView contentContainerStyle={{ paddingBottom: 16 }} onLayout={onLayoutHandler}>
255+
<>
256+
{/* Off-screen measurement - measure content before showing Modal to prevent sudden jumps in layout */}
257+
{!isMeasured && (
258+
<View
259+
style={[MEASUREMENT_CONTAINER_STYLE, { width: Dimensions.get("screen").width }]}
260+
onLayout={onLayoutHandler}
261+
>
225262
{props.itemsBasic.map((item, index) => renderItem(item, index))}
226-
</BottomSheetScrollView>
227-
</BottomSheet>
228-
</Modal>
263+
</View>
264+
)}
265+
266+
<Modal onRequestClose={close} transparent visible={isOpen && isMeasured}>
267+
<BottomSheet
268+
ref={bottomSheetRef}
269+
index={-1}
270+
snapPoints={snapPoints}
271+
enablePanDownToClose
272+
animateOnMount={false}
273+
onClose={() => handleSheetChanges(-1)}
274+
onChange={handleSheetChanges}
275+
style={getContainerStyle()}
276+
backdropComponent={renderBackdrop}
277+
backgroundStyle={props.styles.container}
278+
handleComponent={null}
279+
handleStyle={{ display: "none" }}
280+
>
281+
<BottomSheetScrollView contentContainerStyle={{ paddingBottom: 16 }}>
282+
{props.itemsBasic.map((item, index) => renderItem(item, index))}
283+
</BottomSheetScrollView>
284+
</BottomSheet>
285+
</Modal>
286+
</>
229287
);
230288
};
231289

0 commit comments

Comments
 (0)