├── .expo-shared └── assets.json ├── .github └── FUNDING.yml ├── .gitignore ├── App.tsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── gorhom.png ├── package.json ├── patches └── @react-navigation+stack+6.0.7.patch ├── preview.gif ├── src ├── components │ ├── Button.tsx │ └── Item.tsx ├── hooks │ ├── useScrollableModalGestureInteraction.ts │ └── useScrollableModalGestureInteractionReanimated.ts └── screens │ ├── Home.tsx │ ├── Modal.tsx │ ├── ScrollableModalCallbacks.tsx │ └── ScrollableModalReanimated.tsx ├── tsconfig.json └── yarn.lock /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: gorhom 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | 12 | # macOS 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavigationContainer } from "@react-navigation/native"; 3 | import { createStackNavigator } from "@react-navigation/stack"; 4 | import { Home } from "./src/screens/Home"; 5 | import { ScrollableModalReanimated } from "./src/screens/ScrollableModalReanimated"; 6 | import { ScrollableModalCallbacks } from "./src/screens/ScrollableModalCallbacks"; 7 | import { Modal } from "./src/screens/Modal"; 8 | 9 | const Stack = createStackNavigator(); 10 | 11 | export default () => ( 12 | 13 | 14 | 15 | 20 | 25 | 30 | 31 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Navigation Scrollable Modal 2 | 3 | This is a POC to replicate the native interaction behavior of iOS modal presentation with React Navigation. 4 | 5 | ![React Navigation Scrollable Modal](./preview.gif) 6 | 7 | --- 8 | 9 | ### Usage 10 | 11 | In order to get this functionality working in your project, you have to: 12 | 13 | - Copy `patches/@react-navigation+stack+6.0.7.patch` into your project root folder. 14 | - Copy `src/useCardModalGestureInteraction.ts` into any place in your project. 15 | - add `"postinstall": "npx patch-package"` into your project `package.json` in `scripts` node. 16 | - run `yarn` 17 | 18 | ```tsx 19 | export const ScrollableModalScreen = () => { 20 | const scrollableRef = useAnimatedRef(); 21 | 22 | const { scrollableGestureRef, handleScrolling } = 23 | useCardModalGestureInteraction(scrollableRef); 24 | return ( 25 | 26 | 32 | 33 | ); 34 | }; 35 | ``` 36 | 37 | ## How it works 38 | 39 | React Navigation Stack implements a `PanGestureHandler` in the [Card](https://github.com/react-navigation/react-navigation/blob/6cba517b74f5fd092db21d5574b558ef2d80897b/packages/stack/src/views/Stack/Card.tsx#L530) component, which should allow us to manipulate the gesture behavior as we want. 40 | 41 | To achieve the seamless scrolling / pan gesture interaction, We have to wrap the scrollable component with `NativeGestureHandler` from `react-native-gesture-handler` and pass its reference to the `Card`'s `PanGestureHandler` via the prop `simultaneousHandlers`. 42 | 43 | Then we need to lock the scrollable component, whenever the user is reach to the top and start dragging the `Card`. 44 | 45 | I have already prepare a custom hook `useCardModalGestureInteraction` that will handle all the interaction with the `Card`, all you have to do is to pass the scrollable ref, and attached the return variables to `NativeViewGestureHandler` and your scrollable 46 | 47 | This solution was inspired by the [Bottom Sheet](https://github.com/gorhom/react-native-bottom-sheet) library, thanks to [@haibert](https://twitter.com/haibert8) for highlighting this issue. 48 | 49 | --- 50 | 51 |

52 | Mo Gorhom 53 |

54 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-navigation-modal", 4 | "slug": "react-navigation-modal", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorhom/react-navigation-scrollable-modal/a5c97ce6d72f2be63d35bf83cf40ba75a23e5e8f/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorhom/react-navigation-scrollable-modal/a5c97ce6d72f2be63d35bf83cf40ba75a23e5e8f/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorhom/react-navigation-scrollable-modal/a5c97ce6d72f2be63d35bf83cf40ba75a23e5e8f/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorhom/react-navigation-scrollable-modal/a5c97ce6d72f2be63d35bf83cf40ba75a23e5e8f/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /gorhom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorhom/react-navigation-scrollable-modal/a5c97ce6d72f2be63d35bf83cf40ba75a23e5e8f/gorhom.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "license": "MIT", 4 | "scripts": { 5 | "start": "expo start", 6 | "android": "expo start --android", 7 | "ios": "expo start --ios", 8 | "web": "expo start --web", 9 | "eject": "expo eject", 10 | "postinstall": "npx patch-package" 11 | }, 12 | "dependencies": { 13 | "@react-navigation/native": "^6.0.2", 14 | "@react-navigation/stack": "^6.0.7", 15 | "expo": "~42.0.1", 16 | "expo-status-bar": "~1.0.4", 17 | "react": "16.13.1", 18 | "react-dom": "16.13.1", 19 | "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz", 20 | "react-native-gesture-handler": "~1.10.2", 21 | "react-native-reanimated": "~2.2.1", 22 | "react-native-safe-area-context": "3.2.0", 23 | "react-native-screens": "~3.4.0", 24 | "react-native-web": "~0.13.12" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.9.0", 28 | "@types/react": "~16.9.35", 29 | "@types/react-native": "~0.63.2", 30 | "typescript": "~4.0.0" 31 | }, 32 | "private": true 33 | } 34 | -------------------------------------------------------------------------------- /patches/@react-navigation+stack+6.0.7.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@react-navigation/stack/lib/typescript/src/index.d.ts b/node_modules/@react-navigation/stack/lib/typescript/src/index.d.ts 2 | index 35a863b..d8dbca3 100644 3 | --- a/node_modules/@react-navigation/stack/lib/typescript/src/index.d.ts 4 | +++ b/node_modules/@react-navigation/stack/lib/typescript/src/index.d.ts 5 | @@ -22,6 +22,8 @@ export { default as CardAnimationContext } from './utils/CardAnimationContext'; 6 | export { default as GestureHandlerRefContext } from './utils/GestureHandlerRefContext'; 7 | export { default as useCardAnimation } from './utils/useCardAnimation'; 8 | export { default as useGestureHandlerRef } from './utils/useGestureHandlerRef'; 9 | +export { ModalGestureContext, ModalGestureContextType } from './utils/ModalGestureContext'; 10 | + 11 | /** 12 | * Types 13 | */ 14 | diff --git a/node_modules/@react-navigation/stack/lib/typescript/src/utils/ModalGestureContext.ts b/node_modules/@react-navigation/stack/lib/typescript/src/utils/ModalGestureContext.ts 15 | new file mode 100644 16 | index 0000000..9cd9184 17 | --- /dev/null 18 | +++ b/node_modules/@react-navigation/stack/lib/typescript/src/utils/ModalGestureContext.ts 19 | @@ -0,0 +1,9 @@ 20 | +import * as React from 'react'; 21 | +import type { Animated } from 'react-native'; 22 | + 23 | +export interface ModalGestureContextType { 24 | + scrollableGestureRef: React.RefObject; 25 | + modalTranslateY: Animated.Value; 26 | +} 27 | + 28 | +export declare const ModalGestureContext: React.Context; 29 | diff --git a/node_modules/@react-navigation/stack/src/index.tsx b/node_modules/@react-navigation/stack/src/index.tsx 30 | index f20d3fb..a8475dd 100644 31 | --- a/node_modules/@react-navigation/stack/src/index.tsx 32 | +++ b/node_modules/@react-navigation/stack/src/index.tsx 33 | @@ -31,6 +31,7 @@ export { default as CardAnimationContext } from './utils/CardAnimationContext'; 34 | export { default as GestureHandlerRefContext } from './utils/GestureHandlerRefContext'; 35 | export { default as useCardAnimation } from './utils/useCardAnimation'; 36 | export { default as useGestureHandlerRef } from './utils/useGestureHandlerRef'; 37 | +export { ModalGestureContext } from './utils/ModalGestureContext'; 38 | 39 | /** 40 | * Types 41 | diff --git a/node_modules/@react-navigation/stack/src/utils/ModalGestureContext.ts b/node_modules/@react-navigation/stack/src/utils/ModalGestureContext.ts 42 | new file mode 100644 43 | index 0000000..8b35adf 44 | --- /dev/null 45 | +++ b/node_modules/@react-navigation/stack/src/utils/ModalGestureContext.ts 46 | @@ -0,0 +1,10 @@ 47 | +import React from 'react'; 48 | +import { Animated } from 'react-native'; 49 | + 50 | +export interface ModalGestureContextType { 51 | + scrollableGestureRef: React.RefObject; 52 | + modalTranslateY: Animated.Value; 53 | +} 54 | + 55 | +export const ModalGestureContext = 56 | + React.createContext(null); 57 | diff --git a/node_modules/@react-navigation/stack/src/views/ModalGestureProvider.tsx b/node_modules/@react-navigation/stack/src/views/ModalGestureProvider.tsx 58 | new file mode 100644 59 | index 0000000..6e0157d 60 | --- /dev/null 61 | +++ b/node_modules/@react-navigation/stack/src/views/ModalGestureProvider.tsx 62 | @@ -0,0 +1,38 @@ 63 | +import React from 'react'; 64 | +import { ModalGestureContext, ModalGestureContextType } from "../utils/ModalGestureContext" 65 | + 66 | +interface ModalGestureProviderProps { 67 | + /** 68 | + * Context value. 69 | + * 70 | + * @type ModalGestureContextType 71 | + */ 72 | + value: ModalGestureContextType; 73 | + 74 | + /** 75 | + * Defines if current card is a iOS modal. 76 | + * 77 | + * @type boolean 78 | + * @default false 79 | + */ 80 | + enabled?: boolean; 81 | + 82 | + /** 83 | + * Child component 84 | + * 85 | + * @type React.ReactNode 86 | + */ 87 | + children: React.ReactNode; 88 | +} 89 | + 90 | +const ModalGestureProvider: React.FC = ({ 91 | + value, 92 | + children, 93 | + enabled = false 94 | +}: ModalGestureProviderProps) => enabled ? ( 95 | + 96 | + {children} 97 | + 98 | +) : children as any; 99 | + 100 | +export default ModalGestureProvider; 101 | \ No newline at end of file 102 | diff --git a/node_modules/@react-navigation/stack/src/views/Stack/Card.tsx b/node_modules/@react-navigation/stack/src/views/Stack/Card.tsx 103 | index a013ff9..ce9304b 100755 104 | --- a/node_modules/@react-navigation/stack/src/views/Stack/Card.tsx 105 | +++ b/node_modules/@react-navigation/stack/src/views/Stack/Card.tsx 106 | @@ -21,6 +21,7 @@ import type { 107 | TransitionSpec, 108 | } from '../../types'; 109 | import CardAnimationContext from '../../utils/CardAnimationContext'; 110 | +import ModalGestureProvider from '../ModalGestureProvider' 111 | import getDistanceForDirection from '../../utils/getDistanceForDirection'; 112 | import getInvertedMultiplier from '../../utils/getInvertedMultiplier'; 113 | import memoize from '../../utils/memoize'; 114 | @@ -104,6 +105,9 @@ export default class Card extends React.Component { 115 | ) : null, 116 | }; 117 | 118 | + private scrollableGestureRef = React.createRef(); 119 | + private scrollableOffset = new Animated.Value(0); 120 | + 121 | componentDidMount() { 122 | this.animate({ closing: this.props.closing }); 123 | this.isCurrentlyMounted = true; 124 | @@ -238,6 +242,13 @@ export default class Card extends React.Component { 125 | return getDistanceForDirection(layout, gestureDirection); 126 | }; 127 | 128 | + private getIsScrollableModal = () => { 129 | + return ( 130 | + this.scrollableGestureRef.current && 131 | + getIsModalPresentation(this.props.styleInterpolator) 132 | + ); 133 | + }; 134 | + 135 | private setPointerEventsEnabled = (enabled: boolean) => { 136 | const pointerEvents = enabled ? 'box-none' : 'none'; 137 | 138 | @@ -294,6 +305,21 @@ export default class Card extends React.Component { 139 | case GestureState.END: { 140 | this.isSwiping.setValue(FALSE); 141 | 142 | + this.isSwiping.removeListener 143 | + 144 | + /** 145 | + * if scrollable modal is enabled, and the gesture value is small than the scrollable offset, 146 | + * then we exit the method and reset gesture value. 147 | + */ 148 | + if(this.getIsScrollableModal() && 149 | + // @ts-ignore 150 | + this.props.gesture._value < Math.abs(this.props.gesture._offset) 151 | + ) { 152 | + this.props.gesture.setValue(0); 153 | + this.props.gesture.setOffset(0); 154 | + return; 155 | + } 156 | + 157 | let distance; 158 | let translation; 159 | let velocity; 160 | @@ -392,7 +418,7 @@ export default class Card extends React.Component { 161 | return { 162 | maxDeltaX: 15, 163 | minOffsetY: 5, 164 | - hitSlop: { bottom: -layout.height + distance }, 165 | + hitSlop: this.getIsScrollableModal() ? {} : { bottom: -layout.height + distance }, 166 | enableTrackpadTwoFingerGesture, 167 | }; 168 | } else if (gestureDirection === 'vertical-inverted') { 169 | @@ -448,6 +474,11 @@ export default class Card extends React.Component { 170 | ...rest 171 | } = this.props; 172 | 173 | + const modalGestureValue = { 174 | + scrollableGestureRef: this.scrollableGestureRef, 175 | + modalTranslateY: gesture 176 | + } 177 | + 178 | const interpolationProps = this.getCardAnimation( 179 | interpolationIndex, 180 | current, 181 | @@ -526,43 +557,49 @@ export default class Card extends React.Component { 182 | style={[styles.container, containerStyle, customContainerStyle]} 183 | pointerEvents="box-none" 184 | > 185 | - 194 | - 204 | - {shadowEnabled && shadowStyle && !isTransparent ? ( 205 | - 220 | - ) : null} 221 | - 230 | - {children} 231 | - 232 | - 233 | - 234 | + {shadowEnabled && shadowStyle && !isTransparent ? ( 235 | + 250 | + ) : null} 251 | + 257 | + {children} 258 | + 259 | + 260 | + 261 | + 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorhom/react-navigation-scrollable-modal/a5c97ce6d72f2be63d35bf83cf40ba75a23e5e8f/preview.gif -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { TouchableOpacity, StyleSheet, Text } from 'react-native' 3 | import { useTheme } from '@react-navigation/native' 4 | 5 | interface ButtonProps { 6 | title: string, 7 | subtitle: string, 8 | onPress: () => void 9 | } 10 | 11 | export const Button = ({ 12 | title, 13 | subtitle, 14 | onPress 15 | }: ButtonProps) => { 16 | 17 | const { colors } = useTheme(); 18 | 19 | const containerStyle = useMemo(() => [styles.container, { 20 | backgroundColor: colors.border, 21 | }], []) 22 | 23 | return ( 24 | 25 | 26 | {title} 27 | 28 | 29 | 30 | {subtitle} 31 | 32 | 33 | ) 34 | } 35 | 36 | const styles = StyleSheet.create({ 37 | container: { 38 | padding: 16, 39 | borderRadius: 12, 40 | marginVertical: 6 41 | }, 42 | title: { 43 | fontSize: 18, 44 | }, 45 | subtitle: { 46 | fontSize: 14, 47 | fontWeight: '700', 48 | marginTop: 4, 49 | textTransform: 'uppercase', 50 | opacity: 0.75 51 | } 52 | }) -------------------------------------------------------------------------------- /src/components/Item.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, Text, View } from "react-native"; 3 | 4 | export const Item = ({ item }: any) => ( 5 | 6 | {item} 7 | 8 | ); 9 | 10 | const styles = StyleSheet.create({ 11 | container: { 12 | padding: 12, 13 | marginVertical: 6, 14 | marginHorizontal: 16, 15 | backgroundColor: "#dfdfdf", 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/hooks/useScrollableModalGestureInteraction.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect, useRef } from "react"; 2 | import { ModalGestureContext } from "@react-navigation/stack"; 3 | import type { 4 | FlatList, 5 | NativeScrollEvent, 6 | NativeSyntheticEvent, 7 | ScrollView, 8 | SectionList, 9 | VirtualizedList, 10 | } from "react-native"; 11 | 12 | const scrollToTop = ( 13 | scrollableRef: React.RefObject< 14 | FlatList | ScrollView | SectionList | VirtualizedList 15 | > 16 | ) => { 17 | if (!scrollableRef.current) { 18 | return; 19 | } 20 | 21 | // scrollable is FlatList 22 | if ("scrollToOffset" in scrollableRef.current) { 23 | (scrollableRef.current as FlatList).scrollToOffset({ 24 | animated: false, 25 | offset: 0, 26 | }); 27 | return; 28 | } 29 | 30 | // scrollable is ScrollView 31 | if ("scrollTo" in scrollableRef.current) { 32 | (scrollableRef.current as ScrollView).scrollTo({ 33 | animated: false, 34 | y: 0, 35 | }); 36 | return; 37 | } 38 | 39 | // scrollable is ScrollView 40 | if ("scrollTo" in scrollableRef.current) { 41 | (scrollableRef.current as SectionList).scrollToLocation({ 42 | animated: false, 43 | itemIndex: 0, 44 | sectionIndex: 0, 45 | viewPosition: 0, 46 | viewOffset: 1000, 47 | }); 48 | return; 49 | } 50 | 51 | // scrollable is VirtualizedList 52 | if ("scrollTo" in scrollableRef.current) { 53 | (scrollableRef.current as VirtualizedList).scrollToOffset({ 54 | animated: false, 55 | offset: 0, 56 | }); 57 | return; 58 | } 59 | }; 60 | 61 | export const useScrollableModalGestureInteraction = ( 62 | scrollableRef: React.RefObject< 63 | FlatList | ScrollView | SectionList | VirtualizedList 64 | > 65 | ) => { 66 | // context 67 | const context = useContext(ModalGestureContext); 68 | if (!context) { 69 | throw new Error( 70 | "Couldn't find values for modal gesture. Are you inside a screen in Modal Stack?" 71 | ); 72 | } 73 | 74 | // variables 75 | const { modalTranslateY, scrollableGestureRef } = context; 76 | const lockScrolling = useRef(true); 77 | 78 | // methods 79 | const setLockScrolling = useCallback( 80 | (value: number) => { 81 | lockScrolling.current = value > 0; 82 | }, 83 | [lockScrolling] 84 | ); 85 | const setOffset = useCallback( 86 | (value: number) => { 87 | modalTranslateY.setOffset(-value); 88 | }, 89 | [modalTranslateY] 90 | ); 91 | 92 | // callback 93 | const handleOnBeginDrag = useCallback( 94 | ({ 95 | nativeEvent: { 96 | contentOffset: { y }, 97 | }, 98 | }: NativeSyntheticEvent) => { 99 | // @ts-ignore 100 | if (modalTranslateY._value === 0) { 101 | setLockScrolling(0); 102 | } 103 | 104 | setOffset(y); 105 | }, 106 | [setLockScrolling, setOffset, modalTranslateY] 107 | ); 108 | const handleOnScroll = useCallback( 109 | ({ 110 | nativeEvent: { 111 | contentOffset: { y }, 112 | }, 113 | }: NativeSyntheticEvent) => { 114 | if (y <= 0 || lockScrolling.current) { 115 | scrollToTop(scrollableRef); 116 | } 117 | }, 118 | [] 119 | ); 120 | const handleOnEndDrag = useCallback( 121 | ({ 122 | nativeEvent: { 123 | contentOffset: { y }, 124 | }, 125 | }: NativeSyntheticEvent) => { 126 | setOffset(y); 127 | }, 128 | [setOffset] 129 | ); 130 | 131 | // effects 132 | useEffect(() => { 133 | const listener = modalTranslateY.addListener(({ value }) => 134 | setLockScrolling(value) 135 | ); 136 | return () => { 137 | modalTranslateY.removeListener(listener); 138 | }; 139 | }, [modalTranslateY, setLockScrolling]); 140 | 141 | return { 142 | scrollableGestureRef, 143 | handleOnScroll, 144 | handleOnEndDrag: handleOnEndDrag, 145 | handleOnBeginDrag: handleOnBeginDrag, 146 | }; 147 | }; 148 | -------------------------------------------------------------------------------- /src/hooks/useScrollableModalGestureInteractionReanimated.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect } from "react"; 2 | import { ModalGestureContext } from "@react-navigation/stack"; 3 | import { 4 | runOnJS, 5 | scrollTo, 6 | useAnimatedScrollHandler, 7 | useSharedValue, 8 | } from "react-native-reanimated"; 9 | import type { FlatList, ScrollView } from "react-native"; 10 | 11 | export const useScrollableModalGestureInteractionReanimated = ( 12 | scrollableRef: React.RefObject 13 | ) => { 14 | // context 15 | const context = useContext(ModalGestureContext); 16 | if (!context) { 17 | throw new Error( 18 | "Couldn't find values for modal gesture. Are you inside a screen in Modal Stack?" 19 | ); 20 | } 21 | 22 | // variables 23 | const { modalTranslateY, scrollableGestureRef } = context; 24 | const lockScrolling = useSharedValue(true); 25 | 26 | // methods 27 | const setLockScrolling = useCallback( 28 | ({ value }) => { 29 | lockScrolling.value = value > 0; 30 | }, 31 | [lockScrolling] 32 | ); 33 | const setOffset = useCallback( 34 | (value: number) => { 35 | modalTranslateY.setOffset(-value); 36 | }, 37 | [modalTranslateY] 38 | ); 39 | 40 | // callback 41 | const handleScrolling = useAnimatedScrollHandler( 42 | { 43 | onBeginDrag: ({ contentOffset: { y } }) => { 44 | // @ts-ignore 45 | runOnJS(setOffset)(y); 46 | }, 47 | onScroll: ({ contentOffset: { y } }) => { 48 | if (y <= 0 || lockScrolling.value) { 49 | // @ts-ignore 50 | scrollTo(scrollableRef, 0, 0, false); 51 | } 52 | }, 53 | onEndDrag: ({ contentOffset: { y } }) => { 54 | runOnJS(setOffset)(y); 55 | }, 56 | onMomentumEnd: ({ contentOffset: { y } }) => { 57 | runOnJS(setOffset)(y); 58 | }, 59 | }, 60 | [lockScrolling, setOffset] 61 | ); 62 | 63 | // effects 64 | useEffect(() => { 65 | const listener = modalTranslateY.addListener(({ value }) => 66 | setLockScrolling(value) 67 | ); 68 | return () => { 69 | modalTranslateY.removeListener(listener); 70 | }; 71 | }, [modalTranslateY, setLockScrolling]); 72 | 73 | return { 74 | scrollableGestureRef, 75 | handleScrolling, 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /src/screens/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, View } from "react-native"; 3 | import { useNavigation } from "@react-navigation/core"; 4 | import { Button } from "../components/Button"; 5 | 6 | export const Home = () => { 7 | const { navigate } = useNavigation(); 8 | return ( 9 | 10 |