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) 🔬
- Create a new Uno app (Skia Desktop or WebAssembly).
- 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;
- Run the app, click the
TextBox, press Shift: KeyDown Shift from ...TextBox is logged (both WinUI and Uno).
- 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 🎨
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.
Current behavior 🐛
When no element has keyboard focus, Uno raises
KeyDown,KeyUp,PreviewKeyDown/PreviewKeyUpandCharacterReceivedfrom the internal root element (theXamlIslandRoot/ root visual), which sits aboveWindow.Content. A handler attached toWindow.Content(for example on the rootGridorFrame) is therefore never invoked.A common way to get into this state is clicking a non-focusable area (a
Borderwith a background, an empty part of aGrid, ...). On Uno this clears focus tonull, and from then on key handlers onWindow.Contentstop receiving keys.Expected behavior 🎯
Match WinUI: when nothing is focused (or the root
ScrollVieweris focused), key events are routed from the public root visual, i.e.Window.Content, so aKeyDownhandler onWindow.Contentfires withOriginalSource == Window.Content.WinUI source,
KeyboardInputProcessor::GetKeyRoutedSource:https://github.com/microsoft/microsoft-ui-xaml/blob/5c55b33be6eec5ff02939ba8563523c59a37dfa7/dxaml/xcp/components/ContentRoot/KeyboardInputProcessor.cpp#L746-L768
CharacterReceiveduses 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) 🔬
TextBox, press Shift:KeyDown Shift from ...TextBoxis logged (both WinUI and Uno).Border, then press Shift again.WinUI: focus moves to the root
ScrollViewer, andKeyDown Shift from ...Gridis logged.Uno: focus becomes
nulland nothing is logged; the rootGridnever sees the key.Runtime test that reproduces it on Skia Desktop (fails on current
master):Result on Uno: focused element after the click is
null, andKeyDownis never raised onWindow.Content.Scope note: this is about handlers on
Window.Contentitself. A handler on aPagehosted inside aFramedoesn't receive these keys on WinUI either, because the route starts atWindow.Contentand only bubbles up.Workaround 🛠️
handledEventsToo: trueto the topmost visual instead ofWindow.Content(walkVisualTreeHelper.GetParentup fromXamlRoot.Content). This relies on Uno internals and would need to be removed once fixed.Renderer 🎨
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
masterat 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
Border,FocusManager.GetFocusedElementreturns the rootScrollViewer, and theKeyDownhandler onWindow.Contentfires withOriginalSource= the rootGrid. The same happens when clicking an empty area of theGrid.Root cause
InputManager.KeyboardManager.GetKeyRoutedSourcefalls back toVisualTree.RootElementwhen nothing is focused:uno/src/Uno.UI/UI/Xaml/Internal/InputManager.Keyboard.cs
Lines 193 to 196 in bfc8dff
RootElementis the root visual /XamlIslandRoot, the parent of all roots, so the route starts aboveWindow.Content. It is used forPreviewKeyDown/KeyDown/PreviewKeyUp/KeyUp(OnKey) and forCharacterReceived(RaiseCharacterReceived).Suggested fix
Mirror WinUI in
GetKeyRoutedSource: when focus isnull, useVisualTree.PublicRootVisual, and if that is aWindowChrome(it is on Desktop:DesktopWindowwraps the content in one), unwrap it to itsContent. KeepRootElementonly as a last resort. If a rootScrollVieweris added later, it should take the same path.Related context (not this issue)
The reason focus ends up
nullso often is a known deviation: clicking a non-focusable area clears focus in Uno, while WinUI moves focus to the rootScrollViewer. This is documented in the source:uno/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.cs
Lines 156 to 169 in bfc8dff
Even with that deviation, routing keys from the public root visual when focus is
nullwould match WinUI's observableKeyDownbehaviour onWindow.Content.