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.
Files changed (2) hide show
  1. package/package.json +8 -3
  2. 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.1.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
  }
@@ -1,26 +1,26 @@
1
- import React, { useEffect } from "react";
1
+ import React, { useEffect, useRef, useState } from "react";
2
2
  import {
3
- Dimensions,
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 screen height (0–1).
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 screen height (0–1).
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
- const translateY = useSharedValue(SCREEN_HEIGHT);
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(SCREEN_HEIGHT, spring);
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
- translateY.value = withSpring(SCREEN_HEIGHT, spring);
118
- backdropOpacity.value = withTiming(
119
- 0,
120
- { duration: 200 },
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
- transform: [{ translateY: translateY.value }],
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
- <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}
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
- {children}
200
- </KeyboardAvoidingView>
201
- </Animated.View>
202
- </View>
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