From 36cd1056cd1b28693cb1a81e3f1e86d3f31367f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kelmanson?= Date: Fri, 19 Jun 2026 11:09:56 -0300 Subject: [PATCH] fix(android): touch interceptor must ignore pointerEvents none/box-none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SandboxTouchInterceptor.isObscuredAt` decided whether a sandbox is covered by an overlay using only the sibling's visibility + bounds — it never checked `pointerEvents`. React Native's AppContainer adds a full-screen `pointerEvents="box-none"` overlay in dev (LogBox / the element inspector), drawn on top of everything. That overlay was treated as obscuring, so every touch landing in a sandbox was rerouted via `redispatchWithHiddenSandbox` and the sandbox became untappable — but only in debug builds (release has no such overlay), which makes it very easy to miss. Fix: skip siblings whose `pointerEvents` is NONE (fully transparent to touch), and skip the own-bounds test for BOX_NONE (its own box passes touch through; only its children can catch). Descendant checks are unchanged. Follow-up to #33 (the interceptor introduced there). --- .../rnsandbox/SandboxTouchInterceptor.kt | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxTouchInterceptor.kt b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxTouchInterceptor.kt index df55509..c4699eb 100644 --- a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxTouchInterceptor.kt +++ b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxTouchInterceptor.kt @@ -6,6 +6,8 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.Window +import com.facebook.react.uimanager.PointerEvents +import com.facebook.react.views.view.ReactViewGroup import java.lang.ref.WeakReference /** @@ -338,16 +340,31 @@ object SandboxTouchInterceptor { if (!isDrawnAfter(group, i, childIndex)) continue if (sibling.visibility != View.VISIBLE) continue - // Check the sibling's own bounds - sibling.getLocationOnScreen(siblingLoc) - val sibRect = - Rect( - siblingLoc[0], - siblingLoc[1], - siblingLoc[0] + sibling.width, - siblingLoc[1] + sibling.height, - ) - if (sibRect.contains(screenX, screenY)) return true + // A view with pointerEvents NONE/BOX_NONE does not consume touch in + // its own box, so it does not obscure. This matters in debug builds: + // React Native's AppContainer adds a full-screen pointerEvents + // "box-none" overlay (for LogBox/the element inspector) drawn on top + // of everything. Without this check it is treated as an overlay and + // steals every touch that lands in a sandbox — the sandbox becomes + // untappable in dev (release has no such overlay, so it only repros + // in debug, which makes it easy to miss). + val pointerEvents = (sibling as? ReactViewGroup)?.pointerEvents + if (pointerEvents == PointerEvents.NONE) continue + + // Check the sibling's own bounds — only if the view itself catches + // touch. BOX_NONE lets the touch pass through its own box (only its + // children can catch), so skip the own-bounds test for it. + if (pointerEvents != PointerEvents.BOX_NONE) { + sibling.getLocationOnScreen(siblingLoc) + val sibRect = + Rect( + siblingLoc[0], + siblingLoc[1], + siblingLoc[0] + sibling.width, + siblingLoc[1] + sibling.height, + ) + if (sibRect.contains(screenX, screenY)) return true + } // Check descendants — catches overflow (e.g. a card with // negative margin extending outside its parent's bounds)