├── .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 | 
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 |
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 |
27 | );
28 | };
29 |
30 | const styles = StyleSheet.create({
31 | container: {
32 | paddingHorizontal: 16,
33 | paddingVertical: 8,
34 | },
35 | });
36 |
--------------------------------------------------------------------------------
/src/screens/Modal.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FlatList, StyleSheet, Text, View } from "react-native";
3 | import { Item } from '../components/Item'
4 |
5 | const data = Array(40)
6 | .fill(0)
7 | .map((_, index) => `${index}`);
8 |
9 | const keyExtractor = (item: any) => `item-${item}`;
10 |
11 | // @ts-ignore
12 | const renderItem = ({ item }) => ( );
13 |
14 | export const Modal = () => {
15 | return (
16 |
27 | );
28 | };
29 |
30 | const styles = StyleSheet.create({
31 | container: {
32 | flex: 1,
33 | },
34 | contentContainer: {
35 | paddingVertical: 10,
36 | },
37 | item: {
38 | padding: 12,
39 | marginVertical: 6,
40 | marginHorizontal: 16,
41 | backgroundColor: "#dfdfdf",
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/src/screens/ScrollableModalCallbacks.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import { FlatList, StyleSheet } from "react-native";
3 | import { NativeViewGestureHandler } from "react-native-gesture-handler";
4 | import { useScrollableModalGestureInteraction } from "../hooks/useScrollableModalGestureInteraction";
5 | import { Item } from "../components/Item";
6 |
7 | const data = Array(40)
8 | .fill(0)
9 | .map((_, index) => `${index}`);
10 |
11 | const keyExtractor = (item: any) => `item-${item}`;
12 |
13 | // @ts-ignore
14 | const renderItem = ({ item }) => ;
15 |
16 | export const ScrollableModalCallbacks = () => {
17 | const scrollableRef = useRef(null);
18 | const {
19 | handleOnBeginDrag,
20 | handleOnEndDrag,
21 | handleOnScroll,
22 | scrollableGestureRef,
23 | } = useScrollableModalGestureInteraction(scrollableRef);
24 | return (
25 |
26 |
41 |
42 | );
43 | };
44 |
45 | const styles = StyleSheet.create({
46 | container: {
47 | flex: 1,
48 | },
49 | contentContainer: {
50 | paddingVertical: 10,
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/src/screens/ScrollableModalReanimated.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FlatList, FlatListProps, StyleSheet, Text, View } from "react-native";
3 | import { NativeViewGestureHandler } from "react-native-gesture-handler";
4 | import Animated, { useAnimatedRef } from "react-native-reanimated";
5 | import { useScrollableModalGestureInteractionReanimated } from "../hooks/useScrollableModalGestureInteractionReanimated";
6 | import { Item } from '../components/Item'
7 |
8 | const AnimatedFlatList =
9 | Animated.createAnimatedComponent>(FlatList);
10 |
11 | const data = Array(40)
12 | .fill(0)
13 | .map((_, index) => `${index}`);
14 |
15 | const keyExtractor = (item: any) => `item-${item}`;
16 |
17 | // @ts-ignore
18 | const renderItem = ({ item }) => ( );
19 |
20 | export const ScrollableModalReanimated = () => {
21 | const scrollableRef = useAnimatedRef();
22 |
23 | const { scrollableGestureRef, handleScrolling } =
24 | useScrollableModalGestureInteractionReanimated(scrollableRef);
25 |
26 | return (
27 |
28 |
41 |
42 | );
43 | };
44 |
45 | const styles = StyleSheet.create({
46 | container: {
47 | flex: 1,
48 | },
49 | contentContainer: {
50 | paddingVertical: 10,
51 | },
52 | item: {
53 | padding: 12,
54 | marginVertical: 6,
55 | marginHorizontal: 16,
56 | backgroundColor: "#dfdfdf",
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------