/** * Native event handling - uses RNGH when available, falls back to responder system */ import { composeEventHandlers } from '@tamagui/helpers' import { getGestureHandler } from '@tamagui/native' import React, { useRef } from 'react' import { View } from 'react-native' import { useMainThreadPressEvents } from './helpers/mainThreadPressEvents' import type { StaticConfig, TamaguiComponentStateRef } from './types' const isFabric = !!(globalThis as any).nativeFabricUIManager const responderClaim = () => true const responderDeny = () => false const responderWrapperStyle = { display: 'contents' } as const // web events not used on native export function getWebEvents() { return {} } export function useEvents( events: any, viewProps: any, stateRef: { current: TamaguiComponentStateRef }, staticConfig: StaticConfig, isHOC?: boolean, isInsideNativeMenu?: boolean, debugName?: string | null, hasRealPressEvents?: boolean ) { // focus/blur events always attached directly if (events) { if (events.onFocus) { viewProps['onFocus'] = events.onFocus } if (events.onBlur) { viewProps['onBlur'] = events.onBlur } } // hasPressEvents includes events.onPress synthesized just for pressStyle // visuals; hasRealPressEvents is true only when the caller passed a real // handler. the distinction matters for arbitration: pressStyle-only gestures // must not steal ownership from a real-handler ancestor (e.g. Link asChild's // navigate handler merged onto a child View by Slot). const hasPressEvents = events?.onPress const gh = getGestureHandler() // track whether this component has ever had press events to keep hooks // ordering stable across renders (gesture is created once per mount). if (hasPressEvents) { stateRef.current.hasHadEvents = true } if (hasRealPressEvents) { stateRef.current.hasRealPressEvents = true } const everEnabled = Boolean(hasPressEvents || stateRef.current.hasHadEvents) const isUsingRNGH = gh.isEnabled // NOW handle early returns (after all hooks are called) // THESE BRANCHES ARE NEVER CHANGING RENDER-TO-RENDER // input special case - TextInput needs press events attached directly (not via RNGH) if (staticConfig.isInput) { if (events) { const { onPressIn, onPressOut, onPress } = events const inputEvents: any = { onPressIn, onPressOut: onPressOut || onPress, } if (onPressOut && onPress) { // only supports onPressIn and onPressOut so combine them inputEvents.onPressOut = composeEventHandlers(onPress, onPressOut) } Object.assign(viewProps, inputEvents) } // inputs don't use gesture handler return null } // HOC special case - pass press events to the inner component instead of wrapping // HOC components may return null which crashes GestureDetector (it tries to access // _internalInstanceHandle on a null native view). By passing events down, the inner // component handles gesture detection at its own level. // // Composite component special case - when styled() wraps a non-Tamagui component // (e.g. React.forwardRef), the elementType becomes that composite component. // GestureDetector/responder wrapping around a composite component breaks during // re-renders triggered by pressStyle state changes (the gesture/responder loses // attachment to the native view through the composite layers). Pass events as props // so they flow through to the inner native View. const isCompositeComponent = !isHOC && staticConfig.Component && typeof staticConfig.Component !== 'string' if (isHOC || isCompositeComponent) { if (events) { const { onPressIn, onPressOut, onPress, onLongPress, delayLongPress } = events Object.assign(viewProps, { onPressIn, onPressOut, onPress, onLongPress, delayLongPress, }) } // HOCs and composite components don't use gesture handler at this level return null } // rngh path - logic (hooks already called above) if (isUsingRNGH) { // rngh path - hooks const callbacksRef = useRef(isUsingRNGH ? {} : null) const gestureRef = useRef(null) if (everEnabled) { // store callbacks in refs so gesture doesn't need to be recreated on every render callbacksRef.current = hasPressEvents ? { onPressIn: events.onPressIn, onPressOut: events.onPressOut, onPress: events.onPress, onLongPress: events.onLongPress, } : {} // only create gesture once, callbacks are read from ref if (!gestureRef.current) { const { Gesture } = gh.state if (isInsideNativeMenu) { // Inside native menus on Android: use Manual gesture with manualActivation // so it never goes ACTIVE (which would send ACTION_CANCEL to MenuView). // Press callbacks fire via onTouchesDown/Up instead. const manual = Gesture.Manual() .runOnJS(true) .manualActivation(true) .onTouchesDown(() => { callbacksRef.current.onPressIn?.({}) }) .onTouchesUp(() => { callbacksRef.current.onPress?.({}) callbacksRef.current.onPressOut?.({}) }) .onTouchesCancelled(() => { callbacksRef.current.onPressOut?.({}) }) gestureRef.current = manual } else if (hasRealPressEvents || stateRef.current.hasRealPressEvents) { // real user handler: full PressGesture, participates in the press // ownership token system so nested real-handler children win // arbitration (NestedPressExclusive semantics). gestureRef.current = gh.createPressGesture({ debugName, onPressIn: (e: any) => callbacksRef.current.onPressIn?.(e), onPressOut: (e: any) => callbacksRef.current.onPressOut?.(e), onPress: (e: any) => callbacksRef.current.onPress?.(e), onLongPress: (e: any) => callbacksRef.current.onLongPress?.(e), delayLongPress: events?.delayLongPress, hitSlop: viewProps.hitSlop, }) } else { // pressStyle-only (events.onPress was synthesized to drive pressStyle // visuals, no user handler): use Manual + manualActivation. Touch // observation runs on the UI thread for fast pressStyle feedback, // but the gesture never activates → never claims responder/ownership, // so a real-handler ancestor still wins arbitration. This is the fix // for nested press scenarios like