@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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/mock.js +231 -0
  4. package/package.json +107 -0
  5. package/src/components/bottomSheet/BottomSheet.tsx +1885 -0
  6. package/src/components/bottomSheet/BottomSheetBody.tsx +44 -0
  7. package/src/components/bottomSheet/BottomSheetContent.tsx +261 -0
  8. package/src/components/bottomSheet/constants.ts +58 -0
  9. package/src/components/bottomSheet/index.ts +2 -0
  10. package/src/components/bottomSheet/styles.ts +11 -0
  11. package/src/components/bottomSheet/types.d.ts +358 -0
  12. package/src/components/bottomSheetBackdrop/BottomSheetBackdrop.tsx +165 -0
  13. package/src/components/bottomSheetBackdrop/constants.ts +22 -0
  14. package/src/components/bottomSheetBackdrop/index.ts +2 -0
  15. package/src/components/bottomSheetBackdrop/styles.ts +8 -0
  16. package/src/components/bottomSheetBackdrop/types.d.ts +58 -0
  17. package/src/components/bottomSheetBackground/BottomSheetBackground.tsx +20 -0
  18. package/src/components/bottomSheetBackground/BottomSheetBackgroundContainer.tsx +35 -0
  19. package/src/components/bottomSheetBackground/index.ts +2 -0
  20. package/src/components/bottomSheetBackground/styles.ts +9 -0
  21. package/src/components/bottomSheetBackground/types.d.ts +12 -0
  22. package/src/components/bottomSheetDebugView/BottomSheetDebugView.tsx +26 -0
  23. package/src/components/bottomSheetDebugView/ReText.tsx +72 -0
  24. package/src/components/bottomSheetDebugView/ReText.webx.tsx +55 -0
  25. package/src/components/bottomSheetDebugView/index.ts +1 -0
  26. package/src/components/bottomSheetDebugView/styles.ts +19 -0
  27. package/src/components/bottomSheetDebugView/styles.web.ts +20 -0
  28. package/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx +123 -0
  29. package/src/components/bottomSheetDraggableView/index.ts +1 -0
  30. package/src/components/bottomSheetDraggableView/types.d.ts +9 -0
  31. package/src/components/bottomSheetFooter/BottomSheetFooter.tsx +119 -0
  32. package/src/components/bottomSheetFooter/BottomSheetFooterContainer.tsx +43 -0
  33. package/src/components/bottomSheetFooter/index.ts +3 -0
  34. package/src/components/bottomSheetFooter/styles.ts +12 -0
  35. package/src/components/bottomSheetFooter/types.d.ts +41 -0
  36. package/src/components/bottomSheetGestureHandlersProvider/BottomSheetGestureHandlersProvider.tsx +69 -0
  37. package/src/components/bottomSheetGestureHandlersProvider/index.ts +1 -0
  38. package/src/components/bottomSheetGestureHandlersProvider/types.d.ts +8 -0
  39. package/src/components/bottomSheetHandle/BottomSheetHandle.tsx +51 -0
  40. package/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx +187 -0
  41. package/src/components/bottomSheetHandle/constants.ts +12 -0
  42. package/src/components/bottomSheetHandle/index.ts +6 -0
  43. package/src/components/bottomSheetHandle/styles.ts +23 -0
  44. package/src/components/bottomSheetHandle/types.d.ts +52 -0
  45. package/src/components/bottomSheetHostingContainer/BottomSheetHostingContainer.tsx +130 -0
  46. package/src/components/bottomSheetHostingContainer/index.ts +2 -0
  47. package/src/components/bottomSheetHostingContainer/styles.ts +5 -0
  48. package/src/components/bottomSheetHostingContainer/styles.web.ts +11 -0
  49. package/src/components/bottomSheetHostingContainer/types.d.ts +17 -0
  50. package/src/components/bottomSheetModal/BottomSheetModal.tsx +482 -0
  51. package/src/components/bottomSheetModal/constants.ts +4 -0
  52. package/src/components/bottomSheetModal/index.ts +6 -0
  53. package/src/components/bottomSheetModal/types.d.ts +67 -0
  54. package/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx +211 -0
  55. package/src/components/bottomSheetModalProvider/index.ts +1 -0
  56. package/src/components/bottomSheetModalProvider/types.d.ts +12 -0
  57. package/src/components/bottomSheetRefreshControl/BottomSheetRefreshControl.android.tsx +84 -0
  58. package/src/components/bottomSheetRefreshControl/BottomSheetRefreshControl.tsx +1 -0
  59. package/src/components/bottomSheetRefreshControl/index.ts +20 -0
  60. package/src/components/bottomSheetScrollable/BottomSheetDraggableScrollable.tsx +23 -0
  61. package/src/components/bottomSheetScrollable/BottomSheetFlashList.tsx +88 -0
  62. package/src/components/bottomSheetScrollable/BottomSheetFlashList.web.tsx +1 -0
  63. package/src/components/bottomSheetScrollable/BottomSheetFlatList.tsx +26 -0
  64. package/src/components/bottomSheetScrollable/BottomSheetScrollView.tsx +27 -0
  65. package/src/components/bottomSheetScrollable/BottomSheetSectionList.tsx +29 -0
  66. package/src/components/bottomSheetScrollable/BottomSheetVirtualizedList.tsx +27 -0
  67. package/src/components/bottomSheetScrollable/ScrollableContainer.android.tsx +55 -0
  68. package/src/components/bottomSheetScrollable/ScrollableContainer.tsx +22 -0
  69. package/src/components/bottomSheetScrollable/ScrollableContainer.web.tsx +102 -0
  70. package/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx +153 -0
  71. package/src/components/bottomSheetScrollable/index.ts +15 -0
  72. package/src/components/bottomSheetScrollable/styles.ts +8 -0
  73. package/src/components/bottomSheetScrollable/types.d.ts +280 -0
  74. package/src/components/bottomSheetScrollable/useBottomSheetContentSizeSetter.ts +32 -0
  75. package/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx +127 -0
  76. package/src/components/bottomSheetTextInput/index.ts +2 -0
  77. package/src/components/bottomSheetTextInput/types.ts +3 -0
  78. package/src/components/bottomSheetView/BottomSheetView.tsx +93 -0
  79. package/src/components/bottomSheetView/index.ts +1 -0
  80. package/src/components/bottomSheetView/styles.ts +10 -0
  81. package/src/components/bottomSheetView/types.d.ts +24 -0
  82. package/src/components/touchables/Touchables.ios.tsx +5 -0
  83. package/src/components/touchables/Touchables.tsx +5 -0
  84. package/src/components/touchables/index.ts +20 -0
  85. package/src/constants.ts +159 -0
  86. package/src/contexts/external.ts +8 -0
  87. package/src/contexts/gesture.ts +13 -0
  88. package/src/contexts/index.ts +15 -0
  89. package/src/contexts/internal.ts +65 -0
  90. package/src/contexts/modal/external.ts +11 -0
  91. package/src/contexts/modal/internal.ts +25 -0
  92. package/src/hooks/index.ts +29 -0
  93. package/src/hooks/useAnimatedDetents.ts +119 -0
  94. package/src/hooks/useAnimatedKeyboard.ts +174 -0
  95. package/src/hooks/useAnimatedLayout.ts +109 -0
  96. package/src/hooks/useBottomSheet.ts +12 -0
  97. package/src/hooks/useBottomSheetContentContainerStyle.ts +88 -0
  98. package/src/hooks/useBottomSheetGestureHandlers.ts +12 -0
  99. package/src/hooks/useBottomSheetInternal.ts +25 -0
  100. package/src/hooks/useBottomSheetModal.ts +12 -0
  101. package/src/hooks/useBottomSheetModalInternal.ts +25 -0
  102. package/src/hooks/useBottomSheetScrollableCreator.tsx +60 -0
  103. package/src/hooks/useBottomSheetSpringConfigs.ts +11 -0
  104. package/src/hooks/useBottomSheetTimingConfigs.ts +36 -0
  105. package/src/hooks/useBoundingClientRect.ts +77 -0
  106. package/src/hooks/useGestureEventsHandlersDefault.tsx +436 -0
  107. package/src/hooks/useGestureEventsHandlersDefault.web.tsx +418 -0
  108. package/src/hooks/useGestureHandler.ts +90 -0
  109. package/src/hooks/usePropsValidator.ts +108 -0
  110. package/src/hooks/useReactiveSharedValue.ts +45 -0
  111. package/src/hooks/useScrollEventsHandlersDefault.ts +167 -0
  112. package/src/hooks/useScrollHandler.ts +72 -0
  113. package/src/hooks/useScrollHandler.web.ts +181 -0
  114. package/src/hooks/useScrollable.ts +131 -0
  115. package/src/hooks/useScrollableSetter.ts +56 -0
  116. package/src/hooks/useStableCallback.ts +26 -0
  117. package/src/index.ts +79 -0
  118. package/src/types.d.ts +336 -0
  119. package/src/utilities/animate.ts +56 -0
  120. package/src/utilities/clamp.ts +8 -0
  121. package/src/utilities/easingExp.ts +10 -0
  122. package/src/utilities/findNodeHandle.ts +1 -0
  123. package/src/utilities/findNodeHandle.web.ts +33 -0
  124. package/src/utilities/getKeyboardAnimationConfigs.ts +44 -0
  125. package/src/utilities/getRefNativeTag.web.ts +6 -0
  126. package/src/utilities/id.ts +6 -0
  127. package/src/utilities/index.ts +7 -0
  128. package/src/utilities/isFabricInstalled.ts +9 -0
  129. package/src/utilities/logger.ts +55 -0
  130. package/src/utilities/noop.ts +7 -0
  131. package/src/utilities/normalizeSnapPoint.ts +17 -0
  132. package/src/utilities/snapPoint.ts +11 -0
  133. 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
+ };