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": "fix: Fix tooltip positioning for ContentIsland and dismiss on scroll",
"packageName": "react-native-windows",
"email": "nitchaudhary@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <unicode.h>
#include "JSValueReader.h"
#include "RootComponentView.h"
#include "TooltipService.h"

namespace winrt::Microsoft::ReactNative::Composition::implementation {

Expand Down Expand Up @@ -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<std::chrono::duration<double>>(now - m_lastScrollEventTime).count();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -267,17 +311,24 @@ void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentV
static_cast<int>((tm.width + tooltipHorizontalPadding + tooltipHorizontalPadding) * scaleFactor);
tooltipData->height = static_cast<int>((tm.height + tooltipTopPadding + tooltipBottomPadding) * scaleFactor);

POINT pt = {static_cast<LONG>(m_pos.X * scaleFactor), static_cast<LONG>(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<LONG>(screenPt.X), static_cast<LONG>(screenPt.Y)};

// Calculate initial desired tooltip position and clamp to monitor work area
int tooltipX = pt.x - tooltipData->width / 2;
int tooltipY = static_cast<int>(pt.y - tooltipData->height - (toolTipPlacementMargin * scaleFactor));
ClampTooltipToMonitor(pt, tooltipData->width, tooltipData->height, scaleFactor, tooltipX, tooltipY);

RegisterTooltipWndClass();
HINSTANCE hInstance = GetModuleHandle(NULL);
m_hwndTip = CreateWindow(
c_tooltipWindowClassName,
L"Tooltip",
WS_POPUP,
pt.x - tooltipData->width / 2,
static_cast<int>(pt.y - tooltipData->height - (toolTipPlacementMargin * scaleFactor)),
tooltipX,
tooltipY,
tooltipData->width,
tooltipData->height,
parentHwnd,
Expand Down Expand Up @@ -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<winrt::Microsoft::ReactNative::ReactNonAbiValue<std::shared_ptr<TooltipService>>>
&TooltipServicePropertyId() noexcept {
static const ReactPropertyId<winrt::Microsoft::ReactNative::ReactNonAbiValue<std::shared_ptr<TooltipService>>> prop{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<TooltipService> GetCurrent(
const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept;
Expand Down
Loading