import React, { useContext, useEffect, useMemo } from 'react'; import { Pressable, StyleSheet, View, useWindowDimensions, type StyleProp, type ViewStyle, } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { Extrapolate, interpolate, useAnimatedStyle, useDerivedValue, useSharedValue, withDecay, type SharedValue, } from 'react-native-reanimated'; import { ContextualContext } from './ContextualContext'; export interface ContextualRootViewProps
{
offsetTop?: number;
offsetBottom?: number;
offsetRight?: number;
background?: React.ReactNode;
view: React.ReactNode;
viewLayout: { x: number; y: number; width: number; height: number };
viewStyle: StyleProp ;
menuProps: P;
menuSize: { width: number; height: number };
anim: SharedValue (
props: ContextualRootViewProps
) {
const {
offsetTop: offsetTopProps,
offsetBottom: offsetBottomProps,
offsetRight: offsetRightProps,
background,
view,
viewLayout,
viewStyle,
Menu,
menuProps,
menuSize,
anim,
scroll,
scaleAnim,
} = props;
const offsetTop = offsetTopProps ?? 0;
const offsetBottom = offsetBottomProps ?? 0;
const offsetRight = offsetRightProps ?? 0;
const window = useWindowDimensions();
const context = useContext(ContextualContext);
const { closeContext } = context;
const maxScroll = useSharedValue(0);
const topAnim = useSharedValue(viewLayout.y);
const leftAnim = useSharedValue(viewLayout.x);
useEffect(() => {
const topSafe = offsetTop ?? 0;
const bottomSafe = offsetBottom ?? 0;
const safeHeight = window.height - topSafe - bottomSafe;
const fullHeight = viewLayout.height + menuSize.height;
if (fullHeight > safeHeight) {
maxScroll.value = fullHeight - safeHeight;
}
}, [
maxScroll,
menuSize.height,
offsetBottom,
offsetTop,
viewLayout.height,
window.height,
]);
const topStart = useDerivedValue(() => {
const topSafe = offsetTop ?? 0;
const bottomSafe = offsetBottom ?? 0;
let topResult = topAnim.value;
const isTopOverflow = topResult < topSafe;
if (isTopOverflow) {
topResult = topSafe;
}
const fullHeight = viewLayout.height + menuSize.height;
const isBottomOverflow =
topResult + fullHeight > window.height - bottomSafe;
if (isBottomOverflow) {
const delta = window.height - bottomSafe - topResult - fullHeight;
topResult = topResult + delta;
}
return topResult;
}, [offsetTop, offsetBottom, viewLayout]);
const menuWidth = useSharedValue(0);
useDerivedValue(() => {
'worklet';
topAnim.value = interpolate(
anim.value,
[0, 1],
[viewLayout.y, topStart.value],
Extrapolate.CLAMP
);
}, []);
const gesture = useMemo(
() =>
Gesture.Pan()
.onChange((e) => {
const newTop = scroll.value + e.changeY;
scroll.value = Math.min(maxScroll.value, Math.max(0, newTop));
})
.onEnd((e) => {
scroll.value = withDecay({
velocity: e.velocityY,
clamp: [0, maxScroll.value],
});
}),
[scroll, maxScroll]
);
const viewAnimStyle = useAnimatedStyle(
() => ({
position: 'absolute',
left: viewLayout.x,
top: topAnim.value + scroll.value,
transform: [{ scale: scaleAnim.value }],
}),
[scaleAnim, topAnim, leftAnim, scroll]
);
const menuStyle = useAnimatedStyle(
() => ({
transform: [
{
translateX: Math.min(
window.width - leftAnim.value - menuWidth.value - offsetRight,
0
),
},
],
}),
[menuWidth, leftAnim, window.width, offsetRight]
);
return (