Sitelet https://github.com/unoplatform/uno/issues/25004
Skip to content

With nothing focused, key events are raised above Window.Content, so KeyDown handlers on Window.Content stop firing #25004

Description

@MartinZikmund

Current behavior 🐛

When no element has keyboard focus, Uno raises KeyDown, KeyUp, PreviewKeyDown/PreviewKeyUp and CharacterReceived from the internal root element (the XamlIslandRoot / root visual), which sits above Window.Content. A handler attached to Window.Content (for example on the root Grid or Frame) is therefore never invoked.

A common way to get into this state is clicking a non-focusable area (a Border with a background, an empty part of a Grid, ...). On Uno this clears focus to null, and from then on key handlers on Window.Content stop receiving keys.

Expected behavior 🎯

Match WinUI: when nothing is focused (or the root ScrollViewer is focused), key events are routed from the public root visual, i.e. Window.Content, so a KeyDown handler on Window.Content fires with OriginalSource == Window.Content.

WinUI source, KeyboardInputProcessor::GetKeyRoutedSource:
https://github.com/microsoft/microsoft-ui-xaml/blob/5c55b33be6eec5ff02939ba8563523c59a37dfa7/dxaml/xcp/components/ContentRoot/KeyboardInputProcessor.cpp#L746-L768

// Set the source as the public visual root if the focus isn't set or focus is on the root ScrollViewer.
if (pRoutedSource == nullptr || pRoutedSource == visualTree->GetRootScrollViewer())
{
    pRoutedSource = visualTree->GetPublicRootVisual();
    // if window chrome the is top most element on a visual tree, return its content ...
    if (pRoutedSource && pRoutedSource->OfTypeByIndex<KnownTypeIndex::WindowChrome>()) { /* unwrap to WindowChrome.Content */ }
}

CharacterReceived uses the same rule:
https://github.com/microsoft/microsoft-ui-xaml/blob/5c55b33be6eec5ff02939ba8563523c59a37dfa7/dxaml/xcp/components/ContentRoot/KeyboardInputProcessor.cpp#L692-L696

How to reproduce it (as minimally and precisely as possible) 🔬

  1. Create a new Uno app (Skia Desktop or WebAssembly).
  2. Replace the window content with:
var textBox = new TextBox { Width = 200, HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top };
var border = new Border
{
    Width = 200, Height = 100, Margin = new Thickness(0, 100, 0, 0),
    Background = new SolidColorBrush(Microsoft.UI.Colors.Orange),
    HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top,
};
var root = new Grid { Children = { textBox, border } };
root.AddHandler(UIElement.KeyDownEvent,
    new KeyEventHandler((s, e) => System.Diagnostics.Debug.WriteLine($"KeyDown {e.Key} from {e.OriginalSource}")),
    handledEventsToo: true);

window.Content = root;
  1. Run the app, click the TextBox, press Shift: KeyDown Shift from ...TextBox is logged (both WinUI and Uno).
  2. Click the orange Border, then press Shift again.

WinUI: focus moves to the root ScrollViewer, and KeyDown Shift from ...Grid is logged.
Uno: focus becomes null and nothing is logged; the root Grid never sees the key.

Runtime test that reproduces it on Skia Desktop (fails on current master):

[TestMethod]
[RunsOnUIThread]
public async Task When_Click_NonFocusable_Area_Then_KeyDown_Reaches_Window_Content()
{
    var textBox = new TextBox { Width = 200, HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top };
    var border = new Border
    {
        Width = 200, Height = 100, Margin = new Thickness(0, 100, 0, 0),
        Background = new SolidColorBrush(Microsoft.UI.Colors.Orange),
        HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top,
    };
    await UITestHelper.Load(new Grid { Children = { textBox, border } });

    textBox.Focus(FocusState.Programmatic);
    await TestServices.WindowHelper.WaitForIdle();

    var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector");
    using var mouse = injector.GetMouse();
    mouse.Press(border.GetAbsoluteBoundsRect().GetCenter());
    await TestServices.WindowHelper.WaitForIdle();
    mouse.Release();
    await TestServices.WindowHelper.WaitForIdle();

    var windowContent = (UIElement)TestServices.WindowHelper.XamlRoot.Content;
    object keyDownSource = null;
    var handler = new KeyEventHandler((s, e) => keyDownSource ??= e.OriginalSource);
    windowContent.AddHandler(UIElement.KeyDownEvent, handler, handledEventsToo: true);
    try
    {
        var keyboard = TestServices.WindowHelper.XamlRoot.VisualTree.ContentRoot.InputManager.Keyboard;
        keyboard.OnKeyTestingOnly(new Windows.UI.Core.KeyEventArgs("test", VirtualKey.Shift, VirtualKeyModifiers.None, new CorePhysicalKeyStatus()), true);
        keyboard.OnKeyTestingOnly(new Windows.UI.Core.KeyEventArgs("test", VirtualKey.Shift, VirtualKeyModifiers.None, new CorePhysicalKeyStatus()), false);
        await TestServices.WindowHelper.WaitForIdle();
    }
    finally
    {
        windowContent.RemoveHandler(UIElement.KeyDownEvent, handler);
    }

    Assert.IsNotNull(keyDownSource, "KeyDown did not reach the window content");
}

Result on Uno: focused element after the click is null, and KeyDown is never raised on Window.Content.

Scope note: this is about handlers on Window.Content itself. A handler on a Page hosted inside a Frame doesn't receive these keys on WinUI either, because the route starts at Window.Content and only bubbles up.

Workaround 🛠️

  • Keep a focusable element focused (e.g. refocus a control after pointer presses), or
  • Attach the handler with handledEventsToo: true to the topmost visual instead of Window.Content (walk VisualTreeHelper.GetParent up from XamlRoot.Content). This relies on Uno internals and would need to be removed once fixed.

Renderer 🎨

  • Skia
  • Native

Affected platforms 📱💻🖥️

Desktop (Windows), WebAssembly. The routing code is shared by all Skia targets, so the other Skia heads are likely affected too (only Desktop was runtime-verified).

Uno.Sdk version (and other relevant versions) 📦

Runtime-verified on master at bfc8dff (Skia Desktop, net10.0). Also observed in an app on Uno.Sdk 6.7.22 (WebAssembly).

IDE version 🧑‍💻

N/A (runtime test)

Anything else we need to know? 💬

Evidence

  • WinUI behaviour: confirmed both from the C++ sources linked above and with a native WinAppSDK 2.5.1 app running the repro above. After clicking the Border, FocusManager.GetFocusedElement returns the root ScrollViewer, and the KeyDown handler on Window.Content fires with OriginalSource = the root Grid. The same happens when clicking an empty area of the Grid.
  • Uno behaviour: the runtime test above fails on Skia Desktop at the commit listed.

Root cause

InputManager.KeyboardManager.GetKeyRoutedSource falls back to VisualTree.RootElement when nothing is focused:

private UIElement GetKeyRoutedSource(object focusedElement)
=> focusedElement as UIElement
?? (focusedElement as TextElement)?.GetContainingFrameworkElement()
?? _inputManager.ContentRoot.VisualTree.RootElement;

RootElement is the root visual / XamlIslandRoot, the parent of all roots, so the route starts above Window.Content. It is used for PreviewKeyDown/KeyDown/PreviewKeyUp/KeyUp (OnKey) and for CharacterReceived (RaiseCharacterReceived).

Suggested fix

Mirror WinUI in GetKeyRoutedSource: when focus is null, use VisualTree.PublicRootVisual, and if that is a WindowChrome (it is on Desktop: DesktopWindow wraps the content in one), unwrap it to its Content. Keep RootElement only as a last resort. If a root ScrollViewer is added later, it should take the same path.

Related context (not this issue)

The reason focus ends up null so often is a known deviation: clicking a non-focusable area clears focus in Uno, while WinUI moves focus to the root ScrollViewer. This is documented in the source:

// Uno specific: To ensure focus is properly lost when clicking "outside" the app's content,
// we set focus here. In the case of UWP, the focus is set to the root ScrollViewer instead,
// but Uno does not have it on all targets yet.
var focusedElement = _inputManager.ContentRoot.XamlRoot is { } xamlRoot
? FocusManager.GetFocusedElement(xamlRoot)
: null;
if (!isHandled // so isAfterHandledUp is false!
&& _canUnFocusOnNextLeftPointerRelease
&& args.GetCurrentPoint(null).Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonReleased
&& !PointerCapture.TryGet(args.Pointer, out _)
&& focusedElement is UIElement uiElement)
{
uiElement.Unfocus();
}

Even with that deviation, routing keys from the public root visual when focus is null would match WinUI's observable KeyDown behaviour on Window.Content.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/focusCategorizes an issue as relevant to focus managementarea/skia ✏️Categorizes an issue or PR as relevant to Skiadifficulty/starter 🚀Categorizes an issue for which the difficulty level is reachable by newcomersgood first issueDenotes an issue ready for a new contributor, according to the "help wanted" guidelines.kind/bugSomething isn't workingplatform/desktop 🖥️Categorizes an issue or PR as relevant to Desktopplatform/wasm 🌐Categorizes an issue or PR as relevant to the WebAssembly platformproject/keyboard ⌨️Categories and issue or PR as relevant to keyboard input or keyboard accelerators.triage/untriagedIndicates an issue requires triaging or verification

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions