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" ;
34import BottomSheet , {
45 BottomSheetBackdrop ,
56 BottomSheetBackdropProps ,
@@ -8,20 +9,6 @@ import BottomSheet, {
89import { EditableValue , ValueStatus } from "mendix" ;
910import { 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-
2512interface CustomModalSheetProps {
2613 triggerAttribute ?: EditableValue < boolean > ;
2714 content ?: ReactNode ;
@@ -30,150 +17,85 @@ interface CustomModalSheetProps {
3017
3118export 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