11import { 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" ;
33import BottomSheet , {
44 BottomSheetBackdrop ,
55 BottomSheetBackdropProps ,
@@ -8,6 +8,20 @@ import BottomSheet, {
88import { EditableValue , ValueStatus } from "mendix" ;
99import { 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+
1125interface 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} ;
0 commit comments