Skip to content
Merged
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
Expand Up @@ -46,6 +46,7 @@ public async Task When_Clipped_Inside_ScrollViewer()

[TestMethod]
[GitHubWorkItem("https://github.com/unoplatform/brain-products-private/issues/14")]
[PlatformCondition(ConditionMode.Exclude, RuntimeTestPlatforms.Android)] // https://github.com/unoplatform/uno/issues/22665
public async Task When_Waiting_For_Another_Thread()
{
if (OperatingSystem.IsBrowser())
Expand All @@ -60,6 +61,7 @@ public async Task When_Waiting_For_Another_Thread()

[TestMethod]
[GitHubWorkItem("https://github.com/unoplatform/brain-products-private/issues/14")]
[PlatformCondition(ConditionMode.Exclude, RuntimeTestPlatforms.Android)] // https://github.com/unoplatform/uno/issues/22665
public async Task When_Waiting_For_Another_Thread2()
{
if (OperatingSystem.IsBrowser())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,133 @@ public async Task When_ToolTip_Popup_XamlRoot()
toolTip.IsOpen = false;
}
}
#if HAS_UNO
#if !HAS_INPUT_INJECTOR
[Ignore("InputInjector is not supported on this platform.")]
#endif
[TestMethod]
public async Task When_Programmatic_Focus_Does_Not_Show_ToolTip_At_Pointer_Position()
{
var button = new Button
{
Content = "Button with tooltip",
Width = 200,
Height = 50,
};
var tooltip = new ToolTip
{
Content = "ToolTip content",
};
ToolTipService.SetToolTip(button, tooltip);

// A separate element to tap on, positioned far from the button.
var tapTarget = new Border
{
Width = 200,
Height = 200,
Background = new SolidColorBrush(Colors.Transparent),
};

var stackPanel = new StackPanel
{
Spacing = 100,
Children = { button, tapTarget },
};

await UITestHelper.Load(stackPanel);

var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector");
using var mouse = injector.GetMouse();

// Tap the far-away target to set the last pointer position away from the button.
var tapTargetCenter = tapTarget.GetAbsoluteBoundsRect().GetCenter();
mouse.Press(tapTargetCenter);
await UITestHelper.WaitForIdle();
mouse.Release();
await UITestHelper.WaitForIdle();

// Programmatically focus the button - this should NOT open the tooltip.
button.Focus(FocusState.Programmatic);
await Task.Delay(TimeSpan.FromMilliseconds(FeatureConfiguration.ToolTip.ShowDelay + 500));
await UITestHelper.WaitForIdle();

Assert.IsFalse(tooltip.IsOpen, "ToolTip should not open on programmatic focus.");
}

#if !HAS_INPUT_INJECTOR
[Ignore("InputInjector is not supported on this platform.")]
#endif
[TestMethod]
public async Task When_Keyboard_Focus_Shows_ToolTip_Above_Button()
{
try
{
var button = new Button
{
Content = "Button with tooltip",
Width = 200,
Height = 50,
};
var tooltip = new ToolTip
{
Content = "ToolTip content",
};
ToolTipService.SetToolTip(button, tooltip);

// A separate focusable element placed far below the button.
var tapTarget = new Button
{
Content = "Tap here first",
Width = 200,
Height = 50,
};

var stackPanel = new StackPanel
{
Spacing = 300,
Children = { button, tapTarget },
};

await UITestHelper.Load(stackPanel);

var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector");
using var mouse = injector.GetMouse();

// Click the far-away target to set the last pointer position away from the button.
var tapTargetCenter = tapTarget.GetAbsoluteBoundsRect().GetCenter();
mouse.Press(tapTargetCenter);
await UITestHelper.WaitForIdle();
mouse.Release();
await UITestHelper.WaitForIdle();

// Tab from tapTarget to the button (Shift+Tab since button is before tapTarget).
await TestServices.KeyboardHelper.ShiftTab();
await Task.Delay(TimeSpan.FromMilliseconds(FeatureConfiguration.ToolTip.ShowDelay + 500));
await UITestHelper.WaitForIdle();

Assert.IsTrue(tooltip.IsOpen, "ToolTip should open on keyboard focus.");

var buttonBounds = button.GetAbsoluteBoundsRect();
var popups = VisualTreeHelper.GetOpenPopupsForXamlRoot(button.XamlRoot);
Assert.IsTrue(popups.Count > 0, "Expected at least one open popup.");

var tooltipPopup = popups[0];

// The tooltip should be positioned near the button (above it), not near the tap target.
// With keyboard input mode, placement falls back to Top, so the popup's vertical
// offset should be near or above the button's top edge, not near tapTargetCenter.Y.
Assert.IsTrue(
tooltipPopup.VerticalOffset < tapTargetCenter.Y - 50,
$"ToolTip popup should be positioned above the button (near Y={buttonBounds.Top}), " +
$"not near the tap position (Y={tapTargetCenter.Y}). Actual VerticalOffset={tooltipPopup.VerticalOffset}");
}
finally
{
VisualTreeHelper.CloseAllPopups(TestServices.WindowHelper.XamlRoot);
}
}
#endif

#if HAS_UNO
#if __APPLE_UIKIT__ || __ANDROID__
[Ignore("Currently fails on Android and iOS")]
Expand Down
35 changes: 30 additions & 5 deletions src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
ο»Ώusing System;
using DirectUI;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Uno.Disposables;
using Uno.UI;
using Uno.UI.Xaml.Core;
using Windows.System;
using Windows.UI;


#if __APPLE_UIKIT__
using UIKit;
Expand All @@ -21,6 +25,8 @@ public partial class ToolTipService
private static DispatcherTimer m_OpenTimer;
private static DispatcherTimer m_CloseTimer;

private static AutomaticToolTipInputMode s_lastEnterInputMode = AutomaticToolTipInputMode.Mouse;

private static void RegisterToolTip(
DependencyObject owner,
FrameworkElement container,
Expand Down Expand Up @@ -88,14 +94,16 @@ private static void OnPlacementChanged(DependencyObject dependencyobject, Depend
}
}

private static void OpenToolTipImpl(ToolTip toolTip)
private static void OpenToolTipImpl(ToolTip toolTip, AutomaticToolTipInputMode mode)
{
if (m_CurrentToolTip is { })
{
// Only one instance of the tooltip can be opened at any time.
CloseToolTipImpl(m_CurrentToolTip);
}

s_lastEnterInputMode = mode;

if (toolTip is { })
{
m_CurrentToolTip = toolTip;
Expand Down Expand Up @@ -136,6 +144,7 @@ private static void OnOpenTimerTick(object sender, object e)

if (m_CurrentToolTip is { })
{
m_CurrentToolTip.m_inputMode = s_lastEnterInputMode;
m_CurrentToolTip.IsOpen = true;
}

Expand Down Expand Up @@ -238,10 +247,26 @@ private static void OnGotFocus(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement owner && GetActualToolTipObject(owner) is { } toolTip)
{
if (toolTip.IsOpen) return;
if (toolTip.IsOpen)
{
return;
}

EnsureOpenTimer();
OpenToolTipImpl(toolTip);
ContentRoot contentRoot = VisualTree.GetContentRootForElement(owner);

var focusState = contentRoot?.FocusManager.GetRealFocusStateForFocusedElement();

// If the source of a programmatic focus was UIA, we should show the tooltip:
bool shouldShowToolTip = (focusState == FocusState.Keyboard) ||
(focusState == FocusState.Programmatic
&& sender is not null
&& contentRoot?.InputManager?.GetWasUIAFocusSetSinceLastInput() == true);

if (shouldShowToolTip)
{
EnsureOpenTimer();
OpenToolTipImpl(toolTip, AutomaticToolTipInputMode.Keyboard);
}
}
}

Expand All @@ -258,7 +283,7 @@ private static void OnPointerEntered(object sender, PointerRoutedEventArgs e)

EnsureOpenTimer();
m_LastEnteredFrameId = e.FrameId;
OpenToolTipImpl(toolTip);
OpenToolTipImpl(toolTip, e.Pointer.PointerDeviceType == UI.Input.PointerDeviceType.Touch ? AutomaticToolTipInputMode.Touch : AutomaticToolTipInputMode.Mouse);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Uno.UI/UI/Xaml/Input/Internal/FocusMovement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal FocusMovement(
/// <summary>
/// Gets the focus state.
/// </summary>
internal FocusState FocusState { get; } = FocusState.Unfocused;
internal FocusState FocusState { get; set; } = FocusState.Unfocused;

/// <summary>
/// Gets the focus navigation direction.
Expand Down
1 change: 1 addition & 0 deletions src/Uno.UI/UI/Xaml/Input/Internal/UnoFocusInputHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ void OnFocusDeparting(object s, object e)
focusMovement.IsShiftPressed = isShiftDown;
focusMovement.IsProcessingTab = true;
focusMovement.ForceBringIntoView = true;
focusMovement.FocusState = FocusState.Keyboard;

focusManager.FocusObserver.FocusController.FocusDeparting += OnFocusDeparting;

Expand Down
3 changes: 3 additions & 0 deletions src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ private void InitializePointers(object host)

void IInputInjectorTarget.InjectPointerRemoved(PointerEventArgs args) => InjectPointerRemoved(args);
partial void InjectPointerRemoved(PointerEventArgs args);

#endregion

internal bool GetWasUIAFocusSetSinceLastInput() => false; // TODO Uno: Not implemented yet.

internal partial class PointerManager
{
private static readonly Logger _log = LogExtensionPoint.Log(typeof(PointerManager));
Expand Down
Loading