diff --git a/change/react-native-windows-87ca31c6-96c8-42cc-9b3c-432a69889fdc.json b/change/react-native-windows-87ca31c6-96c8-42cc-9b3c-432a69889fdc.json new file mode 100644 index 00000000000..0b37d599835 --- /dev/null +++ b/change/react-native-windows-87ca31c6-96c8-42cc-9b3c-432a69889fdc.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Show tooltip on keyboard focus, enforce single visible tooltip", + "packageName": "react-native-windows", + "email": "74712637+iamAbhi-916@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp index e65dac0061a..cfa5fa66f93 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp @@ -169,12 +169,22 @@ TooltipTracker::TooltipTracker( view.PointerEntered({this, &TooltipTracker::OnPointerEntered}); view.PointerExited({this, &TooltipTracker::OnPointerExited}); view.PointerMoved({this, &TooltipTracker::OnPointerMoved}); + view.GotFocus({this, &TooltipTracker::OnGotFocus}); + view.LostFocus({this, &TooltipTracker::OnLostFocus}); view.Unmounted({this, &TooltipTracker::OnUnmounted}); } TooltipTracker::~TooltipTracker() { DestroyTimer(); DestroyTooltip(); + m_outer->NotifyDismiss(this); +} + +void TooltipTracker::DismissForExternalRequest() noexcept { + // Service is already updating its active slot; do not call back into it. + m_focusTooltip = false; + DestroyTimer(); + DestroyTooltip(); } facebook::react::Tag TooltipTracker::Tag() const noexcept { @@ -192,6 +202,9 @@ void TooltipTracker::OnPointerEntered( auto pp = args.GetCurrentPoint(-1); m_pos = pp.Position(); + // Claim the single tooltip slot, dismissing any other tracker's pending or visible tooltip. + m_outer->NotifyShow(this); + m_timer = winrt::Microsoft::ReactNative::Timer::Create(m_properties.Handle()); m_timer.Interval(std::chrono::milliseconds(toolTipTimeToShowMs)); m_timer.Tick({this, &TooltipTracker::OnTick}); @@ -225,6 +238,56 @@ void TooltipTracker::OnPointerExited( return; DestroyTimer(); DestroyTooltip(); + m_outer->NotifyDismiss(this); +} + +void TooltipTracker::OnGotFocus( + const winrt::Windows::Foundation::IInspectable & /*sender*/, + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs & /*args*/) noexcept { + // Skip if a mouse-driven tooltip or its dwell timer is already in flight on this view. + if (m_hwndTip || m_timer) { + return; + } + + auto view = m_view.view(); + if (!view) { + return; + } + + auto viewCompView = view.try_as(); + if (!viewCompView) { + return; + } + auto selfView = + winrt::get_self(viewCompView); + RECT rc = selfView->getClientRect(); + auto scaleFactor = view.LayoutMetrics().PointScaleFactor; + if (scaleFactor <= 0) { + return; + } + + // Anchor in DIPs at the horizontal center of the view's top edge; ShowTooltip re-applies scaleFactor. + m_pos = {static_cast(rc.left + rc.right) / 2.0f / scaleFactor, static_cast(rc.top) / scaleFactor}; + + m_focusTooltip = true; + // Claim the single tooltip slot, dismissing any other tracker's pending or visible tooltip. + m_outer->NotifyShow(this); + m_timer = winrt::Microsoft::ReactNative::Timer::Create(m_properties.Handle()); + m_timer.Interval(std::chrono::milliseconds(toolTipTimeToShowMs)); + m_timer.Tick({this, &TooltipTracker::OnTick}); + m_timer.Start(); +} + +void TooltipTracker::OnLostFocus( + const winrt::Windows::Foundation::IInspectable & /*sender*/, + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs & /*args*/) noexcept { + if (!m_focusTooltip) { + return; + } + m_focusTooltip = false; + DestroyTimer(); + DestroyTooltip(); + m_outer->NotifyDismiss(this); } void TooltipTracker::OnUnmounted( @@ -232,6 +295,7 @@ void TooltipTracker::OnUnmounted( const winrt::Microsoft::ReactNative::ComponentView &) noexcept { DestroyTimer(); DestroyTooltip(); + m_outer->NotifyDismiss(this); } void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { @@ -326,6 +390,22 @@ void TooltipService::StopTracking(const winrt::Microsoft::ReactNative::Component } } +void TooltipService::NotifyShow(TooltipTracker *tracker) noexcept { + if (m_activeTracker == tracker) { + return; + } + if (m_activeTracker) { + m_activeTracker->DismissForExternalRequest(); + } + m_activeTracker = tracker; +} + +void TooltipService::NotifyDismiss(TooltipTracker *tracker) noexcept { + if (m_activeTracker == tracker) { + m_activeTracker = nullptr; + } +} + 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..de76bafcd6b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TooltipService.h @@ -27,6 +27,12 @@ struct TooltipTracker { void OnPointerExited( const winrt::Windows::Foundation::IInspectable &sender, const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept; + void OnGotFocus( + const winrt::Windows::Foundation::IInspectable &sender, + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept; + void OnLostFocus( + const winrt::Windows::Foundation::IInspectable &sender, + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept; void OnTick( const winrt::Windows::Foundation::IInspectable &, const winrt::Windows::Foundation::IInspectable &) noexcept; @@ -36,6 +42,10 @@ struct TooltipTracker { facebook::react::Tag Tag() const noexcept; + // Cancel pending dwell timer and close any visible tooltip popup; used by the service when another tracker takes + // over. + void DismissForExternalRequest() noexcept; + private: void ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept; void DestroyTimer() noexcept; @@ -46,6 +56,7 @@ struct TooltipTracker { ::Microsoft::ReactNative::ReactTaggedView m_view; winrt::Microsoft::ReactNative::ITimer m_timer; HWND m_hwndTip{nullptr}; + bool m_focusTooltip{false}; winrt::Microsoft::ReactNative::ReactPropertyBag m_properties; }; @@ -54,12 +65,18 @@ struct TooltipService { void StartTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept; void StopTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept; + // Enforce "only one tooltip visible at a time": dismisses the previously active tracker, if any. + void NotifyShow(TooltipTracker *tracker) noexcept; + // Clears the active-tracker slot if it still points at `tracker`. + void NotifyDismiss(TooltipTracker *tracker) noexcept; + static std::shared_ptr GetCurrent( const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept; private: std::vector> m_enteredTrackers; std::vector> m_trackers; + TooltipTracker *m_activeTracker{nullptr}; winrt::Microsoft::ReactNative::ReactPropertyBag m_properties; };