insyd-bottom-sheet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +280 -0
- package/package.json +52 -0
- package/src/SmoothSheet.tsx +230 -0
- package/src/index.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ishan Gupta (https://github.com/ishan-crd)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# insyd-bottom-sheet
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/insyd-bottom-sheet)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://reactnative.dev)
|
|
6
|
+
[](https://expo.dev)
|
|
7
|
+
|
|
8
|
+
A smooth, physics-based bottom sheet for React Native — built by [Ishan Gupta](https://github.com/ishan-crd).
|
|
9
|
+
|
|
10
|
+
Powered by `react-native-reanimated` and `react-native-gesture-handler`. **Works in Expo Go, Expo bare workflow, and React Native CLI** — no native rebuild required.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Spring physics** — feels natural, not mechanical
|
|
17
|
+
- **Drag-to-dismiss** — with configurable distance and velocity thresholds
|
|
18
|
+
- **Backdrop tap to dismiss**
|
|
19
|
+
- **Keyboard-avoiding** — content shifts up when the keyboard appears (iOS padding / Android height)
|
|
20
|
+
- **Fully typed** — complete TypeScript support with exported interfaces
|
|
21
|
+
- **No native code** — runs in Expo Go out of the box
|
|
22
|
+
- **Zero extra dependencies** — only React Native, Reanimated 3, and Gesture Handler
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
# npm
|
|
30
|
+
npm install insyd-bottom-sheet
|
|
31
|
+
|
|
32
|
+
# yarn
|
|
33
|
+
yarn add insyd-bottom-sheet
|
|
34
|
+
|
|
35
|
+
# Expo (recommended — picks compatible peer dep versions automatically)
|
|
36
|
+
npx expo install insyd-bottom-sheet
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Peer dependencies
|
|
40
|
+
|
|
41
|
+
This package requires `react-native-reanimated` and `react-native-gesture-handler`. If you haven't already installed them:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx expo install react-native-reanimated react-native-gesture-handler
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For **bare React Native**, follow the official setup guides:
|
|
48
|
+
- [Reanimated installation](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/)
|
|
49
|
+
- [Gesture Handler installation](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation/)
|
|
50
|
+
|
|
51
|
+
> **Expo Go users:** both packages ship with Expo Go — no custom dev client needed.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Setup
|
|
56
|
+
|
|
57
|
+
### 1. Wrap your app in `GestureHandlerRootView`
|
|
58
|
+
|
|
59
|
+
This is required by `react-native-gesture-handler`. Wrap your root component once:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// App.tsx (or _layout.tsx for Expo Router)
|
|
63
|
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
64
|
+
|
|
65
|
+
export default function App() {
|
|
66
|
+
return (
|
|
67
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
68
|
+
{/* rest of your app */}
|
|
69
|
+
</GestureHandlerRootView>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. Add the Reanimated Babel plugin
|
|
75
|
+
|
|
76
|
+
In your `babel.config.js`:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
module.exports = {
|
|
80
|
+
presets: ["babel-preset-expo"], // or 'metro-react-native-babel-preset' for RN CLI
|
|
81
|
+
plugins: ["react-native-reanimated/plugin"],
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
After editing `babel.config.js`, restart Metro with a cleared cache:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
npx expo start --clear
|
|
89
|
+
# or for RN CLI
|
|
90
|
+
npx react-native start --reset-cache
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
### Basic
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import React, { useState } from "react";
|
|
101
|
+
import { Button, Text, View } from "react-native";
|
|
102
|
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
103
|
+
import { SmoothSheet } from "insyd-bottom-sheet";
|
|
104
|
+
|
|
105
|
+
export default function App() {
|
|
106
|
+
const [open, setOpen] = useState(false);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
110
|
+
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
|
111
|
+
<Button title="Open Sheet" onPress={() => setOpen(true)} />
|
|
112
|
+
</View>
|
|
113
|
+
|
|
114
|
+
<SmoothSheet isVisible={open} onDismiss={() => setOpen(false)}>
|
|
115
|
+
<View style={{ padding: 24 }}>
|
|
116
|
+
<Text style={{ fontSize: 20, fontWeight: "600" }}>Hello 👋</Text>
|
|
117
|
+
<Text style={{ marginTop: 8, color: "#64748b" }}>
|
|
118
|
+
Drag down or tap the backdrop to dismiss.
|
|
119
|
+
</Text>
|
|
120
|
+
</View>
|
|
121
|
+
</SmoothSheet>
|
|
122
|
+
</GestureHandlerRootView>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Custom height
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<SmoothSheet
|
|
131
|
+
isVisible={open}
|
|
132
|
+
onDismiss={() => setOpen(false)}
|
|
133
|
+
minHeightFraction={0.5} // sheet takes up at least 50% of screen
|
|
134
|
+
maxHeightFraction={0.85} // caps at 85% of screen
|
|
135
|
+
>
|
|
136
|
+
{/* your content */}
|
|
137
|
+
</SmoothSheet>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Custom appearance
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
<SmoothSheet
|
|
144
|
+
isVisible={open}
|
|
145
|
+
onDismiss={() => setOpen(false)}
|
|
146
|
+
backgroundColor="#1e293b"
|
|
147
|
+
borderRadius={20}
|
|
148
|
+
handleColor="#475569"
|
|
149
|
+
backdropColor="rgba(0,0,0,0.7)"
|
|
150
|
+
>
|
|
151
|
+
{/* dark-themed content */}
|
|
152
|
+
</SmoothSheet>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Custom spring physics
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<SmoothSheet
|
|
159
|
+
isVisible={open}
|
|
160
|
+
onDismiss={() => setOpen(false)}
|
|
161
|
+
springConfig={{ damping: 20, stiffness: 220, mass: 0.8 }}
|
|
162
|
+
>
|
|
163
|
+
{/* snappier feel */}
|
|
164
|
+
</SmoothSheet>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### With a ScrollView inside
|
|
168
|
+
|
|
169
|
+
Wrap long content in a `ScrollView`. The sheet handles keyboard avoidance automatically:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { ScrollView, Text } from "react-native";
|
|
173
|
+
import { SmoothSheet } from "insyd-bottom-sheet";
|
|
174
|
+
|
|
175
|
+
<SmoothSheet
|
|
176
|
+
isVisible={open}
|
|
177
|
+
onDismiss={() => setOpen(false)}
|
|
178
|
+
minHeightFraction={0.7}
|
|
179
|
+
>
|
|
180
|
+
<ScrollView
|
|
181
|
+
contentContainerStyle={{ padding: 24, paddingBottom: 48 }}
|
|
182
|
+
showsVerticalScrollIndicator={false}
|
|
183
|
+
keyboardShouldPersistTaps="handled"
|
|
184
|
+
>
|
|
185
|
+
{/* long content */}
|
|
186
|
+
</ScrollView>
|
|
187
|
+
</SmoothSheet>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### With TextInput (keyboard-aware)
|
|
191
|
+
|
|
192
|
+
The built-in `KeyboardAvoidingView` handles this automatically:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { TextInput, View } from "react-native";
|
|
196
|
+
import { SmoothSheet } from "insyd-bottom-sheet";
|
|
197
|
+
|
|
198
|
+
<SmoothSheet isVisible={open} onDismiss={() => setOpen(false)}>
|
|
199
|
+
<View style={{ padding: 24, gap: 12 }}>
|
|
200
|
+
<TextInput
|
|
201
|
+
placeholder="Your name"
|
|
202
|
+
style={{
|
|
203
|
+
borderWidth: 1,
|
|
204
|
+
borderColor: "#e2e8f0",
|
|
205
|
+
borderRadius: 8,
|
|
206
|
+
padding: 12,
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
</View>
|
|
210
|
+
</SmoothSheet>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Props
|
|
216
|
+
|
|
217
|
+
| Prop | Type | Default | Required | Description |
|
|
218
|
+
|------|------|---------|----------|-------------|
|
|
219
|
+
| `isVisible` | `boolean` | — | ✅ | Controls sheet visibility |
|
|
220
|
+
| `onDismiss` | `() => void` | — | ✅ | Called when sheet should close |
|
|
221
|
+
| `children` | `React.ReactNode` | — | ✅ | Content rendered inside the sheet |
|
|
222
|
+
| `minHeightFraction` | `number` | `0.4` | | Min height as a fraction of screen (0–1) |
|
|
223
|
+
| `maxHeightFraction` | `number` | `0.97` | | Max height as a fraction of screen (0–1) |
|
|
224
|
+
| `backgroundColor` | `string` | `"#ffffff"` | | Sheet background color |
|
|
225
|
+
| `borderRadius` | `number` | `28` | | Top corner radius |
|
|
226
|
+
| `handleColor` | `string` | `"#CBD5E1"` | | Drag handle pill color |
|
|
227
|
+
| `backdropColor` | `string` | `"rgba(0,0,0,0.5)"` | | Backdrop overlay color |
|
|
228
|
+
| `dismissThreshold` | `number` | `80` | | Drag distance in px to trigger dismiss |
|
|
229
|
+
| `dismissVelocityThreshold` | `number` | `800` | | Drag velocity in px/s to trigger dismiss |
|
|
230
|
+
| `springConfig` | `SpringConfig` | see below | | Override spring animation |
|
|
231
|
+
|
|
232
|
+
### SpringConfig
|
|
233
|
+
|
|
234
|
+
| Key | Type | Default | Description |
|
|
235
|
+
|-----|------|---------|-------------|
|
|
236
|
+
| `damping` | `number` | `28` | Higher = less bouncy |
|
|
237
|
+
| `stiffness` | `number` | `160` | Higher = faster |
|
|
238
|
+
| `mass` | `number` | `0.9` | Higher = heavier feel |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## TypeScript
|
|
243
|
+
|
|
244
|
+
Both the component and its props are fully typed. You can import the types:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import type { SmoothSheetProps, SpringConfig } from "insyd-bottom-sheet";
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Requirements
|
|
253
|
+
|
|
254
|
+
| | Minimum version |
|
|
255
|
+
|---|---|
|
|
256
|
+
| React Native | 0.71 |
|
|
257
|
+
| React | 18.0 |
|
|
258
|
+
| `react-native-reanimated` | 3.0.0 |
|
|
259
|
+
| `react-native-gesture-handler` | 2.0.0 |
|
|
260
|
+
| Expo SDK *(if using Expo)* | 49+ |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## How it works
|
|
265
|
+
|
|
266
|
+
`insyd-bottom-sheet` uses `useSharedValue` and `withSpring` from Reanimated to animate the sheet on the UI thread — meaning animations are never blocked by JavaScript work. The drag-to-dismiss gesture runs via `Gesture.Pan()` from Gesture Handler, also on the UI thread. No bridge round-trips, no jank.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT © 2024 [Ishan Gupta](https://github.com/ishan-crd)
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Contributing
|
|
277
|
+
|
|
278
|
+
Issues and PRs welcome at [github.com/ishan-crd/insyd-bottom-sheet](https://github.com/ishan-crd/insyd-bottom-sheet).
|
|
279
|
+
|
|
280
|
+
For bug reports, questions, or anything else — reach out at [ishan@insyd.in](mailto:ishan@insyd.in).
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "insyd-bottom-sheet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A smooth, physics-based bottom sheet for React Native — by Ishan Gupta. Works with Expo Go, bare workflow, and React Native CLI.",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"react-native",
|
|
17
|
+
"bottom-sheet",
|
|
18
|
+
"reanimated",
|
|
19
|
+
"gesture-handler",
|
|
20
|
+
"expo",
|
|
21
|
+
"expo-go",
|
|
22
|
+
"modal",
|
|
23
|
+
"sheet",
|
|
24
|
+
"animated",
|
|
25
|
+
"insyd",
|
|
26
|
+
"smooth"
|
|
27
|
+
],
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "Ishan Gupta",
|
|
30
|
+
"url": "https://github.com/ishan-crd"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/ishan-crd/insyd-bottom-sheet.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/ishan-crd/insyd-bottom-sheet/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/ishan-crd/insyd-bottom-sheet#readme",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18.0.0",
|
|
43
|
+
"react-native": ">=0.71.0",
|
|
44
|
+
"react-native-gesture-handler": ">=2.0.0",
|
|
45
|
+
"react-native-reanimated": ">=3.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/react": "^18.2.0",
|
|
49
|
+
"@types/react-native": "^0.73.0",
|
|
50
|
+
"typescript": "^5.3.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Dimensions,
|
|
4
|
+
KeyboardAvoidingView,
|
|
5
|
+
Platform,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
View,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
Gesture,
|
|
12
|
+
GestureDetector,
|
|
13
|
+
} from "react-native-gesture-handler";
|
|
14
|
+
import Animated, {
|
|
15
|
+
runOnJS,
|
|
16
|
+
useAnimatedStyle,
|
|
17
|
+
useSharedValue,
|
|
18
|
+
withSpring,
|
|
19
|
+
withTiming,
|
|
20
|
+
} from "react-native-reanimated";
|
|
21
|
+
|
|
22
|
+
const SCREEN_HEIGHT = Dimensions.get("window").height;
|
|
23
|
+
|
|
24
|
+
const DEFAULT_SPRING = { damping: 28, stiffness: 160, mass: 0.9 };
|
|
25
|
+
|
|
26
|
+
export interface SpringConfig {
|
|
27
|
+
damping?: number;
|
|
28
|
+
stiffness?: number;
|
|
29
|
+
mass?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SmoothSheetProps {
|
|
33
|
+
/** Controls whether the sheet is visible. */
|
|
34
|
+
isVisible: boolean;
|
|
35
|
+
/** Called when the sheet should be dismissed (backdrop tap, drag-to-dismiss, or close button). */
|
|
36
|
+
onDismiss: () => void;
|
|
37
|
+
/** Content rendered inside the sheet. */
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Minimum height of the sheet as a fraction of screen height (0–1).
|
|
41
|
+
* @default 0.4
|
|
42
|
+
*/
|
|
43
|
+
minHeightFraction?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum height of the sheet as a fraction of screen height (0–1).
|
|
46
|
+
* @default 0.97
|
|
47
|
+
*/
|
|
48
|
+
maxHeightFraction?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Background color of the sheet.
|
|
51
|
+
* @default "#ffffff"
|
|
52
|
+
*/
|
|
53
|
+
backgroundColor?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Border radius applied to the top-left and top-right corners.
|
|
56
|
+
* @default 28
|
|
57
|
+
*/
|
|
58
|
+
borderRadius?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Color of the drag handle pill.
|
|
61
|
+
* @default "#CBD5E1"
|
|
62
|
+
*/
|
|
63
|
+
handleColor?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Background color of the backdrop overlay.
|
|
66
|
+
* @default "rgba(0,0,0,0.5)"
|
|
67
|
+
*/
|
|
68
|
+
backdropColor?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Vertical drag distance in px required to trigger dismiss.
|
|
71
|
+
* @default 80
|
|
72
|
+
*/
|
|
73
|
+
dismissThreshold?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Vertical velocity in px/s that triggers dismiss even before reaching dismissThreshold.
|
|
76
|
+
* @default 800
|
|
77
|
+
*/
|
|
78
|
+
dismissVelocityThreshold?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Override the spring animation config.
|
|
81
|
+
* Merged over the default: { damping: 28, stiffness: 160, mass: 0.9 }
|
|
82
|
+
*/
|
|
83
|
+
springConfig?: SpringConfig;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function SmoothSheet({
|
|
87
|
+
isVisible,
|
|
88
|
+
onDismiss,
|
|
89
|
+
children,
|
|
90
|
+
minHeightFraction = 0.4,
|
|
91
|
+
maxHeightFraction = 0.97,
|
|
92
|
+
backgroundColor = "#ffffff",
|
|
93
|
+
borderRadius = 28,
|
|
94
|
+
handleColor = "#CBD5E1",
|
|
95
|
+
backdropColor = "rgba(0,0,0,0.5)",
|
|
96
|
+
dismissThreshold = 80,
|
|
97
|
+
dismissVelocityThreshold = 800,
|
|
98
|
+
springConfig,
|
|
99
|
+
}: SmoothSheetProps) {
|
|
100
|
+
const spring = { ...DEFAULT_SPRING, ...springConfig };
|
|
101
|
+
|
|
102
|
+
const translateY = useSharedValue(SCREEN_HEIGHT);
|
|
103
|
+
const backdropOpacity = useSharedValue(0);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (isVisible) {
|
|
107
|
+
translateY.value = withSpring(0, spring);
|
|
108
|
+
backdropOpacity.value = withTiming(1, { duration: 250 });
|
|
109
|
+
} else {
|
|
110
|
+
translateY.value = withSpring(SCREEN_HEIGHT, spring);
|
|
111
|
+
backdropOpacity.value = withTiming(0, { duration: 200 });
|
|
112
|
+
}
|
|
113
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
|
+
}, [isVisible]);
|
|
115
|
+
|
|
116
|
+
function dismiss() {
|
|
117
|
+
translateY.value = withSpring(SCREEN_HEIGHT, spring);
|
|
118
|
+
backdropOpacity.value = withTiming(
|
|
119
|
+
0,
|
|
120
|
+
{ duration: 200 },
|
|
121
|
+
() => {
|
|
122
|
+
runOnJS(onDismiss)();
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const panGesture = Gesture.Pan()
|
|
128
|
+
.onUpdate((e) => {
|
|
129
|
+
if (e.translationY > 0) {
|
|
130
|
+
translateY.value = e.translationY;
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
.onEnd((e) => {
|
|
134
|
+
if (
|
|
135
|
+
e.translationY > dismissThreshold ||
|
|
136
|
+
e.velocityY > dismissVelocityThreshold
|
|
137
|
+
) {
|
|
138
|
+
runOnJS(dismiss)();
|
|
139
|
+
} else {
|
|
140
|
+
translateY.value = withSpring(0, spring);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const sheetStyle = useAnimatedStyle(() => ({
|
|
145
|
+
transform: [{ translateY: translateY.value }],
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
const backdropStyle = useAnimatedStyle(() => ({
|
|
149
|
+
opacity: backdropOpacity.value,
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
if (!isVisible && translateY.value === SCREEN_HEIGHT) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
158
|
+
{/* Backdrop */}
|
|
159
|
+
<Animated.View
|
|
160
|
+
style={[
|
|
161
|
+
StyleSheet.absoluteFill,
|
|
162
|
+
{ backgroundColor: backdropColor },
|
|
163
|
+
backdropStyle,
|
|
164
|
+
]}
|
|
165
|
+
pointerEvents={isVisible ? "auto" : "none"}
|
|
166
|
+
>
|
|
167
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={dismiss} />
|
|
168
|
+
</Animated.View>
|
|
169
|
+
|
|
170
|
+
{/* Sheet */}
|
|
171
|
+
<Animated.View
|
|
172
|
+
style={[
|
|
173
|
+
styles.sheet,
|
|
174
|
+
{
|
|
175
|
+
borderTopLeftRadius: borderRadius,
|
|
176
|
+
borderTopRightRadius: borderRadius,
|
|
177
|
+
backgroundColor,
|
|
178
|
+
minHeight: SCREEN_HEIGHT * minHeightFraction,
|
|
179
|
+
maxHeight: SCREEN_HEIGHT * maxHeightFraction,
|
|
180
|
+
},
|
|
181
|
+
sheetStyle,
|
|
182
|
+
]}
|
|
183
|
+
pointerEvents="box-none"
|
|
184
|
+
>
|
|
185
|
+
{/* Drag handle */}
|
|
186
|
+
<GestureDetector gesture={panGesture}>
|
|
187
|
+
<View style={styles.handleArea}>
|
|
188
|
+
<View
|
|
189
|
+
style={[styles.handle, { backgroundColor: handleColor }]}
|
|
190
|
+
/>
|
|
191
|
+
</View>
|
|
192
|
+
</GestureDetector>
|
|
193
|
+
|
|
194
|
+
{/* Content */}
|
|
195
|
+
<KeyboardAvoidingView
|
|
196
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
197
|
+
style={styles.content}
|
|
198
|
+
>
|
|
199
|
+
{children}
|
|
200
|
+
</KeyboardAvoidingView>
|
|
201
|
+
</Animated.View>
|
|
202
|
+
</View>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const styles = StyleSheet.create({
|
|
207
|
+
sheet: {
|
|
208
|
+
position: "absolute",
|
|
209
|
+
bottom: 0,
|
|
210
|
+
left: 0,
|
|
211
|
+
right: 0,
|
|
212
|
+
shadowColor: "#000",
|
|
213
|
+
shadowOffset: { width: 0, height: -4 },
|
|
214
|
+
shadowOpacity: 0.12,
|
|
215
|
+
shadowRadius: 16,
|
|
216
|
+
elevation: 24,
|
|
217
|
+
},
|
|
218
|
+
handleArea: {
|
|
219
|
+
alignItems: "center",
|
|
220
|
+
paddingVertical: 12,
|
|
221
|
+
},
|
|
222
|
+
handle: {
|
|
223
|
+
width: 36,
|
|
224
|
+
height: 4,
|
|
225
|
+
borderRadius: 2,
|
|
226
|
+
},
|
|
227
|
+
content: {
|
|
228
|
+
flex: 1,
|
|
229
|
+
},
|
|
230
|
+
});
|
package/src/index.ts
ADDED