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; }