Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file need to be removed, already backported to #16131

"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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
args.CurrentPoint(), strongRootView.ScaleFactor());
strongThis->onPointerRoutedAway(pp, args.KeyModifiers());
}
}
});

m_pointerWheelChangedToken =
pointerSource.PointerWheelChanged([wkThis = weak_from_this()](
winrt::Microsoft::UI::Input::InputPointerSource const &,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
void onPointerCaptureLost(
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept;
void onPointerRoutedAway(
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept;
void onKeyDown(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept;
void onKeyUp(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept;
void onCharacterReceived(
Expand Down Expand Up @@ -156,6 +159,13 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
winrt::Windows::System::VirtualKeyModifiers keyModifiers);

// Look up the active touch for pointerId, erase it, and dispatch cancel events.
// Returns true iff a touch was found and cancel events were dispatched.
bool CancelActiveTouchForPointerInternal(
PointerId pointerId,
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept;

std::vector<winrt::Microsoft::ReactNative::ComponentView> GetTouchableViewsInPathToRoot(
const winrt::Microsoft::ReactNative::ComponentView &componentView);

Expand Down Expand Up @@ -187,6 +197,7 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
winrt::event_token m_pointerMovedToken;
winrt::event_token m_pointerWheelChangedToken;
winrt::event_token m_pointerCaptureLostToken;
winrt::event_token m_pointerRoutedAwayToken;
winrt::event_token m_pointerExitedToken;
winrt::event_token m_keyDownToken;
winrt::event_token m_keyUpToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <functional>
#include "ContentIslandComponentView.h"
#include "JSValueReader.h"
#include "ReactNativeIsland.h"
#include "RootComponentView.h"

namespace winrt::Microsoft::ReactNative::Composition::implementation {
Expand Down Expand Up @@ -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<facebook::react::ScrollViewShadowNode::ConcreteState::Data const>(newData);
});
}
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion vnext/template/cpp-lib/proj/MyLib.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ApplicationType>Windows Store</ApplicationType>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="ReactNativeWindowsProps">
<ReactNativeWindowsDir Condition="'$(ReactNativeWindowsDir)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(SolutionDir), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\</ReactNativeWindowsDir>
</PropertyGroup>
Expand All @@ -23,7 +24,6 @@
<WindowsTargetPlatformVersion Condition=" '$(WindowsTargetPlatformVersion)' == '' ">10.0.22621.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion Condition=" '$(WindowsTargetPlatformMinVersion)' == '' ">10.0.17763.0</WindowsTargetPlatformMinVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
Expand Down
2 changes: 1 addition & 1 deletion vnext/templates/cpp-app/windows/MyApp/MyApp.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
<MinimumVisualStudioVersion>17.0</MinimumVisualStudioVersion>
<AppxPackage>false</AppxPackage>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="ReactNativeWindowsProps">
<ReactNativeWindowsDir Condition="'$(ReactNativeWindowsDir)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\</ReactNativeWindowsDir>
</PropertyGroup>
<Import Project="$(ReactNativeWindowsDir)\PropertySheets\External\Microsoft.ReactNative.WindowsSdk.Default.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
Expand Down
2 changes: 1 addition & 1 deletion vnext/templates/cpp-lib/windows/MyLib/MyLib.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
<MinimumVisualStudioVersion>17.0</MinimumVisualStudioVersion>
<AppxPackage>false</AppxPackage>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="ReactNativeWindowsProps">
<ReactNativeWindowsDir Condition="'$(ReactNativeWindowsDir)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\</ReactNativeWindowsDir>
<RunAutolinkCheck>false</RunAutolinkCheck>
</PropertyGroup>
<Import Project="$(ReactNativeWindowsDir)\PropertySheets\External\Microsoft.ReactNative.WindowsSdk.Default.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
Expand Down
Loading