insyd-bottom-sheet 0.2.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 +69 -28
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,11 +1,9 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
-
Dimensions,
|
|
4
|
-
KeyboardAvoidingView,
|
|
5
3
|
Modal,
|
|
6
|
-
Platform,
|
|
7
4
|
Pressable,
|
|
8
5
|
StyleSheet,
|
|
6
|
+
useWindowDimensions,
|
|
9
7
|
View,
|
|
10
8
|
} from "react-native";
|
|
11
9
|
import {
|
|
@@ -14,15 +12,15 @@ import {
|
|
|
14
12
|
GestureHandlerRootView,
|
|
15
13
|
} from "react-native-gesture-handler";
|
|
16
14
|
import Animated, {
|
|
15
|
+
KeyboardState,
|
|
17
16
|
runOnJS,
|
|
17
|
+
useAnimatedKeyboard,
|
|
18
18
|
useAnimatedStyle,
|
|
19
19
|
useSharedValue,
|
|
20
20
|
withSpring,
|
|
21
21
|
withTiming,
|
|
22
22
|
} from "react-native-reanimated";
|
|
23
23
|
|
|
24
|
-
const SCREEN_HEIGHT = Dimensions.get("window").height;
|
|
25
|
-
|
|
26
24
|
const DEFAULT_SPRING = { damping: 28, stiffness: 160, mass: 0.9 };
|
|
27
25
|
|
|
28
26
|
export interface SpringConfig {
|
|
@@ -39,12 +37,12 @@ export interface SmoothSheetProps {
|
|
|
39
37
|
/** Content rendered inside the sheet. */
|
|
40
38
|
children: React.ReactNode;
|
|
41
39
|
/**
|
|
42
|
-
* Minimum height of the sheet as a fraction of
|
|
40
|
+
* Minimum height of the sheet as a fraction of window height (0–1).
|
|
43
41
|
* @default 0.4
|
|
44
42
|
*/
|
|
45
43
|
minHeightFraction?: number;
|
|
46
44
|
/**
|
|
47
|
-
* Maximum height of the sheet as a fraction of
|
|
45
|
+
* Maximum height of the sheet as a fraction of window height (0–1).
|
|
48
46
|
* @default 0.97
|
|
49
47
|
*/
|
|
50
48
|
maxHeightFraction?: number;
|
|
@@ -83,6 +81,18 @@ export interface SmoothSheetProps {
|
|
|
83
81
|
* Merged over the default: { damping: 28, stiffness: 160, mass: 0.9 }
|
|
84
82
|
*/
|
|
85
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;
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
export function SmoothSheet({
|
|
@@ -98,32 +108,51 @@ export function SmoothSheet({
|
|
|
98
108
|
dismissThreshold = 80,
|
|
99
109
|
dismissVelocityThreshold = 800,
|
|
100
110
|
springConfig,
|
|
111
|
+
bottomInset = 0,
|
|
112
|
+
dismissAccessibilityLabel = "Close",
|
|
101
113
|
}: SmoothSheetProps) {
|
|
102
114
|
const spring = { ...DEFAULT_SPRING, ...springConfig };
|
|
103
115
|
|
|
104
|
-
|
|
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);
|
|
105
126
|
const backdropOpacity = useSharedValue(0);
|
|
106
127
|
// Keep Modal mounted during the closing animation so the sheet can slide out
|
|
107
128
|
const [modalVisible, setModalVisible] = useState(isVisible);
|
|
129
|
+
// Guards against double-dismiss (back button mashing, backdrop tap during close)
|
|
130
|
+
const isClosing = useRef(false);
|
|
108
131
|
|
|
109
132
|
useEffect(() => {
|
|
110
133
|
if (isVisible) {
|
|
134
|
+
isClosing.current = false;
|
|
111
135
|
setModalVisible(true);
|
|
112
136
|
translateY.value = withSpring(0, spring);
|
|
113
137
|
backdropOpacity.value = withTiming(1, { duration: 250 });
|
|
114
138
|
} else {
|
|
115
|
-
translateY.value = withSpring(
|
|
116
|
-
backdropOpacity.value = withTiming(0, { duration: 200 }, () => {
|
|
117
|
-
runOnJS(setModalVisible)(false);
|
|
139
|
+
translateY.value = withSpring(windowHeight, spring);
|
|
140
|
+
backdropOpacity.value = withTiming(0, { duration: 200 }, (finished) => {
|
|
141
|
+
if (finished) runOnJS(setModalVisible)(false);
|
|
118
142
|
});
|
|
119
143
|
}
|
|
120
144
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
121
145
|
}, [isVisible]);
|
|
122
146
|
|
|
123
147
|
function dismiss() {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
154
|
+
runOnJS(onDismiss)();
|
|
155
|
+
}
|
|
127
156
|
});
|
|
128
157
|
}
|
|
129
158
|
|
|
@@ -144,9 +173,17 @@ export function SmoothSheet({
|
|
|
144
173
|
}
|
|
145
174
|
});
|
|
146
175
|
|
|
147
|
-
const sheetStyle = useAnimatedStyle(() =>
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
});
|
|
150
187
|
|
|
151
188
|
const backdropStyle = useAnimatedStyle(() => ({
|
|
152
189
|
opacity: backdropOpacity.value,
|
|
@@ -158,6 +195,7 @@ export function SmoothSheet({
|
|
|
158
195
|
transparent
|
|
159
196
|
animationType="none"
|
|
160
197
|
statusBarTranslucent
|
|
198
|
+
navigationBarTranslucent
|
|
161
199
|
onRequestClose={dismiss}
|
|
162
200
|
>
|
|
163
201
|
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
|
|
@@ -169,7 +207,12 @@ export function SmoothSheet({
|
|
|
169
207
|
backdropStyle,
|
|
170
208
|
]}
|
|
171
209
|
>
|
|
172
|
-
<Pressable
|
|
210
|
+
<Pressable
|
|
211
|
+
style={StyleSheet.absoluteFill}
|
|
212
|
+
onPress={dismiss}
|
|
213
|
+
accessibilityRole="button"
|
|
214
|
+
accessibilityLabel={dismissAccessibilityLabel}
|
|
215
|
+
/>
|
|
173
216
|
</Animated.View>
|
|
174
217
|
|
|
175
218
|
{/* Sheet */}
|
|
@@ -180,12 +223,14 @@ export function SmoothSheet({
|
|
|
180
223
|
borderTopLeftRadius: borderRadius,
|
|
181
224
|
borderTopRightRadius: borderRadius,
|
|
182
225
|
backgroundColor,
|
|
183
|
-
minHeight:
|
|
184
|
-
maxHeight:
|
|
226
|
+
minHeight: windowHeight * minHeightFraction,
|
|
227
|
+
maxHeight: windowHeight * maxHeightFraction,
|
|
228
|
+
paddingBottom: bottomInset,
|
|
185
229
|
},
|
|
186
230
|
sheetStyle,
|
|
187
231
|
]}
|
|
188
232
|
pointerEvents="box-none"
|
|
233
|
+
accessibilityViewIsModal
|
|
189
234
|
>
|
|
190
235
|
{/* Drag handle */}
|
|
191
236
|
<GestureDetector gesture={panGesture}>
|
|
@@ -196,13 +241,9 @@ export function SmoothSheet({
|
|
|
196
241
|
</View>
|
|
197
242
|
</GestureDetector>
|
|
198
243
|
|
|
199
|
-
{/* Content
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
style={styles.content}
|
|
203
|
-
>
|
|
204
|
-
{children}
|
|
205
|
-
</KeyboardAvoidingView>
|
|
244
|
+
{/* Content — keyboard avoidance is handled by translating the whole
|
|
245
|
+
sheet (see sheetStyle); no KeyboardAvoidingView needed. */}
|
|
246
|
+
<View style={styles.content}>{children}</View>
|
|
206
247
|
</Animated.View>
|
|
207
248
|
</GestureHandlerRootView>
|
|
208
249
|
</Modal>
|