Conversation
This PR fixes touch input handling for Pressable and TextInput components on Windows by: (1) detecting and cancelling stale active touches in onPointerPressed when a pointer ID is reused without a prior release, (2) synthesizing a touch-cancel when a pointer is released outside any view (tag == -1), and (3) emitting the onPressIn event from WindowsTextInputComponentView. The new DispatchSynthesizedTouchCancelForActiveTouch helper correctly scopes cancels to a single emitter (avoiding the previous spurious broadcast to all emitters), but its touches list is built per-emitter rather than from all active touches, which is inconsistent with DispatchTouchEvent and the W3C spec.
@microsoft-github-policy-service agree company="Virtual Fulfillment Technologies Inc." |
|
/azp run PR |
|
Azure Pipelines successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
Fixes several touch / pointer input edge cases in the Fabric Composition event pipeline on Windows, improving reliability for Pressable and TextInput interactions (e.g., pointer-id reuse, releases outside any view, and TextInput press-in behavior).
Changes:
- Cancel and clean up “stale” active touches when a pointer ID is reused without a prior release.
- Synthesize a touch-cancel when a pointer is released outside any view (tag == -1), and scope the cancel to the correct emitter.
- Update WindowsTextInput’s pointer message translation (mouse-style messages for non-mouse pointers) and fix submit-key modifier checks.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp | Adjusts pointer message dispatch for TextInput (including touch/pen), emits onPressIn, and fixes modifier handling for submit behavior. |
| vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h | Declares the new helper for synthesized touch-cancel dispatch. |
| vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp | Adds pointer-exited handling and introduces synthesized touch-cancel logic for stale touches / releases outside a view. |
| change/react-native-windows-83b6df1e-ce5d-4de3-9cf1-2a5b8f9b74f5.json | Adds a patch change entry documenting the touch input fix. |
| if (pp.PointerDeviceType() == winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType::Mouse) { | ||
| msg = WM_MOUSEMOVE; | ||
| wParam = PointerRoutedEventArgsToMouseWParam(args); | ||
| } else { | ||
| msg = WM_POINTERUPDATE; | ||
| wParam = PointerPointToPointerWParam(pp); | ||
| msg = WM_MOUSEMOVE; | ||
| wParam = PointerRoutedEventArgsToMouseWParam(args); | ||
| } |
There was a problem hiding this comment.
OnPointerMoved now sets msg = WM_MOUSEMOVE and wParam = PointerRoutedEventArgsToMouseWParam(args) in both branches of the PointerDeviceType check. The conditional is redundant as written and makes future edits risk diverging behavior unintentionally; consider collapsing to a single assignment (or reintroducing device-specific handling if still needed).
| facebook::react::TouchEvent touchEvent; | ||
| touchEvent.changedTouches.insert(cancelledTouch.touch); | ||
|
|
||
| for (const auto &pair : m_activeTouches) { | ||
| if (!pair.second.eventEmitter || pair.second.eventEmitter != cancelledTouch.eventEmitter) { | ||
| continue; | ||
| } | ||
|
|
||
| if (touchEvent.changedTouches.find(pair.second.touch) != touchEvent.changedTouches.end()) { | ||
| continue; | ||
| } | ||
|
|
||
| touchEvent.touches.insert(pair.second.touch); | ||
| } | ||
|
|
||
| for (const auto &pair : m_activeTouches) { | ||
| if (pair.second.eventEmitter == cancelledTouch.eventEmitter) { | ||
| touchEvent.targetTouches.insert(pair.second.touch); | ||
| } | ||
| } |
There was a problem hiding this comment.
DispatchSynthesizedTouchCancelForActiveTouch builds touchEvent.touches by filtering to touches that share the same eventEmitter as the cancelled touch. This makes synthesized onTouchCancel payloads inconsistent with DispatchTouchEvent, which populates touches from all active touches (minus the changed touch for end-ish events). It can break JS code that relies on touches representing all current screen touches per the TouchEvent contract. Consider constructing touchEvent.touches the same way as DispatchTouchEvent (across m_activeTouches, excluding the cancelled touch), while still dispatching only to cancelledTouch.eventEmitter and keeping targetTouches emitter-scoped.
Description
This PR fixes touch input handling for Pressable and TextInput components on Windows by: (1) detecting and cancelling stale active touches in onPointerPressed when a pointer ID is reused without a prior release, (2) synthesizing a touch-cancel when a pointer is released outside any view (tag == -1), and (3) emitting the onPressIn event from WindowsTextInputComponentView. The new DispatchSynthesizedTouchCancelForActiveTouch helper correctly scopes cancels to a single emitter (avoiding the previous spurious broadcast to all emitters), but its touches list is built per-emitter rather than from all active touches, which is inconsistent with DispatchTouchEvent and the W3C spec.
Type of Change
Why
Touch screens cannot focus on text input and have flaky pressable areas.
What
Improved ouch event handling
Changelog
Should this change be included in the release notes: yes
Fixed touch screen events not properly captured for pressable and text inputs
flowchart TD A[Pointer Event] --> B{PointerDeviceType?} B -->|Mouse| C[Switch on PointerUpdateKind] B -->|Touch / Pen| D[IsDoubleClick?] C --> E[WM_LBUTTONDOWN / DBLCLK / UP / RBUTTONUP etc.] D -->|Yes| F[WM_LBUTTONDBLCLK] D -->|No| G[WM_LBUTTONDOWN or WM_LBUTTONUP] E --> H[TxSendMessage to RichEdit] F --> H G --> H H --> I[args.Handled] A --> J[Emit GestureResponderEvent] J -->|Pressed| K[onPressIn] J -->|Released| L[onPressOut]%%{init: {'theme': 'neutral'}}%% flowchart TD A[onPointerPressed] --> B{Stale touch\nfor same pointer ID?} B -- Yes --> C[DispatchSynthesizedTouchCancelForActiveTouch\nstale touch] C --> D[Erase stale touch\nfrom m_activeTouches] D --> E[Register new touch &\nDispatchTouchEvent Start] B -- No --> E F[onPointerReleased] --> G{tag == -1?\nPointer outside view} G -- Yes --> H[DispatchSynthesizedTouchCancelForActiveTouch\nactive touch] H --> I[Erase touch & return] G -- No --> J[DispatchTouchEvent End\nErase touch] subgraph DispatchSynthesizedTouchCancelForActiveTouch K[Fire onPointerCancel\nvia HandleIncomingPointerEvent] --> L[Build TouchEvent:\nchangedTouches = cancelled touch\ntouches = same-emitter touches only ⚠️\ntargetTouches = same-emitter touches] L --> M[Fire onTouchCancel\non cancelled emitter only] endMicrosoft Reviewers: Open in CodeFlow