fix: cancel zombie touch state on ScrollView pointer capture loss#16100
Conversation
When a touch-screen user scrolls a ScrollView, the OS redirects the
pointer to the InteractionTracker via TryRedirectForManipulation and
fires PointerCaptureLost. The existing handler only cleaned up touches
when JS-level CapturePointer was active (m_pointerCapturingComponentTag
!= -1), which ScrollView never uses. This left a zombie entry in
m_activeTouches that kept Pressables visually stuck in a pressed state
and caused subsequent taps to replay events against the original target.
Three changes:
1. Extend onPointerCaptureLost to unconditionally cancel the active
touch for the specific pointer that lost capture, regardless of
whether JS-level CapturePointer was ever issued.
2. Remove the always-true fallback in IsPointerWithinInitialTree that
walked from activeTouch.touch.target (always the initial view) back
to initialTag, returning true on iteration 1 and bypassing the
correct W3C hit-test check. This caused onClick to fire even when
the pointer was released over a different target.
3. Scope per-pointer event dispatch in DispatchTouchEvent to only the
pointer that actually changed, instead of iterating every entry in
m_activeTouches. The old loop fired onPointerDown/Move/Up/Cancel
for all active touches, producing duplicated events in multi-touch
scenarios and replaying events on zombie targets.
|
@acoates-ms What is the best way for me to validate these changes within our app? Happy to do more testing/validation but so far I haven't been able to test in a dev build |
If you need to verify them in your specific app you can sync to a version of RNW that you are running, then build your changes on top of that, you can copy the Microsoft.ReactNative.dll into your apps install directory to verify the changes. Most testing you should be able to do using the test app within RNW - packages\e2e-test-app-fabric. You can either modify packages@react-native\tester\js\examples\Playground\RNTesterPlayground.js to be a temporary test case, or add additional test pages to the tester. Hopefully you can setup something that covers the general use case of your app. Additional test pages can be used for our CI tests - although I'm not sure if you can drive touch input through apium, so that might not be useful for these specific tests. |
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Thanks, I'll give that a shot. Not a need by any means, but was thinking it could be helpful to test in the context of our app when making changes |
In the meantime, this pr should fix our issues |
Description
When a touch-screen user scrolls a ScrollView, the OS redirects the
pointer to the InteractionTracker via TryRedirectForManipulation and
fires PointerCaptureLost. The existing handler only cleaned up touches
when JS-level CapturePointer was active (m_pointerCapturingComponentTag
!= -1), which ScrollView never uses. This left a zombie entry in
m_activeTouches that kept Pressables visually stuck in a pressed state
and caused subsequent taps to replay events against the original target.
Type of Change
Resolves #16047
What
Three changes:
Extend onPointerCaptureLost to unconditionally cancel the active touch for the specific pointer that lost capture, regardless of whether JS-level CapturePointer was ever issued.
Remove the always-true fallback in IsPointerWithinInitialTree that walked from activeTouch.touch.target (always the initial view) back to initialTag, returning true on iteration 1 and bypassing the correct W3C hit-test check. This caused onClick to fire even when the pointer was released over a different target.
Scope per-pointer event dispatch in DispatchTouchEvent to only the pointer that actually changed, instead of iterating every entry in m_activeTouches. The old loop fired onPointerDown/Move/Up/Cancel for all active touches, producing duplicated events in multi-touch scenarios and replaying events on zombie targets.
Changelog
Should this change be included in the release notes: yes
fix: cancel zombie touch state when ScrollView redirects pointer for manipulation, scope per-pointer events to the changed pointer, and remove always-true IsPointerWithinInitialTree fallback
Microsoft Reviewers: Open in CodeFlow