From b5755ba31bec7ad06e79f552a93ebbc6d03c5036 Mon Sep 17 00:00:00 2001 From: Abhijeet Jha <74712637+iamAbhi-916@users.noreply.github.com> Date: Wed, 13 May 2026 12:04:30 +0530 Subject: [PATCH 1/3] Show tooltip on keyboard focus, enforce single visible tooltip (#16123) * Show tooltip on keyboard focus, enforce single visible tooltip * yarn format * Change files --- ...ative-windows-87ca31c6-96c8-42cc-9b3c-432a69889fdc.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-87ca31c6-96c8-42cc-9b3c-432a69889fdc.json diff --git a/change/react-native-windows-87ca31c6-96c8-42cc-9b3c-432a69889fdc.json b/change/react-native-windows-87ca31c6-96c8-42cc-9b3c-432a69889fdc.json new file mode 100644 index 00000000000..0b37d599835 --- /dev/null +++ b/change/react-native-windows-87ca31c6-96c8-42cc-9b3c-432a69889fdc.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Show tooltip on keyboard focus, enforce single visible tooltip", + "packageName": "react-native-windows", + "email": "74712637+iamAbhi-916@users.noreply.github.com", + "dependentChangeType": "patch" +} From ba1f39332b6ef5a6234484358d6da64780f89f21 Mon Sep 17 00:00:00 2001 From: Gordon MacMaster <31481849+gmacmaster@users.noreply.github.com> Date: Wed, 13 May 2026 14:15:57 -0400 Subject: [PATCH 2/3] Fix ScrollView Pressables stuck and dead tap on high-DPI displays (#16140) * fix scroll touch scaling issue * Refactor #16047 Fix A to use InputPointerSource.PointerRoutedAway * format --- ...-d89877b5-21f1-487a-b114-6048bf2284ae.json | 7 ++ .../Composition/CompositionEventHandler.cpp | 80 +++++++++++++++---- .../Composition/CompositionEventHandler.h | 11 +++ .../Composition/ScrollViewComponentView.cpp | 34 ++++++-- 4 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 change/react-native-windows-d89877b5-21f1-487a-b114-6048bf2284ae.json diff --git a/change/react-native-windows-d89877b5-21f1-487a-b114-6048bf2284ae.json b/change/react-native-windows-d89877b5-21f1-487a-b114-6048bf2284ae.json new file mode 100644 index 00000000000..ff435f638cd --- /dev/null +++ b/change/react-native-windows-d89877b5-21f1-487a-b114-6048bf2284ae.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fix #16047: Pressables inside ScrollView remained stuck in the pressed state after a touch-driven scroll, and on non-100% Windows display scales the next tap on a row would not register `press`. Two underlying causes were addressed: (1) VisualInteractionSource::TryRedirectForManipulation does not deliver PointerCaptureLost for the redirected pointer, leaving a zombie entry in CompositionEventHandler::m_activeTouches — now resolved by synthesizing a touchcancel from the InputPointerSource.PointerRoutedAway event, which fires reliably on the redirect path; and (2) ScrollViewComponentView::updateStateWithContentOffset wrote the raw physical-pixel ScrollPosition into ScrollViewShadowNode state's contentOffset, which Fabric layout treats as DIPs, so JS UIManager.measure() over-subtracted the offset by pointScaleFactor after any scroll on a >100% display, causing Pressability to fire LEAVE_PRESS_RECT synchronously and suppress press — now divides by pointScaleFactor to match the JS event-emitter paths in the same file.", + "packageName": "react-native-windows", + "email": "gordomacmaster@gmail.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index bbef5149aa5..e3ee83e10ae 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -227,6 +227,28 @@ void CompositionEventHandler::Initialize() noexcept { } }); + // Issue #16047: when ScrollView calls VisualInteractionSource::TryRedirectForManipulation + // and the OS hands the pointer over to the InteractionTracker, WinAppSDK + // does not fire PointerCaptureLost on this source — but it does fire + // PointerRoutedAway. Treat it the same way as captureloss: cancel any + // active touch RN is tracking for this pointer so Pressables don't get + // stuck in their pressed state. + m_pointerRoutedAwayToken = + pointerSource.PointerRoutedAway([wkThis = weak_from_this()]( + winrt::Microsoft::UI::Input::InputPointerSource const &, + winrt::Microsoft::UI::Input::PointerEventArgs const &args) { + if (auto strongThis = wkThis.lock()) { + if (auto strongRootView = strongThis->m_wkRootView.get()) { + if (strongThis->SurfaceId() == -1) + return; + + auto pp = winrt::make( + args.CurrentPoint(), strongRootView.ScaleFactor()); + strongThis->onPointerRoutedAway(pp, args.KeyModifiers()); + } + } + }); + m_pointerWheelChangedToken = pointerSource.PointerWheelChanged([wkThis = weak_from_this()]( winrt::Microsoft::UI::Input::InputPointerSource const &, @@ -374,6 +396,7 @@ CompositionEventHandler::~CompositionEventHandler() { pointerSource.PointerReleased(m_pointerReleasedToken); pointerSource.PointerMoved(m_pointerMovedToken); pointerSource.PointerCaptureLost(m_pointerCaptureLostToken); + pointerSource.PointerRoutedAway(m_pointerRoutedAwayToken); pointerSource.PointerWheelChanged(m_pointerWheelChangedToken); pointerSource.PointerExited(m_pointerExitedToken); auto keyboardSource = winrt::Microsoft::UI::Input::InputKeyboardSource::GetForIsland(island); @@ -1123,24 +1146,49 @@ void CompositionEventHandler::onPointerCaptureLost( m_pointerCapturingComponentTag = -1; } - // Also cancel any active touch for the specific pointer that lost capture, even - // when no JS-level CapturePointer was ever issued. This handles ScrollView (and - // any other VisualInteractionSource) calling TryRedirectForManipulation: the OS - // reassigns the pointer to the InteractionTracker, fires PointerCaptureLost, and - // then stops delivering PointerMoved/PointerReleased to us. Without this cleanup - // m_activeTouches keeps a zombie entry whose target is the originally-pressed - // Pressable, leaving it visually pressed and causing later taps to be attributed - // to that original target. If the entry was already cleared above (for a JS-level - // capture) or by onPointerReleased running first, the find() is a no-op. - PointerId pointerId = pointerPoint.PointerId(); + // Defense-in-depth cleanup for the specific pointer that lost capture, even + // when no JS-level CapturePointer was ever issued. The ScrollView + // TryRedirectForManipulation path comes in via PointerRoutedAway, not + // PointerCaptureLost (see onPointerRoutedAway and issue #16047), so this + // path covers the remaining system-driven losses (focus change, another + // window stealing input, system back gesture, etc.). + CancelActiveTouchForPointerInternal(pointerPoint.PointerId(), pointerPoint, keyModifiers); +} + +void CompositionEventHandler::onPointerRoutedAway( + const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint, + winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept { + if (SurfaceId() == -1) + return; + + // Issue #16047: WinAppSDK fires PointerRoutedAway when the OS hands the + // pointer to another InputPointerSource — most importantly for us, when + // ScrollView calls VisualInteractionSource::TryRedirectForManipulation and + // the InteractionTracker takes the gesture for scrolling. We never get + // PointerMoved / PointerReleased / PointerCaptureLost for that pointer + // afterwards, so without this cleanup m_activeTouches keeps a zombie entry + // and the originally-pressed Pressable stays stuck in its pressed state. + CancelActiveTouchForPointerInternal(pointerPoint.PointerId(), pointerPoint, keyModifiers); +} + +bool CompositionEventHandler::CancelActiveTouchForPointerInternal( + PointerId pointerId, + const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint, + winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept { auto activeTouch = m_activeTouches.find(pointerId); - if (activeTouch != m_activeTouches.end()) { - ActiveTouch cancelledTouchCopy = std::move(activeTouch->second); - m_activeTouches.erase(activeTouch); - if (cancelledTouchCopy.eventEmitter) { - DispatchSynthesizedTouchCancelForActiveTouch(cancelledTouchCopy, pointerPoint, keyModifiers); - } + if (activeTouch == m_activeTouches.end()) { + return false; + } + + ActiveTouch cancelledTouchCopy = std::move(activeTouch->second); + m_activeTouches.erase(activeTouch); + + if (!cancelledTouchCopy.eventEmitter) { + return false; } + + DispatchSynthesizedTouchCancelForActiveTouch(cancelledTouchCopy, pointerPoint, keyModifiers); + return true; } void CompositionEventHandler::onPointerMoved( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h index c3e27979958..e5757fce284 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h @@ -66,6 +66,9 @@ class CompositionEventHandler : public std::enable_shared_from_this GetTouchableViewsInPathToRoot( const winrt::Microsoft::ReactNative::ComponentView &componentView); @@ -187,6 +197,7 @@ class CompositionEventHandler : public std::enable_shared_from_this #include "ContentIslandComponentView.h" #include "JSValueReader.h" +#include "ReactNativeIsland.h" #include "RootComponentView.h" namespace winrt::Microsoft::ReactNative::Composition::implementation { @@ -849,13 +850,27 @@ void ScrollViewComponentView::updateStateWithContentOffset() noexcept { return; } - auto scrollPosition = m_scrollVisual.ScrollPosition(); - m_verticalScrollbarComponent->ContentOffset(scrollPosition); - m_horizontalScrollbarComponent->ContentOffset(scrollPosition); - - m_state->updateState([scrollPosition](const facebook::react::ScrollViewShadowNode::ConcreteState::Data &data) { + // Issue #16047: m_scrollVisual.ScrollPosition() returns the InteractionTracker + // position in PHYSICAL pixels (the visual is sized as + // layoutMetrics.frame.size.* * pointScaleFactor — see updateLayoutMetrics / + // updateContentVisualSize) but ScrollViewShadowNode state's contentOffset is + // in DIPs. Without the conversion, JS UIManager.measure() over-subtracts by + // pointScaleFactor on non-100% display scales, leaving Pressables inside a + // scrolled ScrollView with stale page-space bounds that don't contain the + // touch — Pressability fires LEAVE_PRESS_RECT inside pressIn and suppresses + // press. The JS-event-emitter paths in this file (see lines using + // args.Position() / pointScaleFactor) already do this division. + auto rawScrollPosition = m_scrollVisual.ScrollPosition(); + const float pointScaleFactor = m_layoutMetrics.pointScaleFactor > 0.0f ? m_layoutMetrics.pointScaleFactor : 1.0f; + facebook::react::Point contentOffsetDips{ + rawScrollPosition.x / pointScaleFactor, rawScrollPosition.y / pointScaleFactor}; + + m_verticalScrollbarComponent->ContentOffset(rawScrollPosition); + m_horizontalScrollbarComponent->ContentOffset(rawScrollPosition); + + m_state->updateState([contentOffsetDips](const facebook::react::ScrollViewShadowNode::ConcreteState::Data &data) { auto newData = data; - newData.contentOffset = {scrollPosition.x, scrollPosition.y}; + newData.contentOffset = contentOffsetDips; return std::make_shared(newData); }); } @@ -1389,6 +1404,13 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp [this]( winrt::IInspectable const & /*sender*/, winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) { + // Issue #16047: push the FINAL settled scroll position into Fabric's + // shadow tree before notifying JS. The per-frame ScrollPositionChanged + // updates can drop the last inertia delta, leaving contentOffset stale + // and JS UIManager.measure() returning pre-settle-relative bounds. + // ScrollEndDrag / ScrollBeginDrag already call this; momentum-end was + // the missing completion path. + updateStateWithContentOffset(); auto eventEmitter = GetEventEmitter(); if (eventEmitter) { auto scrollMetrics = getScrollMetrics(eventEmitter, args); From e65ab0bfac5f0285e92b6b2e8aaa4bda3c0f15fe Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 18 May 2026 13:07:01 -0700 Subject: [PATCH 3/3] Template should import Microsoft.Cpp.Default.props before setting ReactNativeWindowsDir (#16146) * Template should import Microsoft.Cpp.Default.props before setting ReactNativeWindowsDir * Change files --- ...ative-windows-6cd4f931-1b4e-4127-ba02-3b4bb01b2645.json | 7 +++++++ vnext/template/cpp-lib/proj/MyLib.vcxproj | 2 +- vnext/templates/cpp-app/windows/MyApp/MyApp.vcxproj | 2 +- vnext/templates/cpp-lib/windows/MyLib/MyLib.vcxproj | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 change/react-native-windows-6cd4f931-1b4e-4127-ba02-3b4bb01b2645.json diff --git a/change/react-native-windows-6cd4f931-1b4e-4127-ba02-3b4bb01b2645.json b/change/react-native-windows-6cd4f931-1b4e-4127-ba02-3b4bb01b2645.json new file mode 100644 index 00000000000..3c529ef9d01 --- /dev/null +++ b/change/react-native-windows-6cd4f931-1b4e-4127-ba02-3b4bb01b2645.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Template should import Microsoft.Cpp.Default.props before setting ReactNativeWindowsDir", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/template/cpp-lib/proj/MyLib.vcxproj b/vnext/template/cpp-lib/proj/MyLib.vcxproj index 71e8f394146..c2ce9c7ff31 100644 --- a/vnext/template/cpp-lib/proj/MyLib.vcxproj +++ b/vnext/template/cpp-lib/proj/MyLib.vcxproj @@ -15,6 +15,7 @@ Windows Store 10.0 + $([MSBuild]::GetDirectoryNameOfFileAbove($(SolutionDir), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ @@ -23,7 +24,6 @@ 10.0.22621.0 10.0.17763.0 - Debug diff --git a/vnext/templates/cpp-app/windows/MyApp/MyApp.vcxproj b/vnext/templates/cpp-app/windows/MyApp/MyApp.vcxproj index 931514d7acf..65d080cdfef 100644 --- a/vnext/templates/cpp-app/windows/MyApp/MyApp.vcxproj +++ b/vnext/templates/cpp-app/windows/MyApp/MyApp.vcxproj @@ -16,11 +16,11 @@ 17.0 false + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ - Debug diff --git a/vnext/templates/cpp-lib/windows/MyLib/MyLib.vcxproj b/vnext/templates/cpp-lib/windows/MyLib/MyLib.vcxproj index 09df9a72e4b..51d143f25b6 100644 --- a/vnext/templates/cpp-lib/windows/MyLib/MyLib.vcxproj +++ b/vnext/templates/cpp-lib/windows/MyLib/MyLib.vcxproj @@ -14,12 +14,12 @@ 17.0 false + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ false - Debug