UNPKG

@gorhom/bottom-sheet

Version:

A performant interactive bottom sheet with fully configurable options 🚀

1,271 lines (1,188 loc) • 47.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _invariant = _interopRequireDefault(require("invariant")); var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativeGestureHandler = require("react-native-gesture-handler"); var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated")); var _constants = require("../../constants"); var _contexts = require("../../contexts"); var _hooks = require("../../hooks"); var _utilities = require("../../utilities"); var _bottomSheetBackground = require("../bottomSheetBackground"); var _bottomSheetFooter = require("../bottomSheetFooter"); var _bottomSheetGestureHandlersProvider = _interopRequireDefault(require("../bottomSheetGestureHandlersProvider")); var _bottomSheetHandle = require("../bottomSheetHandle"); var _bottomSheetHostingContainer = require("../bottomSheetHostingContainer"); var _BottomSheetBody = require("./BottomSheetBody"); var _BottomSheetContent = require("./BottomSheetContent"); var _constants2 = require("./constants"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } // import BottomSheetDebugView from '../bottomSheetDebugView'; _reactNativeReanimated.default.addWhitelistedUIProps({ decelerationRate: true }); const BottomSheetComponent = /*#__PURE__*/(0, _react.forwardRef)(function BottomSheet(props, ref) { //#region extract props const { // animations configurations animationConfigs: _providedAnimationConfigs, // configurations index: _providedIndex = 0, snapPoints: _providedSnapPoints, animateOnMount = _constants2.DEFAULT_ANIMATE_ON_MOUNT, enableContentPanningGesture = _constants2.DEFAULT_ENABLE_CONTENT_PANNING_GESTURE, enableHandlePanningGesture, enableOverDrag = _constants2.DEFAULT_ENABLE_OVER_DRAG, enablePanDownToClose = _constants2.DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE, enableDynamicSizing = _constants2.DEFAULT_DYNAMIC_SIZING, overDragResistanceFactor = _constants2.DEFAULT_OVER_DRAG_RESISTANCE_FACTOR, overrideReduceMotion: _providedOverrideReduceMotion, // styles style, containerStyle: _providedContainerStyle, backgroundStyle: _providedBackgroundStyle, handleStyle: _providedHandleStyle, handleIndicatorStyle: _providedHandleIndicatorStyle, // hooks gestureEventsHandlersHook, // keyboard keyboardBehavior = _constants2.DEFAULT_KEYBOARD_BEHAVIOR, keyboardBlurBehavior = _constants2.DEFAULT_KEYBOARD_BLUR_BEHAVIOR, android_keyboardInputMode = _constants2.DEFAULT_KEYBOARD_INPUT_MODE, enableBlurKeyboardOnGesture = _constants2.DEFAULT_ENABLE_BLUR_KEYBOARD_ON_GESTURE, // layout containerLayoutState, topInset = 0, bottomInset = 0, maxDynamicContentSize, containerHeight, containerOffset, // 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 = _bottomSheetHandle.BottomSheetHandle, backdropComponent: BackdropComponent, backgroundComponent, footerComponent, children, // accessibility accessible: _providedAccessible = _constants2.DEFAULT_ACCESSIBLE, accessibilityLabel: _providedAccessibilityLabel = _constants2.DEFAULT_ACCESSIBILITY_LABEL, accessibilityRole: _providedAccessibilityRole = _constants2.DEFAULT_ACCESSIBILITY_ROLE } = props; //#endregion //#region validate props if (__DEV__) { // biome-ignore lint/correctness/useHookAtTopLevel: used in development only. (0, _hooks.usePropsValidator)({ index: _providedIndex, snapPoints: _providedSnapPoints, enableDynamicSizing, topInset, bottomInset, containerHeight, containerOffset }); } //#endregion //#region layout variables const animatedLayoutState = (0, _hooks.useAnimatedLayout)(containerLayoutState, topInset, bottomInset, $modal, handleComponent === null); const animatedDetentsState = (0, _hooks.useAnimatedDetents)(_providedSnapPoints, animatedLayoutState, enableDynamicSizing, maxDynamicContentSize, detached, $modal, bottomInset); const animatedSheetHeight = (0, _reactNativeReanimated.useDerivedValue)(() => { const { containerHeight } = animatedLayoutState.get(); const { highestDetentPosition } = animatedDetentsState.get(); if (highestDetentPosition === undefined) { return _constants.INITIAL_LAYOUT_VALUE; } return containerHeight - highestDetentPosition; }, [animatedLayoutState, animatedDetentsState]); const animatedCurrentIndex = (0, _hooks.useReactiveSharedValue)(animateOnMount ? -1 : _providedIndex); const animatedPosition = (0, _reactNativeReanimated.useSharedValue)(_constants2.INITIAL_POSITION); // conditional const isAnimatedOnMount = (0, _reactNativeReanimated.useSharedValue)(!animateOnMount || _providedIndex === -1); const isLayoutCalculated = (0, _reactNativeReanimated.useDerivedValue)(() => { let isContainerHeightCalculated = false; const { containerHeight, handleHeight } = animatedLayoutState.get(); //container height was provided. if (containerHeight !== null || containerHeight !== undefined) { isContainerHeightCalculated = true; } // container height did set. if (containerHeight !== _constants.INITIAL_LAYOUT_VALUE) { isContainerHeightCalculated = true; } let isHandleHeightCalculated = false; // handle component is null. if (handleComponent === null) { isHandleHeightCalculated = true; } // handle height did set. if (handleHeight !== _constants.INITIAL_LAYOUT_VALUE) { isHandleHeightCalculated = true; } let isSnapPointsNormalized = false; const { detents } = animatedDetentsState.get(); // the first snap point did normalized if (detents) { isSnapPointsNormalized = true; } return isContainerHeightCalculated && isHandleHeightCalculated && isSnapPointsNormalized; }, [animatedLayoutState, animatedDetentsState, handleComponent]); const isInTemporaryPosition = (0, _reactNativeReanimated.useSharedValue)(false); const animatedContainerHeightDidChange = (0, _reactNativeReanimated.useSharedValue)(false); // gesture const animatedContentGestureState = (0, _reactNativeReanimated.useSharedValue)(_reactNativeGestureHandler.State.UNDETERMINED); const animatedHandleGestureState = (0, _reactNativeReanimated.useSharedValue)(_reactNativeGestureHandler.State.UNDETERMINED); //#endregion //#region hooks variables // keyboard const animatedKeyboardState = (0, _hooks.useAnimatedKeyboard)(); const userReduceMotionSetting = (0, _reactNativeReanimated.useReducedMotion)(); const reduceMotion = (0, _react.useMemo)(() => { return !_providedOverrideReduceMotion || _providedOverrideReduceMotion === _reactNativeReanimated.ReduceMotion.System ? userReduceMotionSetting : _providedOverrideReduceMotion === _reactNativeReanimated.ReduceMotion.Always; }, [userReduceMotionSetting, _providedOverrideReduceMotion]); //#endregion //#region state/dynamic variables // states const animatedAnimationState = (0, _reactNativeReanimated.useSharedValue)({ status: _constants.ANIMATION_STATUS.UNDETERMINED, source: _constants.ANIMATION_SOURCE.MOUNT }); const animatedSheetState = (0, _reactNativeReanimated.useDerivedValue)(() => { const { detents, closedDetentPosition } = animatedDetentsState.get(); if (!detents || detents.length === 0 || closedDetentPosition === undefined) { return _constants.SHEET_STATE.CLOSED; } // closed position = position >= container height if (animatedPosition.value >= closedDetentPosition) { return _constants.SHEET_STATE.CLOSED; } const { containerHeight } = animatedLayoutState.get(); // extended position = container height - sheet height const extendedPosition = containerHeight - animatedSheetHeight.value; if (animatedPosition.value === extendedPosition) { return _constants.SHEET_STATE.EXTENDED; } // extended position with keyboard = // container height - (sheet height + keyboard height in root container) const keyboardHeightInContainer = animatedKeyboardState.get().heightWithinContainer; const extendedPositionWithKeyboard = Math.max(0, containerHeight - (animatedSheetHeight.value + keyboardHeightInContainer)); // detect if keyboard is open and the sheet is in temporary position if (keyboardBehavior === _constants.KEYBOARD_BEHAVIOR.interactive && isInTemporaryPosition.value && animatedPosition.value === extendedPositionWithKeyboard) { return _constants.SHEET_STATE.EXTENDED; } // fill parent = 0 if (animatedPosition.value === 0) { return _constants.SHEET_STATE.FILL_PARENT; } // detect if position is below extended point if (animatedPosition.value < extendedPosition) { return _constants.SHEET_STATE.OVER_EXTENDED; } return _constants.SHEET_STATE.OPENED; }, [animatedLayoutState, animatedDetentsState, animatedKeyboardState, animatedPosition, animatedSheetHeight, isInTemporaryPosition, keyboardBehavior]); const { state: animatedScrollableState, status: animatedScrollableStatus, setScrollableRef, removeScrollableRef } = (0, _hooks.useScrollable)(enableContentPanningGesture, animatedSheetState, animatedKeyboardState, animatedAnimationState); // dynamic const animatedIndex = (0, _reactNativeReanimated.useDerivedValue)(() => { const { detents } = animatedDetentsState.get(); if (!detents || detents.length === 0) { return -1; } const adjustedSnapPoints = detents.slice().reverse(); const adjustedSnapPointsIndexes = detents.slice().map((_, index) => index).reverse(); const { containerHeight } = animatedLayoutState.get(); /** * we add the close state index `-1` */ adjustedSnapPoints.push(containerHeight); adjustedSnapPointsIndexes.push(-1); const currentIndex = isLayoutCalculated.value ? (0, _reactNativeReanimated.interpolate)(animatedPosition.value, adjustedSnapPoints, adjustedSnapPointsIndexes, _reactNativeReanimated.Extrapolation.CLAMP) : -1; const { status: animationStatus, source: animationSource, nextIndex, nextPosition } = animatedAnimationState.get(); /** * 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 === _constants.KEYBOARD_INPUT_MODE.adjustResize && animationStatus === _constants.ANIMATION_STATUS.RUNNING && animationSource === _constants.ANIMATION_SOURCE.KEYBOARD && 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 (animationStatus === _constants.ANIMATION_STATUS.RUNNING && animationSource === _constants.ANIMATION_SOURCE.SNAP_POINT_CHANGE && nextIndex !== undefined && nextPosition !== undefined) { return nextIndex; } return currentIndex; }, [android_keyboardInputMode, animatedAnimationState, animatedLayoutState, animatedCurrentIndex, animatedPosition, animatedDetentsState, isInTemporaryPosition, isLayoutCalculated]); //#endregion //#region private methods const handleOnChange = (0, _react.useCallback)(function handleOnChange(index, position) { if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleOnChange', category: 'callback', params: { index, position } }); } if (!_providedOnChange) { return; } const { dynamicDetentIndex } = animatedDetentsState.get(); _providedOnChange(index, position, index === dynamicDetentIndex ? _constants.SNAP_POINT_TYPE.DYNAMIC : _constants.SNAP_POINT_TYPE.PROVIDED); }, [_providedOnChange, animatedDetentsState]); const handleOnAnimate = (0, _react.useCallback)(function handleOnAnimate(targetIndex, targetPosition) { if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleOnAnimate', category: 'callback', params: { toIndex: targetIndex, toPosition: targetPosition, fromIndex: animatedCurrentIndex.value, fromPosition: animatedPosition.value } }); } if (targetIndex === animatedCurrentIndex.get()) { return; } if (!_providedOnAnimate) { return; } _providedOnAnimate(animatedCurrentIndex.value, targetIndex, animatedPosition.value, targetPosition); }, [_providedOnAnimate, animatedCurrentIndex, animatedPosition]); const handleOnClose = (0, _react.useCallback)(function handleOnClose() { if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleOnClose', category: 'callback' }); } if (!_providedOnClose) { return; } _providedOnClose(); }, [_providedOnClose]); //#endregion //#region animation const stopAnimation = (0, _react.useCallback)(() => { 'worklet'; (0, _reactNativeReanimated.cancelAnimation)(animatedPosition); animatedAnimationState.set({ status: _constants.ANIMATION_STATUS.STOPPED, source: _constants.ANIMATION_SOURCE.NONE }); }, [animatedPosition, animatedAnimationState]); const animateToPositionCompleted = (0, _react.useCallback)(function animateToPositionCompleted(isFinished) { 'worklet'; if (!isFinished) { return; } const { nextIndex, nextPosition } = animatedAnimationState.get(); if (__DEV__) { (0, _reactNativeReanimated.runOnJS)(_utilities.print)({ component: 'BottomSheet', method: 'animateToPositionCompleted', params: { currentIndex: animatedCurrentIndex.value, nextIndex, nextPosition } }); } if (nextIndex === undefined || nextPosition === undefined) { return; } if (animatedAnimationState.get().source === _constants.ANIMATION_SOURCE.MOUNT) { isAnimatedOnMount.value = true; } // callbacks if (nextIndex !== animatedCurrentIndex.get()) { (0, _reactNativeReanimated.runOnJS)(handleOnChange)(nextIndex, nextPosition); } if (nextIndex === -1) { (0, _reactNativeReanimated.runOnJS)(handleOnClose)(); } animatedCurrentIndex.set(nextIndex); // reset values animatedAnimationState.set({ status: _constants.ANIMATION_STATUS.STOPPED, source: _constants.ANIMATION_SOURCE.NONE, nextIndex: undefined, nextPosition: undefined, isForcedClosing: undefined }); animatedContainerHeightDidChange.value = false; }, [handleOnChange, handleOnClose, animatedCurrentIndex, animatedAnimationState, animatedContainerHeightDidChange, isAnimatedOnMount]); const animateToPosition = (0, _react.useCallback)(function animateToPosition(position, source, velocity = 0, configs) { 'worklet'; if (__DEV__) { (0, _reactNativeReanimated.runOnJS)(_utilities.print)({ component: 'BottomSheet', method: 'animateToPosition', params: { currentPosition: animatedPosition.value, nextPosition: position, source } }); } if (position === undefined) { return; } if (position === animatedPosition.get()) { return; } // early exit if there is a running animation to // the same position const { status: animationStatus, nextPosition } = animatedAnimationState.get(); if (animationStatus === _constants.ANIMATION_STATUS.RUNNING && position === nextPosition) { return; } // stop animation if it is running if (animationStatus === _constants.ANIMATION_STATUS.RUNNING) { stopAnimation(); } /** * offset the position if keyboard is shown, * and behavior not extend. */ let offset = 0; if (animatedKeyboardState.get().status === _constants.KEYBOARD_STATUS.SHOWN && keyboardBehavior !== _constants.KEYBOARD_BEHAVIOR.extend && position < animatedPosition.value) { offset = animatedKeyboardState.get().heightWithinContainer; } const { detents } = animatedDetentsState.get(); const index = detents?.indexOf(position + offset) ?? -1; /** * set the animation state */ animatedAnimationState.set(state => { 'worklet'; return { ...state, status: _constants.ANIMATION_STATUS.RUNNING, source, nextIndex: index, nextPosition: position }; }); /** * fire `onAnimate` callback */ (0, _reactNativeReanimated.runOnJS)(handleOnAnimate)(index, position); /** * start animation */ animatedPosition.value = (0, _utilities.animate)({ point: position, configs: configs || _providedAnimationConfigs, velocity, overrideReduceMotion: _providedOverrideReduceMotion, onComplete: animateToPositionCompleted }); }, [handleOnAnimate, stopAnimation, animateToPositionCompleted, keyboardBehavior, _providedAnimationConfigs, _providedOverrideReduceMotion, animatedDetentsState, animatedAnimationState, animatedKeyboardState, animatedPosition]); /** * Set to position without animation. * * @param targetPosition position to be set. */ const setToPosition = (0, _react.useCallback)(function setToPosition(targetPosition) { 'worklet'; if (!targetPosition) { return; } if (targetPosition === animatedPosition.get()) { return; } const { status: animationStatus, nextPosition } = animatedAnimationState.get(); // early exit if there is a running animation to // the same position if (animationStatus === _constants.ANIMATION_STATUS.RUNNING && targetPosition === nextPosition) { return; } if (__DEV__) { (0, _reactNativeReanimated.runOnJS)(_utilities.print)({ component: 'BottomSheet', method: 'setToPosition', params: { currentPosition: animatedPosition.value, targetPosition } }); } /** * store next position */ const { detents } = animatedDetentsState.get(); const index = detents?.indexOf(targetPosition) ?? -1; animatedAnimationState.set(state => { 'worklet'; return { ...state, nextPosition: targetPosition, nextIndex: index }; }); stopAnimation(); // set values animatedPosition.value = targetPosition; animatedContainerHeightDidChange.value = false; }, [stopAnimation, animatedPosition, animatedContainerHeightDidChange, animatedAnimationState, animatedDetentsState]); //#endregion //#region private methods /** * Calculate and evaluate the current position based on multiple * local states. */ const getEvaluatedPosition = (0, _react.useCallback)(function getEvaluatedPosition(source) { 'worklet'; const currentIndex = animatedCurrentIndex.value; const { detents, highestDetentPosition, closedDetentPosition } = animatedDetentsState.get(); const keyboardStatus = animatedKeyboardState.get().status; if (detents === undefined || highestDetentPosition === undefined || closedDetentPosition === undefined) { return; } /** * if the keyboard blur behavior is restore and keyboard is hidden, * then we return the previous snap point. */ if (source === _constants.ANIMATION_SOURCE.KEYBOARD && keyboardBlurBehavior === _constants.KEYBOARD_BLUR_BEHAVIOR.restore && keyboardStatus === _constants.KEYBOARD_STATUS.HIDDEN && animatedContentGestureState.value !== _reactNativeGestureHandler.State.ACTIVE && animatedHandleGestureState.value !== _reactNativeGestureHandler.State.ACTIVE) { isInTemporaryPosition.value = false; const nextPosition = detents[currentIndex]; return nextPosition; } /** * if the keyboard appearance behavior is extend and keyboard is shown, * then we return the heights snap point. */ if (keyboardBehavior === _constants.KEYBOARD_BEHAVIOR.extend && keyboardStatus === _constants.KEYBOARD_STATUS.SHOWN) { return highestDetentPosition; } /** * if the keyboard appearance behavior is fill parent and keyboard is shown, * then we return 0 ( full screen ). */ if (keyboardBehavior === _constants.KEYBOARD_BEHAVIOR.fillParent && keyboardStatus === _constants.KEYBOARD_STATUS.SHOWN) { isInTemporaryPosition.value = true; return 0; } /** * if the keyboard appearance behavior is interactive and keyboard is shown, * then we return the heights points minus the keyboard in container height. */ if (keyboardBehavior === _constants.KEYBOARD_BEHAVIOR.interactive && keyboardStatus === _constants.KEYBOARD_STATUS.SHOWN && // ensure that this logic does not run on android // with resize input mode !(_reactNative.Platform.OS === 'android' && android_keyboardInputMode === 'adjustResize')) { isInTemporaryPosition.value = true; const keyboardHeightInContainer = animatedKeyboardState.get().heightWithinContainer; return Math.max(0, highestDetentPosition - keyboardHeightInContainer); } /** * if the bottom sheet is in temporary position, then we return * the current position. */ if (isInTemporaryPosition.value) { return animatedPosition.value; } /** * if the bottom sheet did not animate on mount, * then we return the provided index or the closed position. */ if (!isAnimatedOnMount.value) { return _providedIndex === -1 ? closedDetentPosition : detents[_providedIndex]; } /** * return the current index position. */ return detents[currentIndex]; }, [animatedContentGestureState, animatedCurrentIndex, animatedHandleGestureState, animatedKeyboardState, animatedPosition, animatedDetentsState, isInTemporaryPosition, isAnimatedOnMount, keyboardBehavior, keyboardBlurBehavior, _providedIndex, android_keyboardInputMode]); /** * Evaluate the bottom sheet position based based on a event source and other local states. */ const evaluatePosition = (0, _react.useCallback)(function evaluatePosition(source, animationConfigs) { 'worklet'; const { status: animationStatus, nextIndex, isForcedClosing } = animatedAnimationState.get(); const { detents, closedDetentPosition } = animatedDetentsState.get(); if (detents === undefined || detents.length === 0 || closedDetentPosition === undefined) { return; } /** * if a force closing is running and source not from user, then we early exit */ if (isForcedClosing && source !== _constants.ANIMATION_SOURCE.USER) { return; } /** * when evaluating the position while layout is not calculated, then we early exit till it is. */ if (!isLayoutCalculated.value) { return; } const proposedPosition = getEvaluatedPosition(source); if (proposedPosition === undefined) { return; } /** * when evaluating the position while the mount animation not been handled, * then we evaluate on mount use cases. */ if (!isAnimatedOnMount.value) { /** * if animate on mount is set to true, then we animate to the propose position, * else, we set the position with out animation. */ if (animateOnMount) { animateToPosition(proposedPosition, _constants.ANIMATION_SOURCE.MOUNT, undefined, animationConfigs); } else { setToPosition(proposedPosition); isAnimatedOnMount.value = true; } return; } /** * when evaluating the position while the bottom sheet is animating. */ if (animationStatus === _constants.ANIMATION_STATUS.RUNNING) { const nextPositionIndex = nextIndex ?? _constants2.INITIAL_VALUE; /** * when evaluating the position while the bottom sheet is * closing, then we force closing the bottom sheet with no animation. */ if (nextPositionIndex === -1 && !isInTemporaryPosition.value) { setToPosition(closedDetentPosition); return; } /** * when evaluating the position while it's animating to * a position other than the current position, then we * restart the animation. */ if (nextPositionIndex !== animatedCurrentIndex.value) { animateToPosition(detents[nextPositionIndex], source, undefined, animationConfigs); return; } } /** * when evaluating the position while the bottom sheet is in closed * position and not animating, we re-set the position to closed position. */ if (animationStatus !== _constants.ANIMATION_STATUS.RUNNING && animatedCurrentIndex.value === -1) { /** * early exit if reduce motion is enabled and index is out of sync with position. */ if (reduceMotion && detents[animatedIndex.value] !== animatedPosition.value) { return; } setToPosition(closedDetentPosition); return; } /** * when evaluating the position after the container resize, then we * force the bottom sheet to the proposed position with no * animation. */ if (animatedContainerHeightDidChange.value) { setToPosition(proposedPosition); return; } /** * we fall back to the proposed position. */ animateToPosition(proposedPosition, source, undefined, animationConfigs); }, [getEvaluatedPosition, animateToPosition, setToPosition, reduceMotion, animateOnMount, animatedAnimationState, animatedContainerHeightDidChange, animatedCurrentIndex, animatedIndex, animatedPosition, animatedDetentsState, isAnimatedOnMount, isInTemporaryPosition, isLayoutCalculated]); //#endregion //#region public methods const handleSnapToIndex = (0, _hooks.useStableCallback)(function handleSnapToIndex(index, animationConfigs) { const { detents } = animatedDetentsState.get(); const isLayoutReady = isLayoutCalculated.get(); if (detents === undefined || detents.length === 0) { return; } // early exit if layout is not ready yet. if (!isLayoutReady) { return; } (0, _invariant.default)(index >= -1 && index <= detents.length - 1, `'index' was provided but out of the provided snap points range! expected value to be between -1, ${detents.length - 1}`); if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleSnapToIndex', params: { index } }); } const targetPosition = detents[index]; /** * exit method if : * - layout is not calculated. * - already animating to next position. * - sheet is forced closing. */ const { nextPosition, nextIndex, isForcedClosing } = animatedAnimationState.get(); if (!isLayoutCalculated.value || index === nextIndex || targetPosition === nextPosition || isForcedClosing) { return; } /** * reset temporary position boolean. */ isInTemporaryPosition.value = false; (0, _reactNativeReanimated.runOnUI)(animateToPosition)(targetPosition, _constants.ANIMATION_SOURCE.USER, 0, animationConfigs); }); const handleSnapToPosition = (0, _react.useCallback)(function handleSnapToPosition(position, animationConfigs) { 'worklet'; if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleSnapToPosition', params: { position } }); } const { containerHeight } = animatedLayoutState.get(); /** * normalized provided position. */ const targetPosition = (0, _utilities.normalizeSnapPoint)(position, containerHeight); /** * exit method if : * - layout is not calculated. * - already animating to next position. * - sheet is forced closing. */ const { nextPosition, isForcedClosing } = animatedAnimationState.get(); if (!isLayoutCalculated || targetPosition === nextPosition || isForcedClosing) { return; } /** * mark the new position as temporary. */ isInTemporaryPosition.value = true; (0, _reactNativeReanimated.runOnUI)(animateToPosition)(targetPosition, _constants.ANIMATION_SOURCE.USER, 0, animationConfigs); }, [animateToPosition, isInTemporaryPosition, isLayoutCalculated, animatedLayoutState, animatedAnimationState]); const handleClose = (0, _react.useCallback)(function handleClose(animationConfigs) { if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleClose' }); } const closedDetentPosition = animatedDetentsState.get().closedDetentPosition; if (closedDetentPosition === undefined) { return; } const targetPosition = closedDetentPosition; /** * exit method if : * - layout is not calculated. * - already animating to next position. * - sheet is forced closing. */ const { nextPosition, isForcedClosing } = animatedAnimationState.get(); if (!isLayoutCalculated.value || targetPosition === nextPosition || isForcedClosing) { return; } /** * reset temporary position variable. */ isInTemporaryPosition.value = false; (0, _reactNativeReanimated.runOnUI)(animateToPosition)(targetPosition, _constants.ANIMATION_SOURCE.USER, 0, animationConfigs); }, [animateToPosition, isLayoutCalculated, isInTemporaryPosition, animatedDetentsState, animatedAnimationState]); const handleForceClose = (0, _react.useCallback)(function handleForceClose(animationConfigs) { if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleForceClose' }); } const closedDetentPosition = animatedDetentsState.get().closedDetentPosition; if (closedDetentPosition === undefined) { return; } const targetPosition = closedDetentPosition; /** * exit method if : * - already animating to next position. * - sheet is forced closing. */ const { nextPosition, isForcedClosing } = animatedAnimationState.get(); if (targetPosition === nextPosition || isForcedClosing) { return; } /** * reset temporary position variable. */ isInTemporaryPosition.value = false; /** * set force closing variable. */ animatedAnimationState.set(state => { 'worklet'; return { ...state, isForcedClosing: true }; }); (0, _reactNativeReanimated.runOnUI)(animateToPosition)(targetPosition, _constants.ANIMATION_SOURCE.USER, 0, animationConfigs); }, [animateToPosition, isInTemporaryPosition, animatedDetentsState, animatedAnimationState]); const handleExpand = (0, _react.useCallback)(function handleExpand(animationConfigs) { if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleExpand' }); } const { detents } = animatedDetentsState.get(); if (detents === undefined || detents.length === 0) { return; } const targetIndex = detents.length - 1; const targetPosition = detents[targetIndex]; /** * exit method if : * - layout is not calculated. * - already animating to next position. * - sheet is forced closing. */ const { nextPosition, nextIndex, isForcedClosing } = animatedAnimationState.get(); if (!isLayoutCalculated.value || targetIndex === nextIndex || targetPosition === nextPosition || isForcedClosing) { return; } /** * reset temporary position boolean. */ isInTemporaryPosition.value = false; (0, _reactNativeReanimated.runOnUI)(animateToPosition)(targetPosition, _constants.ANIMATION_SOURCE.USER, 0, animationConfigs); }, [animateToPosition, isInTemporaryPosition, isLayoutCalculated, animatedDetentsState, animatedAnimationState]); const handleCollapse = (0, _react.useCallback)(function handleCollapse(animationConfigs) { if (__DEV__) { (0, _utilities.print)({ component: 'BottomSheet', method: 'handleCollapse' }); } const { detents } = animatedDetentsState.get(); if (detents === undefined || detents.length === 0) { return; } const targetPosition = detents[0]; /** * exit method if : * - layout is not calculated. * - already animating to next position. * - sheet is forced closing. */ const { nextPosition, nextIndex, isForcedClosing } = animatedAnimationState.get(); if (!isLayoutCalculated || nextIndex === 0 || targetPosition === nextPosition || isForcedClosing) { return; } /** * reset temporary position boolean. */ isInTemporaryPosition.value = false; (0, _reactNativeReanimated.runOnUI)(animateToPosition)(targetPosition, _constants.ANIMATION_SOURCE.USER, 0, animationConfigs); }, [animateToPosition, isLayoutCalculated, isInTemporaryPosition, animatedDetentsState, animatedAnimationState]); (0, _react.useImperativeHandle)(ref, () => ({ snapToIndex: handleSnapToIndex, snapToPosition: handleSnapToPosition, expand: handleExpand, collapse: handleCollapse, close: handleClose, forceClose: handleForceClose })); //#endregion //#region contexts variables const internalContextVariables = (0, _react.useMemo)(() => ({ enableContentPanningGesture, enableDynamicSizing, overDragResistanceFactor, enableOverDrag, enablePanDownToClose, animatedAnimationState, animatedSheetState, animatedScrollableState, animatedScrollableStatus, animatedContentGestureState, animatedHandleGestureState, animatedKeyboardState, animatedLayoutState, animatedIndex, animatedPosition, animatedSheetHeight, animatedDetentsState, isInTemporaryPosition, simultaneousHandlers: _providedSimultaneousHandlers, waitFor: _providedWaitFor, activeOffsetX: _providedActiveOffsetX, activeOffsetY: _providedActiveOffsetY, failOffsetX: _providedFailOffsetX, failOffsetY: _providedFailOffsetY, enableBlurKeyboardOnGesture, animateToPosition, stopAnimation, setScrollableRef, removeScrollableRef }), [animatedIndex, animatedPosition, animatedSheetHeight, animatedLayoutState, animatedContentGestureState, animatedHandleGestureState, animatedAnimationState, animatedKeyboardState, animatedSheetState, animatedScrollableState, animatedScrollableStatus, animatedDetentsState, isInTemporaryPosition, enableContentPanningGesture, overDragResistanceFactor, enableOverDrag, enablePanDownToClose, enableDynamicSizing, enableBlurKeyboardOnGesture, _providedSimultaneousHandlers, _providedWaitFor, _providedActiveOffsetX, _providedActiveOffsetY, _providedFailOffsetX, _providedFailOffsetY, setScrollableRef, removeScrollableRef, animateToPosition, stopAnimation]); const externalContextVariables = (0, _react.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 effects (0, _reactNativeReanimated.useAnimatedReaction)(() => animatedLayoutState.get().containerHeight, (result, previous) => { if (result === _constants.INITIAL_LAYOUT_VALUE) { return; } animatedContainerHeightDidChange.value = result !== previous; const { closedDetentPosition } = animatedDetentsState.get(); if (closedDetentPosition === undefined) { return; } /** * When user close the bottom sheet while the keyboard open on Android with * software keyboard layout mode set to resize, the close position would be * set to the container height - the keyboard height, and when the keyboard * closes, the container height and here we restart the animation again. * * [read more](https://github.com/gorhom/react-native-bottom-sheet/issues/2163) */ const { status: animationStatus, source: animationSource, nextIndex } = animatedAnimationState.get(); if (animationStatus === _constants.ANIMATION_STATUS.RUNNING && animationSource === _constants.ANIMATION_SOURCE.GESTURE && nextIndex === -1) { animateToPosition(closedDetentPosition, _constants.ANIMATION_SOURCE.GESTURE); } }, [animatedContainerHeightDidChange, animatedAnimationState, animatedDetentsState]); /** * Reaction to the `snapPoints` change, to insure that the sheet position reflect * to the current point correctly. * * @alias OnSnapPointsChange */ (0, _reactNativeReanimated.useAnimatedReaction)(() => animatedDetentsState.get().detents, (result, previous) => { /** * if values did not change, and did handle on mount animation * then we early exit the method. */ if (JSON.stringify(result) === JSON.stringify(previous) && isAnimatedOnMount.value) { return; } /** * if layout is not calculated yet, then we exit the method. */ if (!isLayoutCalculated.value) { return; } if (__DEV__) { (0, _reactNativeReanimated.runOnJS)(_utilities.print)({ component: 'BottomSheet', method: 'useAnimatedReaction::OnSnapPointChange', category: 'effect', params: { result } }); } evaluatePosition(_constants.ANIMATION_SOURCE.SNAP_POINT_CHANGE); }, [isLayoutCalculated, isAnimatedOnMount, animatedDetentsState]); /** * Reaction to the keyboard state change. * * @alias OnKeyboardStateChange */ (0, _reactNativeReanimated.useAnimatedReaction)(() => animatedKeyboardState.get().status + animatedKeyboardState.get().height, (result, _previousResult) => { /** * if keyboard state is equal to the previous state, then exit the method */ if (result === _previousResult) { return; } const { status, height, easing, duration, target } = animatedKeyboardState.get(); /** * if state is undetermined, then we early exit. */ if (status === _constants.KEYBOARD_STATUS.UNDETERMINED) { return; } const { status: animationStatus, source: animationSource } = animatedAnimationState.get(); /** * if keyboard is hidden by customer gesture, then we early exit. */ if (status === _constants.KEYBOARD_STATUS.HIDDEN && animationStatus === _constants.ANIMATION_STATUS.RUNNING && animationSource === _constants.ANIMATION_SOURCE.GESTURE) { return; } if (__DEV__) { (0, _reactNativeReanimated.runOnJS)(_utilities.print)({ component: 'BottomSheet', method: 'useAnimatedReaction::OnKeyboardStateChange', category: 'effect', params: { status, height } }); } /** * Calculate the keyboard height in the container. */ const containerOffset = animatedLayoutState.get().containerOffset; let heightWithinContainer = height === 0 ? 0 : $modal ? Math.abs(height - Math.abs(bottomInset - containerOffset.bottom)) : Math.abs(height - containerOffset.bottom); /** * if platform is android and the input mode is resize, then exit the method */ if (_reactNative.Platform.OS === 'android' && android_keyboardInputMode === _constants.KEYBOARD_INPUT_MODE.adjustResize) { heightWithinContainer = 0; if (keyboardBehavior === _constants.KEYBOARD_BEHAVIOR.interactive) { animatedKeyboardState.set({ target, status, height, easing, duration, heightWithinContainer }); return; } } animatedKeyboardState.set({ target, status, height, easing, duration, heightWithinContainer }); /** * if user is interacting with sheet, then exit the method */ const hasActiveGesture = animatedContentGestureState.value === _reactNativeGestureHandler.State.ACTIVE || animatedContentGestureState.value === _reactNativeGestureHandler.State.BEGAN || animatedHandleGestureState.value === _reactNativeGestureHandler.State.ACTIVE || animatedHandleGestureState.value === _reactNativeGestureHandler.State.BEGAN; if (hasActiveGesture) { return; } /** * if new keyboard state is hidden and blur behavior is none, then exit the method */ if (status === _constants.KEYBOARD_STATUS.HIDDEN && keyboardBlurBehavior === _constants.KEYBOARD_BLUR_BEHAVIOR.none) { return; } const animationConfigs = (0, _utilities.getKeyboardAnimationConfigs)(easing, duration); evaluatePosition(_constants.ANIMATION_SOURCE.KEYBOARD, animationConfigs); }, [$modal, bottomInset, keyboardBehavior, keyboardBlurBehavior, android_keyboardInputMode, animatedKeyboardState, animatedLayoutState, getEvaluatedPosition]); /** * sets provided animated position */ (0, _reactNativeReanimated.useAnimatedReaction)(() => animatedPosition.value, _animatedPosition => { if (_providedAnimatedPosition) { _providedAnimatedPosition.value = _animatedPosition + topInset; } }, [_providedAnimatedPosition, topInset]); /** * sets provided animated index */ (0, _reactNativeReanimated.useAnimatedReaction)(() => animatedIndex.value, _animatedIndex => { if (_providedAnimatedIndex) { _providedAnimatedIndex.value = _animatedIndex; } }, [_providedAnimatedIndex]); /** * React to `index` prop to snap the sheet to the new position. * * @alias onIndexChange */ (0, _react.useEffect)(() => { // early exit, if animate on mount is set and it did not animate yet. if (animateOnMount && !isAnimatedOnMount.value) { return; } handleSnapToIndex(_providedIndex); }, [animateOnMount, _providedIndex, isAnimatedOnMount, handleSnapToIndex]); //#endregion // render return /*#__PURE__*/(0, _jsxRuntime.jsx)(_contexts.BottomSheetProvider, { value: externalContextVariables, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_contexts.BottomSheetInternalProvider, { value: internalContextVariables, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_bottomSheetGestureHandlersProvider.default, { gestureEventsHandlersHook: gestureEventsHandlersHook, children: [BackdropComponent ? /*#__PURE__*/(0, _jsxRuntime.jsx)(BackdropComponent, { animatedIndex: animatedIndex, animatedPosition: animatedPosition, style: _reactNative.StyleSheet.absoluteFillObject }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheetHostingContainer.BottomSheetHostingContainer, { shouldCalculateHeight: !$modal, layoutState: animatedLayoutState, containerLayoutState: containerLayoutState, topInset: topInset, bottomInset: bottomInset, detached: detached, style: _providedContainerStyle, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_BottomSheetBody.BottomSheetBody, { style: style, children: [backgroundComponent === null ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheetBackground.BottomSheetBackgroundContainer, { animatedIndex: animatedIndex, animatedPosition: animatedPosition, backgroundComponent: backgroundComponent, backgroundStyle: _providedBackgroundStyle }, "BottomSheetBackgroundContainer"), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_BottomSheetContent.BottomSheetContent, { pointerEvents: "box-none", accessible: _providedAccessible ?? undefined, accessibilityRole: _providedAccessibilityRole ?? undefined, accessibilityLabel: _providedAccessibilityLabel ?? undefined, keyboardBehavior: keyboardBehavior, detached: detached, children: [children, footerComponent ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheetFooter.BottomSheetFooterContainer, { footerComponent: footerComponent }) : null] }), handleComponent !== null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheetHandle.BottomSheetHandleContainer, { animatedIndex: animatedIndex, animatedPosition: animatedPosition, enableHandlePanningGesture: enableHandlePanningGesture, enableOverDrag: enableOverDrag, enablePanDownToClose: enablePanDownToClose, overDragResistanceFactor: overDragResistanceFactor, keyboardBehavior: keyboardBehavior, handleComponent: handleComponent, handleStyle: _providedHandleStyle, handleIndicatorStyle: _providedHandleIndicatorStyle }, "BottomSheetHandleContainer") : null] }) }, "BottomSheetContainer")] }) }) }); }); const BottomSheet = /*#__PURE__*/(0, _react.memo)(BottomSheetComponent); BottomSheet.displayName = 'BottomSheet'; var _default = exports.default = BottomSheet; //# sourceMappingURL=BottomSheet.js.map