import React, { useCallback, useState } from 'react' import { LayoutChangeEvent, StyleSheet, View } from 'react-native' import { Gesture, GestureDetector } from 'react-native-gesture-handler' import Animated, { FadeOut, FadeIn } from 'react-native-reanimated' import { inputOverlayManager } from './store' import { DetectorProps, InterceptorProps, LayoutProps } from './types' export * from './store' /** * Wraps a screen area so that tapping anywhere outside an `InputOverlay.Layout` closes all open overlays. * `.runOnJS(true)` is mandatory because `inputOverlayManager.closeAll` mutates a nanostores atom on the JS thread. */ const Detector = ({ children, style, withWrapper = true }: DetectorProps) => { const tapGesture = Gesture.Tap() .onEnd((event) => { inputOverlayManager.closeAll() }) .runOnJS(true) if (!withWrapper) { {children} } return ( {children} ) } /** * Stops tap events from bubbling up to the `Detector` layer — without this, tapping inside a dropdown would immediately close it. * RNGH does not bubble gestures natively; this works because both gestures are `Tap` and RNGH resolves same-type competing gestures by which `GestureDetector` is closer in the tree. */ const Interceptor = ({ children }: InterceptorProps) => { const tapGesture = Gesture.Tap() .onEnd(() => { // Intercepts the tap to prevent it from propagating to the parent // Does nothing, just consumes the event }) .runOnJS(true) return ( {children} ) } const Layout = (props: LayoutProps) => { const { style, children, hideOverlay = false, isOpen, animationDuration = 150, mode = 'overlay', position, gap, content, id, } = props const [inputHeight, setInputHeight] = useState(0) const handleLayout = useCallback((event: LayoutChangeEvent) => { const height = event?.nativeEvent?.layout?.height ?? 0 setInputHeight(height) }, []) const isOverlayMode = mode === 'overlay' const shouldShowOverlay = isOpen && !hideOverlay const overlayStyle = isOverlayMode ? { position: 'absolute' as const, zIndex: 1, [position]: 0, top: inputHeight + gap, } : { marginTop: gap, } return ( {children} {shouldShowOverlay ? ( {content} ) : null} ) } const styles = StyleSheet.create({ detectorContainer: { flex: 1, }, layoutContainer: { position: 'relative', }, }) export const InputOverlay = { Detector, Interceptor, Layout, }