diff --git a/example/package.json b/example/package.json
index 0e246e4aa..8630dbe3c 100644
--- a/example/package.json
+++ b/example/package.json
@@ -14,6 +14,7 @@
"@gorhom/showcase-template": "^2.0.4",
"@react-native-community/blur": "^3.6.0",
"@react-native-community/masked-view": "0.1.11",
+ "@react-navigation/bottom-tabs": "^5.11.11",
"@react-navigation/material-top-tabs": "^5.3.15",
"@react-navigation/native": "^5.9.4",
"@react-navigation/stack": "^5.14.4",
diff --git a/example/src/Dev.tsx b/example/src/Dev.tsx
index 38c86a44d..cc6bcebe6 100644
--- a/example/src/Dev.tsx
+++ b/example/src/Dev.tsx
@@ -1,218 +1,164 @@
-/* eslint-disable no-console */
-/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useCallback, useMemo, useRef, useState } from 'react';
-import { View, StyleSheet, Dimensions, StatusBar } from 'react-native';
-import { createStackNavigator } from '@react-navigation/stack';
-import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
+import { StyleSheet, View } from 'react-native';
+import { DarkTheme, NavigationContainer } from '@react-navigation/native';
import {
- SafeAreaProvider,
- useSafeAreaInsets,
-} from 'react-native-safe-area-context';
-import BottomSheet from '@gorhom/bottom-sheet';
+ createBottomTabNavigator,
+ useBottomTabBarHeight,
+} from '@react-navigation/bottom-tabs';
+import {
+ BottomSheetFlatList,
+ BottomSheetModal,
+ BottomSheetModalProvider,
+} from '@gorhom/bottom-sheet';
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+} from 'react-native-reanimated';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { createContactListMockData } from './utilities';
+import ContactItem from './components/contactItem';
import SearchHandle from './components/searchHandle';
-import Button from './components/button';
-import ContactList from './components/contactList';
-import { NavigationContainer, useNavigation } from '@react-navigation/native';
+import { Button } from 'react-native';
+
+const SNAP_POINTS = [150, 300];
+const DATA = createContactListMockData(30);
-const { height: windowHeight } = Dimensions.get('window');
+const keyExtractor = (item: any, index: number) => `${item.name}.${index}`;
-const BasicExample = () => {
+const App = () => {
//#region state
- const shownHeader = useRef(true);
- const [dynamicSnapPoint, setDynamicSnapPoint] = useState(300);
+ const bottomSheetRef = useRef(null);
+ const [filter, setFilter] = useState('');
//#endregion
//#region hooks
- const { setOptions } = useNavigation();
- const bottomSheetRef = useRef(null);
- const { top: topSafeArea, bottom: bottomSafeArea } = useSafeAreaInsets();
+ const { top: topSafeArea } = useSafeAreaInsets();
+ const bottomSafeArea = useBottomTabBarHeight();
//#endregion
- //#region variables
- const snapPoints = useMemo(() => [150, 400, '100%'], []);
- const animatedPosition = useSharedValue(0);
- const animatedContainerHeight = useSharedValue(800);
+ //#region animated values
+ const data = useMemo(
+ () =>
+ filter === '' ? DATA : DATA.filter(item => item.name.includes(filter)),
+ [filter]
+ );
+ const animatedPosition = useSharedValue(0);
//#endregion
//#region styles
- const containerStyle = useMemo(
- () => ({
- ...styles.container,
- paddingTop: topSafeArea,
- }),
- [topSafeArea]
- );
-
- const firstSnapPointLineStyle = useMemo(
- () => [
- styles.line,
- {
- height: snapPoints[0],
- },
- ],
- [snapPoints]
- );
-
- const secondSnapPointLineStyle = useMemo(
- () => [
- styles.line,
- {
- height: snapPoints[1],
- },
- ],
- [snapPoints]
- );
-
- const safeBottomLineStyle = useMemo(
- () => [
- styles.line,
- {
- height: bottomSafeArea,
- },
- ],
- [bottomSafeArea]
- );
-
- const sheetLineAnimatedStyle = useAnimatedStyle(() => ({
- transform: [{ translateY: animatedPosition.value }],
+ const positionLineAnimatedStyle = useAnimatedStyle(() => ({
+ top: animatedPosition.value,
}));
- const sheetLineStyle = useMemo(
- () => [styles.sheetLine, sheetLineAnimatedStyle],
- [sheetLineAnimatedStyle]
- );
- //#endregion
-
- //#region callbacks
- const handleSheetChanges = useCallback((index: number) => {
- console.log('handleSheetChanges', index);
- }, []);
- const handleSnapPress = useCallback(index => {
- bottomSheetRef.current?.snapToIndex(index);
- }, []);
- const handleSnapPosition = useCallback(position => {
- bottomSheetRef.current?.snapToIndex(position);
- }, []);
- const handleClosePress = useCallback(() => {
- bottomSheetRef.current?.close();
- }, []);
- const handleIncreaseDynamicSnapPoint = useCallback(() => {
- setDynamicSnapPoint(state => state + 50);
- }, []);
- const handleHideHeaderPress = useCallback(() => {
- shownHeader.current = !shownHeader.current;
- setOptions({
- headerShown: shownHeader.current,
- });
- }, [setOptions]);
//#endregion
- // renders
- return (
-
- {/* */}
-
+
+ {SNAP_POINTS.map(snapPoint => (
+
+ ))}
+
+
+
+
);
+ //#endregion
};
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#555',
- padding: 24,
- },
- buttonContainer: {
- marginBottom: 6,
- },
- textInput: {
- backgroundColor: 'red',
- opacity: 1,
- padding: 6,
- margin: 6,
- borderRadius: 24,
+ justifyContent: 'center',
},
line: {
position: 'absolute',
left: 0,
right: 0,
- bottom: 0,
- borderWidth: 1,
- },
- sheetLine: {
- position: 'absolute',
- left: 0,
- right: 0,
- top: 0,
height: 1,
backgroundColor: 'red',
},
+ flatlist: {
+ flex: 1,
+ },
+ flatlistContainer: {
+ paddingHorizontal: 24,
+ },
});
-const Stack = createStackNavigator();
+const Tab = createBottomTabNavigator();
export default () => (
-
-
-
-
-
+
+
+
+
+
+
+
);
diff --git a/example/src/components/searchHandle/SearchHandle.tsx b/example/src/components/searchHandle/SearchHandle.tsx
index a86198065..0f7fdd10b 100644
--- a/example/src/components/searchHandle/SearchHandle.tsx
+++ b/example/src/components/searchHandle/SearchHandle.tsx
@@ -6,16 +6,26 @@ import {
NativeSyntheticEvent,
TextInputChangeEventData,
} from 'react-native';
-import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
+import {
+ BottomSheetTextInput,
+ BottomSheetHandleProps,
+} from '@gorhom/bottom-sheet';
import { useShowcaseTheme } from '@gorhom/showcase-template';
-import isEqual from 'lodash.isequal';
const { width: SCREEN_WIDTH } = Dimensions.get('screen');
export const SEARCH_HANDLE_HEIGHT = 69;
-const BottomSheetHandleComponent = () => {
+interface SearchHandleProps extends BottomSheetHandleProps {
+ initialValue?: string;
+ onChange?: (text: string) => void;
+}
+
+const SearchHandleComponent = ({
+ initialValue = '',
+ onChange,
+}: SearchHandleProps) => {
// state
- const [value, setValue] = useState('');
+ const [value, setValue] = useState(initialValue);
// hooks
const { colors } = useShowcaseTheme();
@@ -26,8 +36,12 @@ const BottomSheetHandleComponent = () => {
nativeEvent: { text },
}: NativeSyntheticEvent) => {
setValue(text);
+
+ if (onChange) {
+ onChange(text);
+ }
},
- []
+ [onChange]
);
// render
@@ -46,7 +60,7 @@ const BottomSheetHandleComponent = () => {
);
};
-const BottomSheetHandle = memo(BottomSheetHandleComponent, isEqual);
+const SearchHandle = memo(SearchHandleComponent);
export const styles = StyleSheet.create({
container: {
@@ -71,4 +85,4 @@ export const styles = StyleSheet.create({
},
});
-export default BottomSheetHandle;
+export default SearchHandle;
diff --git a/example/src/screens/advanced/KeyboardHandlingExample.tsx b/example/src/screens/advanced/KeyboardHandlingExample.tsx
index cc724b667..822dbb545 100644
--- a/example/src/screens/advanced/KeyboardHandlingExample.tsx
+++ b/example/src/screens/advanced/KeyboardHandlingExample.tsx
@@ -7,12 +7,10 @@ import ContactList from '../../components/contactList';
const KeyboardHandlingExample = () => {
// state
- const [keyboardBehavior, setKeyboardBehavior] = useState<
- 'none' | 'extend' | 'fullScreen' | 'interactive'
- >('none');
- const [keyboardBlurBehavior, setKeyboardBlurBehavior] = useState<
- 'none' | 'restore'
- >('none');
+ const [keyboardBehavior, setKeyboardBehavior] =
+ useState<'none' | 'extend' | 'fillParent' | 'interactive'>('none');
+ const [keyboardBlurBehavior, setKeyboardBlurBehavior] =
+ useState<'none' | 'restore'>('none');
// hooks
const bottomSheetRef = useRef(null);
@@ -27,8 +25,8 @@ const KeyboardHandlingExample = () => {
case 'none':
return 'extend';
case 'extend':
- return 'fullScreen';
- case 'fullScreen':
+ return 'fillParent';
+ case 'fillParent':
return 'interactive';
case 'interactive':
return 'none';
diff --git a/example/yarn.lock b/example/yarn.lock
index 82dc1fc24..24f2c47e1 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -1197,6 +1197,14 @@
resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-1.0.0.tgz#05bb0031533598f9458cf65a502b8df0eecae780"
integrity sha512-0jbp4RxjYopTsIdLl+/Fy2TiwVYHy4mgeu07DG4b/LyM0OS/+lPP5c9sbnt/AMlnF6qz2JRZpPpGw1eMNS6A4w==
+"@react-navigation/bottom-tabs@^5.11.11":
+ version "5.11.11"
+ resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.11.11.tgz#ad4dfee4316522d8c05b5a8ad460f597bddb9e3c"
+ integrity sha512-hThj6Vfw+ITzAVj5TgLEoxkVEcBD+gYeieWOe6FryBRgokgKNCzFQzqArJ5UCmNMxklNH0rstJfcdyHflLuPtw==
+ dependencies:
+ color "^3.1.3"
+ react-native-iphone-x-helper "^1.3.0"
+
"@react-navigation/core@^5.15.3":
version "5.15.3"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-5.15.3.tgz#dce7090bf3ea0d302993d742c706825e495b812e"
diff --git a/src/components/bottomSheet/BottomSheet.tsx b/src/components/bottomSheet/BottomSheet.tsx
index 9d4ecf9b5..a04e1cc45 100644
--- a/src/components/bottomSheet/BottomSheet.tsx
+++ b/src/components/bottomSheet/BottomSheet.tsx
@@ -6,7 +6,6 @@ import React, {
useImperativeHandle,
memo,
} from 'react';
-import { ViewStyle } from 'react-native';
import invariant from 'invariant';
import Animated, {
useAnimatedReaction,
@@ -38,7 +37,7 @@ import BottomSheetBackdropContainer from '../bottomSheetBackdropContainer';
import BottomSheetHandleContainer from '../bottomSheetHandleContainer';
import BottomSheetBackgroundContainer from '../bottomSheetBackgroundContainer';
import BottomSheetDraggableView from '../bottomSheetDraggableView';
-import BottomSheetDebugView from '../bottomSheetDebugView';
+// import BottomSheetDebugView from '../bottomSheetDebugView';
import {
GESTURE,
ANIMATION_STATE,
@@ -70,8 +69,10 @@ import {
INITIAL_POSITION,
INITIAL_SNAP_POINT,
DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE,
+ INITIAL_CONTAINER_OFFSET,
+ DEFAULT_ANIMATION_CONFIGS,
} from './constants';
-import type { ScrollableRef, BottomSheetMethods } from '../../types';
+import { ScrollableRef, BottomSheetMethods, Insets } from '../../types';
import type { BottomSheetProps } from './types';
import { styles } from './styles';
@@ -93,7 +94,7 @@ const BottomSheetComponent = forwardRef(
animationDuration:
_providedAnimationDuration = DEFAULT_ANIMATION_DURATION,
animationEasing: _providedAnimationEasing = DEFAULT_ANIMATION_EASING,
- animationConfigs: _providedAnimationConfigs,
+ animationConfigs: _providedAnimationConfigs = DEFAULT_ANIMATION_CONFIGS,
// configurations
index: _providedIndex = 0,
@@ -114,6 +115,7 @@ const BottomSheetComponent = forwardRef(
// layout
handleHeight: _providedHandleHeight,
containerHeight: _providedContainerHeight,
+ containerOffset: _providedContainerOffset,
topInset = 0,
bottomInset = 0,
@@ -162,7 +164,10 @@ const BottomSheetComponent = forwardRef(
return $modal
? _animatedContainerHeight.value - verticalInset
: _animatedContainerHeight.value;
- });
+ }, [$modal, topInset, bottomInset]);
+ const animatedContainerOffset = useReactiveSharedValue(
+ _providedContainerOffset ?? INITIAL_CONTAINER_OFFSET
+ ) as Animated.SharedValue;
const animatedHandleHeight = useReactiveSharedValue(
_providedHandleHeight ?? INITIAL_HANDLE_HEIGHT
);
@@ -176,11 +181,8 @@ const BottomSheetComponent = forwardRef(
const animatedLastSnapPoint = useDerivedValue(
() => animatedSnapPoints.value[animatedSnapPoints.value.length - 1]
);
- const animatedContentHeight = useDerivedValue(
- () =>
- animatedContainerHeight.value -
- animatedLastSnapPoint.value -
- animatedHandleHeight.value
+ const animatedSheetHeight = useDerivedValue(
+ () => animatedContainerHeight.value - animatedLastSnapPoint.value
);
const animatedCurrentIndex = useReactiveSharedValue(
animateOnMount ? -1 : _providedIndex
@@ -251,6 +253,13 @@ const BottomSheetComponent = forwardRef(
animationEasing: keyboardAnimationEasing,
shouldHandleKeyboardEvents,
} = useKeyboard();
+ const getKeyboardHeightInContainer = useWorkletCallback(() => {
+ 'worklet';
+ return $modal
+ ? keyboardHeight.value -
+ Math.abs(bottomInset - animatedContainerOffset.value.bottom)
+ : keyboardHeight.value;
+ }, [$modal, bottomInset]);
//#endregion
//#region state/dynamic variables
@@ -258,80 +267,136 @@ const BottomSheetComponent = forwardRef(
const isAnimatedOnMount = useSharedValue(false);
const animatedAnimationState = useSharedValue(ANIMATION_STATE.UNDETERMINED);
const animatedSheetState = useDerivedValue(() => {
- const extendedSheetPosition =
- animatedContainerHeight.value -
- animatedHandleHeight.value -
- animatedContentHeight.value;
- const extendedSheetWithKeyboardPosition =
+ // closed position = position >= container height
+ if (animatedPosition.value >= animatedContainerHeight.value)
+ return SHEET_STATE.CLOSED;
+
+ // extended position = container height - sheet height
+ const extendedPosition =
+ animatedContainerHeight.value - animatedSheetHeight.value;
+ if (animatedPosition.value === extendedPosition)
+ return SHEET_STATE.EXTENDED;
+
+ // extended position with keyboard =
+ // container height - (sheet height + keyboard height in root container)
+ const keyboardHeightInContainer = getKeyboardHeightInContainer();
+ const extendedPositionWithKeyboard = Math.max(
+ 0,
animatedContainerHeight.value -
- animatedHandleHeight.value -
- animatedContentHeight.value -
- keyboardHeight.value;
+ (animatedSheetHeight.value + keyboardHeightInContainer)
+ );
- if (animatedPosition.value >= animatedContainerHeight.value) {
- return SHEET_STATE.CLOSED;
- } else if (
- animatedPosition.value === extendedSheetPosition ||
- (keyboardBehavior === KEYBOARD_BEHAVIOR.interactive &&
- isInTemporaryPosition.value &&
- animatedPosition.value === extendedSheetWithKeyboardPosition)
+ // detect if keyboard is open and the sheet is in temporary position
+ if (
+ keyboardBehavior === KEYBOARD_BEHAVIOR.interactive &&
+ isInTemporaryPosition.value &&
+ animatedPosition.value === extendedPositionWithKeyboard
) {
return SHEET_STATE.EXTENDED;
- } else if (animatedPosition.value === topInset) {
- return SHEET_STATE.FULL_SCREEN;
- } else if (animatedPosition.value < extendedSheetPosition) {
+ }
+
+ // fill parent = 0
+ if (animatedPosition.value === 0) {
+ return SHEET_STATE.FILL_PARENT;
+ }
+
+ // detect if position is below extended point
+ if (animatedPosition.value < extendedPosition) {
return SHEET_STATE.OVER_EXTENDED;
}
return SHEET_STATE.OPENED;
});
const animatedScrollableState = useDerivedValue(() => {
- return animatedSheetState.value === SHEET_STATE.FULL_SCREEN ||
- animatedSheetState.value === SHEET_STATE.EXTENDED
- ? SCROLLABLE_STATE.UNLOCKED
- : SCROLLABLE_STATE.LOCKED;
+ /**
+ * if sheet state is fill parent, then unlock scrolling
+ */
+ if (animatedSheetState.value === SHEET_STATE.FILL_PARENT) {
+ return SCROLLABLE_STATE.UNLOCKED;
+ }
+
+ /**
+ * if sheet state is extended, then unlock scrolling
+ */
+ if (animatedSheetState.value === SHEET_STATE.EXTENDED) {
+ return SCROLLABLE_STATE.UNLOCKED;
+ }
+
+ /**
+ * if keyboard is shown and sheet is animating
+ * then we do not lock the scrolling to not lose
+ * current scrollable scroll position.
+ */
+ if (
+ keyboardState.value === KEYBOARD_STATE.SHOWN &&
+ animatedAnimationState.value === ANIMATION_STATE.RUNNING
+ ) {
+ return SCROLLABLE_STATE.UNLOCKED;
+ }
+
+ return SCROLLABLE_STATE.LOCKED;
});
// dynamic
- const animatedSheetHeight = useDerivedValue(() => {
+ const animatedContentHeight = useDerivedValue(() => {
+ const contentHeight =
+ animatedSheetHeight.value - animatedHandleHeight.value;
+ const keyboardHeightInContainer = getKeyboardHeightInContainer();
+
if (
- keyboardBehavior === KEYBOARD_BEHAVIOR.none ||
- keyboardBehavior === KEYBOARD_BEHAVIOR.extend
+ (keyboardBehavior === KEYBOARD_BEHAVIOR.none ||
+ keyboardBehavior === KEYBOARD_BEHAVIOR.extend) &&
+ keyboardState.value === KEYBOARD_STATE.SHOWN
) {
- return animatedContentHeight.value;
+ return contentHeight - keyboardHeightInContainer;
}
- if (keyboardBehavior === KEYBOARD_BEHAVIOR.fullScreen) {
- return isInTemporaryPosition.value
- ? animatedContainerHeight.value -
- topInset -
- animatedHandleHeight.value
- : animatedContentHeight.value;
+ if (
+ keyboardBehavior === KEYBOARD_BEHAVIOR.fillParent &&
+ isInTemporaryPosition.value
+ ) {
+ if (keyboardState.value === KEYBOARD_STATE.SHOWN) {
+ return (
+ animatedContainerHeight.value -
+ animatedHandleHeight.value -
+ keyboardHeightInContainer
+ );
+ }
+ return animatedContainerHeight.value - animatedHandleHeight.value;
}
if (
keyboardBehavior === KEYBOARD_BEHAVIOR.interactive &&
isInTemporaryPosition.value
) {
- const safeFullScreenSheetHeight =
- animatedContainerHeight.value - topInset - animatedHandleHeight.value;
- const sheetWithKeyboardHeight =
- animatedContentHeight.value + keyboardHeight.value;
-
if (keyboardState.value === KEYBOARD_STATE.SHOWN) {
- if (animatedContentHeight.value >= safeFullScreenSheetHeight) {
- return safeFullScreenSheetHeight - keyboardHeight.value;
+ if (
+ keyboardHeightInContainer + animatedSheetHeight.value >
+ animatedContainerHeight.value
+ ) {
+ return (
+ animatedContainerHeight.value -
+ keyboardHeightInContainer -
+ animatedHandleHeight.value
+ );
}
- return animatedContentHeight.value;
+
+ return contentHeight;
}
- if (sheetWithKeyboardHeight > safeFullScreenSheetHeight) {
- return safeFullScreenSheetHeight;
+ const contentWithKeyboardHeight =
+ contentHeight + keyboardHeightInContainer;
+
+ if (
+ contentWithKeyboardHeight + animatedHandleHeight.value >
+ animatedContainerHeight.value
+ ) {
+ return animatedContainerHeight.value - animatedHandleHeight.value;
}
- return sheetWithKeyboardHeight;
+ return contentWithKeyboardHeight;
}
- return animatedContentHeight.value;
+ return contentHeight;
});
const animatedIndex = useDerivedValue(() => {
const adjustedSnapPoints = animatedSnapPoints.value.slice().reverse();
@@ -411,7 +476,7 @@ const BottomSheetComponent = forwardRef(
return;
}
const snapPoints = animatedSnapPoints.value;
- const toIndex = snapPoints.findIndex(item => item === toPoint);
+ const toIndex = snapPoints.indexOf(toPoint);
if (toIndex !== animatedCurrentIndex.value) {
_providedOnAnimate(animatedCurrentIndex.value, toIndex);
}
@@ -460,7 +525,7 @@ const BottomSheetComponent = forwardRef(
/**
* fire `onAnimate` callback
*/
- // runOnJS(handleOnAnimate)(position);
+ runOnJS(handleOnAnimate)(position);
/**
* force animation configs from parameters, if provided
@@ -729,6 +794,7 @@ const BottomSheetComponent = forwardRef(
animatedIndex,
animatedPosition,
scrollableContentOffsetY,
+ isInTemporaryPosition,
shouldHandleKeyboardEvents,
simultaneousHandlers: _providedSimultaneousHandlers,
waitFor: _providedWaitFor,
@@ -751,6 +817,7 @@ const BottomSheetComponent = forwardRef(
shouldHandleKeyboardEvents,
animatedScrollableState,
scrollableContentOffsetY,
+ isInTemporaryPosition,
enableContentPanningGesture,
_providedSimultaneousHandlers,
_providedWaitFor,
@@ -791,7 +858,7 @@ const BottomSheetComponent = forwardRef(
[_providedStyle, containerAnimatedStyle]
);
const contentContainerAnimatedStyle = useAnimatedStyle(() => ({
- height: animatedSheetHeight.value,
+ height: animate(animatedContentHeight.value, _providedAnimationConfigs),
}));
const contentContainerStyle = useMemo(
() => [styles.contentContainer, contentContainerAnimatedStyle],
@@ -802,12 +869,12 @@ const BottomSheetComponent = forwardRef(
* the bottom of the screen, when sheet being over dragged or
* when the sheet is resized.
*/
- const contentMaskContainerStyle = useMemo(
- () => ({
- ...styles.contentMaskContainer,
- paddingBottom: animatedContentHeight.value,
- }),
- [animatedContentHeight]
+ const contentMaskContainerAnimatedStyle = useAnimatedStyle(() => ({
+ paddingBottom: animatedContainerHeight.value,
+ }));
+ const contentMaskContainerStyle = useMemo(
+ () => [styles.contentMaskContainer, contentMaskContainerAnimatedStyle],
+ [contentMaskContainerAnimatedStyle]
);
//#endregion
@@ -870,7 +937,7 @@ const BottomSheetComponent = forwardRef(
*/
useAnimatedReaction(
() => animatedSnapPoints.value,
- _animatedSnapPoints => {
+ (_animatedSnapPoints, _previousAnimatedSnapPoints) => {
if (
!isLayoutCalculated.value ||
!isAnimatedOnMount.value ||
@@ -909,6 +976,7 @@ const BottomSheetComponent = forwardRef(
}
const snapPoints = animatedSnapPoints.value;
+ const keyboardHeightInContainer = getKeyboardHeightInContainer();
let animationConfigs = getKeyboardAnimationConfigs(
keyboardAnimationEasing.value,
keyboardAnimationDuration.value
@@ -944,11 +1012,11 @@ const BottomSheetComponent = forwardRef(
* Handle full screen behavior
*/
if (
- keyboardBehavior === KEYBOARD_BEHAVIOR.fullScreen &&
+ keyboardBehavior === KEYBOARD_BEHAVIOR.fillParent &&
_keyboardState === KEYBOARD_STATE.SHOWN
) {
isInTemporaryPosition.value = true;
- animateToPosition(topInset, 0, animationConfigs);
+ animateToPosition(0, 0, animationConfigs);
return;
}
@@ -962,7 +1030,7 @@ const BottomSheetComponent = forwardRef(
isInTemporaryPosition.value = true;
const newSnapPoint = snapPoints[snapPoints.length - 1];
animateToPosition(
- Math.max(topInset, newSnapPoint - keyboardHeight.value),
+ Math.max(0, newSnapPoint - keyboardHeightInContainer),
0,
animationConfigs
);
@@ -1043,6 +1111,8 @@ const BottomSheetComponent = forwardRef(
component: BottomSheet.name,
method: 'render',
params: {
+ topInset,
+ bottomInset,
animatedSnapPoints: animatedSnapPoints.value,
},
});
@@ -1058,6 +1128,7 @@ const BottomSheetComponent = forwardRef(
key="BottomSheetContainer"
shouldCalculateHeight={!$modal}
containerHeight={_animatedContainerHeight}
+ containerOffset={animatedContainerOffset}
topInset={topInset}
bottomInset={bottomInset}
>
@@ -1102,19 +1173,27 @@ const BottomSheetComponent = forwardRef(
/>
-
+ /> */}
);
diff --git a/src/components/bottomSheet/constants.ts b/src/components/bottomSheet/constants.ts
index 9418231a0..7e7042e83 100644
--- a/src/components/bottomSheet/constants.ts
+++ b/src/components/bottomSheet/constants.ts
@@ -9,6 +9,15 @@ import { exp } from '../../utilities/easingExp';
// default values
const DEFAULT_ANIMATION_EASING: Animated.EasingFunction = Easing.out(exp);
const DEFAULT_ANIMATION_DURATION = 500;
+const DEFAULT_ANIMATION_CONFIGS: Animated.WithSpringConfig = {
+ damping: 500,
+ stiffness: 1000,
+ mass: 3,
+ overshootClamping: true,
+ restDisplacementThreshold: 10,
+ restSpeedThreshold: 10,
+};
+
const DEFAULT_HANDLE_HEIGHT = 24;
const DEFAULT_OVER_DRAG_RESISTANCE_FACTOR = 2.5;
const DEFAULT_ENABLE_CONTENT_PANNING_GESTURE = true;
@@ -23,10 +32,17 @@ const DEFAULT_KEYBOARD_BLUR_BEHAVIOR = KEYBOARD_BLUR_BEHAVIOR.none;
// initial values
const INITIAL_SNAP_POINT = -999;
const INITIAL_CONTAINER_HEIGHT = -999;
+const INITIAL_CONTAINER_OFFSET = {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+};
const INITIAL_HANDLE_HEIGHT = -999;
const INITIAL_POSITION = WINDOW_HEIGHT;
export {
+ DEFAULT_ANIMATION_CONFIGS,
DEFAULT_ANIMATION_EASING,
DEFAULT_ANIMATION_DURATION,
DEFAULT_HANDLE_HEIGHT,
@@ -42,6 +58,7 @@ export {
// initial
INITIAL_POSITION,
INITIAL_CONTAINER_HEIGHT,
+ INITIAL_CONTAINER_OFFSET,
INITIAL_HANDLE_HEIGHT,
INITIAL_SNAP_POINT,
};
diff --git a/src/components/bottomSheet/types.d.ts b/src/components/bottomSheet/types.d.ts
index 25979bcec..946d7d6eb 100644
--- a/src/components/bottomSheet/types.d.ts
+++ b/src/components/bottomSheet/types.d.ts
@@ -1,5 +1,5 @@
import type React from 'react';
-import type { ViewStyle } from 'react-native';
+import type { ViewStyle, Insets } from 'react-native';
import type Animated from 'react-native-reanimated';
import type { PanGestureHandlerProps } from 'react-native-gesture-handler';
import type { BottomSheetHandleProps } from '../bottomSheetHandle';
@@ -99,6 +99,11 @@ export interface BottomSheetProps
* @type number | Animated.SharedValue;
*/
containerHeight?: number | Animated.SharedValue;
+ /**
+ * Container offset helps to accurately detect container offsets.
+ * @type Animated.SharedValue;
+ */
+ containerOffset?: Animated.SharedValue>;
/**
* Top inset value helps to calculate percentage snap points values,
* usually comes from `@react-navigation/stack` hook `useHeaderHeight` or
@@ -121,7 +126,7 @@ export interface BottomSheetProps
* Defines the keyboard appearance behavior.
* - `none`: do nothing.
* - `extend`: extend the sheet to its maximum snap point.
- * - `fullScreen`: extend the sheet to full screen.
+ * - `fillParent`: extend the sheet to fill parent.
* - `interactive`: offset the sheet by the size of the keyboard.
* @type `none` | `extend` | `interactive`
* @default none
diff --git a/src/components/bottomSheetContainer/BottomSheetContainer.tsx b/src/components/bottomSheetContainer/BottomSheetContainer.tsx
index 95daf1707..aa729c38b 100644
--- a/src/components/bottomSheetContainer/BottomSheetContainer.tsx
+++ b/src/components/bottomSheetContainer/BottomSheetContainer.tsx
@@ -1,16 +1,19 @@
-import React, { memo, useCallback, useMemo } from 'react';
-import { LayoutChangeEvent, View } from 'react-native';
+import React, { memo, useCallback, useMemo, useRef } from 'react';
+import { LayoutChangeEvent, View, StatusBar } from 'react-native';
+import { WINDOW_HEIGHT } from '../../constants';
import { print } from '../../utilities';
import { styles } from './styles';
import type { BottomSheetContainerProps } from './types';
function BottomSheetContainerComponent({
containerHeight,
+ containerOffset,
topInset = 0,
bottomInset = 0,
shouldCalculateHeight = true,
children,
}: BottomSheetContainerProps) {
+ const containerRef = useRef(null);
//#region styles
const containerStyle = useMemo(
() => [
@@ -31,10 +34,22 @@ function BottomSheetContainerComponent({
layout: { height },
},
}: LayoutChangeEvent) {
- if (height === containerHeight.value) {
- return;
+ if (height !== containerHeight.value) {
+ containerHeight.value = height;
}
- containerHeight.value = height;
+
+ containerRef.current?.measure(
+ (_x, _y, _width, _height, _pageX, pageY) => {
+ containerOffset.value = {
+ top: pageY,
+ left: 0,
+ right: 0,
+ bottom:
+ WINDOW_HEIGHT - (pageY + height + (StatusBar.currentHeight ?? 0)),
+ };
+ }
+ );
+
print({
component: BottomSheetContainer.displayName,
method: 'handleContainerLayout',
@@ -43,13 +58,14 @@ function BottomSheetContainerComponent({
},
});
},
- [containerHeight]
+ [containerHeight, containerOffset, containerRef]
);
//#endregion
//#region render
return (
;
+ containerOffset: Animated.SharedValue;
topInset?: number;
bottomInset?: number;
shouldCalculateHeight?: boolean;
diff --git a/src/components/bottomSheetDebugView/styles.ts b/src/components/bottomSheetDebugView/styles.ts
index 282bcd29a..2bf315750 100644
--- a/src/components/bottomSheetDebugView/styles.ts
+++ b/src/components/bottomSheetDebugView/styles.ts
@@ -4,7 +4,7 @@ export const styles = StyleSheet.create({
container: {
position: 'absolute',
right: 4,
- top: 200,
+ top: 0,
padding: 2,
backgroundColor: 'rgba(0, 0,0,0.75)',
},
diff --git a/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx b/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx
index f509a2cd0..afc3a0990 100644
--- a/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx
+++ b/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx
@@ -45,9 +45,10 @@ function BottomSheetHandleContainerComponent({
return refs;
}, [_providedSimultaneousHandlers, _internalSimultaneousHandlers]);
- const shouldRenderHandle = useMemo(() => _providedHandleComponent !== null, [
- _providedHandleComponent,
- ]);
+ const shouldRenderHandle = useMemo(
+ () => _providedHandleComponent !== null,
+ [_providedHandleComponent]
+ );
//#endregion
//#region callbacks
diff --git a/src/components/bottomSheetModal/BottomSheetModal.tsx b/src/components/bottomSheetModal/BottomSheetModal.tsx
index 67a7dbe3d..18d5f0fb4 100644
--- a/src/components/bottomSheetModal/BottomSheetModal.tsx
+++ b/src/components/bottomSheetModal/BottomSheetModal.tsx
@@ -48,8 +48,13 @@ const BottomSheetModalComponent = forwardRef<
//#endregion
//#region hooks
- const { containerHeight, mountSheet, unmountSheet, willUnmountSheet } =
- useBottomSheetModalInternal();
+ const {
+ containerHeight,
+ containerOffset,
+ mountSheet,
+ unmountSheet,
+ willUnmountSheet,
+ } = useBottomSheetModalInternal();
const { removePortal: unmountPortal } = usePortal();
//#endregion
@@ -323,6 +328,7 @@ const BottomSheetModalComponent = forwardRef<
enablePanDownToClose={enablePanDownToClose}
animateOnMount={true}
containerHeight={containerHeight}
+ containerOffset={containerOffset}
onChange={handleBottomSheetOnChange}
children={children}
$modal={true}
diff --git a/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx b/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx
index ba9f21109..ae5e35270 100644
--- a/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx
+++ b/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx
@@ -7,7 +7,10 @@ import {
} from '../../contexts';
import BottomSheetContainer from '../bottomSheetContainer';
import { MODAL_STACK_BEHAVIOR } from '../../constants';
-import { INITIAL_CONTAINER_HEIGHT } from '../bottomSheet/constants';
+import {
+ INITIAL_CONTAINER_HEIGHT,
+ INITIAL_CONTAINER_OFFSET,
+} from '../bottomSheet/constants';
import type { BottomSheetModalStackBehavior } from '../bottomSheetModal';
import type {
BottomSheetModalProviderProps,
@@ -19,6 +22,7 @@ const BottomSheetModalProviderWrapper = ({
}: BottomSheetModalProviderProps) => {
//#region layout variables
const animatedContainerHeight = useSharedValue(INITIAL_CONTAINER_HEIGHT);
+ const animatedContainerOffset = useSharedValue(INITIAL_CONTAINER_OFFSET);
//#endregion
//#region variables
@@ -157,12 +161,14 @@ const BottomSheetModalProviderWrapper = ({
const internalContextVariables = useMemo(
() => ({
containerHeight: animatedContainerHeight,
+ containerOffset: animatedContainerOffset,
mountSheet: handleMountSheet,
unmountSheet: handleUnmountSheet,
willUnmountSheet: handleWillUnmountSheet,
}),
[
animatedContainerHeight,
+ animatedContainerOffset,
handleMountSheet,
handleUnmountSheet,
handleWillUnmountSheet,
@@ -175,6 +181,7 @@ const BottomSheetModalProviderWrapper = ({
diff --git a/src/constants.ts b/src/constants.ts
index f34066550..7fad438e4 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -13,7 +13,7 @@ enum SHEET_STATE {
OPENED,
EXTENDED,
OVER_EXTENDED,
- FULL_SCREEN,
+ FILL_PARENT,
}
enum SCROLLABLE_STATE {
@@ -55,7 +55,7 @@ const MODAL_STACK_BEHAVIOR = {
const KEYBOARD_BEHAVIOR = {
none: 'none',
extend: 'extend',
- fullScreen: 'fullScreen',
+ fillParent: 'fillParent',
interactive: 'interactive',
} as const;
diff --git a/src/contexts/external.ts b/src/contexts/external.ts
index f06fee451..8f70eae2f 100644
--- a/src/contexts/external.ts
+++ b/src/contexts/external.ts
@@ -1,7 +1,7 @@
import { createContext } from 'react';
import type { BottomSheetMethods } from '../types';
-// @ts-ignore
-export const BottomSheetContext = createContext();
+export const BottomSheetContext =
+ createContext(null);
export const BottomSheetProvider = BottomSheetContext.Provider;
diff --git a/src/contexts/internal.ts b/src/contexts/internal.ts
index 15a61fb11..28261ca2b 100644
--- a/src/contexts/internal.ts
+++ b/src/contexts/internal.ts
@@ -41,7 +41,7 @@ export interface BottomSheetInternalContextType
removeScrollableRef: (ref: RefObject) => void;
}
-// @ts-ignore
-export const BottomSheetInternalContext = createContext();
+export const BottomSheetInternalContext =
+ createContext(null);
export const BottomSheetInternalProvider = BottomSheetInternalContext.Provider;
diff --git a/src/contexts/modal/external.ts b/src/contexts/modal/external.ts
index cbd948293..8f0d4582b 100644
--- a/src/contexts/modal/external.ts
+++ b/src/contexts/modal/external.ts
@@ -5,7 +5,7 @@ export interface BottomSheetModalContextType {
dismissAll: () => void;
}
-// @ts-ignore
-export const BottomSheetModalContext = createContext();
+export const BottomSheetModalContext =
+ createContext(null);
export const BottomSheetModalProvider = BottomSheetModalContext.Provider;
diff --git a/src/contexts/modal/internal.ts b/src/contexts/modal/internal.ts
index 2c73ab642..c6f73d605 100644
--- a/src/contexts/modal/internal.ts
+++ b/src/contexts/modal/internal.ts
@@ -1,10 +1,12 @@
import { createContext, Ref } from 'react';
+import type { Insets } from 'react-native';
import type Animated from 'react-native-reanimated';
import type BottomSheet from '../../components/bottomSheet';
import type { BottomSheetModalStackBehavior } from '../../components/bottomSheetModal';
export interface BottomSheetModalInternalContextType {
containerHeight: Animated.SharedValue;
+ containerOffset: Animated.SharedValue>;
mountSheet: (
key: string,
ref: Ref,
@@ -14,8 +16,8 @@ export interface BottomSheetModalInternalContextType {
willUnmountSheet: (key: string) => void;
}
-// @ts-ignore
-export const BottomSheetModalInternalContext = createContext();
+export const BottomSheetModalInternalContext =
+ createContext(null);
export const BottomSheetModalInternalProvider =
BottomSheetModalInternalContext.Provider;
diff --git a/src/hooks/useBottomSheet.ts b/src/hooks/useBottomSheet.ts
index 7b41449b8..a24fe548a 100644
--- a/src/hooks/useBottomSheet.ts
+++ b/src/hooks/useBottomSheet.ts
@@ -2,5 +2,11 @@ import { useContext } from 'react';
import { BottomSheetContext } from '../contexts/external';
export const useBottomSheet = () => {
- return useContext(BottomSheetContext);
+ const context = useContext(BottomSheetContext);
+
+ if (context === null) {
+ throw "'BottomSheetContext' cannot be null!";
+ }
+
+ return context;
};
diff --git a/src/hooks/useBottomSheetInternal.ts b/src/hooks/useBottomSheetInternal.ts
index 647f9aad2..e849df538 100644
--- a/src/hooks/useBottomSheetInternal.ts
+++ b/src/hooks/useBottomSheetInternal.ts
@@ -2,5 +2,11 @@ import { useContext } from 'react';
import { BottomSheetInternalContext } from '../contexts/internal';
export const useBottomSheetInternal = () => {
- return useContext(BottomSheetInternalContext);
+ const context = useContext(BottomSheetInternalContext);
+
+ if (context === null) {
+ throw "'BottomSheetInternalContext' cannot be null!";
+ }
+
+ return context;
};
diff --git a/src/hooks/useBottomSheetModal.ts b/src/hooks/useBottomSheetModal.ts
index 13bb83289..59467802a 100644
--- a/src/hooks/useBottomSheetModal.ts
+++ b/src/hooks/useBottomSheetModal.ts
@@ -1,4 +1,12 @@
import { useContext } from 'react';
import { BottomSheetModalContext } from '../contexts';
-export const useBottomSheetModal = () => useContext(BottomSheetModalContext);
+export const useBottomSheetModal = () => {
+ const context = useContext(BottomSheetModalContext);
+
+ if (context === null) {
+ throw "'BottomSheetModalContext' cannot be null!";
+ }
+
+ return context;
+};
diff --git a/src/hooks/useBottomSheetModalInternal.ts b/src/hooks/useBottomSheetModalInternal.ts
index 76bd73996..03d88832b 100644
--- a/src/hooks/useBottomSheetModalInternal.ts
+++ b/src/hooks/useBottomSheetModalInternal.ts
@@ -1,5 +1,12 @@
import { useContext } from 'react';
import { BottomSheetModalInternalContext } from '../contexts';
-export const useBottomSheetModalInternal = () =>
- useContext(BottomSheetModalInternalContext);
+export const useBottomSheetModalInternal = () => {
+ const context = useContext(BottomSheetModalInternalContext);
+
+ if (context === null) {
+ throw "'BottomSheetModalInternalContext' cannot be null!";
+ }
+
+ return context;
+};
diff --git a/src/hooks/useInteractivePanGestureHandler.ts b/src/hooks/useInteractivePanGestureHandler.ts
index d22b0bb82..39fa959b0 100644
--- a/src/hooks/useInteractivePanGestureHandler.ts
+++ b/src/hooks/useInteractivePanGestureHandler.ts
@@ -78,7 +78,7 @@ export const useInteractivePanGestureHandler = ({
if (
keyboardState.value === KEYBOARD_STATE.SHOWN &&
(keyboardBehavior === KEYBOARD_BEHAVIOR.interactive ||
- keyboardBehavior === KEYBOARD_BEHAVIOR.fullScreen)
+ keyboardBehavior === KEYBOARD_BEHAVIOR.fillParent)
) {
isInTemporaryPosition.value = true;
}
@@ -181,11 +181,11 @@ export const useInteractivePanGestureHandler = ({
gestureState.value = state;
/**
- * if
+ * if the sheet is in a temporary position and the gesture ended above
+ * the current position, then we snap back to the temporary position.
*/
if (
isInTemporaryPosition.value &&
- context.keyboardState === KEYBOARD_STATE.SHOWN &&
context.startPosition >= animatedPosition.value
) {
if (context.startPosition > animatedPosition.value) {
@@ -194,6 +194,17 @@ export const useInteractivePanGestureHandler = ({
return;
}
+ /**
+ * close keyboard if current position is below the recorded
+ * start position and keyboard still shown.
+ */
+ if (
+ animatedPosition.value > context.startPosition &&
+ context.keyboardState === KEYBOARD_STATE.SHOWN
+ ) {
+ runOnJS(Keyboard.dismiss)();
+ }
+
if (isInTemporaryPosition.value) {
isInTemporaryPosition.value = false;
}
diff --git a/src/hooks/useKeyboard.ts b/src/hooks/useKeyboard.ts
index 0b23329b1..b07eb79ff 100644
--- a/src/hooks/useKeyboard.ts
+++ b/src/hooks/useKeyboard.ts
@@ -33,9 +33,8 @@ export const useKeyboard = () => {
KEYBOARD_STATE.UNDETERMINED
);
const keyboardHeight = useSharedValue(0);
- const keyboardAnimationEasing = useSharedValue(
- 'keyboard'
- );
+ const keyboardAnimationEasing =
+ useSharedValue('keyboard');
const keyboardAnimationDuration = useSharedValue(500);
//#endregion
@@ -45,7 +44,6 @@ export const useKeyboard = () => {
if (state === KEYBOARD_STATE.SHOWN && !shouldHandleKeyboardEvents.value) {
return;
}
- keyboardState.value = state;
keyboardHeight.value =
state === KEYBOARD_STATE.SHOWN
? height
@@ -54,6 +52,7 @@ export const useKeyboard = () => {
: height;
keyboardAnimationDuration.value = duration;
keyboardAnimationEasing.value = easing;
+ keyboardState.value = state;
},
[]
);
diff --git a/src/hooks/useReactiveSharedValue.ts b/src/hooks/useReactiveSharedValue.ts
index 4aa6f3400..4291b0976 100644
--- a/src/hooks/useReactiveSharedValue.ts
+++ b/src/hooks/useReactiveSharedValue.ts
@@ -12,13 +12,24 @@ export const useReactiveSharedValue = (
const valueRef = useRef>(null);
if (typeof value === 'object' && 'value' in value) {
- // if provided value is a shared value,
- // then we do not initialize another one.
+ /**
+ * if provided value is a shared value,
+ * then we do not initialize another one.
+ */
} else if (valueRef.current === null) {
// @ts-ignore
initialValueRef.current = value;
- // @ts-ignore
- valueRef.current = makeMutable(value);
+ /**
+ * if value is an object, then we need to
+ * pass a clone.
+ */
+ if (typeof value === 'object') {
+ // @ts-ignore
+ valueRef.current = makeMutable({ ...value });
+ } else {
+ // @ts-ignore
+ valueRef.current = makeMutable(value);
+ }
} else if (initialValueRef.current !== value) {
valueRef.current.value = value as T;
}
diff --git a/src/hooks/useScrollableInternal.ts b/src/hooks/useScrollableInternal.ts
index 01acfa99a..0fa18f864 100644
--- a/src/hooks/useScrollableInternal.ts
+++ b/src/hooks/useScrollableInternal.ts
@@ -52,12 +52,14 @@ export const useScrollableInternal = () => {
_rootScrollableContentOffsetY.value = y;
},
onScroll: () => {
+ /** @TODO remove this */
if (Platform.OS === 'android' && justStartedScrolling.value === 1) {
justStartedScrolling.value = 0;
// @ts-ignore
scrollTo(scrollableRef, 0, initialScrollingPosition.value, false);
return;
}
+
if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) {
// @ts-ignore
scrollTo(scrollableRef, 0, 0, false);
diff --git a/src/types.d.ts b/src/types.d.ts
index 9e028ee9f..eda152d30 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -91,4 +91,10 @@ export type ScrollableRef = {
//#region utils
export type Primitive = string | number | boolean;
+export interface Insets {
+ top: number;
+ bottom: number;
+ left: number;
+ right: number;
+}
//#endregion
diff --git a/src/utilities/animate.ts b/src/utilities/animate.ts
index 7d943bd42..290b8f52a 100644
--- a/src/utilities/animate.ts
+++ b/src/utilities/animate.ts
@@ -4,8 +4,8 @@ import { ANIMATION_METHOD } from '../constants';
export const animate = (
point: number,
configs: Animated.WithSpringConfig | Animated.WithTimingConfig,
- velocity: number,
- onComplete: () => void
+ velocity: number = 0,
+ onComplete?: () => void
) => {
'worklet';
diff --git a/src/utilities/normalizeSnapPoint.ts b/src/utilities/normalizeSnapPoint.ts
index 059116b10..406fcd1cc 100644
--- a/src/utilities/normalizeSnapPoint.ts
+++ b/src/utilities/normalizeSnapPoint.ts
@@ -4,9 +4,9 @@
export const normalizeSnapPoint = (
snapPoint: number | string,
containerHeight: number,
- topInset: number,
+ _topInset: number,
_bottomInset: number,
- $modal: boolean = false
+ _$modal: boolean = false
) => {
'worklet';
let normalizedSnapPoint = snapPoint;
@@ -16,5 +16,5 @@ export const normalizeSnapPoint = (
normalizedSnapPoint =
(Number(normalizedSnapPoint.split('%')[0]) * containerHeight) / 100;
}
- return Math.max($modal ? 0 : topInset, containerHeight - normalizedSnapPoint);
+ return Math.max(0, containerHeight - normalizedSnapPoint);
};