@mustmove/bottom-sheet 1.0.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/LICENSE +21 -0
- package/README.md +66 -0
- package/mock.js +231 -0
- package/package.json +107 -0
- package/src/components/bottomSheet/BottomSheet.tsx +1885 -0
- package/src/components/bottomSheet/BottomSheetBody.tsx +44 -0
- package/src/components/bottomSheet/BottomSheetContent.tsx +261 -0
- package/src/components/bottomSheet/constants.ts +58 -0
- package/src/components/bottomSheet/index.ts +2 -0
- package/src/components/bottomSheet/styles.ts +11 -0
- package/src/components/bottomSheet/types.d.ts +358 -0
- package/src/components/bottomSheetBackdrop/BottomSheetBackdrop.tsx +165 -0
- package/src/components/bottomSheetBackdrop/constants.ts +22 -0
- package/src/components/bottomSheetBackdrop/index.ts +2 -0
- package/src/components/bottomSheetBackdrop/styles.ts +8 -0
- package/src/components/bottomSheetBackdrop/types.d.ts +58 -0
- package/src/components/bottomSheetBackground/BottomSheetBackground.tsx +20 -0
- package/src/components/bottomSheetBackground/BottomSheetBackgroundContainer.tsx +35 -0
- package/src/components/bottomSheetBackground/index.ts +2 -0
- package/src/components/bottomSheetBackground/styles.ts +9 -0
- package/src/components/bottomSheetBackground/types.d.ts +12 -0
- package/src/components/bottomSheetDebugView/BottomSheetDebugView.tsx +26 -0
- package/src/components/bottomSheetDebugView/ReText.tsx +72 -0
- package/src/components/bottomSheetDebugView/ReText.webx.tsx +55 -0
- package/src/components/bottomSheetDebugView/index.ts +1 -0
- package/src/components/bottomSheetDebugView/styles.ts +19 -0
- package/src/components/bottomSheetDebugView/styles.web.ts +20 -0
- package/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx +123 -0
- package/src/components/bottomSheetDraggableView/index.ts +1 -0
- package/src/components/bottomSheetDraggableView/types.d.ts +9 -0
- package/src/components/bottomSheetFooter/BottomSheetFooter.tsx +119 -0
- package/src/components/bottomSheetFooter/BottomSheetFooterContainer.tsx +43 -0
- package/src/components/bottomSheetFooter/index.ts +3 -0
- package/src/components/bottomSheetFooter/styles.ts +12 -0
- package/src/components/bottomSheetFooter/types.d.ts +41 -0
- package/src/components/bottomSheetGestureHandlersProvider/BottomSheetGestureHandlersProvider.tsx +69 -0
- package/src/components/bottomSheetGestureHandlersProvider/index.ts +1 -0
- package/src/components/bottomSheetGestureHandlersProvider/types.d.ts +8 -0
- package/src/components/bottomSheetHandle/BottomSheetHandle.tsx +51 -0
- package/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx +187 -0
- package/src/components/bottomSheetHandle/constants.ts +12 -0
- package/src/components/bottomSheetHandle/index.ts +6 -0
- package/src/components/bottomSheetHandle/styles.ts +23 -0
- package/src/components/bottomSheetHandle/types.d.ts +52 -0
- package/src/components/bottomSheetHostingContainer/BottomSheetHostingContainer.tsx +130 -0
- package/src/components/bottomSheetHostingContainer/index.ts +2 -0
- package/src/components/bottomSheetHostingContainer/styles.ts +5 -0
- package/src/components/bottomSheetHostingContainer/styles.web.ts +11 -0
- package/src/components/bottomSheetHostingContainer/types.d.ts +17 -0
- package/src/components/bottomSheetModal/BottomSheetModal.tsx +482 -0
- package/src/components/bottomSheetModal/constants.ts +4 -0
- package/src/components/bottomSheetModal/index.ts +6 -0
- package/src/components/bottomSheetModal/types.d.ts +67 -0
- package/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx +211 -0
- package/src/components/bottomSheetModalProvider/index.ts +1 -0
- package/src/components/bottomSheetModalProvider/types.d.ts +12 -0
- package/src/components/bottomSheetRefreshControl/BottomSheetRefreshControl.android.tsx +84 -0
- package/src/components/bottomSheetRefreshControl/BottomSheetRefreshControl.tsx +1 -0
- package/src/components/bottomSheetRefreshControl/index.ts +20 -0
- package/src/components/bottomSheetScrollable/BottomSheetDraggableScrollable.tsx +23 -0
- package/src/components/bottomSheetScrollable/BottomSheetFlashList.tsx +88 -0
- package/src/components/bottomSheetScrollable/BottomSheetFlashList.web.tsx +1 -0
- package/src/components/bottomSheetScrollable/BottomSheetFlatList.tsx +26 -0
- package/src/components/bottomSheetScrollable/BottomSheetScrollView.tsx +27 -0
- package/src/components/bottomSheetScrollable/BottomSheetSectionList.tsx +29 -0
- package/src/components/bottomSheetScrollable/BottomSheetVirtualizedList.tsx +27 -0
- package/src/components/bottomSheetScrollable/ScrollableContainer.android.tsx +55 -0
- package/src/components/bottomSheetScrollable/ScrollableContainer.tsx +22 -0
- package/src/components/bottomSheetScrollable/ScrollableContainer.web.tsx +102 -0
- package/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx +153 -0
- package/src/components/bottomSheetScrollable/index.ts +15 -0
- package/src/components/bottomSheetScrollable/styles.ts +8 -0
- package/src/components/bottomSheetScrollable/types.d.ts +280 -0
- package/src/components/bottomSheetScrollable/useBottomSheetContentSizeSetter.ts +32 -0
- package/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx +127 -0
- package/src/components/bottomSheetTextInput/index.ts +2 -0
- package/src/components/bottomSheetTextInput/types.ts +3 -0
- package/src/components/bottomSheetView/BottomSheetView.tsx +93 -0
- package/src/components/bottomSheetView/index.ts +1 -0
- package/src/components/bottomSheetView/styles.ts +10 -0
- package/src/components/bottomSheetView/types.d.ts +24 -0
- package/src/components/touchables/Touchables.ios.tsx +5 -0
- package/src/components/touchables/Touchables.tsx +5 -0
- package/src/components/touchables/index.ts +20 -0
- package/src/constants.ts +159 -0
- package/src/contexts/external.ts +8 -0
- package/src/contexts/gesture.ts +13 -0
- package/src/contexts/index.ts +15 -0
- package/src/contexts/internal.ts +65 -0
- package/src/contexts/modal/external.ts +11 -0
- package/src/contexts/modal/internal.ts +25 -0
- package/src/hooks/index.ts +29 -0
- package/src/hooks/useAnimatedDetents.ts +119 -0
- package/src/hooks/useAnimatedKeyboard.ts +174 -0
- package/src/hooks/useAnimatedLayout.ts +109 -0
- package/src/hooks/useBottomSheet.ts +12 -0
- package/src/hooks/useBottomSheetContentContainerStyle.ts +88 -0
- package/src/hooks/useBottomSheetGestureHandlers.ts +12 -0
- package/src/hooks/useBottomSheetInternal.ts +25 -0
- package/src/hooks/useBottomSheetModal.ts +12 -0
- package/src/hooks/useBottomSheetModalInternal.ts +25 -0
- package/src/hooks/useBottomSheetScrollableCreator.tsx +60 -0
- package/src/hooks/useBottomSheetSpringConfigs.ts +11 -0
- package/src/hooks/useBottomSheetTimingConfigs.ts +36 -0
- package/src/hooks/useBoundingClientRect.ts +77 -0
- package/src/hooks/useGestureEventsHandlersDefault.tsx +436 -0
- package/src/hooks/useGestureEventsHandlersDefault.web.tsx +418 -0
- package/src/hooks/useGestureHandler.ts +90 -0
- package/src/hooks/usePropsValidator.ts +108 -0
- package/src/hooks/useReactiveSharedValue.ts +45 -0
- package/src/hooks/useScrollEventsHandlersDefault.ts +167 -0
- package/src/hooks/useScrollHandler.ts +72 -0
- package/src/hooks/useScrollHandler.web.ts +181 -0
- package/src/hooks/useScrollable.ts +131 -0
- package/src/hooks/useScrollableSetter.ts +56 -0
- package/src/hooks/useStableCallback.ts +26 -0
- package/src/index.ts +79 -0
- package/src/types.d.ts +336 -0
- package/src/utilities/animate.ts +56 -0
- package/src/utilities/clamp.ts +8 -0
- package/src/utilities/easingExp.ts +10 -0
- package/src/utilities/findNodeHandle.ts +1 -0
- package/src/utilities/findNodeHandle.web.ts +33 -0
- package/src/utilities/getKeyboardAnimationConfigs.ts +44 -0
- package/src/utilities/getRefNativeTag.web.ts +6 -0
- package/src/utilities/id.ts +6 -0
- package/src/utilities/index.ts +7 -0
- package/src/utilities/isFabricInstalled.ts +9 -0
- package/src/utilities/logger.ts +55 -0
- package/src/utilities/noop.ts +7 -0
- package/src/utilities/normalizeSnapPoint.ts +17 -0
- package/src/utilities/snapPoint.ts +11 -0
- package/src/utilities/validateSnapPoint.ts +20 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { State } from 'react-native-gesture-handler';
|
|
3
|
+
import { scrollTo } from 'react-native-reanimated';
|
|
4
|
+
import { ANIMATION_STATUS, SCROLLABLE_STATUS, SHEET_STATE } from '../constants';
|
|
5
|
+
import type {
|
|
6
|
+
ScrollEventHandlerCallbackType,
|
|
7
|
+
ScrollEventsHandlersHookType,
|
|
8
|
+
} from '../types';
|
|
9
|
+
import { useBottomSheetInternal } from './useBottomSheetInternal';
|
|
10
|
+
|
|
11
|
+
export type ScrollEventContextType = {
|
|
12
|
+
initialContentOffsetY: number;
|
|
13
|
+
shouldLockInitialPosition: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = (
|
|
17
|
+
scrollableRef,
|
|
18
|
+
scrollableContentOffsetY
|
|
19
|
+
) => {
|
|
20
|
+
// hooks
|
|
21
|
+
const {
|
|
22
|
+
animatedSheetState,
|
|
23
|
+
animatedScrollableState,
|
|
24
|
+
animatedScrollableStatus,
|
|
25
|
+
animatedAnimationState,
|
|
26
|
+
animatedHandleGestureState,
|
|
27
|
+
} = useBottomSheetInternal();
|
|
28
|
+
|
|
29
|
+
//#region callbacks
|
|
30
|
+
const handleOnScroll: ScrollEventHandlerCallbackType<ScrollEventContextType> =
|
|
31
|
+
useCallback(
|
|
32
|
+
({ contentOffset: { y } }, context) => {
|
|
33
|
+
'worklet';
|
|
34
|
+
/**
|
|
35
|
+
* if sheet position is extended or fill parent, then we reset
|
|
36
|
+
* `shouldLockInitialPosition` value to false.
|
|
37
|
+
*/
|
|
38
|
+
if (
|
|
39
|
+
animatedSheetState.value === SHEET_STATE.EXTENDED ||
|
|
40
|
+
animatedSheetState.value === SHEET_STATE.FILL_PARENT
|
|
41
|
+
) {
|
|
42
|
+
context.shouldLockInitialPosition = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* if handle gesture state is active, then we capture the offset y position
|
|
47
|
+
* and lock the scrollable with it.
|
|
48
|
+
*/
|
|
49
|
+
if (animatedHandleGestureState.value === State.ACTIVE) {
|
|
50
|
+
context.shouldLockInitialPosition = true;
|
|
51
|
+
context.initialContentOffsetY = y;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (animatedScrollableStatus.value === SCROLLABLE_STATUS.LOCKED) {
|
|
55
|
+
const lockPosition = context.shouldLockInitialPosition
|
|
56
|
+
? (context.initialContentOffsetY ?? 0)
|
|
57
|
+
: 0;
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
scrollTo(scrollableRef, 0, lockPosition, false);
|
|
60
|
+
scrollableContentOffsetY.value = lockPosition;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[
|
|
65
|
+
scrollableRef,
|
|
66
|
+
scrollableContentOffsetY,
|
|
67
|
+
animatedScrollableStatus,
|
|
68
|
+
animatedSheetState,
|
|
69
|
+
animatedHandleGestureState,
|
|
70
|
+
]
|
|
71
|
+
);
|
|
72
|
+
const handleOnBeginDrag: ScrollEventHandlerCallbackType<ScrollEventContextType> =
|
|
73
|
+
useCallback(
|
|
74
|
+
({ contentOffset: { y } }, context) => {
|
|
75
|
+
'worklet';
|
|
76
|
+
scrollableContentOffsetY.value = y;
|
|
77
|
+
context.initialContentOffsetY = y;
|
|
78
|
+
animatedScrollableState.set(state => ({
|
|
79
|
+
...state,
|
|
80
|
+
contentOffsetY: y,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* if sheet position not extended or fill parent and the scrollable position
|
|
85
|
+
* not at the top, then we should lock the initial scrollable position.
|
|
86
|
+
*/
|
|
87
|
+
if (
|
|
88
|
+
animatedSheetState.value !== SHEET_STATE.EXTENDED &&
|
|
89
|
+
animatedSheetState.value !== SHEET_STATE.FILL_PARENT &&
|
|
90
|
+
y > 0
|
|
91
|
+
) {
|
|
92
|
+
context.shouldLockInitialPosition = true;
|
|
93
|
+
} else {
|
|
94
|
+
context.shouldLockInitialPosition = false;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[scrollableContentOffsetY, animatedSheetState, animatedScrollableState]
|
|
98
|
+
);
|
|
99
|
+
const handleOnEndDrag: ScrollEventHandlerCallbackType<ScrollEventContextType> =
|
|
100
|
+
useCallback(
|
|
101
|
+
({ contentOffset: { y } }, context) => {
|
|
102
|
+
'worklet';
|
|
103
|
+
if (animatedScrollableStatus.value === SCROLLABLE_STATUS.LOCKED) {
|
|
104
|
+
const lockPosition = context.shouldLockInitialPosition
|
|
105
|
+
? (context.initialContentOffsetY ?? 0)
|
|
106
|
+
: 0;
|
|
107
|
+
// @ts-ignore
|
|
108
|
+
scrollTo(scrollableRef, 0, lockPosition, false);
|
|
109
|
+
scrollableContentOffsetY.value = lockPosition;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (animatedAnimationState.get().status !== ANIMATION_STATUS.RUNNING) {
|
|
114
|
+
scrollableContentOffsetY.value = y;
|
|
115
|
+
animatedScrollableState.set(state => ({
|
|
116
|
+
...state,
|
|
117
|
+
contentOffsetY: y,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[
|
|
122
|
+
scrollableRef,
|
|
123
|
+
scrollableContentOffsetY,
|
|
124
|
+
animatedAnimationState,
|
|
125
|
+
animatedScrollableStatus,
|
|
126
|
+
animatedScrollableState,
|
|
127
|
+
]
|
|
128
|
+
);
|
|
129
|
+
const handleOnMomentumEnd: ScrollEventHandlerCallbackType<ScrollEventContextType> =
|
|
130
|
+
useCallback(
|
|
131
|
+
({ contentOffset: { y } }, context) => {
|
|
132
|
+
'worklet';
|
|
133
|
+
if (animatedScrollableStatus.value === SCROLLABLE_STATUS.LOCKED) {
|
|
134
|
+
const lockPosition = context.shouldLockInitialPosition
|
|
135
|
+
? (context.initialContentOffsetY ?? 0)
|
|
136
|
+
: 0;
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
scrollTo(scrollableRef, 0, lockPosition, false);
|
|
139
|
+
scrollableContentOffsetY.value = 0;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (animatedAnimationState.get().status !== ANIMATION_STATUS.RUNNING) {
|
|
144
|
+
scrollableContentOffsetY.value = y;
|
|
145
|
+
animatedScrollableState.set(state => ({
|
|
146
|
+
...state,
|
|
147
|
+
contentOffsetY: y,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
[
|
|
152
|
+
scrollableContentOffsetY,
|
|
153
|
+
scrollableRef,
|
|
154
|
+
animatedAnimationState,
|
|
155
|
+
animatedScrollableStatus,
|
|
156
|
+
animatedScrollableState,
|
|
157
|
+
]
|
|
158
|
+
);
|
|
159
|
+
//#endregion
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
handleOnScroll,
|
|
163
|
+
handleOnBeginDrag,
|
|
164
|
+
handleOnEndDrag,
|
|
165
|
+
handleOnMomentumEnd,
|
|
166
|
+
};
|
|
167
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runOnJS,
|
|
3
|
+
useAnimatedRef,
|
|
4
|
+
useAnimatedScrollHandler,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
import type { Scrollable, ScrollableEvent } from '../types';
|
|
8
|
+
import { workletNoop as noop } from '../utilities';
|
|
9
|
+
import { useScrollEventsHandlersDefault } from './useScrollEventsHandlersDefault';
|
|
10
|
+
|
|
11
|
+
export const useScrollHandler = (
|
|
12
|
+
useScrollEventsHandlers = useScrollEventsHandlersDefault,
|
|
13
|
+
onScroll?: ScrollableEvent,
|
|
14
|
+
onScrollBeginDrag?: ScrollableEvent,
|
|
15
|
+
onScrollEndDrag?: ScrollableEvent
|
|
16
|
+
) => {
|
|
17
|
+
// refs
|
|
18
|
+
const scrollableRef = useAnimatedRef<Scrollable>();
|
|
19
|
+
|
|
20
|
+
// variables
|
|
21
|
+
const scrollableContentOffsetY = useSharedValue<number>(0);
|
|
22
|
+
|
|
23
|
+
// hooks
|
|
24
|
+
const {
|
|
25
|
+
handleOnScroll = noop,
|
|
26
|
+
handleOnBeginDrag = noop,
|
|
27
|
+
handleOnEndDrag = noop,
|
|
28
|
+
handleOnMomentumEnd = noop,
|
|
29
|
+
handleOnMomentumBegin = noop,
|
|
30
|
+
} = useScrollEventsHandlers(scrollableRef, scrollableContentOffsetY);
|
|
31
|
+
|
|
32
|
+
// callbacks
|
|
33
|
+
const scrollHandler = useAnimatedScrollHandler(
|
|
34
|
+
{
|
|
35
|
+
onScroll: (event, context) => {
|
|
36
|
+
handleOnScroll(event, context);
|
|
37
|
+
|
|
38
|
+
if (onScroll) {
|
|
39
|
+
runOnJS(onScroll)({ nativeEvent: event });
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onBeginDrag: (event, context) => {
|
|
43
|
+
handleOnBeginDrag(event, context);
|
|
44
|
+
|
|
45
|
+
if (onScrollBeginDrag) {
|
|
46
|
+
runOnJS(onScrollBeginDrag)({ nativeEvent: event });
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
onEndDrag: (event, context) => {
|
|
50
|
+
handleOnEndDrag(event, context);
|
|
51
|
+
|
|
52
|
+
if (onScrollEndDrag) {
|
|
53
|
+
runOnJS(onScrollEndDrag)({ nativeEvent: event });
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
onMomentumBegin: handleOnMomentumBegin,
|
|
57
|
+
onMomentumEnd: handleOnMomentumEnd,
|
|
58
|
+
},
|
|
59
|
+
[
|
|
60
|
+
handleOnScroll,
|
|
61
|
+
handleOnBeginDrag,
|
|
62
|
+
handleOnEndDrag,
|
|
63
|
+
handleOnMomentumBegin,
|
|
64
|
+
handleOnMomentumEnd,
|
|
65
|
+
onScroll,
|
|
66
|
+
onScrollBeginDrag,
|
|
67
|
+
onScrollEndDrag,
|
|
68
|
+
]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return { scrollHandler, scrollableRef, scrollableContentOffsetY };
|
|
72
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { type TouchEvent, useEffect, useRef } from 'react';
|
|
2
|
+
import { useSharedValue } from 'react-native-reanimated';
|
|
3
|
+
import { ANIMATION_STATUS, SCROLLABLE_STATUS } from '../constants';
|
|
4
|
+
import type { Scrollable, ScrollableEvent } from '../types';
|
|
5
|
+
import { findNodeHandle } from '../utilities/findNodeHandle.web';
|
|
6
|
+
import { useBottomSheetInternal } from './useBottomSheetInternal';
|
|
7
|
+
|
|
8
|
+
export type ScrollEventContextType = {
|
|
9
|
+
initialContentOffsetY: number;
|
|
10
|
+
shouldLockInitialPosition: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const useScrollHandler = (_: never, onScroll?: ScrollableEvent) => {
|
|
14
|
+
//#region refs
|
|
15
|
+
const scrollableRef = useRef<Scrollable>(null);
|
|
16
|
+
//#endregion
|
|
17
|
+
|
|
18
|
+
//#region variables
|
|
19
|
+
const scrollableContentOffsetY = useSharedValue<number>(0);
|
|
20
|
+
//#endregion
|
|
21
|
+
|
|
22
|
+
//#region hooks
|
|
23
|
+
const {
|
|
24
|
+
animatedScrollableState,
|
|
25
|
+
animatedScrollableStatus,
|
|
26
|
+
animatedAnimationState,
|
|
27
|
+
} = useBottomSheetInternal();
|
|
28
|
+
//#endregion
|
|
29
|
+
|
|
30
|
+
//#region effects
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// biome-ignore lint: to be addressed!
|
|
33
|
+
const element = findNodeHandle(scrollableRef.current) as any;
|
|
34
|
+
let scrollOffset = 0;
|
|
35
|
+
let supportsPassive = false;
|
|
36
|
+
let maybePrevent = false;
|
|
37
|
+
let lastTouchY = 0;
|
|
38
|
+
|
|
39
|
+
let initialContentOffsetY = 0;
|
|
40
|
+
const shouldLockInitialPosition = false;
|
|
41
|
+
|
|
42
|
+
function handleOnTouchStart(event: TouchEvent) {
|
|
43
|
+
if (event.touches.length !== 1) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
initialContentOffsetY = element.scrollTop;
|
|
48
|
+
lastTouchY = event.touches[0].clientY;
|
|
49
|
+
maybePrevent = scrollOffset <= 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleOnTouchMove(event: TouchEvent) {
|
|
53
|
+
if (
|
|
54
|
+
animatedScrollableStatus.value === SCROLLABLE_STATUS.LOCKED &&
|
|
55
|
+
event.cancelable
|
|
56
|
+
) {
|
|
57
|
+
return event.preventDefault();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (maybePrevent) {
|
|
61
|
+
maybePrevent = false;
|
|
62
|
+
|
|
63
|
+
const touchY = event.touches[0].clientY;
|
|
64
|
+
const touchYDelta = touchY - lastTouchY;
|
|
65
|
+
|
|
66
|
+
if (touchYDelta > 0 && event.cancelable) {
|
|
67
|
+
return event.preventDefault();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleOnTouchEnd() {
|
|
75
|
+
if (animatedScrollableStatus.value === SCROLLABLE_STATUS.LOCKED) {
|
|
76
|
+
const lockPosition = shouldLockInitialPosition
|
|
77
|
+
? (initialContentOffsetY ?? 0)
|
|
78
|
+
: 0;
|
|
79
|
+
element.scroll({
|
|
80
|
+
top: 0,
|
|
81
|
+
left: 0,
|
|
82
|
+
behavior: 'instant',
|
|
83
|
+
});
|
|
84
|
+
scrollableContentOffsetY.value = lockPosition;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleOnScroll(event: TouchEvent) {
|
|
90
|
+
scrollOffset = element.scrollTop;
|
|
91
|
+
|
|
92
|
+
if (animatedAnimationState.get().status !== ANIMATION_STATUS.RUNNING) {
|
|
93
|
+
const contentOffsetY = Math.max(0, scrollOffset);
|
|
94
|
+
scrollableContentOffsetY.value = contentOffsetY;
|
|
95
|
+
animatedScrollableState.set(state => ({
|
|
96
|
+
...state,
|
|
97
|
+
contentOffsetY,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (scrollOffset <= 0 && event.cancelable) {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
event.stopPropagation();
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// @ts-ignore
|
|
111
|
+
window.addEventListener('test', null, {
|
|
112
|
+
// @ts-ignore
|
|
113
|
+
// biome-ignore lint: to be addressed
|
|
114
|
+
get passive() {
|
|
115
|
+
supportsPassive = true;
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
} catch (_e) {}
|
|
119
|
+
|
|
120
|
+
element.addEventListener(
|
|
121
|
+
'touchstart',
|
|
122
|
+
handleOnTouchStart,
|
|
123
|
+
supportsPassive
|
|
124
|
+
? {
|
|
125
|
+
passive: true,
|
|
126
|
+
}
|
|
127
|
+
: false
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
element.addEventListener(
|
|
131
|
+
'touchmove',
|
|
132
|
+
handleOnTouchMove,
|
|
133
|
+
supportsPassive
|
|
134
|
+
? {
|
|
135
|
+
passive: false,
|
|
136
|
+
}
|
|
137
|
+
: false
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
element.addEventListener(
|
|
141
|
+
'touchend',
|
|
142
|
+
handleOnTouchEnd,
|
|
143
|
+
supportsPassive
|
|
144
|
+
? {
|
|
145
|
+
passive: false,
|
|
146
|
+
}
|
|
147
|
+
: false
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
element.addEventListener(
|
|
151
|
+
'scroll',
|
|
152
|
+
handleOnScroll,
|
|
153
|
+
supportsPassive
|
|
154
|
+
? {
|
|
155
|
+
passive: false,
|
|
156
|
+
}
|
|
157
|
+
: false
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
// @ts-ignore
|
|
162
|
+
window.removeEventListener('test', null);
|
|
163
|
+
element.removeEventListener('touchstart', handleOnTouchStart);
|
|
164
|
+
element.removeEventListener('touchmove', handleOnTouchMove);
|
|
165
|
+
element.removeEventListener('touchend', handleOnTouchEnd);
|
|
166
|
+
element.removeEventListener('scroll', handleOnScroll);
|
|
167
|
+
};
|
|
168
|
+
}, [
|
|
169
|
+
animatedAnimationState,
|
|
170
|
+
animatedScrollableState,
|
|
171
|
+
animatedScrollableStatus,
|
|
172
|
+
scrollableContentOffsetY,
|
|
173
|
+
]);
|
|
174
|
+
//#endregion
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
scrollHandler: onScroll,
|
|
178
|
+
scrollableRef,
|
|
179
|
+
scrollableContentOffsetY,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { type RefObject, useCallback, useRef } from 'react';
|
|
2
|
+
import type { NodeHandle } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
type SharedValue,
|
|
5
|
+
useDerivedValue,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
import {
|
|
9
|
+
ANIMATION_STATUS,
|
|
10
|
+
KEYBOARD_STATUS,
|
|
11
|
+
SCROLLABLE_STATUS,
|
|
12
|
+
SCROLLABLE_TYPE,
|
|
13
|
+
SHEET_STATE,
|
|
14
|
+
} from '../constants';
|
|
15
|
+
import type {
|
|
16
|
+
AnimationState,
|
|
17
|
+
KeyboardState,
|
|
18
|
+
Scrollable,
|
|
19
|
+
ScrollableRef,
|
|
20
|
+
ScrollableState,
|
|
21
|
+
} from '../types';
|
|
22
|
+
import { findNodeHandle } from '../utilities';
|
|
23
|
+
|
|
24
|
+
export const useScrollable = (
|
|
25
|
+
enableContentPanningGesture: boolean,
|
|
26
|
+
animatedSheetState: SharedValue<SHEET_STATE>,
|
|
27
|
+
animatedKeyboardState: SharedValue<KeyboardState>,
|
|
28
|
+
animatedAnimationState: SharedValue<AnimationState>
|
|
29
|
+
) => {
|
|
30
|
+
//#region refs
|
|
31
|
+
const scrollableRef = useRef<ScrollableRef>(null);
|
|
32
|
+
const previousScrollableRef = useRef<ScrollableRef>(null);
|
|
33
|
+
//#endregion
|
|
34
|
+
|
|
35
|
+
//#region variables
|
|
36
|
+
const state = useSharedValue<ScrollableState>({
|
|
37
|
+
type: SCROLLABLE_TYPE.UNDETERMINED,
|
|
38
|
+
contentOffsetY: 0,
|
|
39
|
+
refreshable: false,
|
|
40
|
+
});
|
|
41
|
+
const status = useDerivedValue<SCROLLABLE_STATUS>(() => {
|
|
42
|
+
/**
|
|
43
|
+
* if user had disabled content panning gesture, then we unlock
|
|
44
|
+
* the scrollable state.
|
|
45
|
+
*/
|
|
46
|
+
if (!enableContentPanningGesture) {
|
|
47
|
+
return SCROLLABLE_STATUS.UNLOCKED;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* if sheet state is fill parent, then unlock scrolling
|
|
52
|
+
*/
|
|
53
|
+
if (animatedSheetState.value === SHEET_STATE.FILL_PARENT) {
|
|
54
|
+
return SCROLLABLE_STATUS.UNLOCKED;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* if sheet state is extended, then unlock scrolling
|
|
59
|
+
*/
|
|
60
|
+
if (animatedSheetState.value === SHEET_STATE.EXTENDED) {
|
|
61
|
+
return SCROLLABLE_STATUS.UNLOCKED;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* if keyboard is shown and sheet is animating
|
|
66
|
+
* then we do not lock the scrolling to not lose
|
|
67
|
+
* current scrollable scroll position.
|
|
68
|
+
*/
|
|
69
|
+
if (
|
|
70
|
+
animatedKeyboardState.get().status === KEYBOARD_STATUS.SHOWN &&
|
|
71
|
+
animatedAnimationState.get().status === ANIMATION_STATUS.RUNNING
|
|
72
|
+
) {
|
|
73
|
+
return SCROLLABLE_STATUS.UNLOCKED;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return SCROLLABLE_STATUS.LOCKED;
|
|
77
|
+
}, [
|
|
78
|
+
enableContentPanningGesture,
|
|
79
|
+
animatedSheetState,
|
|
80
|
+
animatedKeyboardState,
|
|
81
|
+
animatedAnimationState,
|
|
82
|
+
state,
|
|
83
|
+
]);
|
|
84
|
+
//#endregion
|
|
85
|
+
|
|
86
|
+
//#region callbacks
|
|
87
|
+
const setScrollableRef = useCallback((ref: ScrollableRef) => {
|
|
88
|
+
// get current node handle id
|
|
89
|
+
const currentRefId = scrollableRef.current?.id ?? null;
|
|
90
|
+
|
|
91
|
+
if (currentRefId !== ref.id) {
|
|
92
|
+
if (scrollableRef.current) {
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
previousScrollableRef.current = scrollableRef.current;
|
|
95
|
+
}
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
scrollableRef.current = ref;
|
|
98
|
+
}
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const removeScrollableRef = useCallback((ref: RefObject<Scrollable>) => {
|
|
102
|
+
// find node handle id
|
|
103
|
+
let id: NodeHandle | null;
|
|
104
|
+
try {
|
|
105
|
+
id = findNodeHandle(ref.current);
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// get current node handle id
|
|
111
|
+
const currentRefId = scrollableRef.current?.id ?? null;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @DEV
|
|
115
|
+
* when the incoming node is actually the current node, we reset
|
|
116
|
+
* the current scrollable ref to the previous one.
|
|
117
|
+
*/
|
|
118
|
+
if (id === currentRefId) {
|
|
119
|
+
// @ts-ignore
|
|
120
|
+
scrollableRef.current = previousScrollableRef.current;
|
|
121
|
+
}
|
|
122
|
+
}, []);
|
|
123
|
+
//#endregion
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
state,
|
|
127
|
+
status,
|
|
128
|
+
setScrollableRef,
|
|
129
|
+
removeScrollableRef,
|
|
130
|
+
};
|
|
131
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useCallback, useEffect } from 'react';
|
|
3
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
4
|
+
import type { SCROLLABLE_TYPE } from '../constants';
|
|
5
|
+
import type { Scrollable } from '../types';
|
|
6
|
+
import { findNodeHandle } from '../utilities';
|
|
7
|
+
import { useBottomSheetInternal } from './useBottomSheetInternal';
|
|
8
|
+
|
|
9
|
+
export const useScrollableSetter = (
|
|
10
|
+
ref: React.RefObject<Scrollable>,
|
|
11
|
+
type: SCROLLABLE_TYPE,
|
|
12
|
+
contentOffsetY: SharedValue<number>,
|
|
13
|
+
refreshable: boolean,
|
|
14
|
+
useFocusHook = useEffect
|
|
15
|
+
) => {
|
|
16
|
+
// hooks
|
|
17
|
+
const { animatedScrollableState, setScrollableRef, removeScrollableRef } =
|
|
18
|
+
useBottomSheetInternal();
|
|
19
|
+
|
|
20
|
+
// callbacks
|
|
21
|
+
const handleSettingScrollable = useCallback(() => {
|
|
22
|
+
// set current content offset
|
|
23
|
+
animatedScrollableState.set(state => ({
|
|
24
|
+
...state,
|
|
25
|
+
contentOffsetY: contentOffsetY.value,
|
|
26
|
+
type,
|
|
27
|
+
refreshable,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// set current scrollable ref
|
|
31
|
+
const id = findNodeHandle(ref.current);
|
|
32
|
+
if (id) {
|
|
33
|
+
setScrollableRef({
|
|
34
|
+
id: id,
|
|
35
|
+
node: ref,
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
console.warn(`Couldn't find the scrollable node handle id!`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
removeScrollableRef(ref);
|
|
43
|
+
};
|
|
44
|
+
}, [
|
|
45
|
+
ref,
|
|
46
|
+
type,
|
|
47
|
+
refreshable,
|
|
48
|
+
contentOffsetY,
|
|
49
|
+
animatedScrollableState,
|
|
50
|
+
setScrollableRef,
|
|
51
|
+
removeScrollableRef,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// effects
|
|
55
|
+
useFocusHook(handleSettingScrollable);
|
|
56
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
type Callback<T extends unknown[], R> = (...args: T) => R;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provide a stable version of useCallback.
|
|
7
|
+
*/
|
|
8
|
+
export function useStableCallback<T extends unknown[], R>(
|
|
9
|
+
callback: Callback<T, R>
|
|
10
|
+
) {
|
|
11
|
+
const callbackRef = useRef<Callback<T, R>>();
|
|
12
|
+
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
callbackRef.current = callback;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
return () => {
|
|
19
|
+
callbackRef.current = undefined;
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return useCallback<Callback<T, R | undefined>>((...args) => {
|
|
24
|
+
return callbackRef.current?.(...args);
|
|
25
|
+
}, []);
|
|
26
|
+
}
|