Skip to content

Commit 7718169

Browse files
author
Nitin Chaudhary
committed
Fix tooltip positioning for ContentIsland and dismiss on scroll
- Replace ClientToScreen with LocalToScreen for correct coordinate conversion in ContentIsland hosting - Extract ClampTooltipToMonitor helper for monitor edge clamping - Add DismissAllTooltips called from ScrollViewComponentView on scroll position change - Add DismissActiveTooltip to TooltipTracker for external dismissal
1 parent 72def5f commit 7718169

3 files changed

Lines changed: 63 additions & 4 deletions

File tree

vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <unicode.h>
2222
#include "JSValueReader.h"
2323
#include "RootComponentView.h"
24+
#include "TooltipService.h"
2425

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

@@ -1300,6 +1301,11 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp
13001301
[this](
13011302
winrt::IInspectable const & /*sender*/,
13021303
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
1304+
// Dismiss any visible tooltips when scroll position changes, since
1305+
// scrolling moves child components and the tooltip would be left at
1306+
// the wrong position on screen.
1307+
TooltipService::GetCurrent(m_reactContext.Properties())->DismissAllTooltips();
1308+
13031309
auto now = std::chrono::steady_clock::now();
13041310
auto elapsed = std::chrono::duration_cast<std::chrono::duration<double>>(now - m_lastScrollEventTime).count();
13051311

vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,39 @@ void RegisterTooltipWndClass() noexcept {
161161
registered = true;
162162
}
163163

164+
// Clamp tooltip position so it stays within the nearest monitor's work area.
165+
// Flips the tooltip below the cursor if it would go above the work area.
166+
void ClampTooltipToMonitor(POINT cursorScreenPt, int tooltipWidth, int tooltipHeight, float scaleFactor, int &x, int &y) noexcept {
167+
HMONITOR hMonitor = MonitorFromPoint(cursorScreenPt, MONITOR_DEFAULTTONEAREST);
168+
if (!hMonitor)
169+
return;
170+
171+
MONITORINFO mi = {};
172+
mi.cbSize = sizeof(mi);
173+
if (!GetMonitorInfo(hMonitor, &mi))
174+
return;
175+
176+
const RECT &workArea = mi.rcWork;
177+
178+
// Clamp horizontally
179+
if (x + tooltipWidth > workArea.right) {
180+
x = workArea.right - tooltipWidth;
181+
}
182+
if (x < workArea.left) {
183+
x = workArea.left;
184+
}
185+
186+
// If tooltip goes above the work area, flip it below the cursor
187+
if (y < workArea.top) {
188+
y = static_cast<int>(cursorScreenPt.y + (toolTipPlacementMargin * scaleFactor));
189+
}
190+
191+
// If tooltip goes below the work area (after flip), clamp to bottom
192+
if (y + tooltipHeight > workArea.bottom) {
193+
y = workArea.bottom - tooltipHeight;
194+
}
195+
}
196+
164197
TooltipTracker::TooltipTracker(
165198
const winrt::Microsoft::ReactNative::ComponentView &view,
166199
const winrt::Microsoft::ReactNative::ReactPropertyBag &properties,
@@ -227,6 +260,11 @@ void TooltipTracker::OnPointerExited(
227260
DestroyTooltip();
228261
}
229262

263+
void TooltipTracker::DismissActiveTooltip() noexcept {
264+
DestroyTimer();
265+
DestroyTooltip();
266+
}
267+
230268
void TooltipTracker::OnUnmounted(
231269
const winrt::Windows::Foundation::IInspectable &,
232270
const winrt::Microsoft::ReactNative::ComponentView &) noexcept {
@@ -267,17 +305,24 @@ void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentV
267305
static_cast<int>((tm.width + tooltipHorizontalPadding + tooltipHorizontalPadding) * scaleFactor);
268306
tooltipData->height = static_cast<int>((tm.height + tooltipTopPadding + tooltipBottomPadding) * scaleFactor);
269307

270-
POINT pt = {static_cast<LONG>(m_pos.X * scaleFactor), static_cast<LONG>(m_pos.Y * scaleFactor)};
271-
ClientToScreen(parentHwnd, &pt);
308+
// Convert island-local DIP coordinates to screen pixel coordinates.
309+
// Use LocalToScreen which properly handles both ContentIsland and HWND hosting.
310+
auto screenPt = selfView->LocalToScreen({m_pos.X, m_pos.Y});
311+
POINT pt = {static_cast<LONG>(screenPt.X), static_cast<LONG>(screenPt.Y)};
312+
313+
// Calculate initial desired tooltip position and clamp to monitor work area
314+
int tooltipX = pt.x - tooltipData->width / 2;
315+
int tooltipY = static_cast<int>(pt.y - tooltipData->height - (toolTipPlacementMargin * scaleFactor));
316+
ClampTooltipToMonitor(pt, tooltipData->width, tooltipData->height, scaleFactor, tooltipX, tooltipY);
272317

273318
RegisterTooltipWndClass();
274319
HINSTANCE hInstance = GetModuleHandle(NULL);
275320
m_hwndTip = CreateWindow(
276321
c_tooltipWindowClassName,
277322
L"Tooltip",
278323
WS_POPUP,
279-
pt.x - tooltipData->width / 2,
280-
static_cast<int>(pt.y - tooltipData->height - (toolTipPlacementMargin * scaleFactor)),
324+
tooltipX,
325+
tooltipY,
281326
tooltipData->width,
282327
tooltipData->height,
283328
parentHwnd,
@@ -326,6 +371,12 @@ void TooltipService::StopTracking(const winrt::Microsoft::ReactNative::Component
326371
}
327372
}
328373

374+
void TooltipService::DismissAllTooltips() noexcept {
375+
for (auto &tracker : m_trackers) {
376+
tracker->DismissActiveTooltip();
377+
}
378+
}
379+
329380
static const ReactPropertyId<winrt::Microsoft::ReactNative::ReactNonAbiValue<std::shared_ptr<TooltipService>>>
330381
&TooltipServicePropertyId() noexcept {
331382
static const ReactPropertyId<winrt::Microsoft::ReactNative::ReactNonAbiValue<std::shared_ptr<TooltipService>>> prop{

vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ struct TooltipTracker {
3535
const winrt::Microsoft::ReactNative::ComponentView &) noexcept;
3636

3737
facebook::react::Tag Tag() const noexcept;
38+
void DismissActiveTooltip() noexcept;
3839

3940
private:
4041
void ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
@@ -53,6 +54,7 @@ struct TooltipService {
5354
TooltipService(const winrt::Microsoft::ReactNative::ReactPropertyBag &properties);
5455
void StartTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
5556
void StopTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
57+
void DismissAllTooltips() noexcept;
5658

5759
static std::shared_ptr<TooltipService> GetCurrent(
5860
const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept;

0 commit comments

Comments
 (0)