@swmansion/react-native-bottom-sheet 0.3.1 → 0.4.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/README.md +28 -0
- package/lib/module/BottomSheetBase.js +28 -143
- package/lib/module/BottomSheetBase.js.map +1 -1
- package/lib/module/BottomSheetProvider.js.map +1 -1
- package/lib/module/bottomSheetUtils.js +55 -0
- package/lib/module/bottomSheetUtils.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/useBottomSheetPanGesture.js +149 -0
- package/lib/module/useBottomSheetPanGesture.js.map +1 -0
- package/lib/typescript/src/BottomSheetBase.d.ts +3 -1
- package/lib/typescript/src/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/src/BottomSheetProvider.d.ts.map +1 -1
- package/lib/typescript/src/bottomSheetUtils.d.ts +16 -0
- package/lib/typescript/src/bottomSheetUtils.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/useBottomSheetPanGesture.d.ts +20 -0
- package/lib/typescript/src/useBottomSheetPanGesture.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/BottomSheetBase.tsx +57 -181
- package/src/BottomSheetProvider.tsx +4 -0
- package/src/bottomSheetUtils.ts +82 -0
- package/src/index.tsx +2 -1
- package/src/useBottomSheetPanGesture.ts +208 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EACV,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EACV,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EACV,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EACV,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PanGesture } from 'react-native-gesture-handler';
|
|
2
|
+
import { type AnimatedRef, type SharedValue } from 'react-native-reanimated';
|
|
3
|
+
interface BottomSheetPanGestureParams {
|
|
4
|
+
animationTarget: SharedValue<number>;
|
|
5
|
+
translateY: SharedValue<number>;
|
|
6
|
+
sheetHeight: SharedValue<number>;
|
|
7
|
+
detentsValue: SharedValue<number[]>;
|
|
8
|
+
isDraggableValue: SharedValue<boolean[]>;
|
|
9
|
+
currentIndex: SharedValue<number>;
|
|
10
|
+
scrollOffset: SharedValue<number>;
|
|
11
|
+
hasScrollable: SharedValue<boolean>;
|
|
12
|
+
isScrollableGestureActive: SharedValue<boolean>;
|
|
13
|
+
isScrollableLocked: SharedValue<boolean>;
|
|
14
|
+
scrollableRef: AnimatedRef<any>;
|
|
15
|
+
handleIndexChange: (nextIndex: number) => void;
|
|
16
|
+
animateToIndex: (targetIndex: number, velocity?: number) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const useBottomSheetPanGesture: ({ animationTarget, translateY, sheetHeight, detentsValue, isDraggableValue, currentIndex, scrollOffset, hasScrollable, isScrollableGestureActive, isScrollableLocked, scrollableRef, handleIndexChange, animateToIndex, }: BottomSheetPanGestureParams) => PanGesture;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=useBottomSheetPanGesture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBottomSheetPanGesture.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetPanGesture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAG/D,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,WAAW,EAEjB,MAAM,yBAAyB,CAAC;AAIjC,UAAU,2BAA2B;IACnC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;IACpC,gBAAgB,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IACzC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,aAAa,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,yBAAyB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAChD,kBAAkB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,aAAa,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,cAAc,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE;AAED,eAAO,MAAM,wBAAwB,GAAI,2NActC,2BAA2B,KAAG,UAoKhC,CAAC"}
|
package/package.json
CHANGED
package/src/BottomSheetBase.tsx
CHANGED
|
@@ -4,8 +4,6 @@ import type { LayoutChangeEvent } from 'react-native';
|
|
|
4
4
|
import { Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
|
|
5
5
|
import type { SharedValue, WithSpringConfig } from 'react-native-reanimated';
|
|
6
6
|
import Animated, {
|
|
7
|
-
measure,
|
|
8
|
-
scrollTo,
|
|
9
7
|
useAnimatedRef,
|
|
10
8
|
useAnimatedReaction,
|
|
11
9
|
useAnimatedStyle,
|
|
@@ -13,13 +11,20 @@ import Animated, {
|
|
|
13
11
|
useSharedValue,
|
|
14
12
|
withSpring,
|
|
15
13
|
} from 'react-native-reanimated';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
14
|
+
import { scheduleOnUI } from 'react-native-worklets';
|
|
15
|
+
import { GestureDetector } from 'react-native-gesture-handler';
|
|
18
16
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
19
17
|
import { Portal } from './BottomSheetProvider';
|
|
20
18
|
import { BottomSheetContextProvider } from './BottomSheetContext';
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
import {
|
|
20
|
+
clampIndex,
|
|
21
|
+
isDetentProgrammatic,
|
|
22
|
+
resolveDetent,
|
|
23
|
+
} from './bottomSheetUtils';
|
|
24
|
+
import type { Detent } from './bottomSheetUtils';
|
|
25
|
+
import { useBottomSheetPanGesture } from './useBottomSheetPanGesture';
|
|
26
|
+
export type { Detent, DetentValue } from './bottomSheetUtils';
|
|
27
|
+
export { programmatic } from './bottomSheetUtils';
|
|
23
28
|
|
|
24
29
|
export interface BottomSheetCommonProps {
|
|
25
30
|
children: ReactNode;
|
|
@@ -48,25 +53,6 @@ const DEFAULT_CLOSE_ANIMATION_CONFIG: WithSpringConfig = {
|
|
|
48
53
|
overshootClamping: true,
|
|
49
54
|
};
|
|
50
55
|
|
|
51
|
-
const VELOCITY_THRESHOLD = 800;
|
|
52
|
-
|
|
53
|
-
const resolveDetent = (
|
|
54
|
-
detent: Detent,
|
|
55
|
-
contentHeight: number,
|
|
56
|
-
maxHeight: number
|
|
57
|
-
) => {
|
|
58
|
-
if (typeof detent === 'number') return detent;
|
|
59
|
-
if (detent === 'max') {
|
|
60
|
-
return contentHeight > 0 ? Math.min(contentHeight, maxHeight) : maxHeight;
|
|
61
|
-
}
|
|
62
|
-
throw new Error(`Invalid detent: \`${detent}\`.`);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const clampIndex = (index: number, detentCount: number) => {
|
|
66
|
-
if (detentCount <= 0) return 0;
|
|
67
|
-
return Math.min(Math.max(index, 0), detentCount - 1);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
56
|
const DefaultScrim = ({ progress }: { progress: SharedValue<number> }) => {
|
|
71
57
|
const style = useAnimatedStyle(() => ({ opacity: progress.value }));
|
|
72
58
|
return (
|
|
@@ -96,14 +82,18 @@ export const BottomSheetBase = ({
|
|
|
96
82
|
const maxHeight = screenHeight - insets.top;
|
|
97
83
|
const resolvedIndex = clampIndex(index, detents.length);
|
|
98
84
|
const [contentHeight, setContentHeight] = useState(0);
|
|
85
|
+
|
|
99
86
|
if (detents.length === 0) {
|
|
100
87
|
throw new Error('detents must include at least one value.');
|
|
101
88
|
}
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
|
|
90
|
+
const normalizedDetents = detents.map((detent) => {
|
|
91
|
+
const resolved = resolveDetent(detent, contentHeight, maxHeight);
|
|
104
92
|
return Math.max(0, Math.min(resolved, maxHeight));
|
|
105
93
|
});
|
|
94
|
+
const isDraggable = detents.map((detent) => !isDetentProgrammatic(detent));
|
|
106
95
|
const initialMaxSnap = Math.max(0, ...normalizedDetents);
|
|
96
|
+
|
|
107
97
|
const translateY = useSharedValue(initialMaxSnap);
|
|
108
98
|
const animationTarget = useSharedValue(NaN);
|
|
109
99
|
const sheetHeight = useSharedValue(initialMaxSnap);
|
|
@@ -112,41 +102,50 @@ export const BottomSheetBase = ({
|
|
|
112
102
|
const isScrollableGestureActive = useSharedValue(false);
|
|
113
103
|
const isScrollableLocked = useSharedValue(false);
|
|
114
104
|
const scrollableRef = useAnimatedRef();
|
|
115
|
-
|
|
116
|
-
const isDraggingFromScrollable = useSharedValue(false);
|
|
117
|
-
const panStartY = useSharedValue(0);
|
|
118
|
-
const panActivated = useSharedValue(false);
|
|
119
|
-
const dragStartTranslateY = useSharedValue(0);
|
|
120
|
-
const isTouchWithinScrollable = useSharedValue(false);
|
|
105
|
+
|
|
121
106
|
const detentsValue = useSharedValue(normalizedDetents);
|
|
107
|
+
const isDraggableValue = useSharedValue(isDraggable);
|
|
122
108
|
const firstNonzeroDetent = useSharedValue(
|
|
123
|
-
normalizedDetents.find((
|
|
109
|
+
normalizedDetents.find((detent) => detent > 0) ?? 0
|
|
124
110
|
);
|
|
125
111
|
const currentIndex = useSharedValue(resolvedIndex);
|
|
126
112
|
const internalPosition = useDerivedValue(() =>
|
|
127
113
|
Math.max(0, sheetHeight.value - translateY.value)
|
|
128
114
|
);
|
|
115
|
+
|
|
129
116
|
useAnimatedReaction(
|
|
130
117
|
() => internalPosition.value,
|
|
131
118
|
(value) => {
|
|
132
119
|
if (externalPosition !== undefined) externalPosition.set(value);
|
|
133
120
|
}
|
|
134
121
|
);
|
|
122
|
+
|
|
135
123
|
const scrimProgress = useDerivedValue(() => {
|
|
136
124
|
const target = firstNonzeroDetent.value;
|
|
137
125
|
if (target <= 0) return 0;
|
|
138
126
|
const progress = internalPosition.value / target;
|
|
139
127
|
return Math.min(1, Math.max(0, progress));
|
|
140
128
|
});
|
|
129
|
+
|
|
141
130
|
const handleIndexChange = (nextIndex: number) => {
|
|
142
131
|
onIndexChange?.(nextIndex);
|
|
143
132
|
};
|
|
133
|
+
|
|
144
134
|
useEffect(() => {
|
|
145
135
|
const maxSnap = Math.max(0, ...normalizedDetents);
|
|
146
136
|
detentsValue.set(normalizedDetents);
|
|
137
|
+
isDraggableValue.set(isDraggable);
|
|
147
138
|
sheetHeight.set(maxSnap);
|
|
148
|
-
firstNonzeroDetent.set(normalizedDetents.find((
|
|
149
|
-
}, [
|
|
139
|
+
firstNonzeroDetent.set(normalizedDetents.find((detent) => detent > 0) ?? 0);
|
|
140
|
+
}, [
|
|
141
|
+
normalizedDetents,
|
|
142
|
+
isDraggable,
|
|
143
|
+
sheetHeight,
|
|
144
|
+
detentsValue,
|
|
145
|
+
isDraggableValue,
|
|
146
|
+
firstNonzeroDetent,
|
|
147
|
+
]);
|
|
148
|
+
|
|
150
149
|
const animateToIndex = useCallback(
|
|
151
150
|
(targetIndex: number, velocity?: number) => {
|
|
152
151
|
'worklet';
|
|
@@ -174,154 +173,27 @@ export const BottomSheetBase = ({
|
|
|
174
173
|
translateY,
|
|
175
174
|
]
|
|
176
175
|
);
|
|
176
|
+
|
|
177
177
|
useEffect(() => {
|
|
178
178
|
scheduleOnUI(animateToIndex, resolvedIndex);
|
|
179
179
|
}, [animateToIndex, resolvedIndex, normalizedDetents]);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
touch.absoluteX <= layout.pageX + layout.width;
|
|
198
|
-
const withinY =
|
|
199
|
-
touch.absoluteY >= layout.pageY &&
|
|
200
|
-
touch.absoluteY <= layout.pageY + layout.height;
|
|
201
|
-
isTouchWithinScrollable.set(withinX && withinY);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
})
|
|
206
|
-
.onTouchesMove((event, stateManager) => {
|
|
207
|
-
'worklet';
|
|
208
|
-
if (panActivated.value) return;
|
|
209
|
-
const touch = event.changedTouches[0] ?? event.allTouches[0];
|
|
210
|
-
if (!touch) return;
|
|
211
|
-
const deltaY = touch.absoluteY - panStartY.value;
|
|
212
|
-
if (
|
|
213
|
-
hasScrollable.value &&
|
|
214
|
-
scrollOffset.value > 0 &&
|
|
215
|
-
isTouchWithinScrollable.value
|
|
216
|
-
) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (deltaY > 0 || translateY.value > 0) {
|
|
220
|
-
panActivated.set(true);
|
|
221
|
-
stateManager.activate();
|
|
222
|
-
}
|
|
223
|
-
})
|
|
224
|
-
.onBegin(() => {
|
|
225
|
-
'worklet';
|
|
226
|
-
animationTarget.set(NaN);
|
|
227
|
-
isDraggingSheet.set(false);
|
|
228
|
-
isDraggingFromScrollable.set(false);
|
|
229
|
-
dragStartTranslateY.set(translateY.value);
|
|
230
|
-
})
|
|
231
|
-
.onUpdate((event) => {
|
|
232
|
-
'worklet';
|
|
233
|
-
if (isDraggingSheet.value) {
|
|
234
|
-
if (isDraggingFromScrollable.value) {
|
|
235
|
-
scrollTo(scrollableRef, 0, 0, false);
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
const isDraggingDown = event.translationY > 0;
|
|
239
|
-
const canStartDrag =
|
|
240
|
-
!hasScrollable.value ||
|
|
241
|
-
scrollOffset.value <= 0 ||
|
|
242
|
-
!isTouchWithinScrollable.value;
|
|
243
|
-
if (!canStartDrag || (!isDraggingDown && translateY.value <= 0)) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const isScrollableActive =
|
|
247
|
-
hasScrollable.value && isScrollableGestureActive.value;
|
|
248
|
-
isDraggingSheet.set(true);
|
|
249
|
-
isDraggingFromScrollable.set(
|
|
250
|
-
isScrollableActive && isTouchWithinScrollable.value
|
|
251
|
-
);
|
|
252
|
-
isScrollableLocked.set(hasScrollable.value);
|
|
253
|
-
if (isTouchWithinScrollable.value && hasScrollable.value) {
|
|
254
|
-
scrollTo(scrollableRef, 0, 0, false);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const nextTranslate = Math.min(
|
|
258
|
-
Math.max(dragStartTranslateY.value + event.translationY, 0),
|
|
259
|
-
sheetHeight.value
|
|
260
|
-
);
|
|
261
|
-
translateY.set(nextTranslate);
|
|
262
|
-
if (
|
|
263
|
-
isDraggingSheet.value &&
|
|
264
|
-
nextTranslate <= 0 &&
|
|
265
|
-
isTouchWithinScrollable.value &&
|
|
266
|
-
hasScrollable.value
|
|
267
|
-
) {
|
|
268
|
-
isDraggingSheet.set(false);
|
|
269
|
-
isScrollableLocked.set(false);
|
|
270
|
-
const resolvedDetents = detentsValue.value;
|
|
271
|
-
const maxSnap = sheetHeight.value;
|
|
272
|
-
for (let i = resolvedDetents.length - 1; i >= 0; i--) {
|
|
273
|
-
if (resolvedDetents[i] === maxSnap) {
|
|
274
|
-
if (i !== currentIndex.value) scheduleOnRN(handleIndexChange, i);
|
|
275
|
-
animateToIndex(i);
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
})
|
|
281
|
-
.onEnd((event) => {
|
|
282
|
-
'worklet';
|
|
283
|
-
const wasDragging = isDraggingSheet.value;
|
|
284
|
-
isScrollableLocked.set(false);
|
|
285
|
-
isDraggingSheet.set(false);
|
|
286
|
-
animationTarget.set(NaN);
|
|
287
|
-
if (!wasDragging) {
|
|
288
|
-
animateToIndex(currentIndex.value);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
const maxSnap = sheetHeight.value;
|
|
292
|
-
const allPositions = detentsValue.value.map((point, snapIndex) => ({
|
|
293
|
-
index: snapIndex,
|
|
294
|
-
translateY: maxSnap - point,
|
|
295
|
-
}));
|
|
296
|
-
const currentTranslate = translateY.value;
|
|
297
|
-
const velocityY = event.velocityY;
|
|
298
|
-
let targetIndex = currentIndex.value;
|
|
299
|
-
let minDistance = Infinity;
|
|
300
|
-
for (const pos of allPositions) {
|
|
301
|
-
const distance = Math.abs(currentTranslate - pos.translateY);
|
|
302
|
-
if (distance < minDistance) {
|
|
303
|
-
minDistance = distance;
|
|
304
|
-
targetIndex = pos.index;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (Math.abs(velocityY) > VELOCITY_THRESHOLD) {
|
|
308
|
-
if (velocityY > 0) {
|
|
309
|
-
const lower = allPositions
|
|
310
|
-
.filter((pos) => pos.translateY > currentTranslate + 1)
|
|
311
|
-
.sort((a, b) => a.translateY - b.translateY)[0];
|
|
312
|
-
if (lower !== undefined) targetIndex = lower.index;
|
|
313
|
-
} else {
|
|
314
|
-
const upper = allPositions
|
|
315
|
-
.filter((pos) => pos.translateY < currentTranslate - 1)
|
|
316
|
-
.sort((a, b) => b.translateY - a.translateY)[0];
|
|
317
|
-
if (upper !== undefined) targetIndex = upper.index;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
const hasIndexChanged = targetIndex !== currentIndex.value;
|
|
321
|
-
if (hasIndexChanged) scheduleOnRN(handleIndexChange, targetIndex);
|
|
322
|
-
const shouldApplyVelocity = hasIndexChanged && Number.isFinite(velocityY);
|
|
323
|
-
animateToIndex(targetIndex, shouldApplyVelocity ? velocityY : undefined);
|
|
324
|
-
});
|
|
180
|
+
|
|
181
|
+
const panGesture = useBottomSheetPanGesture({
|
|
182
|
+
animationTarget,
|
|
183
|
+
translateY,
|
|
184
|
+
sheetHeight,
|
|
185
|
+
detentsValue,
|
|
186
|
+
isDraggableValue,
|
|
187
|
+
currentIndex,
|
|
188
|
+
scrollOffset,
|
|
189
|
+
hasScrollable,
|
|
190
|
+
isScrollableGestureActive,
|
|
191
|
+
isScrollableLocked,
|
|
192
|
+
scrollableRef,
|
|
193
|
+
handleIndexChange,
|
|
194
|
+
animateToIndex,
|
|
195
|
+
});
|
|
196
|
+
|
|
325
197
|
const handleSentinelLayout = (event: LayoutChangeEvent) => {
|
|
326
198
|
setContentHeight(event.nativeEvent.layout.y);
|
|
327
199
|
};
|
|
@@ -331,6 +203,7 @@ export const BottomSheetBase = ({
|
|
|
331
203
|
handleIndexChange(closedIndex);
|
|
332
204
|
scheduleOnUI(animateToIndex, closedIndex);
|
|
333
205
|
};
|
|
206
|
+
|
|
334
207
|
const wrapperStyle = useAnimatedStyle(() => ({
|
|
335
208
|
transform: [{ translateY: translateY.value }],
|
|
336
209
|
height: sheetHeight.value,
|
|
@@ -344,6 +217,7 @@ export const BottomSheetBase = ({
|
|
|
344
217
|
} else if (modal) {
|
|
345
218
|
scrimElement = <DefaultScrim progress={scrimProgress} />;
|
|
346
219
|
}
|
|
220
|
+
|
|
347
221
|
const sheetContent = (
|
|
348
222
|
<BottomSheetContextProvider
|
|
349
223
|
value={{
|
|
@@ -380,6 +254,7 @@ export const BottomSheetBase = ({
|
|
|
380
254
|
</Animated.View>
|
|
381
255
|
</BottomSheetContextProvider>
|
|
382
256
|
);
|
|
257
|
+
|
|
383
258
|
const sheetContainer = (
|
|
384
259
|
<Animated.View
|
|
385
260
|
style={StyleSheet.absoluteFill}
|
|
@@ -394,5 +269,6 @@ export const BottomSheetBase = ({
|
|
|
394
269
|
</Animated.View>
|
|
395
270
|
);
|
|
396
271
|
if (modal) return <Portal>{sheetContainer}</Portal>;
|
|
272
|
+
|
|
397
273
|
return sheetContainer;
|
|
398
274
|
};
|
|
@@ -24,6 +24,7 @@ const PortalHost = () => {
|
|
|
24
24
|
useEffect(() => {
|
|
25
25
|
return context.subscribe(forceRender);
|
|
26
26
|
}, [context]);
|
|
27
|
+
|
|
27
28
|
return Array.from(context.getPortals().entries()).map(([key, element]) => (
|
|
28
29
|
<View key={key} style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
29
30
|
{element}
|
|
@@ -56,6 +57,7 @@ export const BottomSheetProvider = ({ children }: { children: ReactNode }) => {
|
|
|
56
57
|
getPortals: () => portals,
|
|
57
58
|
};
|
|
58
59
|
});
|
|
60
|
+
|
|
59
61
|
return (
|
|
60
62
|
<PortalContext.Provider value={context}>
|
|
61
63
|
{children}
|
|
@@ -69,8 +71,10 @@ export const Portal = ({ children }: { children: ReactNode }) => {
|
|
|
69
71
|
if (context === null) {
|
|
70
72
|
throw new Error('`Portal` must be used within `BottomSheetProvider`.');
|
|
71
73
|
}
|
|
74
|
+
|
|
72
75
|
const { addPortal, removePortal } = context;
|
|
73
76
|
const id = useId();
|
|
77
|
+
|
|
74
78
|
useEffect(() => {
|
|
75
79
|
addPortal(id, children);
|
|
76
80
|
}, [id, children, addPortal]);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type DetentValue = number | 'max';
|
|
2
|
+
|
|
3
|
+
export type Detent =
|
|
4
|
+
| DetentValue
|
|
5
|
+
| { value: DetentValue; programmatic?: boolean };
|
|
6
|
+
|
|
7
|
+
export const programmatic = (value: DetentValue): Detent => ({
|
|
8
|
+
value,
|
|
9
|
+
programmatic: true,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const detentValue = (detent: Detent): DetentValue => {
|
|
13
|
+
if (typeof detent === 'object' && detent !== null) return detent.value;
|
|
14
|
+
return detent;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const isDetentProgrammatic = (detent: Detent): boolean => {
|
|
18
|
+
if (typeof detent === 'object' && detent !== null) {
|
|
19
|
+
return detent.programmatic === true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const VELOCITY_THRESHOLD = 800;
|
|
25
|
+
|
|
26
|
+
export const findSnapTarget = (
|
|
27
|
+
currentTranslate: number,
|
|
28
|
+
velocityY: number,
|
|
29
|
+
currentIndex: number,
|
|
30
|
+
allPositions: { index: number; translateY: number; isDraggable: boolean }[]
|
|
31
|
+
) => {
|
|
32
|
+
'worklet';
|
|
33
|
+
const draggablePositions = allPositions.filter(
|
|
34
|
+
(position) => position.isDraggable
|
|
35
|
+
);
|
|
36
|
+
const effectivePositions =
|
|
37
|
+
draggablePositions.length > 0 ? draggablePositions : allPositions;
|
|
38
|
+
|
|
39
|
+
let targetIndex = currentIndex;
|
|
40
|
+
let minDistance = Infinity;
|
|
41
|
+
|
|
42
|
+
for (const position of effectivePositions) {
|
|
43
|
+
const distance = Math.abs(currentTranslate - position.translateY);
|
|
44
|
+
if (distance < minDistance) {
|
|
45
|
+
minDistance = distance;
|
|
46
|
+
targetIndex = position.index;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Math.abs(velocityY) > VELOCITY_THRESHOLD) {
|
|
51
|
+
if (velocityY > 0) {
|
|
52
|
+
const lowerPosition = effectivePositions
|
|
53
|
+
.filter((position) => position.translateY > currentTranslate + 1)
|
|
54
|
+
.sort((a, b) => a.translateY - b.translateY)[0];
|
|
55
|
+
if (lowerPosition !== undefined) targetIndex = lowerPosition.index;
|
|
56
|
+
} else {
|
|
57
|
+
const upperPosition = effectivePositions
|
|
58
|
+
.filter((position) => position.translateY < currentTranslate - 1)
|
|
59
|
+
.sort((a, b) => b.translateY - a.translateY)[0];
|
|
60
|
+
if (upperPosition !== undefined) targetIndex = upperPosition.index;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return targetIndex;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const resolveDetent = (
|
|
67
|
+
detent: Detent,
|
|
68
|
+
contentHeight: number,
|
|
69
|
+
maxHeight: number
|
|
70
|
+
) => {
|
|
71
|
+
const detentValueInput = detentValue(detent);
|
|
72
|
+
if (typeof detentValueInput === 'number') return detentValueInput;
|
|
73
|
+
if (detentValueInput === 'max') {
|
|
74
|
+
return contentHeight > 0 ? Math.min(contentHeight, maxHeight) : maxHeight;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Invalid detent: \`${detentValueInput}\`.`);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const clampIndex = (index: number, detentCount: number) => {
|
|
80
|
+
if (detentCount <= 0) return 0;
|
|
81
|
+
return Math.min(Math.max(index, 0), detentCount - 1);
|
|
82
|
+
};
|
package/src/index.tsx
CHANGED
|
@@ -13,4 +13,5 @@ export type {
|
|
|
13
13
|
BottomSheetScrollViewMethods,
|
|
14
14
|
BottomSheetScrollViewProps,
|
|
15
15
|
} from './BottomSheetScrollView';
|
|
16
|
-
export type { Detent } from './BottomSheetBase';
|
|
16
|
+
export type { Detent, DetentValue } from './BottomSheetBase';
|
|
17
|
+
export { programmatic } from './BottomSheetBase';
|