Skip to content

fix: cancel zombie touch state on ScrollView pointer capture loss#16100

Merged
acoates-ms merged 1 commit intomicrosoft:mainfrom
Virtual-Fulfillment-Technologies-Inc:vendora/scroll-touch-loss-fix
May 8, 2026
Merged

fix: cancel zombie touch state on ScrollView pointer capture loss#16100
acoates-ms merged 1 commit intomicrosoft:mainfrom
Virtual-Fulfillment-Technologies-Inc:vendora/scroll-touch-loss-fix

Conversation

@gmacmaster
Copy link
Copy Markdown
Contributor

@gmacmaster gmacmaster commented May 8, 2026

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

  • Bug fix (non-breaking change which fixes an issue)

Resolves #16047

What

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.

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

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.
@gmacmaster
Copy link
Copy Markdown
Contributor Author

@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

@acoates-ms
Copy link
Copy Markdown
Contributor

@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.

@acoates-ms
Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines
Copy link
Copy Markdown
Contributor

Azure Pipelines successfully started running 1 pipeline(s).

@gmacmaster
Copy link
Copy Markdown
Contributor Author

@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.

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

@gmacmaster
Copy link
Copy Markdown
Contributor Author

@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.

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

@acoates-ms acoates-ms merged commit 5213c8f into microsoft:main May 8, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scrolling Views not working with touch screen devices

2 participants