microsoft / microsoft/react-native-windows
Fabric touch input unreliable on real touch hardware: touch pointerdown carries button=-1 (presses ignored), and missed releases/capture loss strand active touches that wedge press recognition app-wide
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 17.3k
- Forks
- 1.2k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 33
Description
Problem Description
On real touch hardware (Surface-class tablet), Fabric/Composition touch input is unreliable in ways that mouse input is not — verified with synthetic touch injection (InjectTouchInput) vs mouse_event on the same controls in a production RNW 0.83.2 new-arch app:
-
Finger taps are ignored by press machinery while identical mouse clicks work. In
CompositionEventHandler::onPointerPressed,ActiveTouch.buttonis mapped fromPointerUpdateKind, which only has values for mouse buttons — a touch contact falls through tobutton = -1, andCreatePointerEventFromActiveTouchderivesbuttons = 0from it. The dispatchedpointerdowntherefore reports "no button pressed"; W3C semantics require a touch/pen contact to carry the primary button (button 0,buttons 1). Mouse (LeftButtonPressed →button 0) passes; fingers don't. -
Leaked
m_activeTouchesentries wedge press recognition app-wide. Several paths drop a touch without completing it:onPointerReleasedreturns beforem_activeTouches.erase(...)when the pointer-up hit-tests to no component (tag == -1) — common on touch when the finger drifts a few pixels.onPointerCaptureLostreleases pointer captures but never dispatches a touchCancelnor erases active touches — a release swallowed by direct manipulation / a popup surface strands the contact permanently.onPointerPressed's stale-touch guardreturns when a (leaked) entry with the same reused OS pointer id exists — silently discarding the new press forever.
A leaked entry is then included in every subsequent
DispatchTouchEvent(the dispatch loop iterates all ofm_activeTouches), so the JS responder sees each new tap as a corrupted multi-touch gesture with a phantom finger permanently down — presses animate (pressIn) but never fireonPress, or stop responding entirely, until app restart. Observed live via instrumentation: one orphanedtouchStart, then every later tap double-dispatching with the stale target riding along.
Steps To Reproduce
- New-architecture RNW 0.83.2 app on a touch device (no mouse).
- Tap any
Pressable/TouchableOpacity: with thebutton=-1dispatch, press handlers driven by pointer events don't fire for touch (mouse click on the same control works). - For the leak/wedge: press a control and slide the finger off before release (or press a control inside a ScrollView so direct manipulation steals the contact). Subsequent taps anywhere in the app stop triggering presses — the stranded active touch turns every tap into a phantom multi-touch.
Expected Results
- Touch contacts dispatch with W3C primary-button semantics (
button 0,buttons 1while down). - Every touch that starts is guaranteed to END or CANCEL — missed releases and capture loss must not strand active-touch entries, and a stranded entry must not permanently poison the responder.
CLI version
18.0.0
Environment
System:
OS: Windows 11 10.0.26200 (x64 Surface-class touch tablet)
Binaries:
Node: 22.x
Yarn: 4.5.1
npmPackages:
react-native: 0.83.2
react-native-windows: 0.83.2 (New Architecture / Fabric composition)
Microsoft.WindowsAppSDK: 1.8
Community Modules
Not relevant — reproduces with core Pressable/TouchableOpacity.
Target React Native Architecture
New Architecture (WinAppSDK) Only
Target Platform Version
10.0.22621
Visual Studio Version
Visual Studio 2022
Build Configuration
Release
Snack, code example, screenshot, or link to a repository
// On a touch tablet: taps animate but onPress never fires (button=-1 path),
// and after one finger-slide-off press, taps everywhere stop working (leak path).
<Pressable onPress={() => console.log('never logs on touch')}>
<Text>Tap me with a finger</Text>
</Pressable>
Context: found during a Windows hardening pass of a production RNW app (Facilitron FIT) on physical x64 touch hardware; diagnosed with a JS-side touch probe + native dispatch telemetry + synthetic touch injection. Fix PR from the same investigation to follow. Sibling issues from this hardening pass: #16305, #16306, #16307, #16308, #16315, #16316, #16331.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by locating CompositionEventHandler::onPointerPressed, onPointerReleased, onPointerCaptureLost, CreatePointerEventFromActiveTouch, and the m_activeTouches dispatch loop. Trace how touch button values and capture-loss or missed-release paths affect active-touch state. Done means touch pointer events use primary-button semantics and every contact ends or cancels without leaving later presses wedged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, react-native
- Domain
- frontend, mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100