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

Open
#16,332 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Invalid Triage Needs: Triage :mag:
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:

  1. Finger taps are ignored by press machinery while identical mouse clicks work. In CompositionEventHandler::onPointerPressed, ActiveTouch.button is mapped from PointerUpdateKind, which only has values for mouse buttons — a touch contact falls through to button = -1, and CreatePointerEventFromActiveTouch derives buttons = 0 from it. The dispatched pointerdown therefore 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.

  2. Leaked m_activeTouches entries wedge press recognition app-wide. Several paths drop a touch without completing it:

    • onPointerReleased returns before m_activeTouches.erase(...) when the pointer-up hit-tests to no component (tag == -1) — common on touch when the finger drifts a few pixels.
    • onPointerCaptureLost releases pointer captures but never dispatches a touch Cancel nor erases active touches — a release swallowed by direct manipulation / a popup surface strands the contact permanently.
    • onPointerPressed's stale-touch guard returns 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 of m_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 fire onPress, or stop responding entirely, until app restart. Observed live via instrumentation: one orphaned touchStart, then every later tap double-dispatching with the stale target riding along.

Steps To Reproduce
  1. New-architecture RNW 0.83.2 app on a touch device (no mouse).
  2. Tap any Pressable/TouchableOpacity: with the button=-1 dispatch, press handlers driven by pointer events don't fire for touch (mouse click on the same control works).
  3. 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 1 while 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.