@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,119 @@
|
|
|
1
|
+
import { type SharedValue, useDerivedValue } from 'react-native-reanimated';
|
|
2
|
+
import type { BottomSheetProps } from '../components/bottomSheet';
|
|
3
|
+
import { INITIAL_LAYOUT_VALUE } from '../constants';
|
|
4
|
+
import type { DetentsState, LayoutState } from '../types';
|
|
5
|
+
import { normalizeSnapPoint } from '../utilities';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A custom hook that computes and returns the animated detent positions for a bottom sheet component.
|
|
9
|
+
*
|
|
10
|
+
* This hook normalizes the provided snap points (detents), optionally adds a dynamic detent based on content size,
|
|
11
|
+
* and calculates key positions such as the highest detent and the closed position. It supports both static and dynamic
|
|
12
|
+
* sizing, and adapts to modal and detached sheet modes.
|
|
13
|
+
*
|
|
14
|
+
* @param detents - The snap points for the bottom sheet, which can be an array or an object with a `value` property.
|
|
15
|
+
* @param layoutState - A shared animated value containing the current layout state (container, handle, and content heights).
|
|
16
|
+
* @param enableDynamicSizing - Whether dynamic sizing based on content height is enabled.
|
|
17
|
+
* @param maxDynamicContentSize - The maximum allowed content size for dynamic sizing.
|
|
18
|
+
* @param detached - Whether the bottom sheet is in detached mode.
|
|
19
|
+
* @param $modal - Whether the bottom sheet is presented as a modal.
|
|
20
|
+
* @param bottomInset - The bottom inset to apply when the sheet is modal or detached (default is 0).
|
|
21
|
+
*/
|
|
22
|
+
export const useAnimatedDetents = (
|
|
23
|
+
detents: BottomSheetProps['snapPoints'],
|
|
24
|
+
layoutState: SharedValue<LayoutState>,
|
|
25
|
+
enableDynamicSizing: BottomSheetProps['enableDynamicSizing'],
|
|
26
|
+
maxDynamicContentSize: BottomSheetProps['maxDynamicContentSize'],
|
|
27
|
+
detached: BottomSheetProps['detached'],
|
|
28
|
+
$modal: BottomSheetProps['$modal'],
|
|
29
|
+
bottomInset: BottomSheetProps['bottomInset'] = 0
|
|
30
|
+
) => {
|
|
31
|
+
const state = useDerivedValue<DetentsState>(() => {
|
|
32
|
+
const { containerHeight, handleHeight, contentHeight } = layoutState.get();
|
|
33
|
+
|
|
34
|
+
// early exit, if container layout is not ready
|
|
35
|
+
if (containerHeight === INITIAL_LAYOUT_VALUE) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// extract detents from provided props
|
|
40
|
+
const _detents = detents
|
|
41
|
+
? 'value' in detents
|
|
42
|
+
? detents.value
|
|
43
|
+
: detents
|
|
44
|
+
: [];
|
|
45
|
+
|
|
46
|
+
// normalized all provided detents, converting percentage
|
|
47
|
+
// values into absolute values.
|
|
48
|
+
let _normalizedDetents = _detents.map(snapPoint =>
|
|
49
|
+
normalizeSnapPoint(snapPoint, containerHeight)
|
|
50
|
+
) as number[];
|
|
51
|
+
|
|
52
|
+
let highestDetentPosition =
|
|
53
|
+
_normalizedDetents[_normalizedDetents.length - 1];
|
|
54
|
+
let closedDetentPosition = containerHeight;
|
|
55
|
+
if ($modal || detached) {
|
|
56
|
+
closedDetentPosition = containerHeight + bottomInset;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!enableDynamicSizing) {
|
|
60
|
+
return {
|
|
61
|
+
detents: _normalizedDetents,
|
|
62
|
+
highestDetentPosition,
|
|
63
|
+
closedDetentPosition,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// early exit, if dynamic sizing is enabled and
|
|
68
|
+
// content height is not calculated yet.
|
|
69
|
+
if (contentHeight === INITIAL_LAYOUT_VALUE) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// early exit, if handle height is not calculated yet.
|
|
74
|
+
if (handleHeight === INITIAL_LAYOUT_VALUE) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// calculate a new detents based on content height.
|
|
79
|
+
const dynamicSnapPoint =
|
|
80
|
+
containerHeight -
|
|
81
|
+
Math.min(
|
|
82
|
+
contentHeight + handleHeight,
|
|
83
|
+
maxDynamicContentSize !== undefined
|
|
84
|
+
? maxDynamicContentSize
|
|
85
|
+
: containerHeight
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// push dynamic detent into the normalized detents,
|
|
89
|
+
// only if it does not exists in the provided list already.
|
|
90
|
+
if (!_normalizedDetents.includes(dynamicSnapPoint)) {
|
|
91
|
+
_normalizedDetents.push(dynamicSnapPoint);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// sort all detents.
|
|
95
|
+
_normalizedDetents = _normalizedDetents.sort((a, b) => b - a);
|
|
96
|
+
|
|
97
|
+
// update the highest detent position.
|
|
98
|
+
highestDetentPosition = _normalizedDetents[_normalizedDetents.length - 1];
|
|
99
|
+
|
|
100
|
+
// locate the dynamic detent index.
|
|
101
|
+
const dynamicDetentIndex = _normalizedDetents.indexOf(dynamicSnapPoint);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
detents: _normalizedDetents,
|
|
105
|
+
dynamicDetentIndex,
|
|
106
|
+
highestDetentPosition,
|
|
107
|
+
closedDetentPosition,
|
|
108
|
+
};
|
|
109
|
+
}, [
|
|
110
|
+
detents,
|
|
111
|
+
layoutState,
|
|
112
|
+
enableDynamicSizing,
|
|
113
|
+
maxDynamicContentSize,
|
|
114
|
+
detached,
|
|
115
|
+
$modal,
|
|
116
|
+
bottomInset,
|
|
117
|
+
]);
|
|
118
|
+
return state;
|
|
119
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Keyboard,
|
|
4
|
+
type KeyboardEvent,
|
|
5
|
+
type KeyboardEventEasing,
|
|
6
|
+
type KeyboardEventName,
|
|
7
|
+
Platform,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import {
|
|
10
|
+
runOnUI,
|
|
11
|
+
useAnimatedReaction,
|
|
12
|
+
useSharedValue,
|
|
13
|
+
} from 'react-native-reanimated';
|
|
14
|
+
import { KEYBOARD_STATUS, SCREEN_HEIGHT } from '../constants';
|
|
15
|
+
import type { KeyboardState } from '../types';
|
|
16
|
+
|
|
17
|
+
const KEYBOARD_EVENT_MAPPER = {
|
|
18
|
+
KEYBOARD_SHOW: Platform.select({
|
|
19
|
+
ios: 'keyboardWillShow',
|
|
20
|
+
android: 'keyboardDidShow',
|
|
21
|
+
default: '',
|
|
22
|
+
}) as KeyboardEventName,
|
|
23
|
+
KEYBOARD_HIDE: Platform.select({
|
|
24
|
+
ios: 'keyboardWillHide',
|
|
25
|
+
android: 'keyboardDidHide',
|
|
26
|
+
default: '',
|
|
27
|
+
}) as KeyboardEventName,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const INITIAL_STATE: KeyboardState = {
|
|
31
|
+
status: KEYBOARD_STATUS.UNDETERMINED,
|
|
32
|
+
height: 0,
|
|
33
|
+
heightWithinContainer: 0,
|
|
34
|
+
easing: 'keyboard',
|
|
35
|
+
duration: 500,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const useAnimatedKeyboard = () => {
|
|
39
|
+
//#region variables
|
|
40
|
+
const textInputNodesRef = useRef(new Set<number>());
|
|
41
|
+
const state = useSharedValue(INITIAL_STATE);
|
|
42
|
+
const temporaryCachedState = useSharedValue<Omit<
|
|
43
|
+
KeyboardState,
|
|
44
|
+
'heightWithinContainer' | 'target'
|
|
45
|
+
> | null>(null);
|
|
46
|
+
//#endregion
|
|
47
|
+
|
|
48
|
+
//#region worklets
|
|
49
|
+
const handleKeyboardEvent = useCallback(
|
|
50
|
+
(
|
|
51
|
+
status: KEYBOARD_STATUS,
|
|
52
|
+
height: number,
|
|
53
|
+
duration: number,
|
|
54
|
+
easing: KeyboardEventEasing,
|
|
55
|
+
bottomOffset?: number
|
|
56
|
+
) => {
|
|
57
|
+
'worklet';
|
|
58
|
+
const currentState = state.get();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* if the keyboard event was fired before the `onFocus` on TextInput,
|
|
62
|
+
* then we cache the event, and wait till the `target` is been set
|
|
63
|
+
* to be updated then fire this function again.
|
|
64
|
+
*/
|
|
65
|
+
if (status === KEYBOARD_STATUS.SHOWN && !currentState.target) {
|
|
66
|
+
temporaryCachedState.set({
|
|
67
|
+
status,
|
|
68
|
+
height,
|
|
69
|
+
duration,
|
|
70
|
+
easing,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* clear temporary cached state.
|
|
77
|
+
*/
|
|
78
|
+
temporaryCachedState.set(null);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* if keyboard status is hidden, then we keep old height.
|
|
82
|
+
*/
|
|
83
|
+
let adjustedHeight =
|
|
84
|
+
status === KEYBOARD_STATUS.SHOWN ? height : currentState.height;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* if keyboard had an bottom offset -android bottom bar-, then
|
|
88
|
+
* we add that offset to the keyboard height.
|
|
89
|
+
*/
|
|
90
|
+
if (bottomOffset) {
|
|
91
|
+
adjustedHeight = adjustedHeight + bottomOffset;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
state.set(state => ({
|
|
95
|
+
status,
|
|
96
|
+
easing,
|
|
97
|
+
duration,
|
|
98
|
+
height: adjustedHeight,
|
|
99
|
+
target: state.target,
|
|
100
|
+
heightWithinContainer: state.heightWithinContainer,
|
|
101
|
+
}));
|
|
102
|
+
},
|
|
103
|
+
[state, temporaryCachedState]
|
|
104
|
+
);
|
|
105
|
+
//#endregion
|
|
106
|
+
|
|
107
|
+
//#region effects
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const handleOnKeyboardShow = (event: KeyboardEvent) => {
|
|
110
|
+
runOnUI(handleKeyboardEvent)(
|
|
111
|
+
KEYBOARD_STATUS.SHOWN,
|
|
112
|
+
event.endCoordinates.height,
|
|
113
|
+
event.duration,
|
|
114
|
+
event.easing,
|
|
115
|
+
SCREEN_HEIGHT -
|
|
116
|
+
event.endCoordinates.height -
|
|
117
|
+
event.endCoordinates.screenY
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
const handleOnKeyboardHide = (event: KeyboardEvent) => {
|
|
121
|
+
runOnUI(handleKeyboardEvent)(
|
|
122
|
+
KEYBOARD_STATUS.HIDDEN,
|
|
123
|
+
event.endCoordinates.height,
|
|
124
|
+
event.duration,
|
|
125
|
+
event.easing
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const showSubscription = Keyboard.addListener(
|
|
130
|
+
KEYBOARD_EVENT_MAPPER.KEYBOARD_SHOW,
|
|
131
|
+
handleOnKeyboardShow
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const hideSubscription = Keyboard.addListener(
|
|
135
|
+
KEYBOARD_EVENT_MAPPER.KEYBOARD_HIDE,
|
|
136
|
+
handleOnKeyboardHide
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
showSubscription.remove();
|
|
141
|
+
hideSubscription.remove();
|
|
142
|
+
};
|
|
143
|
+
}, [handleKeyboardEvent]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* This reaction is needed to handle the issue with multiline text input.
|
|
147
|
+
*
|
|
148
|
+
* @link https://github.com/gorhom/react-native-bottom-sheet/issues/411
|
|
149
|
+
*/
|
|
150
|
+
useAnimatedReaction(
|
|
151
|
+
() => state.value.target,
|
|
152
|
+
(result, previous) => {
|
|
153
|
+
if (!result || result === previous) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const cachedState = temporaryCachedState.get();
|
|
158
|
+
if (!cachedState) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
handleKeyboardEvent(
|
|
163
|
+
cachedState.status,
|
|
164
|
+
cachedState.height,
|
|
165
|
+
cachedState.duration,
|
|
166
|
+
cachedState.easing
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
[temporaryCachedState, handleKeyboardEvent]
|
|
170
|
+
);
|
|
171
|
+
//#endregion
|
|
172
|
+
|
|
173
|
+
return { state, textInputNodesRef };
|
|
174
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type SharedValue,
|
|
4
|
+
makeMutable,
|
|
5
|
+
useAnimatedReaction,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
import { INITIAL_CONTAINER_LAYOUT, INITIAL_LAYOUT_VALUE } from '../constants';
|
|
8
|
+
import type { ContainerLayoutState, LayoutState } from '../types';
|
|
9
|
+
|
|
10
|
+
const INITIAL_STATE: LayoutState = {
|
|
11
|
+
rawContainerHeight: INITIAL_LAYOUT_VALUE,
|
|
12
|
+
containerHeight: INITIAL_LAYOUT_VALUE,
|
|
13
|
+
containerOffset: INITIAL_CONTAINER_LAYOUT.offset,
|
|
14
|
+
handleHeight: INITIAL_LAYOUT_VALUE,
|
|
15
|
+
footerHeight: INITIAL_LAYOUT_VALUE,
|
|
16
|
+
contentHeight: INITIAL_LAYOUT_VALUE,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A custom hook that manages and animates the layout state of a container,
|
|
21
|
+
* typically used in bottom sheet components. It calculates the effective
|
|
22
|
+
* container height by considering top and bottom insets, and updates the
|
|
23
|
+
* animated state in response to layout changes. The hook supports both modal
|
|
24
|
+
* and non-modal modes, and ensures the container's animated layout state
|
|
25
|
+
* remains in sync with the actual layout measurements.
|
|
26
|
+
*
|
|
27
|
+
* @param containerLayoutState - A shared value representing the current container layout state.
|
|
28
|
+
* @param topInset - The top inset value to be subtracted from the container height.
|
|
29
|
+
* @param bottomInset - The bottom inset value to be subtracted from the container height.
|
|
30
|
+
* @param modal - Optional flag indicating if the layout is in modal mode.
|
|
31
|
+
* @param shouldOverrideHandleHeight - Optional flag to override the handle height in the layout state, only when handle is set to null.
|
|
32
|
+
* @returns An object containing the animated layout state.
|
|
33
|
+
*/
|
|
34
|
+
export function useAnimatedLayout(
|
|
35
|
+
containerLayoutState: SharedValue<ContainerLayoutState> | undefined,
|
|
36
|
+
topInset: number,
|
|
37
|
+
bottomInset: number,
|
|
38
|
+
modal?: boolean,
|
|
39
|
+
shouldOverrideHandleHeight?: boolean
|
|
40
|
+
) {
|
|
41
|
+
//#region variables
|
|
42
|
+
const verticalInset = useMemo(
|
|
43
|
+
() => topInset + bottomInset,
|
|
44
|
+
[topInset, bottomInset]
|
|
45
|
+
);
|
|
46
|
+
const initialState = useMemo(() => {
|
|
47
|
+
const _state = { ...INITIAL_STATE };
|
|
48
|
+
|
|
49
|
+
if (containerLayoutState) {
|
|
50
|
+
const containerLayout = containerLayoutState.get();
|
|
51
|
+
_state.containerHeight = modal
|
|
52
|
+
? containerLayout.height - verticalInset
|
|
53
|
+
: containerLayout.height;
|
|
54
|
+
_state.containerOffset = containerLayout.offset;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (shouldOverrideHandleHeight) {
|
|
58
|
+
_state.handleHeight = 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return _state;
|
|
62
|
+
}, [containerLayoutState, modal, shouldOverrideHandleHeight, verticalInset]);
|
|
63
|
+
//#endregion
|
|
64
|
+
|
|
65
|
+
//#region state
|
|
66
|
+
const [state] = useState(() => makeMutable(initialState));
|
|
67
|
+
//#endregion
|
|
68
|
+
|
|
69
|
+
//#region effects
|
|
70
|
+
useAnimatedReaction(
|
|
71
|
+
() => state.value.rawContainerHeight,
|
|
72
|
+
(result, previous) => {
|
|
73
|
+
if (result === previous) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (result === INITIAL_LAYOUT_VALUE) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
state.modify(_state => {
|
|
81
|
+
'worklet';
|
|
82
|
+
_state.containerHeight = modal ? result - verticalInset : result;
|
|
83
|
+
return _state;
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
[state, verticalInset, modal]
|
|
87
|
+
);
|
|
88
|
+
useAnimatedReaction(
|
|
89
|
+
() => containerLayoutState?.get().height,
|
|
90
|
+
(result, previous) => {
|
|
91
|
+
if (!result || result === previous) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (result === INITIAL_LAYOUT_VALUE) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
state.modify(_state => {
|
|
99
|
+
'worklet';
|
|
100
|
+
_state.containerHeight = modal ? result - verticalInset : result;
|
|
101
|
+
return _state;
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
[state, verticalInset, modal]
|
|
105
|
+
);
|
|
106
|
+
//#endregion
|
|
107
|
+
|
|
108
|
+
return state;
|
|
109
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { BottomSheetContext } from '../contexts/external';
|
|
3
|
+
|
|
4
|
+
export const useBottomSheet = () => {
|
|
5
|
+
const context = useContext(BottomSheetContext);
|
|
6
|
+
|
|
7
|
+
if (context === null) {
|
|
8
|
+
throw "'useBottomSheet' cannot be used out of the BottomSheet!";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return context;
|
|
12
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
type ViewProps,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
|
|
9
|
+
import { useBottomSheetInternal } from './useBottomSheetInternal';
|
|
10
|
+
|
|
11
|
+
export function useBottomSheetContentContainerStyle(
|
|
12
|
+
enableFooterMarginAdjustment: boolean,
|
|
13
|
+
_style?: ViewProps['style']
|
|
14
|
+
) {
|
|
15
|
+
const [footerHeight, setFooterHeight] = useState(0);
|
|
16
|
+
//#region hooks
|
|
17
|
+
const { animatedLayoutState } = useBottomSheetInternal();
|
|
18
|
+
//#endregion
|
|
19
|
+
|
|
20
|
+
//#region styles
|
|
21
|
+
const flattenStyle = useMemo<ViewStyle>(() => {
|
|
22
|
+
return !_style
|
|
23
|
+
? {}
|
|
24
|
+
: Array.isArray(_style)
|
|
25
|
+
? // @ts-ignore
|
|
26
|
+
(StyleSheet.compose(..._style) as ViewStyle)
|
|
27
|
+
: (_style as ViewStyle);
|
|
28
|
+
}, [_style]);
|
|
29
|
+
const style = useMemo<ViewProps['style']>(() => {
|
|
30
|
+
if (!enableFooterMarginAdjustment) {
|
|
31
|
+
return flattenStyle;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let currentBottomPadding = 0;
|
|
35
|
+
if (flattenStyle && typeof flattenStyle === 'object') {
|
|
36
|
+
const { paddingBottom, padding, paddingVertical } = flattenStyle;
|
|
37
|
+
if (paddingBottom !== undefined && typeof paddingBottom === 'number') {
|
|
38
|
+
currentBottomPadding = paddingBottom;
|
|
39
|
+
} else if (
|
|
40
|
+
paddingVertical !== undefined &&
|
|
41
|
+
typeof paddingVertical === 'number'
|
|
42
|
+
) {
|
|
43
|
+
currentBottomPadding = paddingVertical;
|
|
44
|
+
} else if (padding !== undefined && typeof padding === 'number') {
|
|
45
|
+
currentBottomPadding = padding;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [
|
|
50
|
+
flattenStyle,
|
|
51
|
+
{
|
|
52
|
+
paddingBottom: currentBottomPadding + footerHeight,
|
|
53
|
+
overflow: 'visible',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}, [footerHeight, enableFooterMarginAdjustment, flattenStyle]);
|
|
57
|
+
//#endregion
|
|
58
|
+
|
|
59
|
+
//#region effects
|
|
60
|
+
useAnimatedReaction(
|
|
61
|
+
() => animatedLayoutState.get().footerHeight,
|
|
62
|
+
(result, previousFooterHeight) => {
|
|
63
|
+
if (!enableFooterMarginAdjustment) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
runOnJS(setFooterHeight)(result);
|
|
67
|
+
|
|
68
|
+
if (Platform.OS === 'web') {
|
|
69
|
+
/**
|
|
70
|
+
* a reaction that will append the footer height to the content
|
|
71
|
+
* height if margin adjustment is true.
|
|
72
|
+
*
|
|
73
|
+
* This is needed due to the web layout the footer after the content.
|
|
74
|
+
*/
|
|
75
|
+
if (result && !previousFooterHeight) {
|
|
76
|
+
animatedLayoutState.modify(state => {
|
|
77
|
+
'worklet';
|
|
78
|
+
state.contentHeight = state.contentHeight + result;
|
|
79
|
+
return state;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[animatedLayoutState, enableFooterMarginAdjustment]
|
|
85
|
+
);
|
|
86
|
+
//#endregion
|
|
87
|
+
return style;
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { BottomSheetGestureHandlersContext } from '../contexts/gesture';
|
|
3
|
+
|
|
4
|
+
export const useBottomSheetGestureHandlers = () => {
|
|
5
|
+
const context = useContext(BottomSheetGestureHandlersContext);
|
|
6
|
+
|
|
7
|
+
if (context === null) {
|
|
8
|
+
throw "'useBottomSheetGestureHandlers' cannot be used out of the BottomSheet!";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return context;
|
|
12
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BottomSheetInternalContext,
|
|
4
|
+
type BottomSheetInternalContextType,
|
|
5
|
+
} from '../contexts/internal';
|
|
6
|
+
|
|
7
|
+
export function useBottomSheetInternal(
|
|
8
|
+
unsafe?: false
|
|
9
|
+
): BottomSheetInternalContextType;
|
|
10
|
+
|
|
11
|
+
export function useBottomSheetInternal(
|
|
12
|
+
unsafe: true
|
|
13
|
+
): BottomSheetInternalContextType | null;
|
|
14
|
+
|
|
15
|
+
export function useBottomSheetInternal(
|
|
16
|
+
unsafe?: boolean
|
|
17
|
+
): BottomSheetInternalContextType | null {
|
|
18
|
+
const context = useContext(BottomSheetInternalContext);
|
|
19
|
+
|
|
20
|
+
if (unsafe !== true && context === null) {
|
|
21
|
+
throw "'useBottomSheetInternal' cannot be used out of the BottomSheet!";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return context;
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { BottomSheetModalContext } from '../contexts';
|
|
3
|
+
|
|
4
|
+
export const useBottomSheetModal = () => {
|
|
5
|
+
const context = useContext(BottomSheetModalContext);
|
|
6
|
+
|
|
7
|
+
if (context === null) {
|
|
8
|
+
throw "'BottomSheetModalContext' cannot be null!";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return context;
|
|
12
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BottomSheetModalInternalContext,
|
|
4
|
+
type BottomSheetModalInternalContextType,
|
|
5
|
+
} from '../contexts';
|
|
6
|
+
|
|
7
|
+
export function useBottomSheetModalInternal(
|
|
8
|
+
unsafe?: false
|
|
9
|
+
): BottomSheetModalInternalContextType;
|
|
10
|
+
|
|
11
|
+
export function useBottomSheetModalInternal(
|
|
12
|
+
unsafe: true
|
|
13
|
+
): BottomSheetModalInternalContextType | null;
|
|
14
|
+
|
|
15
|
+
export function useBottomSheetModalInternal(
|
|
16
|
+
unsafe?: boolean
|
|
17
|
+
): BottomSheetModalInternalContextType | null {
|
|
18
|
+
const context = useContext(BottomSheetModalInternalContext);
|
|
19
|
+
|
|
20
|
+
if (unsafe !== true && context === null) {
|
|
21
|
+
throw "'BottomSheetModalInternalContext' cannot be null!";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return context;
|
|
25
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ReactElement, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type BottomSheetScrollableProps,
|
|
4
|
+
BottomSheetScrollView,
|
|
5
|
+
} from '../components/bottomSheetScrollable';
|
|
6
|
+
|
|
7
|
+
type BottomSheetScrollableCreatorConfigs = {} & BottomSheetScrollableProps;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A custom hook that creates a scrollable component for third-party libraries
|
|
11
|
+
* like `LegendList` or `FlashList` to integrate the interaction and scrolling
|
|
12
|
+
* behaviors with th BottomSheet component.
|
|
13
|
+
*
|
|
14
|
+
* @param configs - Configuration options for the scrollable creator.
|
|
15
|
+
* @param configs.focusHook - This needed when bottom sheet used with multiple scrollables to allow bottom sheet
|
|
16
|
+
* detect the current scrollable ref, especially when used with `React Navigation`.
|
|
17
|
+
* You will need to provide `useFocusEffect` from `@react-navigation/native`.
|
|
18
|
+
* @param configs.scrollEventsHandlersHook - Custom hook to provide scroll events handler, which will allow advance and
|
|
19
|
+
* customize handling for scrollables.
|
|
20
|
+
* @param configs.enableFooterMarginAdjustment - Adjust the scrollable bottom margin to avoid the animated footer.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const BottomSheetLegendListScrollable = useBottomSheetScrollableCreator();
|
|
25
|
+
*
|
|
26
|
+
* // Usage in JSX
|
|
27
|
+
* <LegendList
|
|
28
|
+
* renderScrollComponent={BottomSheetLegendListScrollable}
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: out of my control
|
|
33
|
+
export function useBottomSheetScrollableCreator<T = any>({
|
|
34
|
+
focusHook,
|
|
35
|
+
scrollEventsHandlersHook,
|
|
36
|
+
enableFooterMarginAdjustment,
|
|
37
|
+
}: BottomSheetScrollableCreatorConfigs = {}): (
|
|
38
|
+
props: T,
|
|
39
|
+
ref?: never
|
|
40
|
+
) => ReactElement<T> {
|
|
41
|
+
return useCallback(
|
|
42
|
+
function useBottomSheetScrollableCreator(
|
|
43
|
+
// @ts-expect-error
|
|
44
|
+
{ data: _, ...props }: T,
|
|
45
|
+
ref?: never
|
|
46
|
+
): ReactElement<T> {
|
|
47
|
+
return (
|
|
48
|
+
// @ts-expect-error
|
|
49
|
+
<BottomSheetScrollView
|
|
50
|
+
ref={ref}
|
|
51
|
+
{...props}
|
|
52
|
+
focusHook={focusHook}
|
|
53
|
+
scrollEventsHandlersHook={scrollEventsHandlersHook}
|
|
54
|
+
enableFooterMarginAdjustment={enableFooterMarginAdjustment}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
[focusHook, scrollEventsHandlersHook, enableFooterMarginAdjustment]
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WithSpringConfig } from 'react-native-reanimated';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate spring animation configs.
|
|
5
|
+
* @param configs overridable configs.
|
|
6
|
+
*/
|
|
7
|
+
export const useBottomSheetSpringConfigs = (
|
|
8
|
+
configs: Omit<WithSpringConfig, 'velocity'>
|
|
9
|
+
) => {
|
|
10
|
+
return configs;
|
|
11
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { EasingFunction } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
EasingFunctionFactory,
|
|
5
|
+
ReduceMotion,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
import { ANIMATION_DURATION, ANIMATION_EASING } from '../constants';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* this is needed to avoid TS4023
|
|
11
|
+
* https://github.com/microsoft/TypeScript/issues/5711
|
|
12
|
+
*/
|
|
13
|
+
interface TimingConfig {
|
|
14
|
+
duration?: number;
|
|
15
|
+
easing?: EasingFunction | EasingFunctionFactory;
|
|
16
|
+
reduceMotion?: ReduceMotion;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate timing animation configs.
|
|
21
|
+
* @default
|
|
22
|
+
* - easing: Easing.out(Easing.exp)
|
|
23
|
+
* - duration: 250
|
|
24
|
+
* @param configs overridable configs.
|
|
25
|
+
*/
|
|
26
|
+
export const useBottomSheetTimingConfigs = (configs: TimingConfig) => {
|
|
27
|
+
return useMemo(() => {
|
|
28
|
+
const _configs: TimingConfig = {
|
|
29
|
+
easing: configs.easing || ANIMATION_EASING,
|
|
30
|
+
duration: configs.duration || ANIMATION_DURATION,
|
|
31
|
+
reduceMotion: configs.reduceMotion,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return _configs;
|
|
35
|
+
}, [configs.duration, configs.easing, configs.reduceMotion]);
|
|
36
|
+
};
|