import React, {
useCallback,
useContext,
useMemo,
useRef,
useState,
} from 'react';
import {
Platform,
StatusBar,
View,
type LayoutChangeEvent,
type StyleProp,
type ViewStyle,
} from 'react-native';
import Animated, {
runOnJS,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
import { ContextualContext } from './ContextualContext';
import { Waiter } from './utils';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
export type ContextualProps
= React.PropsWithChildren<{
style: StyleProp;
Menu: React.ComponentType;
menuProps: P | (() => P) | (() => Promise
);
onMenuPropsError?: () => void;
}>;
interface DataWaiter
{
anim: true;
view: { x: number; y: number; width: number; height: number };
menu: { width: number; height: number };
menuProps: P;
}
export function Contextual
(props: ContextualProps
) {
const {
style,
children,
Menu,
menuProps: menuPropsLocal,
onMenuPropsError,
} = props;
const context = useContext(ContextualContext);
const { anim, showContext, closeContext } = context;
const [isContexted, setContexted] = useState(false);
const [isTouchDown, setTouchDown] = useState(false);
const [menuProps, setMenuProps] = useState
();
const dataWaiter = useRef> | null>(null);
const viewRef = useRef(null);
const onLayoutMenu = useCallback(
(event: LayoutChangeEvent) => {
dataWaiter.current?.setData({
menu: {
width: normalizeSize(event.nativeEvent.layout.width),
height: normalizeSize(event.nativeEvent.layout.height),
},
});
setMenuProps(undefined);
},
[dataWaiter, setMenuProps]
);
const loadMenuProps = useCallback(async () => {
try {
const menuPropsValue =
menuPropsLocal instanceof Function
? await menuPropsLocal()
: menuPropsLocal;
dataWaiter.current?.setData({ menuProps: menuPropsValue });
setMenuProps(menuPropsValue);
} catch (e) {
onMenuPropsError?.();
}
}, [dataWaiter, menuPropsLocal, setMenuProps, onMenuPropsError]);
const onLongPress = useCallback(() => {
dataWaiter.current = new Waiter>(
{ anim: null, view: null, menu: null, menuProps: null },
(data) => {
setContexted(true);
showContext({
view: children,
viewLayout: {
x: data.view.x,
y: data.view.y,
width: data.view.width,
height: data.view.height,
},
viewStyle: style,
Menu,
menuProps: data.menuProps,
onClose: () => {
setContexted(false);
},
menuSize: { width: data.menu.width, height: data.menu.height },
});
}
);
setTouchDown(true);
loadMenuProps();
setTimeout(() => dataWaiter.current?.setData({ anim: true }), 100);
viewRef.current?.measureInWindow((x, y, width, height) => {
dataWaiter.current?.setData({
view: {
x,
y: y + (Platform.OS === 'android' ? StatusBar.currentHeight ?? 0 : 0),
width: normalizeSize(width),
height: normalizeSize(height),
},
});
});
}, [
dataWaiter,
viewRef,
style,
children,
Menu,
setTouchDown,
showContext,
setContexted,
loadMenuProps,
]);
const onTouchEnd = useCallback(() => setTouchDown(false), [setTouchDown]);
const animStyle = useAnimatedStyle(
() => ({
opacity: isContexted ? (anim.value < 0.1 ? 1 : 0) : 1,
transform: [
{
scale: isTouchDown
? withTiming(0.96, { duration: 300 })
: withTiming(1, { duration: 100 }),
},
],
}),
[isTouchDown, isContexted, anim]
);
const contextValue = useMemo(
() => ({ anim, showContext, closeContext, openContext: onLongPress }),
[anim, showContext, closeContext, onLongPress]
);
const gesture = useMemo(
() =>
Gesture.Pan()
.activateAfterLongPress(300)
.onStart(() => {
'worklet';
runOnJS(onLongPress)();
})
.onEnd(() => {
'worklet';
runOnJS(onTouchEnd)();
}),
[onLongPress, onTouchEnd]
);
return (
{children}
{isTouchDown && menuProps != null && (
)}
);
}
function normalizeSize(size: number) {
return Math.ceil(size * 1000) / 1000;
}