Skip to content

fix(pointer): label a touch/pen contact as the primary button so JS press handling fires - #16338

Merged
Andrew Coates (acoates-ms) merged 1 commit into
microsoft:mainfrom
FacilitronWorks:fix/touch-primary-button-main
Aug 10, 2026
Merged

fix(pointer): label a touch/pen contact as the primary button so JS press handling fires#16338
Andrew Coates (acoates-ms) merged 1 commit into
microsoft:mainfrom
FacilitronWorks:fix/touch-primary-button-main

Conversation

@FaithfulAudio

@FaithfulAudio Collin Schneide (FaithfulAudio) commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

fix(pointer): label a touch/pen contact as the primary button so JS press handling fires

Tracks #16332. This is the main counterpart of the primary-button labeling change in #16333 (which targets 0.83-stable).

Problem

On real touch hardware, finger taps on Pressable / TouchableOpacity are ignored by pointer-event-driven press machinery, while identical mouse clicks on the same element work.

Root cause

In vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp, onPointerPressed maps ActiveTouch.button exclusively from PointerUpdateKind — a mouse-only concept:

default:
  activeTouch.button = -1;
  break;

A touch or pen contact has no mouse PointerUpdateKind, so it falls through to default: and gets button = -1; the derived W3C buttons bitmask then becomes 0. The dispatched pointerdown tells JS "no button is pressed", so press logic driven by pointer events discards finger contacts. Per W3C pointer-events, a touch/pen contact is the primary button: button 0, buttons 1.

Fix

After the existing switch: if the device is not a mouse and activeTouch.button fell through to a negative value, set activeTouch.button = 0 (yielding buttons = 1 while the contact is down). Mouse behaviour is unchanged, and the < 0 guard leaves untouched any non-mouse contact that somehow carried a real button mapping.

Baseline: main @ c69cf55f67f9b03f467502dac1007ac2d9ebe209. One hunk, +10 lines, no header or API change.

Scope — what this PR deliberately does not port

main already addresses the other problems #16333 covers, by different means, so this PR ports only the button-labeling change:

  • The tag == -1 release leak and the stale-pointer-reuse leak were fixed on main by Fix touch event handling, improve reliability, and optimize performance #16048 (Fix touch event handling, improve reliability, and optimize performance, merged 2026-04-24). Note main dispatches a synthesized touch Cancel for these, not a touch End.
  • main also cancels capture loss per-pointer and has onPointerRoutedAway.

#16333 additionally contains a cancel-all-active-touches loop in onPointerCaptureLost, an IsPrimary self-heal purge, and a stale-touch time backstop. None of those exist on main, and I have not assessed whether they are still needed theremain's at-source cleanups may subsume them. I have left them out rather than port hardening whose necessity I cannot demonstrate on this branch. Happy to do that assessment as a follow-up if it would be useful.

Validation

This exact logic — identical condition and assignment, the only textual difference being that this PR spells the enum via the Composition::Input:: alias main already uses in this function — is running in production as part of #16333 on RNW 0.83.2: compiled into Microsoft.ReactNative from source (UseExperimentalNuget=false), x64 Release, new-arch app (Facilitron FIT), on a physical Surface-class touch tablet. There, finger taps on press targets went from ignored to firing, matching mouse clicks. Validated with real PT_TOUCH injection (InitializeTouchInjection / InjectTouchInput), not simulated mouse input — the mouse path does not exercise the touch pipeline at all.

Diff verified against the pinned base: exactly one hunk, +10 lines, LF endings preserved, and the file's current tip is byte-identical to that base (no drift).

Not verified — please weigh before merging:

  • No CI has run on this branch. Azure Pipelines here is gated behind a maintainer comment, so I cannot start it; a /azp run would be appreciated.
  • I have not built main with this change, nor exercised it on a device on main. The behavioural evidence above comes from the 0.83.2 app.
  • Lint/format gates are unrun locally. The added block was written to match the file's existing style (the Composition::Input:: alias used elsewhere in the function; the condition line is 117 columns, within this repo's ColumnLimit: 120) — by inspection, not by running the tools.
  • That main's surrounding code is materially identical to the 0.83 code this was validated on rests on reading main's CompositionEventHandler.cpp at the baseline commit (same switch, same default: button = -1), not on differential testing on main.

Caveats for reviewers:

Microsoft Reviewers: Open in CodeFlow

onPointerPressed maps ActiveTouch.button exclusively from PointerUpdateKind, a
mouse-only concept. A touch or pen contact matches no case, falls through to
default: button = -1, and the derived W3C buttons bitmask becomes 0. The
pointerdown delivered to JS therefore claims no button is pressed, so
pointer-event-driven press handling discards finger contacts while identical
mouse clicks work.

Per W3C pointer-events a touch/pen contact IS the primary button: button 0,
buttons 1. Set that after the switch when the mouse mapping left it negative.

main counterpart of the button-labeling change in microsoft#16333 (0.83-stable), tracking
microsoft#16332. Ports only that change: main already covers the tag == -1 release leak
and the stale-pointer-reuse leak via microsoft#16048 (dispatching a synthesized touch
Cancel), and cancels capture loss per pointer. microsoft#16333's cancel-all loop,
IsPrimary purge and stale-touch backstop are deliberately not ported - they do
not exist on main and their necessity there has not been assessed.
@FaithfulAudio
Collin Schneide (FaithfulAudio) requested a review from a team as a code owner July 26, 2026 07:51
@azure-pipelines

Copy link
Copy Markdown
Contributor
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@acoates-ms

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Contributor
Azure Pipelines:
Successfully started running 1 pipeline(s).

@acoates-ms
Andrew Coates (acoates-ms) merged commit 5411eac into microsoft:main Aug 10, 2026
22 of 25 checks passed
Vladimir Morozov (vmoroz) pushed a commit that referenced this pull request Aug 21, 2026
* fix(scrollview): honor programmatic scrollTo when scrollEnabled={false} (#16336)

scrollEnabled={false} must only disable user scroll gestures, matching iOS and
Android where setContentOffset / scrollToOffset still work when scrolling is
disabled. The scrollTo command (and scrollToIndex / scrollToOffset, which route
through it) previously hit a scrollEnabled early-return and was silently
dropped. User-gesture input is gated separately via m_scrollVisual.ScrollEnabled
(set from scrollEnabled in updateProps), so honoring a programmatic scroll here
does not re-enable user scrolling.

main-branch twin of #16304 (0.83-stable).

* fix(textinput): correct placeholder layout constraints (px vs DIP) and no-op NaN fontSize guard (#16317)

* fix(textinput): correct placeholder layout constraints (px vs DIP) and no-op NaN fontSize guard

Forward-port of #16303 (0.83-stable) to main.

CreatePlaceholderLayout fed m_imgWidth/m_imgHeight - which are physical
pixels (frame * pointScaleFactor) - into LayoutConstraints, which are
expressed in DIPs. The placeholder was laid out in a box pointScaleFactor
times too large, so it measured and positioned at a different height than
the typed text. Divide by pointScaleFactor.

The NaN fontSize guard was also a no-op: it evaluated
defaultTextAttributes().fontSize as a discarded expression statement
instead of assigning it, so a placeholder with no fontSize never picked
up the default.

* add beachball change file

* Update release type to prerelease

Change type from 'patch' to 'prerelease' for react-native-windows.

---------

Co-authored-by: Andrew Coates <30809111+acoates-ms@users.noreply.github.com>

* fix(pointer): label a touch/pen contact as the primary button (#16338)

onPointerPressed maps ActiveTouch.button exclusively from PointerUpdateKind, a
mouse-only concept. A touch or pen contact matches no case, falls through to
default: button = -1, and the derived W3C buttons bitmask becomes 0. The
pointerdown delivered to JS therefore claims no button is pressed, so
pointer-event-driven press handling discards finger contacts while identical
mouse clicks work.

Per W3C pointer-events a touch/pen contact IS the primary button: button 0,
buttons 1. Set that after the switch when the mouse mapping left it negative.

main counterpart of the button-labeling change in #16333 (0.83-stable), tracking
#16332. Ports only that change: main already covers the tag == -1 release leak
and the stale-pointer-reuse leak via #16048 (dispatching a synthesized touch
Cancel), and cancels capture loss per pointer. #16333's cancel-all loop,
IsPrimary purge and stale-touch backstop are deliberately not ported - they do
not exist on main and their necessity there has not been assessed.

* fix(pointer): null-check the capturing component view before notifying OnPointerCaptureLost (#16337)

CapturePointer and releasePointerCapture look the capturing component up by its
cached m_pointerCapturingComponentTag and dereference the result unguarded. That
tag can outlive the component it names: when list/ScrollView virtualization
recycles the capturing row mid-pan, componentViewDescriptorWithTag returns a
descriptor whose .view is null, so winrt::get_self(...)->OnPointerCaptureLost()
dereferences null and terminates the process with 0xc0000005.

Null-check targetComponentView at both sites. Skipping the notify loses no state
transition: CapturePointer overwrites the stale tag immediately below, and
releasePointerCapture clears it via the existing m_capturedPointers.empty()
branch.

main twin of #16334 (0.83-stable).

* Fix modifying outline property

* delete createRnwApp tests in 0.81 branch

* Change files

---------

Co-authored-by: Collin Schneide <27441618+FaithfulAudio@users.noreply.github.com>
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.

2 participants