Skip to content

Commit df36f05

Browse files
committed
fix(bottom-sheet-native): simplify modal and bottom sheet implementation
1 parent e3f4168 commit df36f05

3 files changed

Lines changed: 138 additions & 434 deletions

File tree

packages/pluggableWidgets/bottom-sheet-native/src/__tests__/__snapshots__/BottomSheet.spec.tsx.snap

Lines changed: 2 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,8 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`Bottom sheet renders a custom bottom action sheet for ios (Basic modal) with custom style 1`] = `
4-
<View
5-
onLayout={[Function]}
6-
style={
7-
[
8-
{
9-
"left": 0,
10-
"opacity": 0,
11-
"pointerEvents": "none",
12-
"position": "absolute",
13-
"top": -10000,
14-
},
15-
{
16-
"width": 750,
17-
},
18-
]
19-
}
20-
>
21-
<View
22-
accessibilityValue={
23-
{
24-
"max": undefined,
25-
"min": undefined,
26-
"now": undefined,
27-
"text": undefined,
28-
}
29-
}
30-
accessible={true}
31-
focusable={true}
32-
onClick={[Function]}
33-
onResponderGrant={[Function]}
34-
onResponderMove={[Function]}
35-
onResponderRelease={[Function]}
36-
onResponderTerminate={[Function]}
37-
onResponderTerminationRequest={[Function]}
38-
onStartShouldSetResponder={[Function]}
39-
>
40-
<View
41-
style={
42-
[
43-
{
44-
"flex": 1,
45-
"paddingHorizontal": 16,
46-
},
47-
[
48-
{
49-
"paddingVertical": 12,
50-
"textAlign": "center",
51-
},
52-
{},
53-
],
54-
]
55-
}
56-
>
57-
<Text
58-
style={
59-
{
60-
"color": "red",
61-
"fontSize": 60,
62-
}
63-
}
64-
>
65-
Item 1
66-
</Text>
67-
</View>
68-
</View>
69-
<View
70-
accessibilityValue={
71-
{
72-
"max": undefined,
73-
"min": undefined,
74-
"now": undefined,
75-
"text": undefined,
76-
}
77-
}
78-
accessible={true}
79-
focusable={true}
80-
onClick={[Function]}
81-
onResponderGrant={[Function]}
82-
onResponderMove={[Function]}
83-
onResponderRelease={[Function]}
84-
onResponderTerminate={[Function]}
85-
onResponderTerminationRequest={[Function]}
86-
onStartShouldSetResponder={[Function]}
87-
>
88-
<View
89-
style={
90-
[
91-
{
92-
"flex": 1,
93-
"paddingHorizontal": 16,
94-
},
95-
[
96-
{
97-
"paddingVertical": 12,
98-
"textAlign": "center",
99-
},
100-
{},
101-
],
102-
]
103-
}
104-
>
105-
<Text
106-
style={
107-
{
108-
"color": "red",
109-
"fontSize": 60,
110-
}
111-
}
112-
>
113-
Item 2
114-
</Text>
115-
</View>
116-
</View>
117-
</View>
118-
`;
3+
exports[`Bottom sheet renders a custom bottom action sheet for ios (Basic modal) with custom style 1`] = `null`;
1194

120-
exports[`Bottom sheet renders a custom modal 1`] = `
121-
<View
122-
onLayout={[Function]}
123-
style={
124-
[
125-
{
126-
"left": 0,
127-
"opacity": 0,
128-
"pointerEvents": "none",
129-
"position": "absolute",
130-
"top": -10000,
131-
},
132-
{
133-
"width": 750,
134-
},
135-
]
136-
}
137-
>
138-
<Text />
139-
</View>
140-
`;
5+
exports[`Bottom sheet renders a custom modal 1`] = `null`;
1416

1427
exports[`Bottom sheet renders a expanding 1`] = `
1438
<View
Lines changed: 62 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { ReactElement, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
2-
import { Animated, Dimensions, LayoutChangeEvent, Modal, Pressable, View } from "react-native";
1+
import { ReactElement, ReactNode, useCallback, useRef, useState } from "react";
2+
import { Modal, useWindowDimensions } from "react-native";
3+
import { GestureHandlerRootView } from "react-native-gesture-handler";
34
import BottomSheet, {
45
BottomSheetBackdrop,
56
BottomSheetBackdropProps,
@@ -8,20 +9,6 @@ import BottomSheet, {
89
import { EditableValue, ValueStatus } from "mendix";
910
import { BottomSheetStyle } from "../ui/Styles";
1011

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-
2512
interface CustomModalSheetProps {
2613
triggerAttribute?: EditableValue<boolean>;
2714
content?: ReactNode;
@@ -30,150 +17,85 @@ interface CustomModalSheetProps {
3017

3118
export const CustomModalSheet = (props: CustomModalSheetProps): ReactElement => {
3219
const bottomSheetRef = useRef<BottomSheet>(null);
33-
const [contentHeight, setContentHeight] = useState(0);
34-
const [currentStatus, setCurrentStatus] = useState(false);
35-
const [isMeasured, setIsMeasured] = useState(false);
36-
const backdropOpacity = useRef(new Animated.Value(0)).current;
20+
const { height: windowHeight } = useWindowDimensions();
3721

38-
const isAvailable = props.triggerAttribute && props.triggerAttribute.status === ValueStatus.Available;
22+
const externalOpen =
23+
props.triggerAttribute?.status === ValueStatus.Available && props.triggerAttribute.value === true;
3924

40-
const isOpen =
41-
props.triggerAttribute &&
42-
props.triggerAttribute.status === ValueStatus.Available &&
43-
props.triggerAttribute.value;
25+
const [mounted, setMounted] = useState(externalOpen);
26+
const [ready, setReady] = useState(false);
27+
const didOpenRef = useRef(false);
4428

45-
const onContentLayoutHandler = useCallback(
46-
(event: LayoutChangeEvent): void => {
47-
const layoutHeight = event.nativeEvent.layout.height;
48-
if (layoutHeight > 0 && layoutHeight !== contentHeight) {
49-
setContentHeight(layoutHeight);
50-
setIsMeasured(true);
51-
}
52-
},
53-
[contentHeight]
54-
);
29+
if (externalOpen && !mounted) {
30+
setMounted(true);
31+
}
5532

5633
const close = useCallback(() => {
5734
bottomSheetRef.current?.close();
5835
}, []);
5936

60-
const renderBackdrop = useCallback(
61-
(backdropProps: BottomSheetBackdropProps) => (
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>
83-
),
84-
[close, backdropOpacity]
85-
);
86-
87-
const snapPoints = useMemo(() => {
88-
if (contentHeight === 0) {
89-
return [1]; // During measurement
90-
}
91-
92-
// Use actual measured content height, cap at 90% screen
93-
const maxHeight = Dimensions.get("screen").height * 0.9;
94-
const snapHeight = Math.min(contentHeight, maxHeight);
95-
return [snapHeight];
96-
}, [contentHeight]);
37+
const handleModalShow = useCallback(() => {
38+
setReady(true);
39+
}, []);
9740

98-
const handleSheetChanges = useCallback(
41+
const handleChange = useCallback(
9942
(index: number) => {
100-
if (!isAvailable) {
43+
if (index === 0) {
44+
didOpenRef.current = true;
10145
return;
10246
}
10347

104-
const hasClosed = index === -1;
105-
if (hasClosed && props.triggerAttribute?.value) {
48+
if (index === -1 && didOpenRef.current) {
49+
didOpenRef.current = false;
50+
setReady(false);
10651
props.triggerAttribute?.setValue(false);
107-
setCurrentStatus(false);
52+
setMounted(false);
10853
}
10954
},
110-
[isAvailable, props.triggerAttribute]
55+
[props.triggerAttribute]
11156
);
11257

113-
useEffect(() => {
114-
if (!isAvailable) {
115-
return;
116-
}
117-
118-
const shouldBeOpen = props.triggerAttribute?.value === true;
58+
const renderBackdrop = useCallback(
59+
(backdropProps: BottomSheetBackdropProps) => (
60+
<BottomSheetBackdrop
61+
{...backdropProps}
62+
pressBehavior="close"
63+
opacity={0.3}
64+
appearsOnIndex={0}
65+
disappearsOnIndex={-1}
66+
/>
67+
),
68+
[]
69+
);
11970

120-
if (shouldBeOpen && !currentStatus) {
121-
setCurrentStatus(true);
122-
requestAnimationFrame(() => {
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);
133-
});
134-
} else if (!shouldBeOpen && currentStatus) {
135-
bottomSheetRef.current?.close();
136-
Animated.timing(backdropOpacity, {
137-
toValue: 0,
138-
duration: BACKDROP_FADE_OUT_DURATION,
139-
useNativeDriver: true
140-
}).start(() => {
141-
setCurrentStatus(false);
142-
});
143-
}
144-
}, [props.triggerAttribute?.value, currentStatus, isAvailable, backdropOpacity]);
71+
const maxHeight = windowHeight * 0.9;
14572

14673
return (
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 }]}
152-
onLayout={onContentLayoutHandler}
153-
>
154-
{props.content}
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-
</>
74+
<Modal transparent animationType="none" visible={mounted} onRequestClose={close} onShow={handleModalShow}>
75+
<GestureHandlerRootView style={{ flex: 1 }}>
76+
{ready && (
77+
<BottomSheet
78+
ref={bottomSheetRef}
79+
index={0}
80+
animateOnMount
81+
enableDynamicSizing
82+
maxDynamicContentSize={maxHeight}
83+
containerHeight={windowHeight}
84+
enablePanDownToClose
85+
onChange={handleChange}
86+
onClose={() => handleChange(-1)}
87+
backdropComponent={renderBackdrop}
88+
style={[props.styles.modal]}
89+
backgroundStyle={props.styles.container}
90+
handleComponent={null}
91+
handleStyle={{ display: "none" }}
92+
>
93+
<BottomSheetScrollView style={[{ flex: 1 }]} contentContainerStyle={{ paddingBottom: 16 }}>
94+
{props.content}
95+
</BottomSheetScrollView>
96+
</BottomSheet>
97+
)}
98+
</GestureHandlerRootView>
99+
</Modal>
178100
);
179101
};

0 commit comments

Comments
 (0)