@swmansion/react-native-bottom-sheet 0.7.0-next.2 → 0.7.0-next.4
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/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetView.kt +4 -1
- package/ios/BottomSheetContentView.mm +6 -0
- package/ios/RNSBottomSheetHostingView.swift +44 -0
- package/lib/module/BottomSheet.js +101 -38
- package/lib/module/BottomSheet.js.map +1 -1
- package/lib/module/ModalBottomSheet.js +3 -7
- package/lib/module/ModalBottomSheet.js.map +1 -1
- package/lib/module/bottomSheetUtils.js +0 -2
- package/lib/module/bottomSheetUtils.js.map +1 -1
- package/lib/module/index.js +0 -3
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/BottomSheet.d.ts +4 -3
- package/lib/typescript/src/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/src/ModalBottomSheet.d.ts +3 -6
- package/lib/typescript/src/ModalBottomSheet.d.ts.map +1 -1
- package/lib/typescript/src/bottomSheetUtils.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +0 -5
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +2 -8
- package/src/BottomSheet.tsx +144 -37
- package/src/ModalBottomSheet.tsx +5 -13
- package/src/bottomSheetUtils.ts +0 -1
- package/src/index.tsx +0 -5
- package/lib/module/BottomSheetBase.js +0 -207
- package/lib/module/BottomSheetBase.js.map +0 -1
- package/lib/module/BottomSheetContext.js +0 -13
- package/lib/module/BottomSheetContext.js.map +0 -1
- package/lib/module/BottomSheetFlatList.js +0 -6
- package/lib/module/BottomSheetFlatList.js.map +0 -1
- package/lib/module/BottomSheetScrollView.js +0 -6
- package/lib/module/BottomSheetScrollView.js.map +0 -1
- package/lib/module/bottomSheetScrollable.js +0 -35
- package/lib/module/bottomSheetScrollable.js.map +0 -1
- package/lib/module/useBottomSheetPanGesture.js +0 -202
- package/lib/module/useBottomSheetPanGesture.js.map +0 -1
- package/lib/module/useBottomSheetScrollable.js +0 -61
- package/lib/module/useBottomSheetScrollable.js.map +0 -1
- package/lib/typescript/src/BottomSheetBase.d.ts +0 -20
- package/lib/typescript/src/BottomSheetBase.d.ts.map +0 -1
- package/lib/typescript/src/BottomSheetContext.d.ts +0 -19
- package/lib/typescript/src/BottomSheetContext.d.ts.map +0 -1
- package/lib/typescript/src/BottomSheetFlatList.d.ts +0 -10
- package/lib/typescript/src/BottomSheetFlatList.d.ts.map +0 -1
- package/lib/typescript/src/BottomSheetScrollView.d.ts +0 -10
- package/lib/typescript/src/BottomSheetScrollView.d.ts.map +0 -1
- package/lib/typescript/src/bottomSheetScrollable.d.ts +0 -9
- package/lib/typescript/src/bottomSheetScrollable.d.ts.map +0 -1
- package/lib/typescript/src/useBottomSheetPanGesture.d.ts +0 -18
- package/lib/typescript/src/useBottomSheetPanGesture.d.ts.map +0 -1
- package/lib/typescript/src/useBottomSheetScrollable.d.ts +0 -13
- package/lib/typescript/src/useBottomSheetScrollable.d.ts.map +0 -1
- package/src/BottomSheetBase.tsx +0 -276
- package/src/BottomSheetContext.tsx +0 -33
- package/src/BottomSheetFlatList.tsx +0 -21
- package/src/BottomSheetScrollView.tsx +0 -22
- package/src/bottomSheetScrollable.tsx +0 -42
- package/src/useBottomSheetPanGesture.ts +0 -253
- package/src/useBottomSheetScrollable.ts +0 -68
package/src/BottomSheetBase.tsx
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import type { ReactNode } from 'react';
|
|
3
|
-
import type { LayoutChangeEvent } from 'react-native';
|
|
4
|
-
import { Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
|
|
5
|
-
import type { SharedValue, WithSpringConfig } from 'react-native-reanimated';
|
|
6
|
-
import Animated, {
|
|
7
|
-
useAnimatedReaction,
|
|
8
|
-
useAnimatedStyle,
|
|
9
|
-
useDerivedValue,
|
|
10
|
-
useSharedValue,
|
|
11
|
-
withSpring,
|
|
12
|
-
} from 'react-native-reanimated';
|
|
13
|
-
import { scheduleOnUI } from 'react-native-worklets';
|
|
14
|
-
import { GestureDetector } from 'react-native-gesture-handler';
|
|
15
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
16
|
-
import { Portal } from './BottomSheetProvider';
|
|
17
|
-
import {
|
|
18
|
-
BottomSheetContextProvider,
|
|
19
|
-
type ScrollableEntry,
|
|
20
|
-
} from './BottomSheetContext';
|
|
21
|
-
import {
|
|
22
|
-
clampIndex,
|
|
23
|
-
isDetentProgrammatic,
|
|
24
|
-
resolveDetent,
|
|
25
|
-
} from './bottomSheetUtils';
|
|
26
|
-
import type { Detent } from './bottomSheetUtils';
|
|
27
|
-
import { useBottomSheetPanGesture } from './useBottomSheetPanGesture';
|
|
28
|
-
export type { Detent, DetentValue } from './bottomSheetUtils';
|
|
29
|
-
export { programmatic } from './bottomSheetUtils';
|
|
30
|
-
|
|
31
|
-
export interface BottomSheetCommonProps {
|
|
32
|
-
children: ReactNode;
|
|
33
|
-
detents?: Detent[];
|
|
34
|
-
index: number;
|
|
35
|
-
onIndexChange?: (index: number) => void;
|
|
36
|
-
position?: SharedValue<number>;
|
|
37
|
-
openAnimationConfig?: WithSpringConfig;
|
|
38
|
-
closeAnimationConfig?: WithSpringConfig;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface BottomSheetBaseProps extends BottomSheetCommonProps {
|
|
42
|
-
modal?: boolean;
|
|
43
|
-
renderScrim?: (progress: SharedValue<number>) => ReactNode;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const DEFAULT_OPEN_ANIMATION_CONFIG: WithSpringConfig = {
|
|
47
|
-
dampingRatio: 1,
|
|
48
|
-
duration: 300,
|
|
49
|
-
overshootClamping: true,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const DEFAULT_CLOSE_ANIMATION_CONFIG: WithSpringConfig = {
|
|
53
|
-
dampingRatio: 1,
|
|
54
|
-
duration: 250,
|
|
55
|
-
overshootClamping: true,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const DefaultScrim = ({ progress }: { progress: SharedValue<number> }) => {
|
|
59
|
-
const style = useAnimatedStyle(() => ({ opacity: progress.value }));
|
|
60
|
-
return (
|
|
61
|
-
<Animated.View
|
|
62
|
-
style={[
|
|
63
|
-
StyleSheet.absoluteFill,
|
|
64
|
-
{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)' },
|
|
65
|
-
style,
|
|
66
|
-
]}
|
|
67
|
-
/>
|
|
68
|
-
);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export const BottomSheetBase = ({
|
|
72
|
-
children,
|
|
73
|
-
detents = [0, 'max'],
|
|
74
|
-
index,
|
|
75
|
-
onIndexChange,
|
|
76
|
-
position: externalPosition,
|
|
77
|
-
openAnimationConfig = DEFAULT_OPEN_ANIMATION_CONFIG,
|
|
78
|
-
closeAnimationConfig = DEFAULT_CLOSE_ANIMATION_CONFIG,
|
|
79
|
-
modal = false,
|
|
80
|
-
renderScrim,
|
|
81
|
-
}: BottomSheetBaseProps) => {
|
|
82
|
-
const { height: screenHeight } = useWindowDimensions();
|
|
83
|
-
const insets = useSafeAreaInsets();
|
|
84
|
-
const maxHeight = screenHeight - insets.top;
|
|
85
|
-
const resolvedIndex = clampIndex(index, detents.length);
|
|
86
|
-
const [contentHeight, setContentHeight] = useState(0);
|
|
87
|
-
|
|
88
|
-
if (detents.length === 0) {
|
|
89
|
-
throw new Error('detents must include at least one value.');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const normalizedDetents = detents.map((detent) => {
|
|
93
|
-
const resolved = resolveDetent(detent, contentHeight, maxHeight);
|
|
94
|
-
return Math.max(0, Math.min(resolved, maxHeight));
|
|
95
|
-
});
|
|
96
|
-
const isDraggable = detents.map((detent) => !isDetentProgrammatic(detent));
|
|
97
|
-
const initialMaxSnap = Math.max(0, ...normalizedDetents);
|
|
98
|
-
|
|
99
|
-
const translateY = useSharedValue(initialMaxSnap);
|
|
100
|
-
const animationTarget = useSharedValue(NaN);
|
|
101
|
-
const sheetHeight = useSharedValue(initialMaxSnap);
|
|
102
|
-
const [scrollableEntries, setScrollableEntries] = useState<ScrollableEntry[]>(
|
|
103
|
-
[]
|
|
104
|
-
);
|
|
105
|
-
const isScrollableLocked = useSharedValue(false);
|
|
106
|
-
|
|
107
|
-
const registerScrollable = useCallback((entry: ScrollableEntry) => {
|
|
108
|
-
setScrollableEntries((prev) => [...prev, entry]);
|
|
109
|
-
return () => {
|
|
110
|
-
setScrollableEntries((prev) => prev.filter((e) => e !== entry));
|
|
111
|
-
};
|
|
112
|
-
}, []);
|
|
113
|
-
|
|
114
|
-
const detentsValue = useSharedValue(normalizedDetents);
|
|
115
|
-
const isDraggableValue = useSharedValue(isDraggable);
|
|
116
|
-
const firstNonzeroDetent = useSharedValue(
|
|
117
|
-
normalizedDetents.find((detent) => detent > 0) ?? 0
|
|
118
|
-
);
|
|
119
|
-
const currentIndex = useSharedValue(resolvedIndex);
|
|
120
|
-
const internalPosition = useDerivedValue(() =>
|
|
121
|
-
Math.max(0, sheetHeight.value - translateY.value)
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
useAnimatedReaction(
|
|
125
|
-
() => internalPosition.value,
|
|
126
|
-
(value) => {
|
|
127
|
-
if (externalPosition !== undefined) externalPosition.set(value);
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const scrimProgress = useDerivedValue(() => {
|
|
132
|
-
const target = firstNonzeroDetent.value;
|
|
133
|
-
if (target <= 0) return 0;
|
|
134
|
-
const progress = internalPosition.value / target;
|
|
135
|
-
return Math.min(1, Math.max(0, progress));
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const handleIndexChange = (nextIndex: number) => {
|
|
139
|
-
onIndexChange?.(nextIndex);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
const maxSnap = Math.max(0, ...normalizedDetents);
|
|
144
|
-
detentsValue.set(normalizedDetents);
|
|
145
|
-
isDraggableValue.set(isDraggable);
|
|
146
|
-
sheetHeight.set(maxSnap);
|
|
147
|
-
firstNonzeroDetent.set(normalizedDetents.find((detent) => detent > 0) ?? 0);
|
|
148
|
-
}, [
|
|
149
|
-
normalizedDetents,
|
|
150
|
-
isDraggable,
|
|
151
|
-
sheetHeight,
|
|
152
|
-
detentsValue,
|
|
153
|
-
isDraggableValue,
|
|
154
|
-
firstNonzeroDetent,
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
const animateToIndex = useCallback(
|
|
158
|
-
(targetIndex: number, velocity?: number) => {
|
|
159
|
-
'worklet';
|
|
160
|
-
const maxSnap = sheetHeight.value;
|
|
161
|
-
const targetTranslate = maxSnap - (detentsValue.value[targetIndex] ?? 0);
|
|
162
|
-
if (animationTarget.value === targetTranslate && velocity === undefined) {
|
|
163
|
-
currentIndex.set(targetIndex);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
animationTarget.set(targetTranslate);
|
|
167
|
-
const isOpening = targetTranslate < translateY.value;
|
|
168
|
-
const baseConfig = isOpening ? openAnimationConfig : closeAnimationConfig;
|
|
169
|
-
const springConfig =
|
|
170
|
-
velocity === undefined ? baseConfig : { ...baseConfig, velocity };
|
|
171
|
-
translateY.set(withSpring(targetTranslate, springConfig));
|
|
172
|
-
currentIndex.set(targetIndex);
|
|
173
|
-
},
|
|
174
|
-
[
|
|
175
|
-
animationTarget,
|
|
176
|
-
closeAnimationConfig,
|
|
177
|
-
currentIndex,
|
|
178
|
-
detentsValue,
|
|
179
|
-
openAnimationConfig,
|
|
180
|
-
sheetHeight,
|
|
181
|
-
translateY,
|
|
182
|
-
]
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
scheduleOnUI(animateToIndex, resolvedIndex);
|
|
187
|
-
}, [animateToIndex, resolvedIndex, normalizedDetents]);
|
|
188
|
-
|
|
189
|
-
const panGesture = useBottomSheetPanGesture({
|
|
190
|
-
animationTarget,
|
|
191
|
-
translateY,
|
|
192
|
-
sheetHeight,
|
|
193
|
-
detentsValue,
|
|
194
|
-
isDraggableValue,
|
|
195
|
-
currentIndex,
|
|
196
|
-
scrollableEntries,
|
|
197
|
-
isScrollableLocked,
|
|
198
|
-
handleIndexChange,
|
|
199
|
-
animateToIndex,
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const handleSentinelLayout = (event: LayoutChangeEvent) => {
|
|
203
|
-
setContentHeight(event.nativeEvent.layout.y);
|
|
204
|
-
};
|
|
205
|
-
const closedIndex = normalizedDetents.indexOf(0);
|
|
206
|
-
const handleScrimPress = () => {
|
|
207
|
-
if (closedIndex === -1 || resolvedIndex === closedIndex) return;
|
|
208
|
-
handleIndexChange(closedIndex);
|
|
209
|
-
scheduleOnUI(animateToIndex, closedIndex);
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const wrapperStyle = useAnimatedStyle(() => ({
|
|
213
|
-
transform: [{ translateY: translateY.value }],
|
|
214
|
-
height: sheetHeight.value,
|
|
215
|
-
opacity: translateY.value >= sheetHeight.value ? 0 : 1,
|
|
216
|
-
}));
|
|
217
|
-
const isCollapsed = normalizedDetents[resolvedIndex] === 0;
|
|
218
|
-
const pointerEvents = modal ? (isCollapsed ? 'none' : 'auto') : 'box-none';
|
|
219
|
-
let scrimElement: ReactNode | null = null;
|
|
220
|
-
if (renderScrim !== undefined) {
|
|
221
|
-
scrimElement = renderScrim(scrimProgress);
|
|
222
|
-
} else if (modal) {
|
|
223
|
-
scrimElement = <DefaultScrim progress={scrimProgress} />;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const sheetContent = (
|
|
227
|
-
<BottomSheetContextProvider
|
|
228
|
-
value={{
|
|
229
|
-
translateY,
|
|
230
|
-
position: internalPosition,
|
|
231
|
-
index: currentIndex,
|
|
232
|
-
sheetHeight,
|
|
233
|
-
isScrollableLocked,
|
|
234
|
-
registerScrollable,
|
|
235
|
-
panGesture,
|
|
236
|
-
}}
|
|
237
|
-
>
|
|
238
|
-
<Animated.View
|
|
239
|
-
style={[
|
|
240
|
-
{
|
|
241
|
-
position: 'absolute',
|
|
242
|
-
bottom: 0,
|
|
243
|
-
left: 0,
|
|
244
|
-
right: 0,
|
|
245
|
-
},
|
|
246
|
-
wrapperStyle,
|
|
247
|
-
]}
|
|
248
|
-
pointerEvents="box-none"
|
|
249
|
-
>
|
|
250
|
-
<GestureDetector gesture={panGesture}>
|
|
251
|
-
<View style={{ flex: 1 }} pointerEvents="box-none">
|
|
252
|
-
{children}
|
|
253
|
-
<View onLayout={handleSentinelLayout} pointerEvents="none" />
|
|
254
|
-
</View>
|
|
255
|
-
</GestureDetector>
|
|
256
|
-
</Animated.View>
|
|
257
|
-
</BottomSheetContextProvider>
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
const sheetContainer = (
|
|
261
|
-
<Animated.View
|
|
262
|
-
style={StyleSheet.absoluteFill}
|
|
263
|
-
pointerEvents={pointerEvents}
|
|
264
|
-
>
|
|
265
|
-
{modal && scrimElement !== null ? (
|
|
266
|
-
<Pressable style={StyleSheet.absoluteFill} onPress={handleScrimPress}>
|
|
267
|
-
{scrimElement}
|
|
268
|
-
</Pressable>
|
|
269
|
-
) : null}
|
|
270
|
-
{sheetContent}
|
|
271
|
-
</Animated.View>
|
|
272
|
-
);
|
|
273
|
-
if (modal) return <Portal>{sheetContainer}</Portal>;
|
|
274
|
-
|
|
275
|
-
return sheetContainer;
|
|
276
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext } from 'react';
|
|
2
|
-
import type { PanGesture } from 'react-native-gesture-handler';
|
|
3
|
-
import type { AnimatedRef, SharedValue } from 'react-native-reanimated';
|
|
4
|
-
|
|
5
|
-
export interface ScrollableEntry {
|
|
6
|
-
ref: AnimatedRef<any>;
|
|
7
|
-
scrollOffset: SharedValue<number>;
|
|
8
|
-
isGestureActive: SharedValue<boolean>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface BottomSheetContextType {
|
|
12
|
-
translateY: SharedValue<number>;
|
|
13
|
-
position: SharedValue<number>;
|
|
14
|
-
index: SharedValue<number>;
|
|
15
|
-
sheetHeight: SharedValue<number>;
|
|
16
|
-
isScrollableLocked: SharedValue<boolean>;
|
|
17
|
-
registerScrollable: (entry: ScrollableEntry) => () => void;
|
|
18
|
-
panGesture: PanGesture;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const BottomSheetContext = createContext<BottomSheetContextType | null>(null);
|
|
22
|
-
|
|
23
|
-
export const BottomSheetContextProvider = BottomSheetContext.Provider;
|
|
24
|
-
|
|
25
|
-
export const useBottomSheetContext = () => {
|
|
26
|
-
const context = useContext(BottomSheetContext);
|
|
27
|
-
if (context === null) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
'`useBottomSheetContext` must be used within `BottomSheet`.'
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
return context;
|
|
33
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { FlatList, type NativeScrollEvent } from 'react-native';
|
|
2
|
-
import type {
|
|
3
|
-
FlatListPropsWithLayout,
|
|
4
|
-
SharedValue,
|
|
5
|
-
} from 'react-native-reanimated';
|
|
6
|
-
import type { Ref, ReactElement } from 'react';
|
|
7
|
-
|
|
8
|
-
import { bottomSheetScrollable } from './bottomSheetScrollable';
|
|
9
|
-
|
|
10
|
-
export type BottomSheetFlatListProps<T> = Omit<
|
|
11
|
-
FlatListPropsWithLayout<T>,
|
|
12
|
-
'onScroll' | 'scrollEnabled' | 'ref'
|
|
13
|
-
> & {
|
|
14
|
-
scrollEnabled?: boolean | SharedValue<boolean | undefined>;
|
|
15
|
-
onScroll?: (event: NativeScrollEvent) => void;
|
|
16
|
-
ref?: Ref<FlatList<T>>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const BottomSheetFlatList = bottomSheetScrollable(FlatList) as <T>(
|
|
20
|
-
props: BottomSheetFlatListProps<T>
|
|
21
|
-
) => ReactElement;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { Ref, ReactElement } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
ScrollView,
|
|
4
|
-
type NativeScrollEvent,
|
|
5
|
-
type ScrollViewProps,
|
|
6
|
-
} from 'react-native';
|
|
7
|
-
import type { SharedValue } from 'react-native-reanimated';
|
|
8
|
-
|
|
9
|
-
import { bottomSheetScrollable } from './bottomSheetScrollable';
|
|
10
|
-
|
|
11
|
-
export type BottomSheetScrollViewProps = Omit<
|
|
12
|
-
ScrollViewProps,
|
|
13
|
-
'onScroll' | 'scrollEnabled' | 'ref'
|
|
14
|
-
> & {
|
|
15
|
-
scrollEnabled?: boolean | SharedValue<boolean | undefined>;
|
|
16
|
-
onScroll?: (event: NativeScrollEvent) => void;
|
|
17
|
-
ref?: Ref<ScrollView>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const BottomSheetScrollView = bottomSheetScrollable(ScrollView) as (
|
|
21
|
-
props: BottomSheetScrollViewProps
|
|
22
|
-
) => ReactElement;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { type ComponentType, type Ref, useImperativeHandle } from 'react';
|
|
2
|
-
import type { NativeScrollEvent } from 'react-native';
|
|
3
|
-
import { GestureDetector } from 'react-native-gesture-handler';
|
|
4
|
-
import Animated, { type SharedValue } from 'react-native-reanimated';
|
|
5
|
-
|
|
6
|
-
import { useBottomSheetScrollable } from './useBottomSheetScrollable';
|
|
7
|
-
|
|
8
|
-
export function bottomSheetScrollable<
|
|
9
|
-
P extends Record<string, any>,
|
|
10
|
-
R = unknown
|
|
11
|
-
>(ScrollableComponent: ComponentType<P>) {
|
|
12
|
-
const AnimatedComponent =
|
|
13
|
-
Animated.createAnimatedComponent(ScrollableComponent);
|
|
14
|
-
|
|
15
|
-
return ({
|
|
16
|
-
scrollEnabled,
|
|
17
|
-
onScroll,
|
|
18
|
-
ref,
|
|
19
|
-
...rest
|
|
20
|
-
}: Omit<P, 'onScroll' | 'scrollEnabled' | 'ref'> & {
|
|
21
|
-
scrollEnabled?: boolean | SharedValue<boolean | undefined>;
|
|
22
|
-
onScroll?: (event: NativeScrollEvent) => void;
|
|
23
|
-
ref?: Ref<R>;
|
|
24
|
-
}) => {
|
|
25
|
-
const { scrollHandler, scrollableRef, nativeGesture, animatedProps } =
|
|
26
|
-
useBottomSheetScrollable(scrollEnabled, onScroll);
|
|
27
|
-
|
|
28
|
-
useImperativeHandle(ref, () => scrollableRef.current as R, [scrollableRef]);
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<GestureDetector gesture={nativeGesture}>
|
|
32
|
-
<AnimatedComponent
|
|
33
|
-
{...(rest as any)}
|
|
34
|
-
animatedProps={animatedProps}
|
|
35
|
-
ref={scrollableRef}
|
|
36
|
-
onScroll={scrollHandler}
|
|
37
|
-
scrollEventThrottle={16}
|
|
38
|
-
/>
|
|
39
|
-
</GestureDetector>
|
|
40
|
-
);
|
|
41
|
-
};
|
|
42
|
-
}
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import type { PanGesture } from 'react-native-gesture-handler';
|
|
2
|
-
import { Gesture } from 'react-native-gesture-handler';
|
|
3
|
-
import { scheduleOnRN } from 'react-native-worklets';
|
|
4
|
-
import {
|
|
5
|
-
measure,
|
|
6
|
-
scrollTo,
|
|
7
|
-
type SharedValue,
|
|
8
|
-
useSharedValue,
|
|
9
|
-
} from 'react-native-reanimated';
|
|
10
|
-
|
|
11
|
-
import type { ScrollableEntry } from './BottomSheetContext';
|
|
12
|
-
import { findSnapTarget } from './bottomSheetUtils';
|
|
13
|
-
|
|
14
|
-
interface BottomSheetPanGestureParams {
|
|
15
|
-
animationTarget: SharedValue<number>;
|
|
16
|
-
translateY: SharedValue<number>;
|
|
17
|
-
sheetHeight: SharedValue<number>;
|
|
18
|
-
detentsValue: SharedValue<number[]>;
|
|
19
|
-
isDraggableValue: SharedValue<boolean[]>;
|
|
20
|
-
currentIndex: SharedValue<number>;
|
|
21
|
-
scrollableEntries: ScrollableEntry[];
|
|
22
|
-
isScrollableLocked: SharedValue<boolean>;
|
|
23
|
-
handleIndexChange: (nextIndex: number) => void;
|
|
24
|
-
animateToIndex: (targetIndex: number, velocity?: number) => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const useBottomSheetPanGesture = ({
|
|
28
|
-
animationTarget,
|
|
29
|
-
translateY,
|
|
30
|
-
sheetHeight,
|
|
31
|
-
detentsValue,
|
|
32
|
-
isDraggableValue,
|
|
33
|
-
currentIndex,
|
|
34
|
-
scrollableEntries,
|
|
35
|
-
isScrollableLocked,
|
|
36
|
-
handleIndexChange,
|
|
37
|
-
animateToIndex,
|
|
38
|
-
}: BottomSheetPanGestureParams): PanGesture => {
|
|
39
|
-
const isDraggingSheet = useSharedValue(false);
|
|
40
|
-
const isDraggingFromScrollable = useSharedValue(false);
|
|
41
|
-
const panStartX = useSharedValue(0);
|
|
42
|
-
const panStartY = useSharedValue(0);
|
|
43
|
-
const panActivated = useSharedValue(false);
|
|
44
|
-
const dragStartTranslateY = useSharedValue(0);
|
|
45
|
-
const activeScrollableIndex = useSharedValue(-1);
|
|
46
|
-
|
|
47
|
-
return Gesture.Pan()
|
|
48
|
-
.manualActivation(true)
|
|
49
|
-
.onTouchesDown((event) => {
|
|
50
|
-
'worklet';
|
|
51
|
-
panActivated.set(false);
|
|
52
|
-
isDraggingSheet.set(false);
|
|
53
|
-
isDraggingFromScrollable.set(false);
|
|
54
|
-
isScrollableLocked.set(false);
|
|
55
|
-
activeScrollableIndex.set(-1);
|
|
56
|
-
const touch = event.changedTouches[0] ?? event.allTouches[0];
|
|
57
|
-
if (touch !== undefined) {
|
|
58
|
-
panStartX.set(touch.absoluteX);
|
|
59
|
-
panStartY.set(touch.absoluteY);
|
|
60
|
-
const entries = scrollableEntries;
|
|
61
|
-
for (let i = 0; i < entries.length; i++) {
|
|
62
|
-
const entry = entries[i];
|
|
63
|
-
if (entry === undefined) continue;
|
|
64
|
-
const layout = measure(entry.ref);
|
|
65
|
-
if (layout === null) continue;
|
|
66
|
-
const withinX =
|
|
67
|
-
touch.absoluteX >= layout.pageX &&
|
|
68
|
-
touch.absoluteX <= layout.pageX + layout.width;
|
|
69
|
-
const withinY =
|
|
70
|
-
touch.absoluteY >= layout.pageY &&
|
|
71
|
-
touch.absoluteY <= layout.pageY + layout.height;
|
|
72
|
-
if (withinX && withinY) {
|
|
73
|
-
activeScrollableIndex.set(i);
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
.onTouchesMove((event, stateManager) => {
|
|
80
|
-
'worklet';
|
|
81
|
-
if (panActivated.value) return;
|
|
82
|
-
const touch = event.changedTouches[0] ?? event.allTouches[0];
|
|
83
|
-
if (!touch) return;
|
|
84
|
-
const deltaX = touch.absoluteX - panStartX.value;
|
|
85
|
-
const deltaY = touch.absoluteY - panStartY.value;
|
|
86
|
-
// When multiple scrollables overlap (e.g. stacked views), the hit-test
|
|
87
|
-
// in onTouchesDown may pick the wrong one. Prefer the scrollable whose
|
|
88
|
-
// native gesture is already active — that is definitively the one
|
|
89
|
-
// receiving touches (respects pointerEvents, z-order, etc.).
|
|
90
|
-
// If multiple scrollables are registered but none has confirmed via
|
|
91
|
-
// isGestureActive yet, defer the decision to avoid acting on a
|
|
92
|
-
// potentially incorrect hit-test result.
|
|
93
|
-
const entries = scrollableEntries;
|
|
94
|
-
let gestureActiveIdx = -1;
|
|
95
|
-
for (let i = 0; i < entries.length; i++) {
|
|
96
|
-
const entry = entries[i];
|
|
97
|
-
if (entry !== undefined && entry.isGestureActive.value) {
|
|
98
|
-
gestureActiveIdx = i;
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (gestureActiveIdx !== -1) {
|
|
103
|
-
activeScrollableIndex.set(gestureActiveIdx);
|
|
104
|
-
} else if (entries.length > 1 && activeScrollableIndex.value !== -1) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const activeIdx = activeScrollableIndex.value;
|
|
108
|
-
if (activeIdx !== -1) {
|
|
109
|
-
const active = scrollableEntries[activeIdx];
|
|
110
|
-
if (
|
|
111
|
-
active !== undefined &&
|
|
112
|
-
active.scrollOffset.value > 0 &&
|
|
113
|
-
translateY.value <= 0
|
|
114
|
-
) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
119
|
-
stateManager.fail();
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (
|
|
123
|
-
Math.abs(deltaY) > Math.abs(deltaX) &&
|
|
124
|
-
(deltaY > 0 || translateY.value > 0)
|
|
125
|
-
) {
|
|
126
|
-
panActivated.set(true);
|
|
127
|
-
stateManager.activate();
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
.onBegin(() => {
|
|
131
|
-
'worklet';
|
|
132
|
-
animationTarget.set(NaN);
|
|
133
|
-
isDraggingSheet.set(false);
|
|
134
|
-
isDraggingFromScrollable.set(false);
|
|
135
|
-
dragStartTranslateY.set(translateY.value);
|
|
136
|
-
})
|
|
137
|
-
.onUpdate((event) => {
|
|
138
|
-
'worklet';
|
|
139
|
-
const activeIdx = activeScrollableIndex.value;
|
|
140
|
-
const hasActive = activeIdx !== -1;
|
|
141
|
-
const active = hasActive ? scrollableEntries[activeIdx] : undefined;
|
|
142
|
-
const activeOffset = active !== undefined ? active.scrollOffset.value : 0;
|
|
143
|
-
|
|
144
|
-
if (isDraggingSheet.value) {
|
|
145
|
-
if (isDraggingFromScrollable.value && active !== undefined) {
|
|
146
|
-
scrollTo(active.ref, 0, 0, false);
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
const isDraggingDown = event.translationY > 0;
|
|
150
|
-
const canStartDrag =
|
|
151
|
-
!hasActive || activeOffset <= 0 || translateY.value > 0;
|
|
152
|
-
if (!canStartDrag || (!isDraggingDown && translateY.value <= 0)) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const isScrollableActive =
|
|
156
|
-
hasActive && active !== undefined && active.isGestureActive.value;
|
|
157
|
-
isDraggingSheet.set(true);
|
|
158
|
-
isDraggingFromScrollable.set(isScrollableActive && activeOffset <= 0);
|
|
159
|
-
dragStartTranslateY.set(translateY.value - event.translationY);
|
|
160
|
-
isScrollableLocked.set(hasActive);
|
|
161
|
-
if (hasActive && active !== undefined && activeOffset <= 0) {
|
|
162
|
-
scrollTo(active.ref, 0, 0, false);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const rawTranslate = dragStartTranslateY.value + event.translationY;
|
|
166
|
-
const resolvedDetents = detentsValue.value;
|
|
167
|
-
const draggable = isDraggableValue.value;
|
|
168
|
-
let maxDraggableTranslateY = sheetHeight.value;
|
|
169
|
-
let minDraggableTranslateY = 0;
|
|
170
|
-
let foundDraggable = false;
|
|
171
|
-
for (let i = 0; i < resolvedDetents.length; i++) {
|
|
172
|
-
if (!(draggable[i] ?? true)) continue;
|
|
173
|
-
const t = sheetHeight.value - (resolvedDetents[i] ?? 0);
|
|
174
|
-
if (!foundDraggable) {
|
|
175
|
-
maxDraggableTranslateY = t;
|
|
176
|
-
minDraggableTranslateY = t;
|
|
177
|
-
foundDraggable = true;
|
|
178
|
-
} else {
|
|
179
|
-
if (t > maxDraggableTranslateY) maxDraggableTranslateY = t;
|
|
180
|
-
if (t < minDraggableTranslateY) minDraggableTranslateY = t;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const nextTranslate = Math.min(
|
|
184
|
-
Math.max(rawTranslate, minDraggableTranslateY),
|
|
185
|
-
maxDraggableTranslateY
|
|
186
|
-
);
|
|
187
|
-
translateY.set(nextTranslate);
|
|
188
|
-
if (isDraggingSheet.value && rawTranslate < 0 && hasActive) {
|
|
189
|
-
isDraggingSheet.set(false);
|
|
190
|
-
isScrollableLocked.set(false);
|
|
191
|
-
let targetSnapIndex = -1;
|
|
192
|
-
let targetSnapValue = -1;
|
|
193
|
-
for (let i = resolvedDetents.length - 1; i >= 0; i--) {
|
|
194
|
-
const detentValue = resolvedDetents[i];
|
|
195
|
-
if (
|
|
196
|
-
detentValue !== undefined &&
|
|
197
|
-
(draggable[i] ?? true) &&
|
|
198
|
-
detentValue > targetSnapValue
|
|
199
|
-
) {
|
|
200
|
-
targetSnapValue = detentValue;
|
|
201
|
-
targetSnapIndex = i;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (targetSnapIndex === -1) {
|
|
205
|
-
const maxSnap = sheetHeight.value;
|
|
206
|
-
for (let i = resolvedDetents.length - 1; i >= 0; i--) {
|
|
207
|
-
if (resolvedDetents[i] === maxSnap) {
|
|
208
|
-
targetSnapIndex = i;
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (targetSnapIndex !== -1) {
|
|
214
|
-
if (targetSnapIndex !== currentIndex.value) {
|
|
215
|
-
scheduleOnRN(handleIndexChange, targetSnapIndex);
|
|
216
|
-
}
|
|
217
|
-
animateToIndex(targetSnapIndex);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
})
|
|
221
|
-
.onEnd((event) => {
|
|
222
|
-
'worklet';
|
|
223
|
-
const wasDragging = isDraggingSheet.value;
|
|
224
|
-
isScrollableLocked.set(false);
|
|
225
|
-
isDraggingSheet.set(false);
|
|
226
|
-
animationTarget.set(NaN);
|
|
227
|
-
if (!wasDragging) {
|
|
228
|
-
animateToIndex(currentIndex.value);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const maxSnap = sheetHeight.value;
|
|
232
|
-
const draggable = isDraggableValue.value;
|
|
233
|
-
const allPositions = detentsValue.value.map((detentValue, snapIndex) => ({
|
|
234
|
-
index: snapIndex,
|
|
235
|
-
translateY: maxSnap - detentValue,
|
|
236
|
-
isDraggable: draggable[snapIndex] ?? true,
|
|
237
|
-
}));
|
|
238
|
-
const targetIndex = findSnapTarget(
|
|
239
|
-
translateY.value,
|
|
240
|
-
event.velocityY,
|
|
241
|
-
currentIndex.value,
|
|
242
|
-
allPositions
|
|
243
|
-
);
|
|
244
|
-
const hasIndexChanged = targetIndex !== currentIndex.value;
|
|
245
|
-
if (hasIndexChanged) scheduleOnRN(handleIndexChange, targetIndex);
|
|
246
|
-
const shouldApplyVelocity =
|
|
247
|
-
hasIndexChanged && Number.isFinite(event.velocityY);
|
|
248
|
-
animateToIndex(
|
|
249
|
-
targetIndex,
|
|
250
|
-
shouldApplyVelocity ? event.velocityY : undefined
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
};
|