@gorhom/bottom-sheet
Version:
A performant interactive bottom sheet with fully configurable options 🚀
1,115 lines (978 loc) • 41.9 kB
JavaScript
import React, { useMemo, useCallback, forwardRef, useImperativeHandle, memo, useEffect } from 'react';
import { Platform } from 'react-native';
import invariant from 'invariant';
import Animated, { useAnimatedReaction, useSharedValue, useAnimatedStyle, useDerivedValue, runOnJS, interpolate, Extrapolate, runOnUI, cancelAnimation, useWorkletCallback } from 'react-native-reanimated';
import { State } from 'react-native-gesture-handler';
import { useScrollable, usePropsValidator, useReactiveSharedValue, useNormalizedSnapPoints, useKeyboard } from '../../hooks';
import { BottomSheetInternalProvider, BottomSheetProvider } from '../../contexts';
import BottomSheetContainer from '../bottomSheetContainer';
import BottomSheetGestureHandlersProvider from '../bottomSheetGestureHandlersProvider';
import BottomSheetBackdropContainer from '../bottomSheetBackdropContainer';
import BottomSheetHandleContainer from '../bottomSheetHandleContainer';
import BottomSheetBackgroundContainer from '../bottomSheetBackgroundContainer';
import BottomSheetFooterContainer from '../bottomSheetFooterContainer/BottomSheetFooterContainer';
import BottomSheetDraggableView from '../bottomSheetDraggableView'; // import BottomSheetDebugView from '../bottomSheetDebugView';
import { ANIMATION_STATE, KEYBOARD_STATE, KEYBOARD_BEHAVIOR, SHEET_STATE, SCROLLABLE_STATE, KEYBOARD_BLUR_BEHAVIOR, KEYBOARD_INPUT_MODE, ANIMATION_SOURCE } from '../../constants';
import { animate, getKeyboardAnimationConfigs, normalizeSnapPoint, print } from '../../utilities';
import { DEFAULT_OVER_DRAG_RESISTANCE_FACTOR, DEFAULT_ENABLE_CONTENT_PANNING_GESTURE, DEFAULT_ENABLE_HANDLE_PANNING_GESTURE, DEFAULT_ENABLE_OVER_DRAG, DEFAULT_ANIMATE_ON_MOUNT, DEFAULT_KEYBOARD_BEHAVIOR, DEFAULT_KEYBOARD_BLUR_BEHAVIOR, DEFAULT_KEYBOARD_INPUT_MODE, INITIAL_CONTAINER_HEIGHT, INITIAL_HANDLE_HEIGHT, INITIAL_POSITION, INITIAL_SNAP_POINT, DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE, INITIAL_CONTAINER_OFFSET } from './constants';
import { styles } from './styles';
Animated.addWhitelistedUIProps({
decelerationRate: true
});
const BottomSheetComponent = /*#__PURE__*/forwardRef(function BottomSheet(props, ref) {
//#region validate props
usePropsValidator(props); //#endregion
//#region extract props
const {
// animations configurations
animationConfigs: _providedAnimationConfigs,
// configurations
index: _providedIndex = 0,
snapPoints: _providedSnapPoints,
animateOnMount = DEFAULT_ANIMATE_ON_MOUNT,
enableContentPanningGesture = DEFAULT_ENABLE_CONTENT_PANNING_GESTURE,
enableHandlePanningGesture = DEFAULT_ENABLE_HANDLE_PANNING_GESTURE,
enableOverDrag = DEFAULT_ENABLE_OVER_DRAG,
enablePanDownToClose = DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE,
overDragResistanceFactor = DEFAULT_OVER_DRAG_RESISTANCE_FACTOR,
// styles
style: _providedStyle,
backgroundStyle: _providedBackgroundStyle,
handleStyle: _providedHandleStyle,
handleIndicatorStyle: _providedHandleIndicatorStyle,
// hooks
gestureEventsHandlersHook,
// keyboard
keyboardBehavior = DEFAULT_KEYBOARD_BEHAVIOR,
keyboardBlurBehavior = DEFAULT_KEYBOARD_BLUR_BEHAVIOR,
android_keyboardInputMode = DEFAULT_KEYBOARD_INPUT_MODE,
// layout
handleHeight: _providedHandleHeight,
containerHeight: _providedContainerHeight,
contentHeight: _providedContentHeight,
containerOffset: _providedContainerOffset,
topInset = 0,
bottomInset = 0,
// animated callback shared values
animatedPosition: _providedAnimatedPosition,
animatedIndex: _providedAnimatedIndex,
// gestures
simultaneousHandlers: _providedSimultaneousHandlers,
waitFor: _providedWaitFor,
activeOffsetX: _providedActiveOffsetX,
activeOffsetY: _providedActiveOffsetY,
failOffsetX: _providedFailOffsetX,
failOffsetY: _providedFailOffsetY,
// callbacks
onChange: _providedOnChange,
onClose: _providedOnClose,
onAnimate: _providedOnAnimate,
// private
$modal = false,
detached = false,
// components
handleComponent,
backdropComponent,
backgroundComponent,
footerComponent,
children
} = props; //#endregion
//#region layout variables
/**
* This variable is consider an internal variable,
* that will be used conditionally in `animatedContainerHeight`
*/
const _animatedContainerHeight = useReactiveSharedValue(_providedContainerHeight !== null && _providedContainerHeight !== void 0 ? _providedContainerHeight : INITIAL_CONTAINER_HEIGHT);
/**
* This is a conditional variable, where if the `BottomSheet` is used
* in a modal, then it will subset vertical insets (top+bottom) from
* provided container height.
*/
const animatedContainerHeight = useDerivedValue(() => {
const verticalInset = topInset + bottomInset;
return $modal ? _animatedContainerHeight.value - verticalInset : _animatedContainerHeight.value;
}, [$modal, topInset, bottomInset]);
const animatedContainerOffset = useReactiveSharedValue(_providedContainerOffset !== null && _providedContainerOffset !== void 0 ? _providedContainerOffset : INITIAL_CONTAINER_OFFSET);
const animatedHandleHeight = useReactiveSharedValue(_providedHandleHeight !== null && _providedHandleHeight !== void 0 ? _providedHandleHeight : INITIAL_HANDLE_HEIGHT);
const animatedFooterHeight = useSharedValue(0);
const animatedSnapPoints = useNormalizedSnapPoints(_providedSnapPoints, animatedContainerHeight, topInset, bottomInset, $modal);
const animatedHighestSnapPoint = useDerivedValue(() => animatedSnapPoints.value[animatedSnapPoints.value.length - 1]);
const animatedClosedPosition = useDerivedValue(() => {
let closedPosition = animatedContainerHeight.value;
if ($modal || detached) {
closedPosition = animatedContainerHeight.value + bottomInset;
}
return closedPosition;
}, [$modal, detached, bottomInset]);
const animatedSheetHeight = useDerivedValue(() => animatedContainerHeight.value - animatedHighestSnapPoint.value);
const animatedCurrentIndex = useReactiveSharedValue(animateOnMount ? -1 : _providedIndex);
const animatedPosition = useSharedValue(INITIAL_POSITION);
const animatedNextPosition = useSharedValue(0);
const animatedNextPositionIndex = useSharedValue(0); // conditional
const isAnimatedOnMount = useSharedValue(false);
const isContentHeightFixed = useSharedValue(false);
const isLayoutCalculated = useDerivedValue(() => {
let isContainerHeightCalculated = false; //container height was provided.
if (_providedContainerHeight !== null || _providedContainerHeight !== undefined) {
isContainerHeightCalculated = true;
} // container height did set.
if (animatedContainerHeight.value !== INITIAL_CONTAINER_HEIGHT) {
isContainerHeightCalculated = true;
}
let isHandleHeightCalculated = false; // handle height is provided.
if (_providedHandleHeight !== null && _providedHandleHeight !== undefined && typeof _providedHandleHeight === 'number') {
isHandleHeightCalculated = true;
} // handle component is null.
if (handleComponent === null) {
animatedHandleHeight.value = 0;
isHandleHeightCalculated = true;
} // handle height did set.
if (animatedHandleHeight.value !== INITIAL_HANDLE_HEIGHT) {
isHandleHeightCalculated = true;
}
let isSnapPointsNormalized = false; // the first snap point did normalized
if (animatedSnapPoints.value[0] !== INITIAL_SNAP_POINT) {
isSnapPointsNormalized = true;
}
return isContainerHeightCalculated && isHandleHeightCalculated && isSnapPointsNormalized;
});
const isInTemporaryPosition = useSharedValue(false);
const isForcedClosing = useSharedValue(false); // gesture
const animatedContentGestureState = useSharedValue(State.UNDETERMINED);
const animatedHandleGestureState = useSharedValue(State.UNDETERMINED); //#endregion
//#region hooks variables
// scrollable variables
const {
animatedScrollableType,
animatedScrollableContentOffsetY,
animatedScrollableOverrideState,
isScrollableRefreshable,
setScrollableRef,
removeScrollableRef
} = useScrollable(); // keyboard
const {
state: animatedKeyboardState,
height: animatedKeyboardHeight,
animationDuration: keyboardAnimationDuration,
animationEasing: keyboardAnimationEasing,
shouldHandleKeyboardEvents
} = useKeyboard();
/**
* Returns keyboard height that in the root container.
*/
const getKeyboardHeightInContainer = useWorkletCallback(() => {
/**
* if android software input mode is not `adjustPan`, than keyboard
* height will be 0 all the time.
*/
if (Platform.OS === 'android' && android_keyboardInputMode === KEYBOARD_INPUT_MODE.adjustResize) {
return 0;
}
return $modal ? Math.abs(animatedKeyboardHeight.value - Math.abs(bottomInset - animatedContainerOffset.value.bottom)) : Math.abs(animatedKeyboardHeight.value - animatedContainerOffset.value.bottom);
}, [$modal, bottomInset]); //#endregion
//#region state/dynamic variables
// states
const animatedAnimationState = useSharedValue(ANIMATION_STATE.UNDETERMINED);
const animatedAnimationSource = useSharedValue(ANIMATION_SOURCE.MOUNT);
const animatedSheetState = useDerivedValue(() => {
// closed position = position >= container height
if (animatedPosition.value >= animatedClosedPosition.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 - (animatedSheetHeight.value + keyboardHeightInContainer)); // 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;
} // 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;
}, [keyboardBehavior]);
const animatedScrollableState = useDerivedValue(() => {
/**
* if scrollable override state is set, then we just return its value.
*/
if (animatedScrollableOverrideState.value !== SCROLLABLE_STATE.UNDETERMINED) {
return animatedScrollableOverrideState.value;
}
/**
* 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 (animatedKeyboardState.value === KEYBOARD_STATE.SHOWN && animatedAnimationState.value === ANIMATION_STATE.RUNNING) {
return SCROLLABLE_STATE.UNLOCKED;
}
return SCROLLABLE_STATE.LOCKED;
}); // dynamic
const animatedContentHeight = useDerivedValue(() => {
const keyboardHeightInContainer = getKeyboardHeightInContainer();
const handleHeight = Math.max(0, animatedHandleHeight.value);
let contentHeight = animatedSheetHeight.value - handleHeight;
if (keyboardBehavior === KEYBOARD_BEHAVIOR.extend && animatedKeyboardState.value === KEYBOARD_STATE.SHOWN) {
contentHeight = contentHeight - keyboardHeightInContainer;
} else if (keyboardBehavior === KEYBOARD_BEHAVIOR.fillParent && isInTemporaryPosition.value) {
if (animatedKeyboardState.value === KEYBOARD_STATE.SHOWN) {
contentHeight = animatedContainerHeight.value - handleHeight - keyboardHeightInContainer;
} else {
contentHeight = animatedContainerHeight.value - handleHeight;
}
} else if (keyboardBehavior === KEYBOARD_BEHAVIOR.interactive && isInTemporaryPosition.value) {
const contentWithKeyboardHeight = contentHeight + keyboardHeightInContainer;
if (animatedKeyboardState.value === KEYBOARD_STATE.SHOWN) {
if (keyboardHeightInContainer + animatedSheetHeight.value > animatedContainerHeight.value) {
contentHeight = animatedContainerHeight.value - keyboardHeightInContainer - handleHeight;
}
} else if (contentWithKeyboardHeight + handleHeight > animatedContainerHeight.value) {
contentHeight = animatedContainerHeight.value - handleHeight;
} else {
contentHeight = contentWithKeyboardHeight;
}
}
/**
* before the container is measured, `contentHeight` value will be below zero,
* which will lead to freeze the scrollable.
*
* @link (https://github.com/gorhom/react-native-bottom-sheet/issues/470)
*/
return Math.max(contentHeight, 0);
}, [keyboardBehavior]);
const animatedIndex = useDerivedValue(() => {
const adjustedSnapPoints = animatedSnapPoints.value.slice().reverse();
const adjustedSnapPointsIndexes = animatedSnapPoints.value.slice().map((_, index) => index).reverse();
/**
* we add the close state index `-1`
*/
adjustedSnapPoints.push(animatedContainerHeight.value);
adjustedSnapPointsIndexes.push(-1);
const currentIndex = isLayoutCalculated.value ? interpolate(animatedPosition.value, adjustedSnapPoints, adjustedSnapPointsIndexes, Extrapolate.CLAMP) : -1;
/**
* if the sheet is currently running an animation by the keyboard opening,
* then we clamp the index on android with resize keyboard mode.
*/
if (android_keyboardInputMode === KEYBOARD_INPUT_MODE.adjustResize && animatedAnimationSource.value === ANIMATION_SOURCE.KEYBOARD && animatedAnimationState.value === ANIMATION_STATE.RUNNING && isInTemporaryPosition.value) {
return Math.max(animatedCurrentIndex.value, currentIndex);
}
/**
* if the sheet is currently running an animation by snap point change - usually caused
* by dynamic content height -, then we return the next position index.
*/
if (animatedAnimationSource.value === ANIMATION_SOURCE.SNAP_POINT_CHANGE && animatedAnimationState.value === ANIMATION_STATE.RUNNING) {
return animatedNextPositionIndex.value;
}
return currentIndex;
}, [android_keyboardInputMode]); //#endregion
//#region private methods
/**
* Calculate the next position based on keyboard state.
*/
const getNextPosition = useWorkletCallback(function getNextPosition() {
'worklet';
const currentIndex = animatedCurrentIndex.value;
const snapPoints = animatedSnapPoints.value;
const keyboardState = animatedKeyboardState.value;
const highestSnapPoint = animatedHighestSnapPoint.value;
/**
* Handle restore sheet position on blur
*/
if (keyboardBlurBehavior === KEYBOARD_BLUR_BEHAVIOR.restore && keyboardState === KEYBOARD_STATE.HIDDEN && animatedContentGestureState.value !== State.ACTIVE && animatedHandleGestureState.value !== State.ACTIVE) {
isInTemporaryPosition.value = false;
const nextPosition = snapPoints[currentIndex];
return nextPosition;
}
/**
* Handle extend behavior
*/
if (keyboardBehavior === KEYBOARD_BEHAVIOR.extend && keyboardState === KEYBOARD_STATE.SHOWN) {
return highestSnapPoint;
}
/**
* Handle full screen behavior
*/
if (keyboardBehavior === KEYBOARD_BEHAVIOR.fillParent && keyboardState === KEYBOARD_STATE.SHOWN) {
isInTemporaryPosition.value = true;
return 0;
}
/**
* handle interactive behavior
*/
if (keyboardBehavior === KEYBOARD_BEHAVIOR.interactive && keyboardState === KEYBOARD_STATE.SHOWN) {
isInTemporaryPosition.value = true;
const keyboardHeightInContainer = getKeyboardHeightInContainer();
return Math.max(0, highestSnapPoint - keyboardHeightInContainer);
}
if (isInTemporaryPosition.value) {
return animatedPosition.value;
}
return snapPoints[currentIndex];
}, [keyboardBehavior, keyboardBlurBehavior]);
const handleOnChange = useCallback(function handleOnChange(index) {
print({
component: BottomSheet.name,
method: handleOnChange.name,
params: {
index,
animatedCurrentIndex: animatedCurrentIndex.value
}
});
if (_providedOnChange) {
_providedOnChange(index);
}
}, [_providedOnChange, animatedCurrentIndex]);
const handleOnAnimate = useCallback(function handleOnAnimate(toPoint) {
const snapPoints = animatedSnapPoints.value;
const toIndex = snapPoints.indexOf(toPoint);
print({
component: BottomSheet.name,
method: handleOnAnimate.name,
params: {
toIndex,
fromIndex: animatedCurrentIndex.value
}
});
if (!_providedOnAnimate) {
return;
}
if (toIndex !== animatedCurrentIndex.value) {
_providedOnAnimate(animatedCurrentIndex.value, toIndex);
}
}, [_providedOnAnimate, animatedSnapPoints, animatedCurrentIndex]); //#endregion
//#region animation
const stopAnimation = useWorkletCallback(() => {
cancelAnimation(animatedPosition);
isForcedClosing.value = false;
animatedAnimationSource.value = ANIMATION_SOURCE.NONE;
animatedAnimationState.value = ANIMATION_STATE.STOPPED;
}, [animatedPosition, animatedAnimationState, animatedAnimationSource]);
const animateToPositionCompleted = useWorkletCallback(function animateToPositionCompleted(isFinished) {
isForcedClosing.value = false;
if (!isFinished) {
return;
}
runOnJS(print)({
component: BottomSheet.name,
method: animateToPositionCompleted.name,
params: {
animatedCurrentIndex: animatedCurrentIndex.value,
animatedNextPosition: animatedNextPosition.value,
animatedNextPositionIndex: animatedNextPositionIndex.value
}
});
animatedAnimationSource.value = ANIMATION_SOURCE.NONE;
animatedAnimationState.value = ANIMATION_STATE.STOPPED;
animatedNextPosition.value = Number.NEGATIVE_INFINITY;
animatedNextPositionIndex.value = Number.NEGATIVE_INFINITY;
});
const animateToPosition = useWorkletCallback(function animateToPosition(position, source, velocity = 0, configs) {
if (position === animatedPosition.value || position === undefined || animatedAnimationState.value === ANIMATION_STATE.RUNNING && position === animatedNextPosition.value) {
return;
}
runOnJS(print)({
component: BottomSheet.name,
method: animateToPosition.name,
params: {
currentPosition: animatedPosition.value,
position,
velocity
}
});
stopAnimation();
/**
* set animation state to running, and source
*/
animatedAnimationState.value = ANIMATION_STATE.RUNNING;
animatedAnimationSource.value = source;
/**
* store next position
*/
animatedNextPosition.value = position;
animatedNextPositionIndex.value = animatedSnapPoints.value.indexOf(position);
/**
* fire `onAnimate` callback
*/
runOnJS(handleOnAnimate)(position);
/**
* force animation configs from parameters, if provided
*/
if (configs !== undefined) {
animatedPosition.value = animate({
point: position,
configs,
velocity,
onComplete: animateToPositionCompleted
});
} else {
/**
* use animationConfigs callback, if provided
*/
animatedPosition.value = animate({
point: position,
velocity,
configs: _providedAnimationConfigs,
onComplete: animateToPositionCompleted
});
}
}, [handleOnAnimate, _providedAnimationConfigs]); //#endregion
//#region public methods
const handleSnapToIndex = useCallback(function handleSnapToIndex(index, animationConfigs) {
const snapPoints = animatedSnapPoints.value;
invariant(index >= -1 && index <= snapPoints.length - 1, `'index' was provided but out of the provided snap points range! expected value to be between -1, ${snapPoints.length - 1}`);
print({
component: BottomSheet.name,
method: handleSnapToIndex.name,
params: {
index
}
});
const nextPosition = snapPoints[index];
/**
* exit method if :
* - layout is not calculated.
* - already animating to next position.
* - sheet is forced closing.
*/
if (!isLayoutCalculated.value || index === animatedNextPositionIndex.value || nextPosition === animatedNextPosition.value || isForcedClosing.value) {
return;
}
/**
* reset temporary position boolean.
*/
isInTemporaryPosition.value = false;
runOnUI(animateToPosition)(nextPosition, ANIMATION_SOURCE.USER, 0, animationConfigs);
}, [animateToPosition, isLayoutCalculated, isInTemporaryPosition, isForcedClosing, animatedSnapPoints, animatedNextPosition, animatedNextPositionIndex]);
const handleSnapToPosition = useWorkletCallback(function handleSnapToPosition(position, animationConfigs) {
print({
component: BottomSheet.name,
method: handleSnapToPosition.name,
params: {
position
}
});
/**
* normalized provided position.
*/
const nextPosition = normalizeSnapPoint(position, animatedContainerHeight.value, topInset, bottomInset);
/**
* exit method if :
* - layout is not calculated.
* - already animating to next position.
* - sheet is forced closing.
*/
if (!isLayoutCalculated || nextPosition === animatedNextPosition.value || isForcedClosing.value) {
return;
}
/**
* mark the new position as temporary.
*/
isInTemporaryPosition.value = true;
runOnUI(animateToPosition)(nextPosition, ANIMATION_SOURCE.USER, 0, animationConfigs);
}, [animateToPosition, bottomInset, topInset, isLayoutCalculated, isForcedClosing, animatedContainerHeight, animatedPosition]);
const handleClose = useCallback(function handleClose(animationConfigs) {
print({
component: BottomSheet.name,
method: handleClose.name
});
const nextPosition = animatedClosedPosition.value;
/**
* exit method if :
* - layout is not calculated.
* - already animating to next position.
* - sheet is forced closing.
*/
if (!isLayoutCalculated.value || nextPosition === animatedNextPosition.value || isForcedClosing.value) {
return;
}
/**
* reset temporary position variable.
*/
isInTemporaryPosition.value = false;
runOnUI(animateToPosition)(nextPosition, ANIMATION_SOURCE.USER, 0, animationConfigs);
}, [animateToPosition, isForcedClosing, isLayoutCalculated, isInTemporaryPosition, animatedNextPosition, animatedClosedPosition]);
const handleForceClose = useCallback(function handleForceClose(animationConfigs) {
print({
component: BottomSheet.name,
method: handleForceClose.name
});
const nextPosition = animatedClosedPosition.value;
/**
* exit method if :
* - already animating to next position.
* - sheet is forced closing.
*/
if (nextPosition === animatedNextPosition.value || isForcedClosing.value) {
return;
}
/**
* reset temporary position variable.
*/
isInTemporaryPosition.value = false;
/**
* set force closing variable.
*/
isForcedClosing.value = true;
runOnUI(animateToPosition)(nextPosition, ANIMATION_SOURCE.USER, 0, animationConfigs);
}, [animateToPosition, isForcedClosing, isInTemporaryPosition, animatedNextPosition, animatedClosedPosition]);
const handleExpand = useCallback(function handleExpand(animationConfigs) {
print({
component: BottomSheet.name,
method: handleExpand.name
});
const snapPoints = animatedSnapPoints.value;
const nextPosition = snapPoints[snapPoints.length - 1];
/**
* exit method if :
* - layout is not calculated.
* - already animating to next position.
* - sheet is forced closing.
*/
if (!isLayoutCalculated.value || snapPoints.length - 1 === animatedNextPositionIndex.value || nextPosition === animatedNextPosition.value || isForcedClosing.value) {
return;
}
/**
* reset temporary position boolean.
*/
isInTemporaryPosition.value = false;
runOnUI(animateToPosition)(nextPosition, ANIMATION_SOURCE.USER, 0, animationConfigs);
}, [animateToPosition, isInTemporaryPosition, isLayoutCalculated, isForcedClosing, animatedSnapPoints, animatedNextPosition, animatedNextPositionIndex]);
const handleCollapse = useCallback(function handleCollapse(animationConfigs) {
print({
component: BottomSheet.name,
method: handleCollapse.name
});
const nextPosition = animatedSnapPoints.value[0];
/**
* exit method if :
* - layout is not calculated.
* - already animating to next position.
* - sheet is forced closing.
*/
if (!isLayoutCalculated || animatedNextPositionIndex.value === 0 || nextPosition === animatedNextPosition.value || isForcedClosing.value) {
return;
}
/**
* reset temporary position boolean.
*/
isInTemporaryPosition.value = false;
runOnUI(animateToPosition)(nextPosition, ANIMATION_SOURCE.USER, 0, animationConfigs);
}, [animateToPosition, isForcedClosing, isLayoutCalculated, isInTemporaryPosition, animatedSnapPoints, animatedNextPosition, animatedNextPositionIndex]);
useImperativeHandle(ref, () => ({
snapToIndex: handleSnapToIndex,
snapToPosition: handleSnapToPosition,
expand: handleExpand,
collapse: handleCollapse,
close: handleClose,
forceClose: handleForceClose
})); //#endregion
//#region contexts variables
const internalContextVariables = useMemo(() => ({
enableContentPanningGesture,
overDragResistanceFactor,
enableOverDrag,
enablePanDownToClose,
animatedAnimationState,
animatedSheetState,
animatedScrollableState,
animatedScrollableOverrideState,
animatedContentGestureState,
animatedHandleGestureState,
animatedKeyboardState,
animatedScrollableType,
animatedIndex,
animatedPosition,
animatedContentHeight,
animatedClosedPosition,
animatedHandleHeight,
animatedFooterHeight,
animatedKeyboardHeight,
animatedContainerHeight,
animatedSnapPoints,
animatedHighestSnapPoint,
animatedScrollableContentOffsetY,
isInTemporaryPosition,
isContentHeightFixed,
isScrollableRefreshable,
shouldHandleKeyboardEvents,
simultaneousHandlers: _providedSimultaneousHandlers,
waitFor: _providedWaitFor,
activeOffsetX: _providedActiveOffsetX,
activeOffsetY: _providedActiveOffsetY,
failOffsetX: _providedFailOffsetX,
failOffsetY: _providedFailOffsetY,
animateToPosition,
stopAnimation,
getKeyboardHeightInContainer,
setScrollableRef,
removeScrollableRef
}), [animatedIndex, animatedPosition, animatedContentHeight, animatedScrollableType, animatedContentGestureState, animatedHandleGestureState, animatedClosedPosition, animatedFooterHeight, animatedContainerHeight, animatedHandleHeight, animatedAnimationState, animatedKeyboardState, animatedKeyboardHeight, animatedSheetState, animatedHighestSnapPoint, animatedScrollableState, animatedScrollableOverrideState, animatedSnapPoints, shouldHandleKeyboardEvents, animatedScrollableContentOffsetY, isScrollableRefreshable, isContentHeightFixed, isInTemporaryPosition, enableContentPanningGesture, overDragResistanceFactor, enableOverDrag, enablePanDownToClose, _providedSimultaneousHandlers, _providedWaitFor, _providedActiveOffsetX, _providedActiveOffsetY, _providedFailOffsetX, _providedFailOffsetY, getKeyboardHeightInContainer, setScrollableRef, removeScrollableRef, animateToPosition, stopAnimation]);
const externalContextVariables = useMemo(() => ({
animatedIndex,
animatedPosition,
snapToIndex: handleSnapToIndex,
snapToPosition: handleSnapToPosition,
expand: handleExpand,
collapse: handleCollapse,
close: handleClose,
forceClose: handleForceClose
}), [animatedIndex, animatedPosition, handleSnapToIndex, handleSnapToPosition, handleExpand, handleCollapse, handleClose, handleForceClose]); //#endregion
//#region styles
const containerAnimatedStyle = useAnimatedStyle(() => ({
opacity: Platform.OS === 'android' && animatedIndex.value === -1 ? 0 : 1,
transform: [{
translateY: animatedPosition.value
}]
}), [animatedPosition, animatedIndex]);
const containerStyle = useMemo(() => [_providedStyle, styles.container, containerAnimatedStyle], [_providedStyle, containerAnimatedStyle]);
const contentContainerAnimatedStyle = useAnimatedStyle(() => {
/**
* if content height was provided, then we skip setting
* calculated height.
*/
if (_providedContentHeight) {
return {};
}
return {
height: animate({
point: animatedContentHeight.value,
configs: _providedAnimationConfigs
})
};
}, [animatedContentHeight, _providedContentHeight]);
const contentContainerStyle = useMemo(() => [styles.contentContainer, contentContainerAnimatedStyle], [contentContainerAnimatedStyle]);
/**
* added safe area to prevent the sheet from floating above
* the bottom of the screen, when sheet being over dragged or
* when the sheet is resized.
*/
const contentMaskContainerAnimatedStyle = useAnimatedStyle(() => {
if (detached) {
return {
overflow: 'visible'
};
}
return {
paddingBottom: animatedContainerHeight.value
};
}, [detached]);
const contentMaskContainerStyle = useMemo(() => [styles.contentMaskContainer, contentMaskContainerAnimatedStyle], [contentMaskContainerAnimatedStyle]); //#endregion
//#region effects
/**
* React to `isLayoutCalculated` change, to insure that the sheet will
* appears/mounts only when all layout is been calculated.
*
* @alias OnMount
*/
useAnimatedReaction(() => isLayoutCalculated.value, _isLayoutCalculated => {
/**
* exit method if:
* - layout is not calculated yet.
* - already did animate on mount.
*/
if (!_isLayoutCalculated || isAnimatedOnMount.value) {
return;
}
let nextPosition;
if (_providedIndex === -1) {
nextPosition = animatedClosedPosition.value;
animatedNextPositionIndex.value = -1;
} else {
nextPosition = animatedSnapPoints.value[_providedIndex];
}
runOnJS(print)({
component: BottomSheet.name,
method: 'useAnimatedReaction::OnMount',
params: {
isLayoutCalculated: _isLayoutCalculated,
animatedSnapPoints: animatedSnapPoints.value,
nextPosition
}
});
/**
* here we exit method early because the next position
* is out of the screen, this happens when `snapPoints`
* still being calculated.
*/
if (nextPosition === INITIAL_POSITION || nextPosition === animatedClosedPosition.value) {
isAnimatedOnMount.value = true;
animatedCurrentIndex.value = _providedIndex;
return;
}
if (animateOnMount) {
animateToPosition(nextPosition, ANIMATION_SOURCE.MOUNT);
} else {
animatedPosition.value = nextPosition;
}
isAnimatedOnMount.value = true;
}, [_providedIndex, animateOnMount]);
/**
* React to `snapPoints` change, to insure that the sheet position reflect
* to the current point correctly.
*
* @alias OnSnapPointsChange
*/
useAnimatedReaction(() => ({
snapPoints: animatedSnapPoints.value,
containerHeight: animatedContainerHeight.value
}), (result, _previousResult) => {
const {
snapPoints,
containerHeight
} = result;
const _previousSnapPoints = _previousResult === null || _previousResult === void 0 ? void 0 : _previousResult.snapPoints;
const _previousContainerHeight = _previousResult === null || _previousResult === void 0 ? void 0 : _previousResult.containerHeight;
if (JSON.stringify(snapPoints) === JSON.stringify(_previousSnapPoints) || !isLayoutCalculated.value || !isAnimatedOnMount.value) {
return;
}
runOnJS(print)({
component: BottomSheet.name,
method: 'useAnimatedReaction::OnSnapPointChange',
params: {
snapPoints
}
});
let nextPosition;
let animationConfig;
let animationSource = ANIMATION_SOURCE.SNAP_POINT_CHANGE;
/**
* if snap points changed while sheet is animating, then
* we stop the animation and animate to the updated point.
*/
if (animatedAnimationState.value === ANIMATION_STATE.RUNNING) {
nextPosition = animatedNextPositionIndex.value !== -1 ? snapPoints[animatedNextPositionIndex.value] : animatedNextPosition.value;
} else if (animatedCurrentIndex.value === -1) {
nextPosition = animatedClosedPosition.value;
} else if (isInTemporaryPosition.value) {
nextPosition = getNextPosition();
} else {
nextPosition = snapPoints[animatedCurrentIndex.value];
/**
* if snap points changes because of the container height change,
* then we skip the snap animation by setting the duration to 0.
*/
if (containerHeight !== _previousContainerHeight) {
animationSource = ANIMATION_SOURCE.CONTAINER_RESIZE;
animationConfig = {
duration: 0
};
}
}
animateToPosition(nextPosition, animationSource, 0, animationConfig);
});
/**
* React to keyboard appearance state.
*
* @alias OnKeyboardStateChange
*/
useAnimatedReaction(() => ({
_keyboardState: animatedKeyboardState.value,
_keyboardHeight: animatedKeyboardHeight.value
}), (result, _previousResult) => {
const {
_keyboardState,
_keyboardHeight
} = result;
const _previousKeyboardState = _previousResult === null || _previousResult === void 0 ? void 0 : _previousResult._keyboardState;
const _previousKeyboardHeight = _previousResult === null || _previousResult === void 0 ? void 0 : _previousResult._keyboardHeight;
const hasActiveGesture = animatedContentGestureState.value === State.ACTIVE || animatedContentGestureState.value === State.BEGAN || animatedHandleGestureState.value === State.ACTIVE || animatedHandleGestureState.value === State.BEGAN;
if (
/**
* if keyboard state is equal to the previous state, then exit the method
*/
_keyboardState === _previousKeyboardState && _keyboardHeight === _previousKeyboardHeight ||
/**
* if user is interacting with sheet, then exit the method
*/
hasActiveGesture ||
/**
* if sheet not animated on mount yet, then exit the method
*/
!isAnimatedOnMount.value || _keyboardState === KEYBOARD_STATE.HIDDEN && keyboardBlurBehavior === KEYBOARD_BLUR_BEHAVIOR.none || Platform.OS === 'android' && keyboardBehavior === KEYBOARD_BEHAVIOR.interactive && android_keyboardInputMode === KEYBOARD_INPUT_MODE.adjustResize) {
return;
}
runOnJS(print)({
component: BottomSheet.name,
method: 'useAnimatedReaction::OnKeyboardStateChange',
params: {
keyboardState: _keyboardState,
keyboardHeight: _keyboardHeight
}
});
let animationConfigs = getKeyboardAnimationConfigs(keyboardAnimationEasing.value, keyboardAnimationDuration.value);
const nextPosition = getNextPosition();
animateToPosition(nextPosition, ANIMATION_SOURCE.KEYBOARD, 0, animationConfigs);
}, [keyboardBehavior, keyboardBlurBehavior, android_keyboardInputMode, getNextPosition]);
/**
* sets provided animated position
*/
useAnimatedReaction(() => animatedPosition.value, _animatedPosition => {
if (_providedAnimatedPosition) {
_providedAnimatedPosition.value = _animatedPosition + topInset;
}
});
/**
* sets provided animated index
*/
useAnimatedReaction(() => animatedIndex.value, _animatedIndex => {
if (_providedAnimatedIndex) {
_providedAnimatedIndex.value = _animatedIndex;
}
});
/**
* React to internal variables to detect change in snap position.
*
* @alias OnChange
*/
useAnimatedReaction(() => ({
_animatedIndex: animatedIndex.value,
_animatedPosition: animatedPosition.value,
_animationState: animatedAnimationState.value,
_contentGestureState: animatedContentGestureState.value,
_handleGestureState: animatedHandleGestureState.value
}), ({
_animatedIndex,
_animationState,
_contentGestureState,
_handleGestureState
}) => {
/**
* exit the method if animation state is not stopped.
*/
if (_animationState !== ANIMATION_STATE.STOPPED) {
return;
}
/**
* exit the method if animated index value
* has fraction, e.g. 1.99, 0.52
*/
if (_animatedIndex % 1 !== 0) {
return;
}
/**
* exit the method if there any active gesture.
*/
const hasNoActiveGesture = (_contentGestureState === State.END || _contentGestureState === State.UNDETERMINED || _contentGestureState === State.CANCELLED) && (_handleGestureState === State.END || _handleGestureState === State.UNDETERMINED || _handleGestureState === State.CANCELLED);
if (!hasNoActiveGesture) {
return;
}
/**
* if the index is not equal to the current index,
* than the sheet position had changed and we trigger
* the `onChange` callback.
*/
if (_animatedIndex !== animatedCurrentIndex.value) {
runOnJS(print)({
component: BottomSheet.name,
method: 'useAnimatedReaction::OnChange',
params: {
animatedCurrentIndex: animatedCurrentIndex.value,
animatedIndex: _animatedIndex
}
});
animatedCurrentIndex.value = _animatedIndex;
runOnJS(handleOnChange)(_animatedIndex);
}
/**
* if index is `-1` than we fire the `onClose` callback.
*/
if (_animatedIndex === -1 && _providedOnClose) {
runOnJS(print)({
component: BottomSheet.name,
method: 'useAnimatedReaction::onClose',
params: {
animatedCurrentIndex: animatedCurrentIndex.value,
animatedIndex: _animatedIndex
}
});
runOnJS(_providedOnClose)();
}
}, [handleOnChange, _providedOnClose]);
/**
* React to `index` prop to snap the sheet to the new position.
*
* @alias onIndexChange
*/
useEffect(() => {
if (isAnimatedOnMount.value) {
handleSnapToIndex(_providedIndex);
}
}, [_providedIndex, animatedCurrentIndex, isAnimatedOnMount, handleSnapToIndex]); //#endregion
// render
print({
component: BottomSheet.name,
method: 'render',
params: {
animatedSnapPoints: animatedSnapPoints.value,
animatedCurrentIndex: animatedCurrentIndex.value,
providedIndex: _providedIndex
}
});
return /*#__PURE__*/React.createElement(BottomSheetProvider, {
value: externalContextVariables
}, /*#__PURE__*/React.createElement(BottomSheetInternalProvider, {
value: internalContextVariables
}, /*#__PURE__*/React.createElement(BottomSheetGestureHandlersProvider, {
gestureEventsHandlersHook: gestureEventsHandlersHook
}, /*#__PURE__*/React.createElement(BottomSheetBackdropContainer, {
key: "BottomSheetBackdropContainer",
animatedIndex: animatedIndex,
animatedPosition: animatedPosition,
backdropComponent: backdropComponent
}), /*#__PURE__*/React.createElement(BottomSheetContainer, {
key: "BottomSheetContainer",
shouldCalculateHeight: !$modal,
containerHeight: _animatedContainerHeight,
containerOffset: animatedContainerOffset,
topInset: topInset,
bottomInset: bottomInset,
detached: detached
}, /*#__PURE__*/React.createElement(Animated.View, {
style: containerStyle
}, /*#__PURE__*/React.createElement(BottomSheetBackgroundContainer, {
key: "BottomSheetBackgroundContainer",
animatedIndex: animatedIndex,
animatedPosition: animatedPosition,
backgroundComponent: backgroundComponent,
backgroundStyle: _providedBackgroundStyle
}), /*#__PURE__*/React.createElement(Animated.View, {
pointerEvents: "box-none",
style: contentMaskContainerStyle
}, /*#__PURE__*/React.createElement(BottomSheetDraggableView, {
key: "BottomSheetRootDraggableView",
style: contentContainerStyle
}, typeof children === 'function' ? children() : children, footerComponent && /*#__PURE__*/React.createElement(BottomSheetFooterContainer, {
footerComponent: footerComponent
}))), /*#__PURE__*/React.createElement(BottomSheetHandleContainer, {
key: "BottomSheetHandleContainer",
animatedIndex: animatedIndex,
animatedPosition: animatedPosition,
handleHeight: animatedHandleHeight,
enableHandlePanningGesture: enableHandlePanningGesture,
enableOverDrag: enableOverDrag,
enablePanDownToClose: enablePanDownToClose,
overDragResistanceFactor: overDragResistanceFactor,
keyboardBehavior: keyboardBehavior,
handleComponent: handleComponent,
handleStyle: _providedHandleStyle,
handleIndicatorStyle: _providedHandleIndicatorStyle
}))))));
});
const BottomSheet = /*#__PURE__*/memo(BottomSheetComponent);
BottomSheet.displayName = 'BottomSheet';
export default BottomSheet;
//# sourceMappingURL=BottomSheet.js.map