11import type { VideoReadyForDisplayEvent } from 'expo-av' ;
22import type { ImageContentFit } from 'expo-image' ;
3- import React , { useCallback , useEffect , useLayoutEffect , useRef , useState } from 'react' ;
3+ import React , { useCallback , useEffect , useLayoutEffect , useMemo , useRef , useState } from 'react' ;
44import { Image , InteractionManager , View } from 'react-native' ;
5- import type { ImageResizeMode , ImageSourcePropType , StyleProp , TextStyle , ViewStyle } from 'react-native' ;
5+ // eslint-disable-next-line no-restricted-imports
6+ import type { ImageResizeMode , ImageSourcePropType , LayoutChangeEvent , ScrollView as RNScrollView , StyleProp , TextStyle , ViewStyle } from 'react-native' ;
67import { GestureHandlerRootView } from 'react-native-gesture-handler' ;
78import type { MergeExclusive } from 'type-fest' ;
9+ import useKeyboardState from '@hooks/useKeyboardState' ;
810import useLocalize from '@hooks/useLocalize' ;
911import useNetwork from '@hooks/useNetwork' ;
1012import useResponsiveLayout from '@hooks/useResponsiveLayout' ;
13+ import useSafeAreaInsets from '@hooks/useSafeAreaInsets' ;
1114import useStyleUtils from '@hooks/useStyleUtils' ;
1215import useThemeStyles from '@hooks/useThemeStyles' ;
1316import { parseFSAttributes } from '@libs/Fullstory' ;
@@ -209,6 +212,11 @@ function FeatureTrainingModal({
209212 const { shouldUseNarrowLayout} = useResponsiveLayout ( ) ;
210213 const { isOffline} = useNetwork ( ) ;
211214 const hasHelpButtonBeenPressed = useRef ( false ) ;
215+ const scrollViewRef = useRef < RNScrollView > ( null ) ;
216+ const [ containerHeight , setContainerHeight ] = useState ( 0 ) ;
217+ const [ contentHeight , setContentHeight ] = useState ( 0 ) ;
218+ const insets = useSafeAreaInsets ( ) ;
219+ const { isKeyboardActive} = useKeyboardState ( ) ;
212220
213221 useEffect ( ( ) => {
214222 InteractionManager . runAfterInteractions ( ( ) => {
@@ -355,8 +363,21 @@ function FeatureTrainingModal({
355363 */
356364 useLayoutEffect ( parseFSAttributes , [ ] ) ;
357365
366+ // Scrolls modal to the bottom when keyboard appears so the action buttons are visible.
367+ useEffect ( ( ) => {
368+ if ( contentHeight <= containerHeight || onboardingIsMediumOrLargerScreenWidth || ! shouldUseScrollView ) {
369+ return ;
370+ }
371+ scrollViewRef . current ?. scrollToEnd ( { animated : false } ) ;
372+ } , [ contentHeight , containerHeight , onboardingIsMediumOrLargerScreenWidth , shouldUseScrollView ] ) ;
373+
358374 const Wrapper = shouldUseScrollView ? ScrollView : View ;
359375
376+ const wrapperStyles = useMemo (
377+ ( ) => ( shouldUseScrollView ? StyleUtils . getScrollableFeatureTrainingModalStyles ( insets , isKeyboardActive ) : { } ) ,
378+ [ shouldUseScrollView , StyleUtils , insets , isKeyboardActive ] ,
379+ ) ;
380+
360381 return (
361382 < Modal
362383 avoidKeyboard = { avoidKeyboard }
@@ -384,11 +405,16 @@ function FeatureTrainingModal({
384405 onHelp ( ) ;
385406 } }
386407 shouldUseReanimatedModal
408+ shouldDisableBottomSafeAreaPadding = { shouldUseScrollView }
387409 >
388410 < Wrapper
389- style = { [ styles . mh100 , onboardingIsMediumOrLargerScreenWidth && StyleUtils . getWidthStyle ( width ) ] }
390- contentContainerStyle = { shouldUseScrollView ? styles . pb5 : undefined }
411+ scrollsToTop = { false }
412+ style = { [ styles . mh100 , onboardingIsMediumOrLargerScreenWidth && StyleUtils . getWidthStyle ( width ) , wrapperStyles . style ] }
413+ contentContainerStyle = { wrapperStyles . containerStyle }
391414 keyboardShouldPersistTaps = { shouldUseScrollView ? 'handled' : undefined }
415+ ref = { shouldUseScrollView ? scrollViewRef : undefined }
416+ onLayout = { shouldUseScrollView ? ( e : LayoutChangeEvent ) => setContainerHeight ( e . nativeEvent . layout . height ) : undefined }
417+ onContentSizeChange = { shouldUseScrollView ? ( _w : number , h : number ) => setContentHeight ( h ) : undefined }
392418 fsClass = { CONST . FULL_STORY . UNMASK }
393419 testID = { CONST . FULL_STORY . UNMASK }
394420 >
@@ -397,9 +423,20 @@ function FeatureTrainingModal({
397423 </ View >
398424 < View style = { [ styles . mt5 , styles . mh5 , contentOuterContainerStyles ] } >
399425 { ! ! title && ! ! description && (
400- < View style = { [ onboardingIsMediumOrLargerScreenWidth ? [ styles . gap1 , styles . mb8 ] : [ styles . mb10 ] , contentInnerContainerStyles ] } >
426+ < View
427+ style = { [
428+ onboardingIsMediumOrLargerScreenWidth ? [ styles . gap1 , styles . mb8 ] : [ shouldRenderHTMLDescription ? styles . mb5 : styles . mb10 ] ,
429+ contentInnerContainerStyles ,
430+ ] }
431+ >
401432 { typeof title === 'string' ? < Text style = { [ styles . textHeadlineH1 , titleStyles ] } > { title } </ Text > : title }
402- { shouldRenderHTMLDescription ? < RenderHTML html = { description } /> : < Text style = { styles . textSupporting } > { description } </ Text > }
433+ { shouldRenderHTMLDescription ? (
434+ < View style = { styles . mb2 } >
435+ < RenderHTML html = { description } />
436+ </ View >
437+ ) : (
438+ < Text style = { styles . textSupporting } > { description } </ Text >
439+ ) }
403440 { secondaryDescription . length > 0 && < Text style = { [ styles . textSupporting , styles . mt4 ] } > { secondaryDescription } </ Text > }
404441 { children }
405442 </ View >
0 commit comments