diff --git a/change/react-native-windows-57a959a1-829b-488a-9101-1780b1e847cd.json b/change/react-native-windows-57a959a1-829b-488a-9101-1780b1e847cd.json new file mode 100644 index 00000000000..5996d6b2666 --- /dev/null +++ b/change/react-native-windows-57a959a1-829b-488a-9101-1780b1e847cd.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix: Fix tooltip positioning for ContentIsland and dismiss on scroll", + "packageName": "react-native-windows", + "email": "nitchaudhary@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index ca5b83decdc..048bb785702 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -21,6 +21,7 @@ #include #include "JSValueReader.h" #include "RootComponentView.h" +#include "TooltipService.h" namespace winrt::Microsoft::ReactNative::Composition::implementation { @@ -1300,6 +1301,11 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp [this]( winrt::IInspectable const & /*sender*/, winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) { + // Dismiss any visible tooltips when scroll position changes, since + // scrolling moves child components and the tooltip would be left at + // the wrong position on screen. + TooltipService::GetCurrent(m_reactContext.Properties())->DismissAllTooltips(); + auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast>(now - m_lastScrollEventTime).count(); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp index e65dac0061a..d1ad65e845c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp @@ -161,6 +161,45 @@ void RegisterTooltipWndClass() noexcept { registered = true; } +// Clamp tooltip position so it stays within the nearest monitor's work area. +// Flips the tooltip below the cursor if it would go above the work area. +void ClampTooltipToMonitor( + POINT cursorScreenPt, + int tooltipWidth, + int tooltipHeight, + float scaleFactor, + int &x, + int &y) noexcept { + HMONITOR hMonitor = MonitorFromPoint(cursorScreenPt, MONITOR_DEFAULTTONEAREST); + if (!hMonitor) + return; + + MONITORINFO mi = {}; + mi.cbSize = sizeof(mi); + if (!GetMonitorInfo(hMonitor, &mi)) + return; + + const RECT &workArea = mi.rcWork; + + // Clamp horizontally + if (x + tooltipWidth > workArea.right) { + x = workArea.right - tooltipWidth; + } + if (x < workArea.left) { + x = workArea.left; + } + + // If tooltip goes above the work area, flip it below the cursor + if (y < workArea.top) { + y = static_cast(cursorScreenPt.y + (toolTipPlacementMargin * scaleFactor)); + } + + // If tooltip goes below the work area (after flip), clamp to bottom + if (y + tooltipHeight > workArea.bottom) { + y = workArea.bottom - tooltipHeight; + } +} + TooltipTracker::TooltipTracker( const winrt::Microsoft::ReactNative::ComponentView &view, const winrt::Microsoft::ReactNative::ReactPropertyBag &properties, @@ -227,6 +266,11 @@ void TooltipTracker::OnPointerExited( DestroyTooltip(); } +void TooltipTracker::DismissActiveTooltip() noexcept { + DestroyTimer(); + DestroyTooltip(); +} + void TooltipTracker::OnUnmounted( const winrt::Windows::Foundation::IInspectable &, const winrt::Microsoft::ReactNative::ComponentView &) noexcept { @@ -267,8 +311,15 @@ void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentV static_cast((tm.width + tooltipHorizontalPadding + tooltipHorizontalPadding) * scaleFactor); tooltipData->height = static_cast((tm.height + tooltipTopPadding + tooltipBottomPadding) * scaleFactor); - POINT pt = {static_cast(m_pos.X * scaleFactor), static_cast(m_pos.Y * scaleFactor)}; - ClientToScreen(parentHwnd, &pt); + // Convert island-local DIP coordinates to screen pixel coordinates. + // Use LocalToScreen which properly handles both ContentIsland and HWND hosting. + auto screenPt = selfView->LocalToScreen({m_pos.X, m_pos.Y}); + POINT pt = {static_cast(screenPt.X), static_cast(screenPt.Y)}; + + // Calculate initial desired tooltip position and clamp to monitor work area + int tooltipX = pt.x - tooltipData->width / 2; + int tooltipY = static_cast(pt.y - tooltipData->height - (toolTipPlacementMargin * scaleFactor)); + ClampTooltipToMonitor(pt, tooltipData->width, tooltipData->height, scaleFactor, tooltipX, tooltipY); RegisterTooltipWndClass(); HINSTANCE hInstance = GetModuleHandle(NULL); @@ -276,8 +327,8 @@ void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentV c_tooltipWindowClassName, L"Tooltip", WS_POPUP, - pt.x - tooltipData->width / 2, - static_cast(pt.y - tooltipData->height - (toolTipPlacementMargin * scaleFactor)), + tooltipX, + tooltipY, tooltipData->width, tooltipData->height, parentHwnd, @@ -326,6 +377,12 @@ void TooltipService::StopTracking(const winrt::Microsoft::ReactNative::Component } } +void TooltipService::DismissAllTooltips() noexcept { + for (auto &tracker : m_trackers) { + tracker->DismissActiveTooltip(); + } +} + static const ReactPropertyId>> &TooltipServicePropertyId() noexcept { static const ReactPropertyId>> prop{ diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h index b3090061a54..a39caf32fdb 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h @@ -35,6 +35,7 @@ struct TooltipTracker { const winrt::Microsoft::ReactNative::ComponentView &) noexcept; facebook::react::Tag Tag() const noexcept; + void DismissActiveTooltip() noexcept; private: void ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept; @@ -53,6 +54,7 @@ struct TooltipService { TooltipService(const winrt::Microsoft::ReactNative::ReactPropertyBag &properties); void StartTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept; void StopTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept; + void DismissAllTooltips() noexcept; static std::shared_ptr GetCurrent( const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept;