Fix tooltip positioning for ContentIsland and dismiss on scroll#15644
Fix tooltip positioning for ContentIsland and dismiss on scroll#15644Nitin-100 wants to merge 7 commits intomicrosoft:mainfrom
Conversation
- 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
df61636 to
55c9416
Compare
| // 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(); |
There was a problem hiding this comment.
GetCurrent can return nullptr if the TooltipService hasn't been initialized, right ?
This might lead to crashes
| DestroyTooltip(); | ||
| } | ||
|
|
||
| void TooltipTracker::DismissActiveTooltip() noexcept { |
There was a problem hiding this comment.
we should also dismiss the current tracking state, next hover should always new state
|
|
||
| // 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( |
There was a problem hiding this comment.
we should also handle negative or zero tooltip size , in case of integer overflow from bad text metrics
| const RECT &workArea = mi.rcWork; | ||
|
|
||
| // Clamp horizontally | ||
| if (x + tooltipWidth > workArea.right) { |
There was a problem hiding this comment.
in case of very wide tooltip , tooltipWidth > (workArea.right - workArea.left), the first clamp sets x to a value less than workArea.left, and then the second clamp overrides it to workArea.left.
I guess tooltip will overflow on the right
suggestion
could be addressed with x = workArea.left and capping width to monitor width
Description
Fixes tooltip positioning being incorrect (trimmed/offset) when scrolling in Fabric ContentIsland-hosted apps, and ensures tooltips are dismissed immediately when the user scrolls.
Why
In the RNW Gallery app (and any Fabric Composition app), hovering over a component with a
tooltipprop and then scrolling causes:Wrong tooltip position — The tooltip appears at an incorrect screen location because the coordinate conversion used
ClientToScreen(parentHwnd), which assumes HWND client coordinates. In ContentIsland hosting,m_pos(fromPointerPoint::Position()) is in island-local DIP coordinates, not HWND client coordinates.ClientToScreenproduces the wrong screen position.Stale tooltip on scroll — After scrolling, the tooltip remains visible at the old screen position while the component underneath has moved, making the tooltip appear detached or trimmed.
What
Files Changed
TooltipService.cppClientToScreenwithLocalToScreenfor correct coordinate conversion; extractClampTooltipToMonitorhelper; addDismissActiveTooltipimplementationTooltipService.hDismissActiveTooltip()toTooltipTracker; addDismissAllTooltips()toTooltipServiceScrollViewComponentView.cppTooltipService::DismissAllTooltips()on scroll position changeChanges in Detail
TooltipService.cpp— Coordinate fix (core fix)POINT pt = {m_pos.X * scaleFactor, m_pos.Y * scaleFactor}; ClientToScreen(parentHwnd, &pt);auto screenPt = selfView->LocalToScreen({m_pos.X, m_pos.Y}); POINT pt = {static_cast<LONG>(screenPt.X), static_cast<LONG>(screenPt.Y)};LocalToScreenfollows the correct Fabric conversion chain:ComponentView::LocalToScreen()→RootComponentView::ConvertLocalToScreen()→ReactNativeIsland::ConvertLocalToScreen()which usesm_island.CoordinateConverter().ConvertLocalToScreen()for ContentIsland hosting, or falls back toClientToScreen(m_hwnd)for legacy HWND hosting.TooltipService.cpp— Monitor edge clamping (extracted helper)ClampTooltipToMonitor()— clamps tooltip X/Y to the nearest monitor's work area and flips tooltip below cursor if it would go above the screen.TooltipService.h/.cpp— Dismiss APITooltipTracker::DismissActiveTooltip()— destroys any active timer and tooltip window (both have null guards, so calling on inactive trackers is a no-op).TooltipService::DismissAllTooltips()— iterates all trackers and callsDismissActiveTooltip().ScrollViewComponentView.cpp— Scroll dismiss integrationTooltipService::GetCurrent(m_reactContext.Properties())->DismissAllTooltips()at the top of theScrollPositionChangedcallback. This covers all scroll types: mouse wheel, touch drag, keyboard navigation, and programmatic scroll.How
Root Cause Analysis
The
LocalToScreencall properly traverses the Fabric view hierarchy:This handles both hosting models correctly without hardcoding either one.
Scroll Dismissal
ScrollPositionChangedon theIScrollVisualfires for all scroll types (wheel, touch, keyboard, programmatic). CallingDismissAllTooltips()here is the single integration point. Each tracker'sDismissActiveTooltip()delegates toDestroyTimer()+DestroyTooltip(), both of which haveif (m_timer)/if (m_hwndTip)null guards — so inactive trackers cost just two pointer comparisons.Testing
tooltipprop until the tooltip appearsScreenshots
Before
failedtooltip.mp4
After
fixedtooltip.mp4
Microsoft Reviewers: Open in CodeFlow