react-native-windows 0.84.0-preview.5 → 0.84.0-preview.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -250,7 +250,10 @@ void CompositionEventHandler::Initialize() noexcept {
250
250
  if (strongThis->SurfaceId() == -1)
251
251
  return;
252
252
 
253
- auto focusedComponent = strongThis->RootComponentView().GetFocusedComponent();
253
+ auto *rootView = strongThis->RootComponentView();
254
+ if (!rootView)
255
+ return;
256
+ auto focusedComponent = rootView->GetFocusedComponent();
254
257
  auto keyboardSource = winrt::make<CompositionInputKeyboardSource>(source);
255
258
  auto keyArgs =
256
259
  winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::KeyRoutedEventArgs>(
@@ -276,7 +279,10 @@ void CompositionEventHandler::Initialize() noexcept {
276
279
  if (strongThis->SurfaceId() == -1)
277
280
  return;
278
281
 
279
- auto focusedComponent = strongThis->RootComponentView().GetFocusedComponent();
282
+ auto *rootView = strongThis->RootComponentView();
283
+ if (!rootView)
284
+ return;
285
+ auto focusedComponent = rootView->GetFocusedComponent();
280
286
  auto keyboardSource = winrt::make<CompositionInputKeyboardSource>(source);
281
287
  auto keyArgs =
282
288
  winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::KeyRoutedEventArgs>(
@@ -303,7 +309,10 @@ void CompositionEventHandler::Initialize() noexcept {
303
309
  if (strongThis->SurfaceId() == -1)
304
310
  return;
305
311
 
306
- auto focusedComponent = strongThis->RootComponentView().GetFocusedComponent();
312
+ auto *rootView = strongThis->RootComponentView();
313
+ if (!rootView)
314
+ return;
315
+ auto focusedComponent = rootView->GetFocusedComponent();
307
316
  auto keyboardSource = winrt::make<CompositionInputKeyboardSource>(source);
308
317
  auto charArgs = winrt::make<
309
318
  winrt::Microsoft::ReactNative::Composition::Input::implementation::CharacterReceivedRoutedEventArgs>(
@@ -330,7 +339,10 @@ void CompositionEventHandler::Initialize() noexcept {
330
339
  if (strongThis->SurfaceId() == -1)
331
340
  return;
332
341
 
333
- auto focusedComponent = strongThis->RootComponentView().GetFocusedComponent();
342
+ auto *rootView = strongThis->RootComponentView();
343
+ if (!rootView)
344
+ return;
345
+ auto focusedComponent = rootView->GetFocusedComponent();
334
346
  if (focusedComponent) {
335
347
  auto tag =
336
348
  winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(focusedComponent)
@@ -358,6 +370,7 @@ CompositionEventHandler::~CompositionEventHandler() {
358
370
  pointerSource.PointerMoved(m_pointerMovedToken);
359
371
  pointerSource.PointerCaptureLost(m_pointerCaptureLostToken);
360
372
  pointerSource.PointerWheelChanged(m_pointerWheelChangedToken);
373
+ pointerSource.PointerExited(m_pointerExitedToken);
361
374
  auto keyboardSource = winrt::Microsoft::UI::Input::InputKeyboardSource::GetForIsland(island);
362
375
  keyboardSource.KeyDown(m_keyDownToken);
363
376
  keyboardSource.KeyUp(m_keyUpToken);
@@ -380,10 +393,15 @@ facebook::react::SurfaceId CompositionEventHandler::SurfaceId() const noexcept {
380
393
  return -1;
381
394
  }
382
395
 
383
- winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView &
396
+ winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView *
384
397
  CompositionEventHandler::RootComponentView() const noexcept {
385
398
  auto island = m_wkRootView.get();
386
- return *winrt::get_self<winrt::Microsoft::ReactNative::implementation::ReactNativeIsland>(island)->GetComponentView();
399
+ if (!island) {
400
+ return nullptr;
401
+ }
402
+ return winrt::get_self<winrt::Microsoft::ReactNative::implementation::ReactNativeIsland>(island)
403
+ ->GetComponentView()
404
+ .get();
387
405
  }
388
406
 
389
407
  void CompositionEventHandler::onPointerWheelChanged(
@@ -398,8 +416,11 @@ void CompositionEventHandler::onPointerWheelChanged(
398
416
 
399
417
  // In the case of a sub rootview, we may have a non-zero origin. hitTest takes a pt in the parent coords, so we
400
418
  // need to apply the current origin
401
- ptScaled += RootComponentView().layoutMetrics().frame.origin;
402
- auto tag = RootComponentView().hitTest(ptScaled, ptLocal);
419
+ auto *rootView = RootComponentView();
420
+ if (!rootView)
421
+ return;
422
+ ptScaled += rootView->layoutMetrics().frame.origin;
423
+ auto tag = rootView->hitTest(ptScaled, ptLocal);
403
424
 
404
425
  if (tag == -1)
405
426
  return;
@@ -553,7 +574,10 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w
553
574
  case WM_CHAR:
554
575
  case WM_SYSCHAR: {
555
576
  if (auto strongRootView = m_wkRootView.get()) {
556
- auto focusedComponent = RootComponentView().GetFocusedComponent();
577
+ auto *rootView = RootComponentView();
578
+ if (!rootView)
579
+ break;
580
+ auto focusedComponent = rootView->GetFocusedComponent();
557
581
  auto keyboardSource = winrt::make<CompositionKeyboardSource>(this);
558
582
  auto args = winrt::make<
559
583
  winrt::Microsoft::ReactNative::Composition::Input::implementation::CharacterReceivedRoutedEventArgs>(
@@ -576,7 +600,10 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w
576
600
  case WM_SYSKEYDOWN:
577
601
  case WM_SYSKEYUP: {
578
602
  if (auto strongRootView = m_wkRootView.get()) {
579
- auto focusedComponent = RootComponentView().GetFocusedComponent();
603
+ auto *rootView = RootComponentView();
604
+ if (!rootView)
605
+ break;
606
+ auto focusedComponent = rootView->GetFocusedComponent();
580
607
  auto keyboardSource = winrt::make<CompositionKeyboardSource>(this);
581
608
  auto args = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::KeyRoutedEventArgs>(
582
609
  focusedComponent
@@ -608,9 +635,12 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w
608
635
 
609
636
  void CompositionEventHandler::onKeyDown(
610
637
  const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
611
- RootComponentView().UseKeyboardForProgrammaticFocus(true);
638
+ auto *rootView = RootComponentView();
639
+ if (!rootView)
640
+ return;
641
+ rootView->UseKeyboardForProgrammaticFocus(true);
612
642
 
613
- if (auto focusedComponent = RootComponentView().GetFocusedComponent()) {
643
+ if (auto focusedComponent = rootView->GetFocusedComponent()) {
614
644
  winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(focusedComponent)->OnKeyDown(args);
615
645
 
616
646
  if (args.Handled())
@@ -633,7 +663,7 @@ void CompositionEventHandler::onKeyDown(
633
663
  }
634
664
 
635
665
  if (!fCtrl && args.Key() == winrt::Windows::System::VirtualKey::Tab) {
636
- if (RootComponentView().TryMoveFocus(!fShift, winrt::Microsoft::ReactNative::FocusState::Keyboard)) {
666
+ if (rootView->TryMoveFocus(!fShift, winrt::Microsoft::ReactNative::FocusState::Keyboard)) {
637
667
  args.Handled(true);
638
668
  }
639
669
 
@@ -643,9 +673,12 @@ void CompositionEventHandler::onKeyDown(
643
673
 
644
674
  void CompositionEventHandler::onKeyUp(
645
675
  const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
646
- RootComponentView().UseKeyboardForProgrammaticFocus(true);
676
+ auto *rootView = RootComponentView();
677
+ if (!rootView)
678
+ return;
679
+ rootView->UseKeyboardForProgrammaticFocus(true);
647
680
 
648
- if (auto focusedComponent = RootComponentView().GetFocusedComponent()) {
681
+ if (auto focusedComponent = rootView->GetFocusedComponent()) {
649
682
  winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(focusedComponent)->OnKeyUp(args);
650
683
 
651
684
  if (args.Handled())
@@ -655,7 +688,10 @@ void CompositionEventHandler::onKeyUp(
655
688
 
656
689
  void CompositionEventHandler::onCharacterReceived(
657
690
  const winrt::Microsoft::ReactNative::Composition::Input::CharacterReceivedRoutedEventArgs &args) noexcept {
658
- if (auto focusedComponent = RootComponentView().GetFocusedComponent()) {
691
+ auto *rootView = RootComponentView();
692
+ if (!rootView)
693
+ return;
694
+ if (auto focusedComponent = rootView->GetFocusedComponent()) {
659
695
  winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(focusedComponent)
660
696
  ->OnCharacterReceived(args);
661
697
 
@@ -664,7 +700,7 @@ void CompositionEventHandler::onCharacterReceived(
664
700
  }
665
701
  }
666
702
 
667
- std::vector<winrt::Microsoft::ReactNative::ComponentView> GetTouchableViewsInPathToRoot(
703
+ std::vector<winrt::Microsoft::ReactNative::ComponentView> CompositionEventHandler::GetTouchableViewsInPathToRoot(
668
704
  const winrt::Microsoft::ReactNative::ComponentView &componentView) {
669
705
  std::vector<winrt::Microsoft::ReactNative::ComponentView> results;
670
706
  auto view = componentView;
@@ -674,6 +710,7 @@ std::vector<winrt::Microsoft::ReactNative::ComponentView> GetTouchableViewsInPat
674
710
  }
675
711
  view = view.Parent();
676
712
  }
713
+
677
714
  return results;
678
715
  }
679
716
 
@@ -974,8 +1011,8 @@ void CompositionEventHandler::UpdateActiveTouch(
974
1011
  // activeTouch.touch.isEraser = false;
975
1012
  activeTouch.touch.pagePoint.x = ptScaled.x;
976
1013
  activeTouch.touch.pagePoint.y = ptScaled.y;
977
- activeTouch.touch.screenPoint.x = ptLocal.x;
978
- activeTouch.touch.screenPoint.y = ptLocal.y;
1014
+ activeTouch.touch.screenPoint.x = ptScaled.x;
1015
+ activeTouch.touch.screenPoint.y = ptScaled.y;
979
1016
  activeTouch.touch.offsetPoint.x = ptLocal.x;
980
1017
  activeTouch.touch.offsetPoint.y = ptLocal.y;
981
1018
  activeTouch.touch.timestamp = static_cast<facebook::react::Float>(
@@ -1028,9 +1065,12 @@ void CompositionEventHandler::getTargetPointerArgs(
1028
1065
 
1029
1066
  // In the case of a sub rootview, we may have a non-zero origin. hitTest takes a pt in the parent coords, so we need
1030
1067
  // to apply the current origin
1031
- ptScaled += RootComponentView().layoutMetrics().frame.origin;
1068
+ auto *rootView = RootComponentView();
1069
+ if (!rootView)
1070
+ return;
1071
+ ptScaled += rootView->layoutMetrics().frame.origin;
1032
1072
 
1033
- if (std::find(m_capturedPointers.begin(), m_capturedPointers.end(), pointerId) != m_capturedPointers.end()) {
1073
+ if (m_capturedPointers.count(pointerId)) {
1034
1074
  assert(m_pointerCapturingComponentTag != -1);
1035
1075
  tag = m_pointerCapturingComponentTag;
1036
1076
 
@@ -1042,7 +1082,7 @@ void CompositionEventHandler::getTargetPointerArgs(
1042
1082
  ptLocal.y = ptScaled.y - (clientRect.top / strongRootView.ScaleFactor());
1043
1083
  }
1044
1084
  } else {
1045
- tag = RootComponentView().hitTest(ptScaled, ptLocal);
1085
+ tag = rootView->hitTest(ptScaled, ptLocal);
1046
1086
  }
1047
1087
  }
1048
1088
 
@@ -1054,7 +1094,7 @@ void CompositionEventHandler::onPointerCaptureLost(
1054
1094
 
1055
1095
  if (m_pointerCapturingComponentTag) {
1056
1096
  // copy array to avoid iterator being invalidated during deletion
1057
- std::vector<PointerId> capturedPointers = m_capturedPointers;
1097
+ std::unordered_set<PointerId> capturedPointers = m_capturedPointers;
1058
1098
 
1059
1099
  for (auto pointerId : capturedPointers) {
1060
1100
  releasePointerCapture(pointerId, m_pointerCapturingComponentTag);
@@ -1101,10 +1141,11 @@ void CompositionEventHandler::onPointerMoved(
1101
1141
 
1102
1142
  auto handler = [&, targetView, pointerEvent, isActiveTouch](
1103
1143
  std::vector<winrt::Microsoft::ReactNative::ComponentView> &eventPathViews) {
1144
+ auto *rootViewForEmitter = RootComponentView();
1104
1145
  const auto eventEmitter = targetView
1105
1146
  ? winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(targetView)
1106
1147
  ->eventEmitterAtPoint(pointerEvent.offsetPoint)
1107
- : RootComponentView().eventEmitterAtPoint(pointerEvent.offsetPoint);
1148
+ : (rootViewForEmitter ? rootViewForEmitter->eventEmitterAtPoint(pointerEvent.offsetPoint) : nullptr);
1108
1149
 
1109
1150
  if (eventEmitter != nullptr) {
1110
1151
  eventEmitter->onPointerMove(pointerEvent);
@@ -1130,7 +1171,10 @@ void CompositionEventHandler::ClearAllHoveredForPointer(const facebook::react::P
1130
1171
  // events. If we get null for the targetView, that means that the mouse is no over any components, so we have no
1131
1172
  // element to send the move event to. However we need to send something so that any previously hovered elements
1132
1173
  // are no longer hovered.
1133
- auto children = RootComponentView().Children();
1174
+ auto *rootView = RootComponentView();
1175
+ if (!rootView)
1176
+ return;
1177
+ auto children = rootView->Children();
1134
1178
  if (auto size = children.Size()) {
1135
1179
  auto firstChild = children.GetAt(0);
1136
1180
  if (auto childEventEmitter =
@@ -1170,28 +1214,67 @@ void CompositionEventHandler::onPointerExited(
1170
1214
  }
1171
1215
  }
1172
1216
 
1217
+ // Windows touch pointer IDs can be arbitrarily large (e.g. 2233). React Native's JS
1218
+ // touch handler uses identifiers as array indices and warns/misbehaves for values > 20.
1219
+ // This function maps each live Windows pointer to a small identifier in [0, 19] by
1220
+ // scanning m_activeTouches for in-use slots and cycling from the last assigned index.
1221
+ // Identifier MOUSE_POINTER_ID (1) is permanently reserved for mouse and never returned here.
1222
+ int CompositionEventHandler::AllocateTouchIdentifier() noexcept {
1223
+ constexpr int kMaxTouchIdentifier = 20;
1224
+ for (int i = 0; i < kMaxTouchIdentifier; i++) {
1225
+ int candidate = (m_touchId + i) % kMaxTouchIdentifier;
1226
+ if (candidate == static_cast<int>(MOUSE_POINTER_ID)) {
1227
+ continue; // reserved for mouse
1228
+ }
1229
+ bool inUse = std::any_of(m_activeTouches.begin(), m_activeTouches.end(), [candidate](const auto &pair) {
1230
+ return pair.second.touch.identifier == candidate;
1231
+ });
1232
+ if (!inUse) {
1233
+ m_touchId = (candidate + 1) % kMaxTouchIdentifier;
1234
+ return candidate;
1235
+ }
1236
+ }
1237
+ // All non-mouse slots occupied (> 19 simultaneous touch/pen points) — wrap anyway,
1238
+ // skipping the mouse-reserved slot.
1239
+ int fallback = m_touchId;
1240
+ m_touchId = (m_touchId + 1) % kMaxTouchIdentifier;
1241
+ if (fallback == static_cast<int>(MOUSE_POINTER_ID)) {
1242
+ fallback = m_touchId;
1243
+ m_touchId = (m_touchId + 1) % kMaxTouchIdentifier;
1244
+ }
1245
+ return fallback;
1246
+ }
1247
+
1173
1248
  void CompositionEventHandler::onPointerPressed(
1174
1249
  const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
1175
1250
  winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
1176
1251
  namespace Composition = winrt::Microsoft::ReactNative::Composition;
1177
1252
 
1178
- RootComponentView().UseKeyboardForProgrammaticFocus(false);
1253
+ auto *rootView = RootComponentView();
1254
+ if (!rootView)
1255
+ return;
1256
+ rootView->UseKeyboardForProgrammaticFocus(false);
1179
1257
 
1180
1258
  // Clears any active text selection when left pointer is pressed
1181
1259
  if (pointerPoint.Properties().PointerUpdateKind() != Composition::Input::PointerUpdateKind::RightButtonPressed) {
1182
- RootComponentView().ClearCurrentTextSelection();
1260
+ rootView->ClearCurrentTextSelection();
1183
1261
  }
1184
1262
 
1185
1263
  PointerId pointerId = pointerPoint.PointerId();
1186
1264
 
1187
- auto staleTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) {
1188
- return pair.second.touch.identifier == pointerId;
1189
- });
1265
+ auto staleTouch = m_activeTouches.find(pointerId);
1190
1266
 
1191
1267
  if (staleTouch != m_activeTouches.end()) {
1192
- // A pointer with this ID already exists - Should we fire a button cancel or something?
1193
- // assert(false);
1194
- return;
1268
+ // A previous pointer with this ID was never properly released (e.g., app lost focus,
1269
+ // pointer left window). Cancel the stale touch and clean it up so the new press can proceed.
1270
+ // Copy and erase before dispatching to avoid holding a reference into m_activeTouches
1271
+ // across DispatchSynthesizedTouchCancelForActiveTouch, which calls HandleIncomingPointerEvent
1272
+ // and iterates m_activeTouches internally.
1273
+ ActiveTouch staleTouchCopy = std::move(staleTouch->second);
1274
+ m_activeTouches.erase(staleTouch);
1275
+ if (staleTouchCopy.eventEmitter) {
1276
+ DispatchSynthesizedTouchCancelForActiveTouch(staleTouchCopy, pointerPoint, keyModifiers);
1277
+ }
1195
1278
  }
1196
1279
 
1197
1280
  const auto eventType = TouchEventType::Start;
@@ -1212,7 +1295,18 @@ void CompositionEventHandler::onPointerPressed(
1212
1295
  ->OnPointerPressed(args);
1213
1296
 
1214
1297
  ActiveTouch activeTouch{0};
1215
- activeTouch.touchType = UITouchType::Mouse;
1298
+ switch (pointerPoint.PointerDeviceType()) {
1299
+ case Composition::Input::PointerDeviceType::Touch:
1300
+ activeTouch.touchType = UITouchType::Touch;
1301
+ break;
1302
+ case Composition::Input::PointerDeviceType::Pen:
1303
+ activeTouch.touchType = UITouchType::Pen;
1304
+ break;
1305
+ case Composition::Input::PointerDeviceType::Mouse:
1306
+ default:
1307
+ activeTouch.touchType = UITouchType::Mouse;
1308
+ break;
1309
+ }
1216
1310
 
1217
1311
  // Map PointerUpdateKind to W3C button value
1218
1312
  // https://developer.mozilla.org/docs/Web/API/MouseEvent/button
@@ -1250,14 +1344,27 @@ void CompositionEventHandler::onPointerPressed(
1250
1344
  targetComponentView = targetComponentView.Parent();
1251
1345
  }
1252
1346
 
1347
+ // Don't register the touch if no eventEmitter was found — inserting a null-emitter entry
1348
+ // into m_activeTouches would block future presses with the same pointer ID.
1349
+ if (!activeTouch.eventEmitter) {
1350
+ return;
1351
+ }
1352
+
1253
1353
  UpdateActiveTouch(activeTouch, ptScaled, ptLocal);
1254
1354
 
1255
1355
  activeTouch.isPrimary = pointerId == 1;
1256
- activeTouch.touch.identifier = pointerId;
1356
+ // Map the Windows pointer ID to a small identifier (0–19) safe for use as a JS array index.
1357
+ // Windows touch IDs can be arbitrarily large (e.g. 2233), which causes React Native to warn
1358
+ // and corrupts touch state, leaving Pressables stuck after a scroll.
1359
+ // Mouse pointer ID is always 1 (MOUSE_POINTER_ID), which is already within the safe range —
1360
+ // use it directly to preserve stable, predictable identifier assignment for mouse input.
1361
+ activeTouch.touch.identifier = (pointerPoint.PointerDeviceType() == Composition::Input::PointerDeviceType::Mouse)
1362
+ ? static_cast<int>(MOUSE_POINTER_ID)
1363
+ : AllocateTouchIdentifier();
1257
1364
 
1258
1365
  // If the pointer has not been marked as hovering over views before the touch started, we register
1259
1366
  // that the activeTouch should not maintain its hovered state once the pointer has been lifted.
1260
- auto currentlyHoveredTags = m_currentlyHoveredViewsPerPointer.find(activeTouch.touch.identifier);
1367
+ auto currentlyHoveredTags = m_currentlyHoveredViewsPerPointer.find(pointerId);
1261
1368
  if (currentlyHoveredTags == m_currentlyHoveredViewsPerPointer.end() || currentlyHoveredTags->second.empty()) {
1262
1369
  activeTouch.shouldLeaveWhenReleased = true;
1263
1370
  }
@@ -1273,11 +1380,12 @@ void CompositionEventHandler::onPointerReleased(
1273
1380
  winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
1274
1381
  int pointerId = pointerPoint.PointerId();
1275
1382
 
1276
- RootComponentView().UseKeyboardForProgrammaticFocus(false);
1383
+ auto *rootView = RootComponentView();
1384
+ if (!rootView)
1385
+ return;
1386
+ rootView->UseKeyboardForProgrammaticFocus(false);
1277
1387
 
1278
- auto activeTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) {
1279
- return pair.second.touch.identifier == pointerId;
1280
- });
1388
+ auto activeTouch = m_activeTouches.find(pointerId);
1281
1389
 
1282
1390
  if (activeTouch == m_activeTouches.end()) {
1283
1391
  return;
@@ -1289,8 +1397,13 @@ void CompositionEventHandler::onPointerReleased(
1289
1397
  facebook::react::Point ptLocal, ptScaled;
1290
1398
  getTargetPointerArgs(fabricuiManager, pointerPoint, tag, ptScaled, ptLocal);
1291
1399
 
1292
- if (tag == -1)
1400
+ if (tag == -1) {
1401
+ if (activeTouch->second.eventEmitter) {
1402
+ DispatchSynthesizedTouchCancelForActiveTouch(activeTouch->second, pointerPoint, keyModifiers);
1403
+ }
1404
+ m_activeTouches.erase(pointerId);
1293
1405
  return;
1406
+ }
1294
1407
 
1295
1408
  auto targetComponentView = fabricuiManager->GetViewRegistry().componentViewDescriptorWithTag(tag).view;
1296
1409
  auto args = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerRoutedEventArgs>(
@@ -1322,7 +1435,7 @@ bool CompositionEventHandler::CapturePointer(
1322
1435
  }
1323
1436
 
1324
1437
  m_pointerCapturingComponentTag = tag;
1325
- m_capturedPointers.push_back(pointer.PointerId());
1438
+ m_capturedPointers.insert(pointer.PointerId());
1326
1439
  return true;
1327
1440
  }
1328
1441
 
@@ -1337,11 +1450,9 @@ bool CompositionEventHandler::releasePointerCapture(PointerId pointerId, faceboo
1337
1450
  bool result = false;
1338
1451
 
1339
1452
  if (m_pointerCapturingComponentTag == tag) {
1340
- auto it = std::find(m_capturedPointers.begin(), m_capturedPointers.end(), pointerId);
1341
- if (it == m_capturedPointers.end()) {
1453
+ if (m_capturedPointers.erase(pointerId) == 0) {
1342
1454
  return false;
1343
1455
  }
1344
- m_capturedPointers.erase(it);
1345
1456
 
1346
1457
  if (std::shared_ptr<FabricUIManager> fabricuiManager =
1347
1458
  ::Microsoft::ReactNative::FabricUIManager::FromProperties(m_context.Properties())) {
@@ -1352,7 +1463,7 @@ bool CompositionEventHandler::releasePointerCapture(PointerId pointerId, faceboo
1352
1463
  ->OnPointerCaptureLost();
1353
1464
  }
1354
1465
 
1355
- if (m_capturedPointers.size() == 0) {
1466
+ if (m_capturedPointers.empty()) {
1356
1467
  m_pointerCapturingComponentTag = -1;
1357
1468
  return true;
1358
1469
  }
@@ -1467,16 +1578,83 @@ bool CompositionEventHandler::IsPointerWithinInitialTree(const ActiveTouch &acti
1467
1578
  if (!initialComponentView)
1468
1579
  return false;
1469
1580
 
1470
- auto initialViewSet = GetTouchableViewsInPathToRoot(initialComponentView);
1581
+ auto *rootView = RootComponentView();
1582
+ if (!rootView)
1583
+ return false;
1584
+
1585
+ facebook::react::Point ptLocal;
1586
+ auto currentTag = rootView->hitTest(activeTouch.touch.pagePoint, ptLocal);
1587
+ if (currentTag == -1)
1588
+ return false;
1589
+
1590
+ auto fabricuiManager = ::Microsoft::ReactNative::FabricUIManager::FromProperties(m_context.Properties());
1591
+ if (!fabricuiManager)
1592
+ return false;
1593
+
1594
+ auto initialTag = initialComponentView.Tag();
1595
+ auto &viewRegistry = fabricuiManager->GetViewRegistry();
1596
+ auto currentView = viewRegistry.componentViewDescriptorWithTag(currentTag).view;
1597
+ while (currentView) {
1598
+ if (currentView.Tag() == initialTag)
1599
+ return true;
1600
+ currentView = currentView.Parent();
1601
+ }
1471
1602
 
1472
- for (const auto &view : initialViewSet) {
1473
- if (view.Tag() == activeTouch.touch.target)
1603
+ // Fallback: if the pointer drifted spatially but the original target
1604
+ // is still structurally within the initial tree, honor the tap.
1605
+ // This provides touch-device tolerance for finger drift.
1606
+ auto targetView = viewRegistry.componentViewDescriptorWithTag(activeTouch.touch.target).view;
1607
+ while (targetView) {
1608
+ if (targetView.Tag() == initialTag)
1474
1609
  return true;
1610
+ targetView = targetView.Parent();
1475
1611
  }
1476
1612
 
1477
1613
  return false;
1478
1614
  }
1479
1615
 
1616
+ void CompositionEventHandler::DispatchSynthesizedTouchCancelForActiveTouch(
1617
+ const ActiveTouch &cancelledTouch,
1618
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
1619
+ winrt::Windows::System::VirtualKeyModifiers keyModifiers) {
1620
+ if (!cancelledTouch.eventEmitter) {
1621
+ return;
1622
+ }
1623
+
1624
+ facebook::react::PointerEvent pointerEvent =
1625
+ CreatePointerEventFromActiveTouch(cancelledTouch, TouchEventType::Cancel);
1626
+ winrt::Microsoft::ReactNative::ComponentView targetView{nullptr};
1627
+ facebook::react::SharedTouchEventEmitter emitter = cancelledTouch.eventEmitter;
1628
+ auto pointerHandler = [emitter, pointerEvent](std::vector<winrt::Microsoft::ReactNative::ComponentView> &) {
1629
+ emitter->onPointerCancel(pointerEvent);
1630
+ };
1631
+ HandleIncomingPointerEvent(pointerEvent, targetView, pointerPoint, keyModifiers, pointerHandler);
1632
+
1633
+ facebook::react::TouchEvent touchEvent;
1634
+ touchEvent.changedTouches.insert(cancelledTouch.touch);
1635
+
1636
+ for (const auto &pair : m_activeTouches) {
1637
+ if (!pair.second.eventEmitter) {
1638
+ continue;
1639
+ }
1640
+
1641
+ if (touchEvent.changedTouches.find(pair.second.touch) != touchEvent.changedTouches.end()) {
1642
+ continue;
1643
+ }
1644
+
1645
+ touchEvent.touches.insert(pair.second.touch);
1646
+ }
1647
+
1648
+ for (const auto &pair : m_activeTouches) {
1649
+ if (pair.second.eventEmitter == cancelledTouch.eventEmitter &&
1650
+ touchEvent.changedTouches.find(pair.second.touch) == touchEvent.changedTouches.end()) {
1651
+ touchEvent.targetTouches.insert(pair.second.touch);
1652
+ }
1653
+ }
1654
+
1655
+ cancelledTouch.eventEmitter->onTouchCancel(touchEvent);
1656
+ }
1657
+
1480
1658
  // If we have events that include multiple pointer updates, we should change arg from pointerId to vector<pointerId>
1481
1659
  void CompositionEventHandler::DispatchTouchEvent(
1482
1660
  TouchEventType eventType,
@@ -1501,7 +1679,7 @@ void CompositionEventHandler::DispatchTouchEvent(
1501
1679
  continue;
1502
1680
  }
1503
1681
 
1504
- if (activeTouch.touch.identifier == pointerId) {
1682
+ if (pair.first == pointerId) {
1505
1683
  event.changedTouches.insert(activeTouch.touch);
1506
1684
  }
1507
1685
  uniqueEventEmitters.insert(activeTouch.eventEmitter);
@@ -1512,16 +1690,19 @@ void CompositionEventHandler::DispatchTouchEvent(
1512
1690
  bool shouldLeave = (eventType == TouchEventType::End && activeTouch.shouldLeaveWhenReleased) ||
1513
1691
  eventType == TouchEventType::Cancel;
1514
1692
  if (!shouldLeave) {
1515
- const auto &viewRegistry = fabricuiManager->GetViewRegistry();
1516
- facebook::react::Point ptLocal;
1517
- auto targetTag = RootComponentView().hitTest(pointerEvent.clientPoint, ptLocal);
1518
- if (targetTag != -1) {
1519
- auto targetComponentViewDescriptor = viewRegistry.componentViewDescriptorWithTag(targetTag);
1520
- targetView = FindClosestFabricManagedTouchableView(targetComponentViewDescriptor.view);
1693
+ auto *rootViewForHit = RootComponentView();
1694
+ if (rootViewForHit) {
1695
+ const auto &viewRegistry = fabricuiManager->GetViewRegistry();
1696
+ facebook::react::Point ptLocal;
1697
+ auto targetTag = rootViewForHit->hitTest(pointerEvent.clientPoint, ptLocal);
1698
+ if (targetTag != -1) {
1699
+ auto targetComponentViewDescriptor = viewRegistry.componentViewDescriptorWithTag(targetTag);
1700
+ targetView = FindClosestFabricManagedTouchableView(targetComponentViewDescriptor.view);
1701
+ }
1521
1702
  }
1522
1703
  }
1523
1704
 
1524
- auto handler = [&activeTouch, eventType, &pointerEvent](
1705
+ auto handler = [this, &activeTouch, eventType, &pointerEvent](
1525
1706
  std::vector<winrt::Microsoft::ReactNative::ComponentView> &eventPathViews) {
1526
1707
  switch (eventType) {
1527
1708
  case TouchEventType::Start:
@@ -14,6 +14,7 @@
14
14
  #include <winrt/Windows.Devices.Input.h>
15
15
  #include <optional>
16
16
  #include <set>
17
+ #include <unordered_set>
17
18
 
18
19
  namespace winrt {
19
20
  using namespace Windows::UI;
@@ -79,7 +80,7 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
79
80
  bool releasePointerCapture(PointerId pointerId, facebook::react::Tag tag) noexcept;
80
81
 
81
82
  facebook::react::SurfaceId SurfaceId() const noexcept;
82
- winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView &RootComponentView() const noexcept;
83
+ winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView *RootComponentView() const noexcept;
83
84
 
84
85
  enum class UITouchType {
85
86
  Mouse,
@@ -141,7 +142,7 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
141
142
  ReactTaggedView initialComponentView{nullptr};
142
143
  };
143
144
 
144
- static bool IsPointerWithinInitialTree(const ActiveTouch &activeTouch) noexcept;
145
+ bool IsPointerWithinInitialTree(const ActiveTouch &activeTouch) noexcept;
145
146
  static bool IsEndishEventType(TouchEventType eventType) noexcept;
146
147
  static const char *PointerTypeCStringFromUITouchType(UITouchType type) noexcept;
147
148
  static facebook::react::PointerEvent CreatePointerEventFromActiveTouch(
@@ -150,18 +151,31 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
150
151
  static void
151
152
  UpdateActiveTouch(ActiveTouch &activeTouch, facebook::react::Point ptScaled, facebook::react::Point ptLocal) noexcept;
152
153
 
154
+ void DispatchSynthesizedTouchCancelForActiveTouch(
155
+ const ActiveTouch &cancelledTouch,
156
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
157
+ winrt::Windows::System::VirtualKeyModifiers keyModifiers);
158
+
159
+ std::vector<winrt::Microsoft::ReactNative::ComponentView> GetTouchableViewsInPathToRoot(
160
+ const winrt::Microsoft::ReactNative::ComponentView &componentView);
161
+
153
162
  void UpdateCursor() noexcept;
154
163
  void SetCursor(facebook::react::Cursor cursor, HCURSOR hcur) noexcept;
155
164
 
165
+ // Allocates a small touch identifier (0–19) that is safe to use as a JS array index.
166
+ // Windows pointer IDs can be arbitrarily large, which causes React Native to warn and
167
+ // back-fill huge arrays, corrupting touch state after scrolling.
168
+ int AllocateTouchIdentifier() noexcept;
169
+
156
170
  std::map<PointerId, ActiveTouch> m_activeTouches; // iOS is map of touch event args to ActiveTouch..?
157
- PointerId m_touchId = 0;
171
+ int m_touchId = 0; // cycling base used by AllocateTouchIdentifier
158
172
 
159
173
  std::map<PointerId, std::vector<ReactTaggedView>> m_currentlyHoveredViewsPerPointer;
160
174
  winrt::weak_ref<winrt::Microsoft::ReactNative::ReactNativeIsland> m_wkRootView;
161
175
  winrt::Microsoft::ReactNative::ReactContext m_context;
162
176
 
163
177
  facebook::react::Tag m_pointerCapturingComponentTag{-1}; // Component that has captured input
164
- std::vector<PointerId> m_capturedPointers;
178
+ std::unordered_set<PointerId> m_capturedPointers;
165
179
  HCURSOR m_hcursor{nullptr};
166
180
  bool m_hcursorOwned{false}; // If we create the cursor, so we need to destroy it
167
181
  facebook::react::Cursor m_currentCursor{facebook::react::Cursor::Auto};
@@ -6,6 +6,8 @@
6
6
 
7
7
  #include "CompositionViewComponentView.h"
8
8
 
9
+ #include <vector>
10
+
9
11
  #include <AutoDraw.h>
10
12
  #include <Fabric/AbiState.h>
11
13
  #include <Fabric/AbiViewProps.h>
@@ -1013,15 +1015,18 @@ bool ComponentView::anyHitTestHelper(
1013
1015
  facebook::react::Tag &targetTag,
1014
1016
  facebook::react::Point &ptContent,
1015
1017
  facebook::react::Point &localPt) const noexcept {
1016
- if (auto index = m_children.Size()) {
1017
- do {
1018
- index--;
1019
- targetTag = winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(m_children.GetAt(index))
1020
- ->hitTest(ptContent, localPt);
1021
- if (targetTag != -1) {
1022
- return true;
1023
- }
1024
- } while (index != 0);
1018
+ auto size = m_children.Size();
1019
+ if (size == 0) {
1020
+ return false;
1021
+ }
1022
+
1023
+ // m_children is backed by single_threaded_vector (std::vector), so GetAt is O(1)
1024
+ for (uint32_t i = size; i > 0; --i) {
1025
+ targetTag = winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(m_children.GetAt(i - 1))
1026
+ ->hitTest(ptContent, localPt);
1027
+ if (targetTag != -1) {
1028
+ return true;
1029
+ }
1025
1030
  }
1026
1031
 
1027
1032
  return false;
@@ -981,8 +981,6 @@ void ScrollViewComponentView::OnPointerPressed(
981
981
  Super::OnPointerPressed(args);
982
982
 
983
983
  if (!args.Handled()) {
984
- auto f = args.Pointer();
985
- auto g = f.PointerDeviceType();
986
984
  m_scrollVisual.OnPointerPressed(args);
987
985
  }
988
986
  }