@swmansion/react-native-bottom-sheet 0.5.5 → 0.6.1

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 (40) hide show
  1. package/README.md +19 -5
  2. package/lib/module/BottomSheetBase.js +10 -13
  3. package/lib/module/BottomSheetBase.js.map +1 -1
  4. package/lib/module/BottomSheetContext.js.map +1 -1
  5. package/lib/module/BottomSheetFlatList.js +3 -42
  6. package/lib/module/BottomSheetFlatList.js.map +1 -1
  7. package/lib/module/BottomSheetScrollView.js +3 -43
  8. package/lib/module/BottomSheetScrollView.js.map +1 -1
  9. package/lib/module/bottomSheetScrollable.js +35 -0
  10. package/lib/module/bottomSheetScrollable.js.map +1 -0
  11. package/lib/module/index.js +1 -1
  12. package/lib/module/index.js.map +1 -1
  13. package/lib/module/useBottomSheetPanGesture.js +53 -22
  14. package/lib/module/useBottomSheetPanGesture.js.map +1 -1
  15. package/lib/module/useBottomSheetScrollable.js +14 -17
  16. package/lib/module/useBottomSheetScrollable.js.map +1 -1
  17. package/lib/typescript/src/BottomSheetBase.d.ts.map +1 -1
  18. package/lib/typescript/src/BottomSheetContext.d.ts +6 -4
  19. package/lib/typescript/src/BottomSheetContext.d.ts.map +1 -1
  20. package/lib/typescript/src/BottomSheetFlatList.d.ts +7 -12
  21. package/lib/typescript/src/BottomSheetFlatList.d.ts.map +1 -1
  22. package/lib/typescript/src/BottomSheetScrollView.d.ts +7 -14
  23. package/lib/typescript/src/BottomSheetScrollView.d.ts.map +1 -1
  24. package/lib/typescript/src/bottomSheetScrollable.d.ts +9 -0
  25. package/lib/typescript/src/bottomSheetScrollable.d.ts.map +1 -0
  26. package/lib/typescript/src/index.d.ts +3 -3
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/lib/typescript/src/useBottomSheetPanGesture.d.ts +4 -6
  29. package/lib/typescript/src/useBottomSheetPanGesture.d.ts.map +1 -1
  30. package/lib/typescript/src/useBottomSheetScrollable.d.ts +1 -1
  31. package/lib/typescript/src/useBottomSheetScrollable.d.ts.map +1 -1
  32. package/package.json +2 -1
  33. package/src/BottomSheetBase.tsx +16 -14
  34. package/src/BottomSheetContext.tsx +7 -4
  35. package/src/BottomSheetFlatList.tsx +16 -58
  36. package/src/BottomSheetScrollView.tsx +17 -61
  37. package/src/bottomSheetScrollable.tsx +42 -0
  38. package/src/index.tsx +3 -9
  39. package/src/useBottomSheetPanGesture.ts +64 -52
  40. package/src/useBottomSheetScrollable.ts +16 -23
@@ -4,11 +4,11 @@ import { scheduleOnRN } from 'react-native-worklets';
4
4
  import {
5
5
  measure,
6
6
  scrollTo,
7
- type AnimatedRef,
8
7
  type SharedValue,
9
8
  useSharedValue,
10
9
  } from 'react-native-reanimated';
11
10
 
11
+ import type { ScrollableEntry } from './BottomSheetContext';
12
12
  import { findSnapTarget } from './bottomSheetUtils';
13
13
 
14
14
  interface BottomSheetPanGestureParams {
@@ -18,11 +18,8 @@ interface BottomSheetPanGestureParams {
18
18
  detentsValue: SharedValue<number[]>;
19
19
  isDraggableValue: SharedValue<boolean[]>;
20
20
  currentIndex: SharedValue<number>;
21
- scrollOffset: SharedValue<number>;
22
- hasScrollable: SharedValue<boolean>;
23
- isScrollableGestureActive: SharedValue<boolean>;
21
+ scrollableEntries: ScrollableEntry[];
24
22
  isScrollableLocked: SharedValue<boolean>;
25
- scrollableRef: AnimatedRef<any>;
26
23
  handleIndexChange: (nextIndex: number) => void;
27
24
  animateToIndex: (targetIndex: number, velocity?: number) => void;
28
25
  }
@@ -34,11 +31,8 @@ export const useBottomSheetPanGesture = ({
34
31
  detentsValue,
35
32
  isDraggableValue,
36
33
  currentIndex,
37
- scrollOffset,
38
- hasScrollable,
39
- isScrollableGestureActive,
34
+ scrollableEntries,
40
35
  isScrollableLocked,
41
- scrollableRef,
42
36
  handleIndexChange,
43
37
  animateToIndex,
44
38
  }: BottomSheetPanGestureParams): PanGesture => {
@@ -48,7 +42,7 @@ export const useBottomSheetPanGesture = ({
48
42
  const panStartY = useSharedValue(0);
49
43
  const panActivated = useSharedValue(false);
50
44
  const dragStartTranslateY = useSharedValue(0);
51
- const isTouchWithinScrollable = useSharedValue(false);
45
+ const activeScrollableIndex = useSharedValue(-1);
52
46
 
53
47
  return Gesture.Pan()
54
48
  .manualActivation(true)
@@ -58,21 +52,26 @@ export const useBottomSheetPanGesture = ({
58
52
  isDraggingSheet.set(false);
59
53
  isDraggingFromScrollable.set(false);
60
54
  isScrollableLocked.set(false);
61
- isTouchWithinScrollable.set(false);
55
+ activeScrollableIndex.set(-1);
62
56
  const touch = event.changedTouches[0] ?? event.allTouches[0];
63
57
  if (touch !== undefined) {
64
58
  panStartX.set(touch.absoluteX);
65
59
  panStartY.set(touch.absoluteY);
66
- if (hasScrollable.value) {
67
- const layout = measure(scrollableRef);
68
- if (layout !== null) {
69
- const withinX =
70
- touch.absoluteX >= layout.pageX &&
71
- touch.absoluteX <= layout.pageX + layout.width;
72
- const withinY =
73
- touch.absoluteY >= layout.pageY &&
74
- touch.absoluteY <= layout.pageY + layout.height;
75
- isTouchWithinScrollable.set(withinX && withinY);
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;
76
75
  }
77
76
  }
78
77
  }
@@ -84,14 +83,38 @@ export const useBottomSheetPanGesture = ({
84
83
  if (!touch) return;
85
84
  const deltaX = touch.absoluteX - panStartX.value;
86
85
  const deltaY = touch.absoluteY - panStartY.value;
87
- if (
88
- hasScrollable.value &&
89
- scrollOffset.value > 0 &&
90
- isTouchWithinScrollable.value &&
91
- translateY.value <= 0
92
- ) {
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) {
93
105
  return;
94
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
+ }
95
118
  if (Math.abs(deltaX) > Math.abs(deltaY)) {
96
119
  stateManager.fail();
97
120
  return;
@@ -113,36 +136,30 @@ export const useBottomSheetPanGesture = ({
113
136
  })
114
137
  .onUpdate((event) => {
115
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
+
116
144
  if (isDraggingSheet.value) {
117
- if (isDraggingFromScrollable.value) {
118
- scrollTo(scrollableRef, 0, 0, false);
145
+ if (isDraggingFromScrollable.value && active !== undefined) {
146
+ scrollTo(active.ref, 0, 0, false);
119
147
  }
120
148
  } else {
121
149
  const isDraggingDown = event.translationY > 0;
122
150
  const canStartDrag =
123
- !hasScrollable.value ||
124
- scrollOffset.value <= 0 ||
125
- translateY.value > 0 ||
126
- !isTouchWithinScrollable.value;
151
+ !hasActive || activeOffset <= 0 || translateY.value > 0;
127
152
  if (!canStartDrag || (!isDraggingDown && translateY.value <= 0)) {
128
153
  return;
129
154
  }
130
155
  const isScrollableActive =
131
- hasScrollable.value && isScrollableGestureActive.value;
156
+ hasActive && active !== undefined && active.isGestureActive.value;
132
157
  isDraggingSheet.set(true);
133
- isDraggingFromScrollable.set(
134
- isScrollableActive &&
135
- isTouchWithinScrollable.value &&
136
- scrollOffset.value <= 0
137
- );
158
+ isDraggingFromScrollable.set(isScrollableActive && activeOffset <= 0);
138
159
  dragStartTranslateY.set(translateY.value - event.translationY);
139
- isScrollableLocked.set(hasScrollable.value);
140
- if (
141
- isTouchWithinScrollable.value &&
142
- hasScrollable.value &&
143
- scrollOffset.value <= 0
144
- ) {
145
- scrollTo(scrollableRef, 0, 0, false);
160
+ isScrollableLocked.set(hasActive);
161
+ if (hasActive && active !== undefined && activeOffset <= 0) {
162
+ scrollTo(active.ref, 0, 0, false);
146
163
  }
147
164
  }
148
165
  const rawTranslate = dragStartTranslateY.value + event.translationY;
@@ -163,12 +180,7 @@ export const useBottomSheetPanGesture = ({
163
180
  maxDraggableTranslateY
164
181
  );
165
182
  translateY.set(nextTranslate);
166
- if (
167
- isDraggingSheet.value &&
168
- rawTranslate < 0 &&
169
- isTouchWithinScrollable.value &&
170
- hasScrollable.value
171
- ) {
183
+ if (isDraggingSheet.value && rawTranslate < 0 && hasActive) {
172
184
  isDraggingSheet.set(false);
173
185
  isScrollableLocked.set(false);
174
186
  let targetSnapIndex = -1;
@@ -5,7 +5,9 @@ import { isWorkletFunction, scheduleOnRN } from 'react-native-worklets';
5
5
  import {
6
6
  type SharedValue,
7
7
  useAnimatedProps,
8
+ useAnimatedRef,
8
9
  useAnimatedScrollHandler,
10
+ useSharedValue,
9
11
  } from 'react-native-reanimated';
10
12
 
11
13
  import { useBottomSheetContext } from './BottomSheetContext';
@@ -16,14 +18,11 @@ export const useBottomSheetScrollable = (
16
18
  baseScrollEnabled: boolean | SharedValue<boolean | undefined> = true,
17
19
  onScroll?: ScrollHandler
18
20
  ) => {
19
- const {
20
- scrollOffset,
21
- scrollableRef,
22
- hasScrollable,
23
- isScrollableGestureActive,
24
- isScrollableLocked,
25
- panGesture,
26
- } = useBottomSheetContext();
21
+ const { isScrollableLocked, registerScrollable, panGesture } =
22
+ useBottomSheetContext();
23
+ const scrollableRef = useAnimatedRef();
24
+ const scrollOffset = useSharedValue(0);
25
+ const isGestureActive = useSharedValue(false);
27
26
  const isWorkletScrollHandler =
28
27
  onScroll !== undefined ? isWorkletFunction(onScroll) : false;
29
28
  const scrollHandler = useAnimatedScrollHandler({
@@ -42,11 +41,11 @@ export const useBottomSheetScrollable = (
42
41
  .simultaneousWithExternalGesture(panGesture)
43
42
  .onStart(() => {
44
43
  'worklet';
45
- isScrollableGestureActive.set(true);
44
+ isGestureActive.set(true);
46
45
  })
47
46
  .onFinalize(() => {
48
47
  'worklet';
49
- isScrollableGestureActive.set(false);
48
+ isGestureActive.set(false);
50
49
  });
51
50
  const animatedProps = useAnimatedProps(() => {
52
51
  const resolvedScrollEnabled =
@@ -58,18 +57,12 @@ export const useBottomSheetScrollable = (
58
57
  };
59
58
  });
60
59
  useEffect(() => {
61
- if (__DEV__ && hasScrollable.value) {
62
- console.warn(
63
- 'Multiple scrollables within a single bottom sheet. Only one `BottomSheetScrollView` ' +
64
- 'or `BottomSheetFlatList` is supported per bottom sheet.'
65
- );
66
- }
67
- hasScrollable.set(true);
68
- return () => {
69
- hasScrollable.set(false);
70
- isScrollableGestureActive.set(false);
71
- isScrollableLocked.set(false);
72
- };
73
- }, [hasScrollable, isScrollableGestureActive, isScrollableLocked]);
60
+ const unregister = registerScrollable({
61
+ ref: scrollableRef,
62
+ scrollOffset,
63
+ isGestureActive,
64
+ });
65
+ return unregister;
66
+ }, [registerScrollable, scrollableRef, scrollOffset, isGestureActive]);
74
67
  return { scrollHandler, scrollableRef, nativeGesture, animatedProps };
75
68
  };