import { useImperativeHandle, type PointerEvent, forwardRef, useEffect, useState, useRef } from "react"; import { type LayoutChangeEvent, useWindowDimensions, PanResponder, ScrollView, Platform, Animated, Easing, View } from "react-native"; import type IBottomSheetProps from "./type"; import type { IBottomSheetRef } from "./type"; import stylesheet from "./stylesheet"; import { NCoreUIKitTheme } from "../../core/hooks"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { type RefForwardingComponent, type SafePointerEvent, safeGlobal } from "../../types"; import type { IModalRef } from "../modal/type"; import { webStyle, uuid } from "../../utils"; import Modal from "../modal"; const BottomSheet: RefForwardingComponent = ({ isContentReady: isContentReadyProp = true, renderBottom: RenderBottomComponent, renderHeader: RenderHeaderComponent, isForceFullScreenOnSwipe = false, handleContainerBackgroundColor, handleHeight: handleHeightProp, isCanFullScreenOnSwipe = false, isWrapSafeAreaContext = true, backgroundColor = "default", isWorkAsFullScreen = false, scrollEndThreshold = 0.85, isCloseOnOverlay = true, isWorkWithPortal = true, handleContainerSpacing, isActive: isActiveProp, handleBackgroundColor, isAutoHeight = false, isShowHandle = true, isSwipeClose = true, isCanSwipe = true, onOverlayPressed, scrollViewProps, scrollViewStyle, customTheme, onScrollEnd, modalProps, portalName, customKey, id: outID, snapPoint, children, onClosed, onOpened, onClose, onOpen, style, ...props }, ref) => { const { radiuses, colors, spaces } = NCoreUIKitTheme.useContext(customTheme); const { bottom, top } = useSafeAreaInsets(); const [ isMeasured, setIsMeasured ] = useState(false); const [ isActive, setIsActive ] = useState(isActiveProp === undefined ? false : isActiveProp); const { height: windowHeight, width: windowWidth } = useWindowDimensions(); const id = useRef(outID ? outID : uuid()); const bottomSheetKey = useRef(customKey ? customKey : uuid()); let bottomSafeArea = isWrapSafeAreaContext ? bottom : 0; let topSafeArea = isWrapSafeAreaContext ? top : 0; if(isForceFullScreenOnSwipe) { topSafeArea = 0; } if(!isWorkWithPortal) { bottomSafeArea = 0; topSafeArea = 0; } const scrollViewRef = useRef(null); const modalRef = useRef(null); const containerHeightRef = useRef(windowHeight); const animatedTranslateY = useRef(new Animated.Value(snapPoint && !isWorkAsFullScreen ? snapPoint : containerHeightRef.current)).current; const animatedHeight = useRef(new Animated.Value( isAutoHeight ? 0 : snapPoint ?? 0 )).current; const TOP_GRAB_AREA = 140; const maxHeight = useRef(isWorkAsFullScreen ? containerHeightRef.current - (isWrapSafeAreaContext ? topSafeArea : 0) : containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0)); const heightValue = useRef(isWorkAsFullScreen ? containerHeightRef.current : snapPoint ?? 0); const initialTranslateY = useRef(0); const translateYValue = useRef(0); const contentHeight = useRef(-1); const initialHeight = useRef(0); const scrollViewContentHeight = useRef(-1); const scrollViewLayoutHeight = useRef(-1); const initialScrollOffset = useRef(0); const scrollOffset = useRef(0); const gestureStartY = useRef(0); const isCanFullScreenOnSwipeRef = useRef(isCanFullScreenOnSwipe); const isWorkAsFullScreenRef = useRef(isWorkAsFullScreen); const isSwipeCloseRef = useRef(isSwipeClose); const isCanSwipeRef = useRef(isCanSwipe); const isContentReadyRef = useRef(isContentReadyProp); if(!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) { maxHeight.current = snapPoint; } useImperativeHandle( ref, () => ({ close: (onClosed?) => { closeAnimation(undefined, onClosed); }, setIsContentReady: (status) => { isContentReadyRef.current = status; }, open: () => { setIsActive(true); } }), [] ); useEffect(() => { isCanSwipeRef.current = isCanSwipe; }, [ isCanSwipe ]); useEffect(() => { isSwipeCloseRef.current = isSwipeClose; }, [ isSwipeClose ]); useEffect(() => { isCanFullScreenOnSwipeRef.current = isCanFullScreenOnSwipe; }, [ isCanFullScreenOnSwipe ]); useEffect(() => { isWorkAsFullScreenRef.current = isWorkAsFullScreen; }, [ isWorkAsFullScreen ]); useEffect(() => { isContentReadyRef.current = isContentReadyProp; }, [ isContentReadyProp ]); useEffect(() => { if (!isMeasured || contentHeight.current === -1) return; if (isCanFullScreenOnSwipe && !isWorkAsFullScreen) { maxHeight.current = containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : topSafeArea); } else if (isAutoHeight || !snapPoint) { maxHeight.current = contentHeight.current; } if (isActive) { open(); } }, [ isActive, isMeasured ]); if(isActiveProp !== undefined) { useEffect(() => { setIsActive(isActiveProp); }, [ isActiveProp ]); } useEffect(() => { if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) { maxHeight.current = snapPoint; } else if (!isWorkAsFullScreen) { maxHeight.current = containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0); } else { maxHeight.current = containerHeightRef.current - (isWrapSafeAreaContext ? topSafeArea : 0); } }, [ isForceFullScreenOnSwipe, isCanFullScreenOnSwipe, isWrapSafeAreaContext, isWorkAsFullScreen, topSafeArea, snapPoint ]); useEffect(() => { if (!isActive) { const newSnapValue = isWorkAsFullScreen ? containerHeightRef.current : (snapPoint ?? contentHeight.current); animatedHeight.setValue(isAutoHeight ? 0 : newSnapValue); animatedTranslateY.setValue(newSnapValue); heightValue.current = newSnapValue; translateYValue.current = newSnapValue; initialHeight.current = newSnapValue; } if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) { maxHeight.current = snapPoint; } else if (!isWorkAsFullScreen) { maxHeight.current = containerHeightRef.current - ( isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0 ); } else { maxHeight.current = containerHeightRef.current - ( isWrapSafeAreaContext ? topSafeArea : 0 ); } }, [ snapPoint, isWorkAsFullScreen ]); useEffect(() => { const listenerAHeightId = animatedHeight.addListener(({ value }) => { heightValue.current = value; }); const listenerTYId = animatedTranslateY.addListener(({ value }) => { translateYValue.current = value; }); return () => { animatedHeight.removeListener(listenerAHeightId); animatedTranslateY.removeListener(listenerTYId); }; }, []); const checkScrollThreshold = () => { if (!onScrollEnd) return; const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current); if (maxS <= 0) return; const ratio = scrollOffset.current / maxS; if (ratio >= scrollEndThreshold) { onScrollEnd(); } }; const openAnimation = (runIndex?: number) => { if(isContentReadyRef.current) { Animated.timing(animatedTranslateY, { useNativeDriver: false, duration: 300, toValue: 0 }).start(({ finished }) => { if(finished) { if(onOpened) onOpened(); } }); } else { if(!runIndex || runIndex < 10) { setTimeout(() => { openAnimation(runIndex ? runIndex + 1 : 1); }, 100); } } }; const open = () => { if (translateYValue.current < 0) { const safeValue = snapPoint ?? contentHeight.current; animatedTranslateY.setValue(safeValue); translateYValue.current = safeValue; } resetState(); if (isAutoHeight || !snapPoint) { animatedHeight.setValue(contentHeight.current); } if(onOpen) onOpen(); setTimeout(() => { openAnimation(); }, 100); }; const closeAnimation = (toValue?: number, _onClosed?: (props: { id: string; }) => void) => { if(onClose) onClose({ id: id.current }); const currentSnapPoint = isWorkAsFullScreen ? containerHeightRef.current : (isAutoHeight || !snapPoint ? contentHeight.current : snapPoint); Animated.timing(animatedTranslateY, { toValue: toValue ?? currentSnapPoint, useNativeDriver: false, duration: 300 }).start(({ finished }) => { if(finished) { resetState(); setIsActive(false); if(onClosed) onClosed({ id: id.current }); if(_onClosed) _onClosed({ id: id.current }); } }); }; const onLayout = (event: LayoutChangeEvent) => { const { height } = event.nativeEvent.layout; if (height === contentHeight.current) return; contentHeight.current = height; if(!isMeasured) { animatedHeight.setValue(height); setIsMeasured(true); } }; const resetState = () => { animatedHeight.setOffset(0); animatedTranslateY.setOffset(0); scrollOffset.current = 0; initialScrollOffset.current = 0; const pivot = isWorkAsFullScreen ? containerHeightRef.current : (isAutoHeight ? 0 : (snapPoint ?? contentHeight.current)); initialHeight.current = pivot; initialTranslateY.current = 0; heightValue.current = pivot; translateYValue.current = 0; }; const panResponder = useRef( PanResponder.create({ onPanResponderMove: (_, gestureState) => { if(!isCanSwipeRef.current) return; const { dy } = gestureState; const isAtTop = scrollOffset.current <= 0; const isAtTavan = heightValue.current >= maxHeight.current - 1; const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current; const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA; if (dy > 0 && isAtTavan && hasScroll && !isAtTop && !isFromTopArea) { const currentDelta = -dy; const initialS = initialScrollOffset.current; const usedForS = Math.max(currentDelta, -initialS); scrollOffset.current = initialS + usedForS; scrollViewRef.current?.scrollTo({ y: scrollOffset.current, animated: false }); return; } const delta = -dy; const pivot = snapPoint ?? contentHeight.current; const initialH = initialHeight.current; const initialS = initialScrollOffset.current; const initialT = initialTranslateY.current; const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current); if (dy < 0) { let currentDelta = delta; const effectiveInitialT = Math.max(0, initialT); const usedForT = Math.min(currentDelta, effectiveInitialT); animatedTranslateY.setValue(-usedForT); currentDelta -= usedForT; if (currentDelta > 0) { if (initialH < pivot) { const spaceToPivot = pivot - initialH; const usedForH = Math.min(currentDelta, spaceToPivot); animatedHeight.setValue(usedForH); currentDelta -= usedForH; } const isRestoringHeight = initialH < pivot; const canScroll = isCanFullScreenOnSwipeRef.current || isWorkAsFullScreenRef.current || !isRestoringHeight; if (currentDelta > 0 && canScroll) { const remainingScroll = maxS - initialS; if (remainingScroll > 0) { const usedForS = Math.min(currentDelta, remainingScroll); scrollOffset.current = initialS + usedForS; scrollViewRef.current?.scrollTo({ y: scrollOffset.current, animated: false }); currentDelta -= usedForS; animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0); } } if (currentDelta > 0) { if (isCanFullScreenOnSwipeRef.current) { const totalUsedBefore = (initialH < pivot ? (pivot - initialH) : 0); animatedHeight.setValue(totalUsedBefore + currentDelta); } else { animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0); } } } } else { let currentDelta = delta; if (initialH > pivot || initialS > 0) { if (initialS > 0) { const usedForS = Math.max(currentDelta, -initialS); scrollOffset.current = initialS + usedForS; scrollViewRef.current?.scrollTo({ y: scrollOffset.current, animated: false }); currentDelta -= usedForS; animatedHeight.setValue(initialH > pivot ? 0 : pivot - initialH); } if (currentDelta < 0 && initialH > pivot) { const distanceToPivot = pivot - initialH; const usedForH = Math.max(currentDelta, distanceToPivot); animatedHeight.setValue(usedForH); currentDelta -= usedForH; } } if (currentDelta < 0) { animatedHeight.setValue(initialH > pivot ? (pivot - initialH) : 0); animatedTranslateY.setValue(-currentDelta); scrollOffset.current = 0; scrollViewRef.current?.scrollTo({ animated: false, y: 0 }); } } }, onPanResponderEnd: (_, gestureState) => { if(!isCanSwipeRef.current) return; const isAtTop = scrollOffset.current <= 1; const isAtTavan = heightValue.current >= maxHeight.current - 1; const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current; const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA; if (isAtTavan && hasScroll && !isAtTop && !isFromTopArea) { const velocity = gestureState.vy; if (Math.abs(velocity) > 0.1) { const momentum = velocity * -350; const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current); const targetOffset = Math.max(0, Math.min(scrollOffset.current + momentum, maxS)); const scrollAnim = new Animated.Value(scrollOffset.current); scrollAnim.addListener(({ value }) => { scrollViewRef.current?.scrollTo({ animated: false, y: value }); scrollOffset.current = value; }); Animated.timing(scrollAnim, { duration: Math.min(Math.abs(momentum) * 2, 600), easing: Easing.out(Easing.quad), useNativeDriver: false, toValue: targetOffset }).start(() => { scrollAnim.removeAllListeners(); checkScrollThreshold(); }); } else { checkScrollThreshold(); } return; } const currentH = (animatedHeight as Animated.Value & { __getValue(): number; }).__getValue(); const currentT = (animatedTranslateY as Animated.Value & { __getValue(): number; }).__getValue(); animatedHeight.flattenOffset(); animatedTranslateY.flattenOffset(); heightValue.current = currentH; translateYValue.current = currentT; const pivot = snapPoint ?? contentHeight.current; const tavan = maxHeight.current; const isFastSwipeDown = gestureState.vy > 0.5; const isFastSwipeUp = gestureState.vy < -0.5; if (currentT <= Math.max(pivot * 0.2, 60)) { let toValue = pivot; if (isFastSwipeUp && isCanFullScreenOnSwipeRef.current) { toValue = tavan; } else if (isFastSwipeDown) { toValue = pivot; } else { if (!isCanFullScreenOnSwipeRef.current) { toValue = pivot; } else { const totalRange = tavan - pivot; const distFromPivot = currentH - pivot; if (distFromPivot < totalRange * 0.33) { toValue = pivot; } else if (distFromPivot > totalRange * 0.66) { toValue = tavan; } else { toValue = (tavan - currentH) < (currentH - pivot) ? tavan : pivot; } } } Animated.spring(animatedHeight, { useNativeDriver: false, toValue: toValue, friction: 10, tension: 40 }).start(); Animated.spring(animatedTranslateY, { useNativeDriver: false, friction: 10, tension: 40, toValue: 0 }).start(); } else { let toValueT = 0; let isClosing = false; if (!isSwipeCloseRef.current) { Animated.spring(animatedHeight, { useNativeDriver: false, toValue: pivot, friction: 10, tension: 40 }).start(); Animated.timing(animatedTranslateY, { useNativeDriver: false, duration: 300, toValue: 0 }).start(); } else { if (isFastSwipeDown) { toValueT = pivot + 100; isClosing = true; } else { if (currentT > pivot * 0.5) { toValueT = pivot + 100; isClosing = true; } else { toValueT = 0; isClosing = false; } } if(onClose) onClose({ id: id.current }); Animated.timing(animatedTranslateY, { useNativeDriver: false, toValue: toValueT, duration: 300 }).start(({ finished }) => { if (finished && isClosing) { setIsActive(false); if (onClosed) onClosed({ id: id.current }); } }); } } }, onMoveShouldSetPanResponderCapture: (_, gestureState) => { const { dy } = gestureState; if(!isCanSwipeRef.current) { return false; } if(Math.abs(dy) < 20) { return false; } return true; }, onPanResponderGrant: (evt) => { console.log("GRANT", evt.nativeEvent); if(!isCanSwipeRef.current) return; gestureStartY.current = evt.nativeEvent.pageY; animatedTranslateY.stopAnimation((currentY) => { translateYValue.current = currentY; }); animatedHeight.stopAnimation((currentH) => { heightValue.current = currentH; }); animatedTranslateY.flattenOffset(); animatedHeight.flattenOffset(); initialTranslateY.current = translateYValue.current; initialScrollOffset.current = scrollOffset.current; initialHeight.current = heightValue.current; animatedHeight.setOffset(heightValue.current); animatedHeight.setValue(0); animatedTranslateY.setOffset(translateYValue.current); animatedTranslateY.setValue(0); }, onPanResponderTerminationRequest: () => false, onStartShouldSetPanResponder: () => false, onShouldBlockNativeResponder: () => true }) ).current; const renderView = () => { return { const nativeEvt = evt as unknown as PointerEvent; const startY = nativeEvt.clientY; gestureStartY.current = nativeEvt.clientY; const initialT = translateYValue.current; const initialH = heightValue.current; const initialS = scrollOffset.current; animatedTranslateY.setOffset(initialT); animatedTranslateY.setValue(0); animatedHeight.setOffset(initialH); animatedHeight.setValue(0); let lastY = startY; let lastTime = Date.now(); let velocityY = 0; const handlePointerMove = (moveEvt: SafePointerEvent) => { if (!isCanSwipeRef.current) return; const dy = moveEvt.clientY - startY; if (Math.abs(dy) < 5) return; const now = Date.now(); const dt = now - lastTime; if (dt > 0) { velocityY = (moveEvt.clientY - lastY) / dt; } lastY = moveEvt.clientY; lastTime = now; const delta = -dy; const pivot = snapPoint ?? contentHeight.current; const isAtTavan = heightValue.current >= maxHeight.current - 1; const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current; const isAtTop = scrollOffset.current <= 0; const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA; const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current); if (dy > 0 && isAtTavan && hasScroll && !isAtTop && !isFromTopArea) { const usedForS = Math.max(-dy, -initialS); scrollOffset.current = initialS + usedForS; scrollViewRef.current?.scrollTo({ y: scrollOffset.current, animated: false }); return; } if (dy < 0) { let currentDelta = delta; const effectiveInitialT = Math.max(0, initialT); const usedForT = Math.min(currentDelta, effectiveInitialT); animatedTranslateY.setValue(-usedForT); currentDelta -= usedForT; if (currentDelta > 0) { if (initialH < pivot) { const spaceToPivot = pivot - initialH; const usedForH = Math.min(currentDelta, spaceToPivot); animatedHeight.setValue(usedForH); currentDelta -= usedForH; } const isRestoringHeight = initialH < pivot; const canScroll = isCanFullScreenOnSwipeRef.current || isWorkAsFullScreenRef.current || !isRestoringHeight; if (currentDelta > 0 && canScroll) { const remainingScroll = maxS - initialS; if (remainingScroll > 0) { const usedForS = Math.min(currentDelta, remainingScroll); scrollOffset.current = initialS + usedForS; scrollViewRef.current?.scrollTo({ y: scrollOffset.current, animated: false }); currentDelta -= usedForS; animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0); } } if (currentDelta > 0) { if (isCanFullScreenOnSwipeRef.current) { const totalUsedBefore = initialH < pivot ? (pivot - initialH) : 0; animatedHeight.setValue(totalUsedBefore + currentDelta); } else { animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0); } } } } else { let currentDelta = delta; if (initialH > pivot || initialS > 0) { if (initialS > 0) { const usedForS = Math.max(currentDelta, -initialS); scrollOffset.current = initialS + usedForS; scrollViewRef.current?.scrollTo({ y: scrollOffset.current, animated: false }); currentDelta -= usedForS; animatedHeight.setValue(initialH > pivot ? 0 : pivot - initialH); } if (currentDelta < 0 && initialH > pivot) { const distanceToPivot = pivot - initialH; const usedForH = Math.max(currentDelta, distanceToPivot); animatedHeight.setValue(usedForH); currentDelta -= usedForH; } } if (currentDelta < 0) { animatedHeight.setValue(initialH > pivot ? (pivot - initialH) : 0); animatedTranslateY.setValue(-currentDelta); scrollOffset.current = 0; scrollViewRef.current?.scrollTo({ animated: false, y: 0 }); } } }; const handlePointerUp = () => { safeGlobal.removeEventListener("pointermove", handlePointerMove); safeGlobal.removeEventListener("pointerup", handlePointerUp); animatedHeight.flattenOffset(); animatedTranslateY.flattenOffset(); const currentT = translateYValue.current; const currentH = heightValue.current; const pivot = snapPoint ?? contentHeight.current; const tavan = maxHeight.current; const isAtTavan = currentH >= tavan - 1; const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current; const isAtTop = scrollOffset.current <= 1; const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA; if (isAtTavan && hasScroll && !isAtTop && !isFromTopArea) { if (Math.abs(velocityY) > 0.1) { const momentum = velocityY * -350; const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current); const targetOffset = Math.max(0, Math.min(scrollOffset.current + momentum, maxS)); const scrollAnim = new Animated.Value(scrollOffset.current); scrollAnim.addListener(({ value }) => { scrollViewRef.current?.scrollTo({ animated: false, y: value }); scrollOffset.current = value; }); Animated.timing(scrollAnim, { duration: Math.min(Math.abs(momentum) * 2, 600), easing: Easing.out(Easing.quad), toValue: targetOffset, useNativeDriver: false }).start(() => { scrollAnim.removeAllListeners(); checkScrollThreshold(); }); } else { checkScrollThreshold(); } return; } const isFastSwipeDown = velocityY > 0.5; const isFastSwipeUp = velocityY < -0.5; if (currentT <= Math.max(pivot * 0.2, 60)) { let toValue = pivot; if (isFastSwipeUp && isCanFullScreenOnSwipeRef.current) { toValue = tavan; } else if (isFastSwipeDown) { toValue = pivot; } else { if (!isCanFullScreenOnSwipeRef.current) { toValue = pivot; } else { const totalRange = tavan - pivot; const distFromPivot = currentH - pivot; if (distFromPivot < totalRange * 0.33) toValue = pivot; else if (distFromPivot > totalRange * 0.66) toValue = tavan; else toValue = (tavan - currentH) < (currentH - pivot) ? tavan : pivot; } } Animated.spring(animatedHeight, { useNativeDriver: false, friction: 10, tension: 40, toValue }).start(); Animated.spring(animatedTranslateY, { useNativeDriver: false, friction: 10, tension: 40, toValue: 0 }).start(); } else { if (!isSwipeCloseRef.current) { Animated.spring(animatedHeight, { useNativeDriver: false, toValue: pivot, friction: 10, tension: 40 }).start(); Animated.timing(animatedTranslateY, { useNativeDriver: false, duration: 300, toValue: 0 }).start(); } else { let toValueT = 0; let isClosing = false; if (isFastSwipeDown) { toValueT = pivot + 100; isClosing = true; } else { if (currentT > pivot * 0.5) { toValueT = pivot + 100; isClosing = true; } else { toValueT = 0; isClosing = false; } } if (onClose) onClose({ id: id.current }); Animated.timing(animatedTranslateY, { useNativeDriver: false, toValue: toValueT, duration: 300 }).start(({ finished }) => { if (finished && isClosing) { setIsActive(false); if (onClosed) onClosed({ id: id.current }); } }); } } }; safeGlobal.addEventListener("pointermove", handlePointerMove); safeGlobal.addEventListener("pointerup", handlePointerUp); } : undefined} style={[ { borderTopStartRadius: radiuses.lg, borderTopEndRadius: radiuses.lg }, style, { height: (isAutoHeight || !snapPoint) && !isMeasured ? "auto" : animatedHeight, backgroundColor: colors.content.container[backgroundColor], paddingBottom: bottomSafeArea + spaces.spacingMd, opacity: isMeasured || !isAutoHeight ? 1 : 0, transform: [ { translateY: animatedTranslateY } ], paddingRight: spaces.spacingMd, paddingLeft: spaces.spacingMd, paddingTop: spaces.spacingMd, maxHeight: maxHeight.current }, stylesheet.container, webStyle({ touchAction: "none", userSelect: "none", cursor: "grab" }) ]} onLayout={onLayout} > {renderHeader()} { scrollViewLayoutHeight.current = e.nativeEvent.layout.height; }} style={[ scrollViewStyle, webStyle({ maxWidth: windowWidth > 500 ? 500 : undefined, width: windowWidth > 500 ? 500 : "100%", alignSelf: "center" }) ]} onStartShouldSetResponderCapture={() => false} onMoveShouldSetResponderCapture={() => false} onContentSizeChange={(_, h) => { scrollViewContentHeight.current = h; }} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} scrollEnabled={!isCanSwipe} scrollEventThrottle={1} overScrollMode="never" ref={scrollViewRef} bounces={false} > {children} {renderBottom()} {renderHandle()} ; }; const renderHeader = () => { if(!RenderHeaderComponent) { return null; } if(Platform.OS === "web") { return 500 ? 500 : undefined, width: windowWidth > 500 ? 500 : "100%", alignSelf: "center" }) ]} > {RenderHeaderComponent()} ; } return RenderHeaderComponent(); }; const renderBottom = () => { if(!RenderBottomComponent) { return null; } if(Platform.OS === "web") { return 500 ? 500 : undefined, width: windowWidth > 500 ? 500 : "100%", alignSelf: "center" }) ]} > ; } return ; }; const renderHandle = () => { if(!isShowHandle || isWorkAsFullScreen) { return null; } const handleHeight = handleHeightProp ? handleHeightProp : 8; const handleContainerHeight = handleHeight + ((handleContainerSpacing ? spaces[handleContainerSpacing] : spaces.spacingSm) * 2); const handleBorderRadius = Math.ceil(handleHeight / 2); return Platform.OS === "web"} onMoveShouldSetResponderCapture={() => Platform.OS === "web"} > ; }; return { const containerLayoutHeight = e.nativeEvent.layout.height; if (containerLayoutHeight && containerLayoutHeight !== containerHeightRef.current) { containerHeightRef.current = containerLayoutHeight; if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) { maxHeight.current = snapPoint; } else if (!isWorkAsFullScreen) { maxHeight.current = containerLayoutHeight - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0); } else { maxHeight.current = containerLayoutHeight; } } }} portalName={portalName ? portalName : "bottomSheet-system"} overlayProps={{ onStartShouldSetResponderCapture: () => false, onMoveShouldSetResponderCapture: () => false }} onOverlayPress={() => { if(onOverlayPressed) onOverlayPressed(); if(isCloseOnOverlay) { closeAnimation(); } }} isWorkWithPortal={isWorkWithPortal} key={`${bottomSheetKey}-modal`} style={[ { paddingTop: topSafeArea } ]} isContentRequired={false} isActive={isActive} alignContent="free" isAnimated={false} id={id.current} ref={modalRef} > {renderView()} ; }; export default forwardRef(BottomSheet);