|
@@ -4,12 +4,14 @@ import {
|
|
|
forwardRef,
|
|
forwardRef,
|
|
|
useEffect,
|
|
useEffect,
|
|
|
useState,
|
|
useState,
|
|
|
- useRef
|
|
|
|
|
|
|
+ useRef,
|
|
|
|
|
+ type PointerEvent
|
|
|
} from "react";
|
|
} from "react";
|
|
|
import {
|
|
import {
|
|
|
type LayoutChangeEvent,
|
|
type LayoutChangeEvent,
|
|
|
PanResponder,
|
|
PanResponder,
|
|
|
ScrollView,
|
|
ScrollView,
|
|
|
|
|
+ Platform,
|
|
|
Animated,
|
|
Animated,
|
|
|
Easing,
|
|
Easing,
|
|
|
View
|
|
View
|
|
@@ -25,15 +27,19 @@ import {
|
|
|
import {
|
|
import {
|
|
|
useSafeAreaInsets
|
|
useSafeAreaInsets
|
|
|
} from "react-native-safe-area-context";
|
|
} from "react-native-safe-area-context";
|
|
|
-import type {
|
|
|
|
|
- RefForwardingComponent
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ safeGlobal,
|
|
|
|
|
+ webStyle,
|
|
|
|
|
+ type RefForwardingComponent,
|
|
|
|
|
+ type SafePointerEvent
|
|
|
} from "../../types";
|
|
} from "../../types";
|
|
|
import type {
|
|
import type {
|
|
|
IModalRef
|
|
IModalRef
|
|
|
} from "../modal/type";
|
|
} from "../modal/type";
|
|
|
import {
|
|
import {
|
|
|
windowHeight,
|
|
windowHeight,
|
|
|
- uuid
|
|
|
|
|
|
|
+ uuid,
|
|
|
|
|
+ windowWidth
|
|
|
} from "../../utils";
|
|
} from "../../utils";
|
|
|
import Modal from "../modal";
|
|
import Modal from "../modal";
|
|
|
|
|
|
|
@@ -419,6 +425,7 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
|
|
|
return true;
|
|
return true;
|
|
|
},
|
|
},
|
|
|
onPanResponderGrant: (evt) => {
|
|
onPanResponderGrant: (evt) => {
|
|
|
|
|
+ console.log("GRANT", evt.nativeEvent);
|
|
|
if(!isCanSwipeRef.current) return;
|
|
if(!isCanSwipeRef.current) return;
|
|
|
|
|
|
|
|
gestureStartY.current = evt.nativeEvent.pageY;
|
|
gestureStartY.current = evt.nativeEvent.pageY;
|
|
@@ -743,7 +750,274 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
|
|
|
const renderView = () => {
|
|
const renderView = () => {
|
|
|
return <Animated.View
|
|
return <Animated.View
|
|
|
{...props}
|
|
{...props}
|
|
|
- {...panResponder.panHandlers}
|
|
|
|
|
|
|
+ {...(Platform.OS === "web" ? {} : panResponder.panHandlers)}
|
|
|
|
|
+ onPointerDown={Platform.OS === "web" ? (evt) => {
|
|
|
|
|
+ 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({
|
|
|
|
|
+ y: 0,
|
|
|
|
|
+ animated: false
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ 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({
|
|
|
|
|
+ y: value,
|
|
|
|
|
+ animated: false
|
|
|
|
|
+ });
|
|
|
|
|
+ scrollOffset.current = value;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Animated.timing(scrollAnim, {
|
|
|
|
|
+ toValue: targetOffset,
|
|
|
|
|
+ duration: Math.min(Math.abs(momentum) * 2, 600),
|
|
|
|
|
+ easing: Easing.out(Easing.quad),
|
|
|
|
|
+ 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, {
|
|
|
|
|
+ toValue,
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ friction: 10,
|
|
|
|
|
+ tension: 40
|
|
|
|
|
+ }).start();
|
|
|
|
|
+ Animated.spring(animatedTranslateY, {
|
|
|
|
|
+ toValue: 0,
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ friction: 10,
|
|
|
|
|
+ tension: 40
|
|
|
|
|
+ }).start();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (!isSwipeCloseRef.current) {
|
|
|
|
|
+ Animated.spring(animatedHeight, {
|
|
|
|
|
+ toValue: pivot,
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ friction: 10,
|
|
|
|
|
+ tension: 40
|
|
|
|
|
+ }).start();
|
|
|
|
|
+ Animated.timing(animatedTranslateY, {
|
|
|
|
|
+ toValue: 0,
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ duration: 300
|
|
|
|
|
+ }).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, {
|
|
|
|
|
+ toValue: toValueT,
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ duration: 300
|
|
|
|
|
+ }).start(({
|
|
|
|
|
+ finished
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ if (finished && isClosing) {
|
|
|
|
|
+ setIsActive(false);
|
|
|
|
|
+ if (onClosed) onClosed({
|
|
|
|
|
+ id: id.current
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ safeGlobal.addEventListener("pointermove", handlePointerMove);
|
|
|
|
|
+ safeGlobal.addEventListener("pointerup", handlePointerUp);
|
|
|
|
|
+ } : undefined}
|
|
|
onLayout={onLayout}
|
|
onLayout={onLayout}
|
|
|
style={[
|
|
style={[
|
|
|
{
|
|
{
|
|
@@ -764,7 +1038,12 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
|
|
|
translateY: animatedTranslateY
|
|
translateY: animatedTranslateY
|
|
|
}]
|
|
}]
|
|
|
},
|
|
},
|
|
|
- stylesheet.container
|
|
|
|
|
|
|
+ stylesheet.container,
|
|
|
|
|
+ webStyle({
|
|
|
|
|
+ touchAction: "none",
|
|
|
|
|
+ userSelect: "none",
|
|
|
|
|
+ cursor: "grab"
|
|
|
|
|
+ })
|
|
|
]}
|
|
]}
|
|
|
>
|
|
>
|
|
|
{renderHeader()}
|
|
{renderHeader()}
|
|
@@ -786,7 +1065,12 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
|
|
|
ref={scrollViewRef}
|
|
ref={scrollViewRef}
|
|
|
bounces={false}
|
|
bounces={false}
|
|
|
style={[
|
|
style={[
|
|
|
- scrollViewStyle
|
|
|
|
|
|
|
+ scrollViewStyle,
|
|
|
|
|
+ webStyle({
|
|
|
|
|
+ maxWidth: windowWidth > 500 ? 500 : undefined,
|
|
|
|
|
+ width: windowWidth > 500 ? 500 : undefined,
|
|
|
|
|
+ alignSelf: "center"
|
|
|
|
|
+ })
|
|
|
]}
|
|
]}
|
|
|
>
|
|
>
|
|
|
{children}
|
|
{children}
|
|
@@ -801,6 +1085,20 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if(Platform.OS === "web") {
|
|
|
|
|
+ return <View
|
|
|
|
|
+ style={[
|
|
|
|
|
+ webStyle({
|
|
|
|
|
+ maxWidth: windowWidth > 500 ? 500 : undefined,
|
|
|
|
|
+ width: windowWidth > 500 ? 500 : undefined,
|
|
|
|
|
+ alignSelf: "center"
|
|
|
|
|
+ })
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ {RenderHeaderComponent()}
|
|
|
|
|
+ </View>;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return RenderHeaderComponent();
|
|
return RenderHeaderComponent();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -809,6 +1107,20 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if(Platform.OS === "web") {
|
|
|
|
|
+ return <View
|
|
|
|
|
+ style={[
|
|
|
|
|
+ webStyle({
|
|
|
|
|
+ maxWidth: windowWidth > 500 ? 500 : undefined,
|
|
|
|
|
+ width: windowWidth > 500 ? 500 : undefined,
|
|
|
|
|
+ alignSelf: "center"
|
|
|
|
|
+ })
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <RenderBottomComponent/>
|
|
|
|
|
+ </View>;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return <RenderBottomComponent/>;
|
|
return <RenderBottomComponent/>;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -822,9 +1134,8 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
|
|
|
const handleBorderRadius = Math.ceil(handleHeight / 2);
|
|
const handleBorderRadius = Math.ceil(handleHeight / 2);
|
|
|
|
|
|
|
|
return <View
|
|
return <View
|
|
|
- {...panResponder.panHandlers}
|
|
|
|
|
- onStartShouldSetResponderCapture={() => false}
|
|
|
|
|
- onMoveShouldSetResponderCapture={() => false}
|
|
|
|
|
|
|
+ onStartShouldSetResponderCapture={() => Platform.OS === "web"}
|
|
|
|
|
+ onMoveShouldSetResponderCapture={() => Platform.OS === "web"}
|
|
|
style={[
|
|
style={[
|
|
|
stylesheet.handleContainer,
|
|
stylesheet.handleContainer,
|
|
|
{
|
|
{
|
|
@@ -833,9 +1144,14 @@ const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> =
|
|
|
: "transparent",
|
|
: "transparent",
|
|
|
height: handleContainerHeight,
|
|
height: handleContainerHeight,
|
|
|
transform: [{
|
|
transform: [{
|
|
|
- translateY: -handleContainerHeight
|
|
|
|
|
|
|
+ translateY: -handleContainerHeight * (Platform.OS === "web" ? 1.75 : 1)
|
|
|
}]
|
|
}]
|
|
|
- }
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ webStyle({
|
|
|
|
|
+ touchAction: "none !important",
|
|
|
|
|
+ userSelect: "none",
|
|
|
|
|
+ cursor: "grab"
|
|
|
|
|
+ })
|
|
|
]}
|
|
]}
|
|
|
>
|
|
>
|
|
|
<View
|
|
<View
|