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.
Files changed (2) hide show
  1. package/package.json +8 -3
  2. 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.2.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,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 screen height (0–1).
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 screen height (0–1).
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
- 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);
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(SCREEN_HEIGHT, spring);
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
- translateY.value = withSpring(SCREEN_HEIGHT, spring);
125
- backdropOpacity.value = withTiming(0, { duration: 200 }, () => {
126
- runOnJS(onDismiss)();
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
- transform: [{ translateY: translateY.value }],
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 style={StyleSheet.absoluteFill} onPress={dismiss} />
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: SCREEN_HEIGHT * minHeightFraction,
184
- maxHeight: SCREEN_HEIGHT * maxHeightFraction,
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
- <KeyboardAvoidingView
201
- behavior={Platform.OS === "ios" ? "padding" : "height"}
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>