Skip to content

Commit 528d52b

Browse files
authored
Show tooltip on keyboard focus, enforce single visible tooltip (#16123)
* Show tooltip on keyboard focus, enforce single visible tooltip * yarn format * Change files
1 parent cec070e commit 528d52b

3 files changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Show tooltip on keyboard focus, enforce single visible tooltip",
4+
"packageName": "react-native-windows",
5+
"email": "74712637+iamAbhi-916@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,22 @@ TooltipTracker::TooltipTracker(
169169
view.PointerEntered({this, &TooltipTracker::OnPointerEntered});
170170
view.PointerExited({this, &TooltipTracker::OnPointerExited});
171171
view.PointerMoved({this, &TooltipTracker::OnPointerMoved});
172+
view.GotFocus({this, &TooltipTracker::OnGotFocus});
173+
view.LostFocus({this, &TooltipTracker::OnLostFocus});
172174
view.Unmounted({this, &TooltipTracker::OnUnmounted});
173175
}
174176

175177
TooltipTracker::~TooltipTracker() {
176178
DestroyTimer();
177179
DestroyTooltip();
180+
m_outer->NotifyDismiss(this);
181+
}
182+
183+
void TooltipTracker::DismissForExternalRequest() noexcept {
184+
// Service is already updating its active slot; do not call back into it.
185+
m_focusTooltip = false;
186+
DestroyTimer();
187+
DestroyTooltip();
178188
}
179189

180190
facebook::react::Tag TooltipTracker::Tag() const noexcept {
@@ -192,6 +202,9 @@ void TooltipTracker::OnPointerEntered(
192202
auto pp = args.GetCurrentPoint(-1);
193203
m_pos = pp.Position();
194204

205+
// Claim the single tooltip slot, dismissing any other tracker's pending or visible tooltip.
206+
m_outer->NotifyShow(this);
207+
195208
m_timer = winrt::Microsoft::ReactNative::Timer::Create(m_properties.Handle());
196209
m_timer.Interval(std::chrono::milliseconds(toolTipTimeToShowMs));
197210
m_timer.Tick({this, &TooltipTracker::OnTick});
@@ -225,13 +238,64 @@ void TooltipTracker::OnPointerExited(
225238
return;
226239
DestroyTimer();
227240
DestroyTooltip();
241+
m_outer->NotifyDismiss(this);
242+
}
243+
244+
void TooltipTracker::OnGotFocus(
245+
const winrt::Windows::Foundation::IInspectable & /*sender*/,
246+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs & /*args*/) noexcept {
247+
// Skip if a mouse-driven tooltip or its dwell timer is already in flight on this view.
248+
if (m_hwndTip || m_timer) {
249+
return;
250+
}
251+
252+
auto view = m_view.view();
253+
if (!view) {
254+
return;
255+
}
256+
257+
auto viewCompView = view.try_as<winrt::Microsoft::ReactNative::Composition::ViewComponentView>();
258+
if (!viewCompView) {
259+
return;
260+
}
261+
auto selfView =
262+
winrt::get_self<winrt::Microsoft::ReactNative::Composition::implementation::ViewComponentView>(viewCompView);
263+
RECT rc = selfView->getClientRect();
264+
auto scaleFactor = view.LayoutMetrics().PointScaleFactor;
265+
if (scaleFactor <= 0) {
266+
return;
267+
}
268+
269+
// Anchor in DIPs at the horizontal center of the view's top edge; ShowTooltip re-applies scaleFactor.
270+
m_pos = {static_cast<float>(rc.left + rc.right) / 2.0f / scaleFactor, static_cast<float>(rc.top) / scaleFactor};
271+
272+
m_focusTooltip = true;
273+
// Claim the single tooltip slot, dismissing any other tracker's pending or visible tooltip.
274+
m_outer->NotifyShow(this);
275+
m_timer = winrt::Microsoft::ReactNative::Timer::Create(m_properties.Handle());
276+
m_timer.Interval(std::chrono::milliseconds(toolTipTimeToShowMs));
277+
m_timer.Tick({this, &TooltipTracker::OnTick});
278+
m_timer.Start();
279+
}
280+
281+
void TooltipTracker::OnLostFocus(
282+
const winrt::Windows::Foundation::IInspectable & /*sender*/,
283+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs & /*args*/) noexcept {
284+
if (!m_focusTooltip) {
285+
return;
286+
}
287+
m_focusTooltip = false;
288+
DestroyTimer();
289+
DestroyTooltip();
290+
m_outer->NotifyDismiss(this);
228291
}
229292

230293
void TooltipTracker::OnUnmounted(
231294
const winrt::Windows::Foundation::IInspectable &,
232295
const winrt::Microsoft::ReactNative::ComponentView &) noexcept {
233296
DestroyTimer();
234297
DestroyTooltip();
298+
m_outer->NotifyDismiss(this);
235299
}
236300

237301
void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
@@ -326,6 +390,22 @@ void TooltipService::StopTracking(const winrt::Microsoft::ReactNative::Component
326390
}
327391
}
328392

393+
void TooltipService::NotifyShow(TooltipTracker *tracker) noexcept {
394+
if (m_activeTracker == tracker) {
395+
return;
396+
}
397+
if (m_activeTracker) {
398+
m_activeTracker->DismissForExternalRequest();
399+
}
400+
m_activeTracker = tracker;
401+
}
402+
403+
void TooltipService::NotifyDismiss(TooltipTracker *tracker) noexcept {
404+
if (m_activeTracker == tracker) {
405+
m_activeTracker = nullptr;
406+
}
407+
}
408+
329409
static const ReactPropertyId<winrt::Microsoft::ReactNative::ReactNonAbiValue<std::shared_ptr<TooltipService>>>
330410
&TooltipServicePropertyId() noexcept {
331411
static const ReactPropertyId<winrt::Microsoft::ReactNative::ReactNonAbiValue<std::shared_ptr<TooltipService>>> prop{

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ struct TooltipTracker {
2727
void OnPointerExited(
2828
const winrt::Windows::Foundation::IInspectable &sender,
2929
const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept;
30+
void OnGotFocus(
31+
const winrt::Windows::Foundation::IInspectable &sender,
32+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept;
33+
void OnLostFocus(
34+
const winrt::Windows::Foundation::IInspectable &sender,
35+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept;
3036
void OnTick(
3137
const winrt::Windows::Foundation::IInspectable &,
3238
const winrt::Windows::Foundation::IInspectable &) noexcept;
@@ -36,6 +42,10 @@ struct TooltipTracker {
3642

3743
facebook::react::Tag Tag() const noexcept;
3844

45+
// Cancel pending dwell timer and close any visible tooltip popup; used by the service when another tracker takes
46+
// over.
47+
void DismissForExternalRequest() noexcept;
48+
3949
private:
4050
void ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
4151
void DestroyTimer() noexcept;
@@ -46,6 +56,7 @@ struct TooltipTracker {
4656
::Microsoft::ReactNative::ReactTaggedView m_view;
4757
winrt::Microsoft::ReactNative::ITimer m_timer;
4858
HWND m_hwndTip{nullptr};
59+
bool m_focusTooltip{false};
4960
winrt::Microsoft::ReactNative::ReactPropertyBag m_properties;
5061
};
5162

@@ -54,12 +65,18 @@ struct TooltipService {
5465
void StartTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
5566
void StopTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
5667

68+
// Enforce "only one tooltip visible at a time": dismisses the previously active tracker, if any.
69+
void NotifyShow(TooltipTracker *tracker) noexcept;
70+
// Clears the active-tracker slot if it still points at `tracker`.
71+
void NotifyDismiss(TooltipTracker *tracker) noexcept;
72+
5773
static std::shared_ptr<TooltipService> GetCurrent(
5874
const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept;
5975

6076
private:
6177
std::vector<std::shared_ptr<TooltipTracker>> m_enteredTrackers;
6278
std::vector<std::shared_ptr<TooltipTracker>> m_trackers;
79+
TooltipTracker *m_activeTracker{nullptr};
6380
winrt::Microsoft::ReactNative::ReactPropertyBag m_properties;
6481
};
6582

0 commit comments

Comments
 (0)