fix: share a single touch listener across press pseudo-selectors#9729
Merged
Conversation
A View exposes a single OnTouchListener slot, so attaching both :active and :active-deepest to one view made whichever selector registered second overwrite the first. Funnel both through one shared listener keyed by activeCallbacks and deepestCallbacks so they coexist.
MatiPl01
force-pushed
the
@matipl01/share-press-touch-listener
branch
from
June 22, 2026 01:31
e72ae7e to
e9e76cd
Compare
MatiPl01
marked this pull request as ready for review
June 22, 2026 01:51
tjzel
approved these changes
Jun 22, 2026
pull Bot
pushed a commit
to esinanturan/react-native-reanimated
that referenced
this pull request
Jun 25, 2026
> Stacked on software-mansion#9729 (shared press touch listener). The diff here is just the `:hover` additions on top of it; review/merge software-mansion#9729 first. ## Summary `:hover` was a no-op on touchscreens (the pointer recognizers never fire for a finger). This makes it respond to touch with the **sticky** model the major browser engines use: a tapped view gains `:hover` and keeps it after the finger lifts, clearing only when a later touch lands elsewhere or on scroll. `:active` stays press-only and distinct; real-pointer hover (trackpad/mouse/stylus) is unchanged. Native-only change (the pseudo-state flow is selector-agnostic). On each touch-down a `:hover` view is hovered when its on-screen bounds contain the point. iOS uses a shared coordinator watching the key window through a passive, non-recognizing gesture recognizer (so it never competes with the `:active` recognizers); Android recomputes per view plus a `Window.Callback` observer to catch taps on blank space. > A `:hover` style needs a non-zero `transitionDuration` to apply (zero-duration is a pre-existing no-op). ## Demo Tap the box: it turns green and stays green after you lift; tap elsewhere to clear. <!-- drag hover-touch-android.gif here --> <!-- drag hover-touch-ios.gif here --> --------- Co-authored-by: Jakub Wiśniewski <wisniewskij514@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jakub Wiśniewski <119816982+wisniewskij@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Android a
Viewexposes a singleOnTouchListenerslot.:activeand:active-deepesteach installed their own listener, so registering both on one view left only whichever attached last working.This routes both selectors through one shared listener (
ensureTouchListener), keyed byactiveCallbacksanddeepestCallbacks, so they coexist. The:active-deepestarbitration (hasDeepestDescendantAt) is unchanged.Android only: iOS already attaches a separate gesture recognizer per selector, so there is no collision there.
Test plan
On the Android emulator, on one view with both
:activeand:active-deepest, and on nested:active-deepestviews: both selectors now fire, and the deeper view still wins the arbitration.