insyd-bottom-sheet 0.1.0 → 0.3.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/package.json +8 -3
- package/src/SmoothSheet.tsx +117 -70
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insyd-bottom-sheet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -42,11 +42,16 @@
|
|
|
42
42
|
"react": ">=18.0.0",
|
|
43
43
|
"react-native": ">=0.71.0",
|
|
44
44
|
"react-native-gesture-handler": ">=2.0.0",
|
|
45
|
-
"react-native-reanimated": ">=3.0.0"
|
|
45
|
+
"react-native-reanimated": ">=3.0.0",
|
|
46
|
+
"react-native-worklets": "*"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react-native-worklets": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
46
52
|
},
|
|
47
53
|
"devDependencies": {
|
|
48
54
|
"@types/react": "^18.2.0",
|
|
49
|
-
"@types/react-native": "^0.73.0",
|
|
50
55
|
"typescript": "^5.3.0"
|
|
51
56
|
}
|
|
52
57
|
}
|
package/src/SmoothSheet.tsx
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import React, { useEffect } from "react";
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
KeyboardAvoidingView,
|
|
5
|
-
Platform,
|
|
3
|
+
Modal,
|
|
6
4
|
Pressable,
|
|
7
5
|
StyleSheet,
|
|
6
|
+
useWindowDimensions,
|
|
8
7
|
View,
|
|
9
8
|
} from "react-native";
|
|
10
9
|
import {
|
|
11
10
|
Gesture,
|
|
12
11
|
GestureDetector,
|
|
12
|
+
GestureHandlerRootView,
|
|
13
13
|
} from "react-native-gesture-handler";
|
|
14
14
|
import Animated, {
|
|
15
|
+
KeyboardState,
|
|
15
16
|
runOnJS,
|
|
17
|
+
useAnimatedKeyboard,
|
|
16
18
|
useAnimatedStyle,
|
|
17
19
|
useSharedValue,
|
|
18
20
|
withSpring,
|
|
19
21
|
withTiming,
|
|
20
22
|
} from "react-native-reanimated";
|
|
21
23
|
|
|
22
|
-
const SCREEN_HEIGHT = Dimensions.get("window").height;
|
|
23
|
-
|
|
24
24
|
const DEFAULT_SPRING = { damping: 28, stiffness: 160, mass: 0.9 };
|
|
25
25
|
|
|
26
26
|
export interface SpringConfig {
|
|
@@ -37,12 +37,12 @@ export interface SmoothSheetProps {
|
|
|
37
37
|
/** Content rendered inside the sheet. */
|
|
38
38
|
children: React.ReactNode;
|
|
39
39
|
/**
|
|
40
|
-
* Minimum height of the sheet as a fraction of
|
|
40
|
+
* Minimum height of the sheet as a fraction of window height (0–1).
|
|
41
41
|
* @default 0.4
|
|
42
42
|
*/
|
|
43
43
|
minHeightFraction?: number;
|
|
44
44
|
/**
|
|
45
|
-
* Maximum height of the sheet as a fraction of
|
|
45
|
+
* Maximum height of the sheet as a fraction of window height (0–1).
|
|
46
46
|
* @default 0.97
|
|
47
47
|
*/
|
|
48
48
|
maxHeightFraction?: number;
|
|
@@ -81,6 +81,18 @@ export interface SmoothSheetProps {
|
|
|
81
81
|
* Merged over the default: { damping: 28, stiffness: 160, mass: 0.9 }
|
|
82
82
|
*/
|
|
83
83
|
springConfig?: SpringConfig;
|
|
84
|
+
/**
|
|
85
|
+
* Extra padding applied below the sheet content, e.g. the bottom safe-area
|
|
86
|
+
* inset on gesture-nav Android devices / notched iPhones. Pass
|
|
87
|
+
* `useSafeAreaInsets().bottom` from the host app.
|
|
88
|
+
* @default 0
|
|
89
|
+
*/
|
|
90
|
+
bottomInset?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Accessibility label announced for the backdrop's dismiss action.
|
|
93
|
+
* @default "Close"
|
|
94
|
+
*/
|
|
95
|
+
dismissAccessibilityLabel?: string;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
export function SmoothSheet({
|
|
@@ -96,32 +108,52 @@ export function SmoothSheet({
|
|
|
96
108
|
dismissThreshold = 80,
|
|
97
109
|
dismissVelocityThreshold = 800,
|
|
98
110
|
springConfig,
|
|
111
|
+
bottomInset = 0,
|
|
112
|
+
dismissAccessibilityLabel = "Close",
|
|
99
113
|
}: SmoothSheetProps) {
|
|
100
114
|
const spring = { ...DEFAULT_SPRING, ...springConfig };
|
|
101
115
|
|
|
102
|
-
|
|
116
|
+
// Live window height — correct across rotation, split-screen, and foldables.
|
|
117
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
118
|
+
|
|
119
|
+
// The keyboard is tracked on the UI thread and the sheet is *translated*
|
|
120
|
+
// above it. This works identically under edge-to-edge (forced on Android
|
|
121
|
+
// 15+/targetSdk 35+, where adjustResize is a no-op) and in legacy resize
|
|
122
|
+
// mode — the sheet never depends on the window resizing.
|
|
123
|
+
const keyboard = useAnimatedKeyboard();
|
|
124
|
+
|
|
125
|
+
const translateY = useSharedValue(windowHeight);
|
|
103
126
|
const backdropOpacity = useSharedValue(0);
|
|
127
|
+
// Keep Modal mounted during the closing animation so the sheet can slide out
|
|
128
|
+
const [modalVisible, setModalVisible] = useState(isVisible);
|
|
129
|
+
// Guards against double-dismiss (back button mashing, backdrop tap during close)
|
|
130
|
+
const isClosing = useRef(false);
|
|
104
131
|
|
|
105
132
|
useEffect(() => {
|
|
106
133
|
if (isVisible) {
|
|
134
|
+
isClosing.current = false;
|
|
135
|
+
setModalVisible(true);
|
|
107
136
|
translateY.value = withSpring(0, spring);
|
|
108
137
|
backdropOpacity.value = withTiming(1, { duration: 250 });
|
|
109
138
|
} else {
|
|
110
|
-
translateY.value = withSpring(
|
|
111
|
-
backdropOpacity.value = withTiming(0, { duration: 200 })
|
|
139
|
+
translateY.value = withSpring(windowHeight, spring);
|
|
140
|
+
backdropOpacity.value = withTiming(0, { duration: 200 }, (finished) => {
|
|
141
|
+
if (finished) runOnJS(setModalVisible)(false);
|
|
142
|
+
});
|
|
112
143
|
}
|
|
113
144
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
145
|
}, [isVisible]);
|
|
115
146
|
|
|
116
147
|
function dismiss() {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
()
|
|
148
|
+
if (isClosing.current) return;
|
|
149
|
+
isClosing.current = true;
|
|
150
|
+
translateY.value = withSpring(windowHeight, spring);
|
|
151
|
+
backdropOpacity.value = withTiming(0, { duration: 200 }, (finished) => {
|
|
152
|
+
if (finished) {
|
|
153
|
+
runOnJS(setModalVisible)(false);
|
|
122
154
|
runOnJS(onDismiss)();
|
|
123
|
-
}
|
|
124
|
-
);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
const panGesture = Gesture.Pan()
|
|
@@ -141,65 +173,80 @@ export function SmoothSheet({
|
|
|
141
173
|
}
|
|
142
174
|
});
|
|
143
175
|
|
|
144
|
-
const sheetStyle = useAnimatedStyle(() =>
|
|
145
|
-
|
|
146
|
-
|
|
176
|
+
const sheetStyle = useAnimatedStyle(() => {
|
|
177
|
+
// Only lift the sheet while the keyboard is actually present; when it is
|
|
178
|
+
// fully closed the translation collapses back to the drag/entry offset.
|
|
179
|
+
const keyboardOffset =
|
|
180
|
+
keyboard.state.value === KeyboardState.CLOSED
|
|
181
|
+
? 0
|
|
182
|
+
: keyboard.height.value;
|
|
183
|
+
return {
|
|
184
|
+
transform: [{ translateY: translateY.value - keyboardOffset }],
|
|
185
|
+
};
|
|
186
|
+
});
|
|
147
187
|
|
|
148
188
|
const backdropStyle = useAnimatedStyle(() => ({
|
|
149
189
|
opacity: backdropOpacity.value,
|
|
150
190
|
}));
|
|
151
191
|
|
|
152
|
-
if (!isVisible && translateY.value === SCREEN_HEIGHT) {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
192
|
return (
|
|
157
|
-
<
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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}
|
|
193
|
+
<Modal
|
|
194
|
+
visible={modalVisible}
|
|
195
|
+
transparent
|
|
196
|
+
animationType="none"
|
|
197
|
+
statusBarTranslucent
|
|
198
|
+
navigationBarTranslucent
|
|
199
|
+
onRequestClose={dismiss}
|
|
200
|
+
>
|
|
201
|
+
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
|
|
202
|
+
{/* Backdrop */}
|
|
203
|
+
<Animated.View
|
|
204
|
+
style={[
|
|
205
|
+
StyleSheet.absoluteFill,
|
|
206
|
+
{ backgroundColor: backdropColor },
|
|
207
|
+
backdropStyle,
|
|
208
|
+
]}
|
|
198
209
|
>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
210
|
+
<Pressable
|
|
211
|
+
style={StyleSheet.absoluteFill}
|
|
212
|
+
onPress={dismiss}
|
|
213
|
+
accessibilityRole="button"
|
|
214
|
+
accessibilityLabel={dismissAccessibilityLabel}
|
|
215
|
+
/>
|
|
216
|
+
</Animated.View>
|
|
217
|
+
|
|
218
|
+
{/* Sheet */}
|
|
219
|
+
<Animated.View
|
|
220
|
+
style={[
|
|
221
|
+
styles.sheet,
|
|
222
|
+
{
|
|
223
|
+
borderTopLeftRadius: borderRadius,
|
|
224
|
+
borderTopRightRadius: borderRadius,
|
|
225
|
+
backgroundColor,
|
|
226
|
+
minHeight: windowHeight * minHeightFraction,
|
|
227
|
+
maxHeight: windowHeight * maxHeightFraction,
|
|
228
|
+
paddingBottom: bottomInset,
|
|
229
|
+
},
|
|
230
|
+
sheetStyle,
|
|
231
|
+
]}
|
|
232
|
+
pointerEvents="box-none"
|
|
233
|
+
accessibilityViewIsModal
|
|
234
|
+
>
|
|
235
|
+
{/* Drag handle */}
|
|
236
|
+
<GestureDetector gesture={panGesture}>
|
|
237
|
+
<View style={styles.handleArea}>
|
|
238
|
+
<View
|
|
239
|
+
style={[styles.handle, { backgroundColor: handleColor }]}
|
|
240
|
+
/>
|
|
241
|
+
</View>
|
|
242
|
+
</GestureDetector>
|
|
243
|
+
|
|
244
|
+
{/* Content — keyboard avoidance is handled by translating the whole
|
|
245
|
+
sheet (see sheetStyle); no KeyboardAvoidingView needed. */}
|
|
246
|
+
<View style={styles.content}>{children}</View>
|
|
247
|
+
</Animated.View>
|
|
248
|
+
</GestureHandlerRootView>
|
|
249
|
+
</Modal>
|
|
203
250
|
);
|
|
204
251
|
}
|
|
205
252
|
|