From 6d61ed91bad5f9b06d60aa4c5fe7a74b535e1876 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 13 Aug 2026 05:07:16 -0700 Subject: [PATCH 1/3] Add INativeWebDialog.Focus() implementations --- .../Android/AndroidNativeWebViewDialog.cs | 2 ++ .../Browser/BrowserWindowNativeWebViewDialog.cs | 7 +++++++ .../Browser/WebViewInterop.cs | 3 +++ .../Gtk/GtkNativeWebViewDialog.cs | 6 ++++++ src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs | 5 +++++ .../staticwebassets/av-webview.mjs | 6 ++++++ 6 files changed, 29 insertions(+) diff --git a/src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs b/src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs index ee7e54e..3a1c9fd 100644 --- a/src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs +++ b/src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs @@ -80,6 +80,8 @@ private async void ShowCore(IPlatformHandle? owner) _isOpen = true; } + public void Focus() => (TryGetAdapter() as IWebViewAdapterWithFocus)?.Focus(); + public void Close() { if (!_isOpen || _context is null) return; diff --git a/src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs b/src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs index e66be92..f2c02b1 100644 --- a/src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs +++ b/src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs @@ -94,6 +94,13 @@ public bool Show(IPlatformHandle owner) return true; } + public void Focus() + { + if (_popup is { } popup) + WebViewInterop.FocusDialogWindow(popup); + (_adapter as IWebViewAdapterWithFocus)?.Focus(); + } + public void Close() { if (_popup is { } popup) diff --git a/src/Avalonia.Controls.WebView.Core/Browser/WebViewInterop.cs b/src/Avalonia.Controls.WebView.Core/Browser/WebViewInterop.cs index d36184d..8ec6d07 100644 --- a/src/Avalonia.Controls.WebView.Core/Browser/WebViewInterop.cs +++ b/src/Avalonia.Controls.WebView.Core/Browser/WebViewInterop.cs @@ -96,6 +96,9 @@ public static partial Action SubscribeMessages( [JSImport("closeDialogWindow", "av-webview")] public static partial void CloseDialogWindow(JSObject popup); + [JSImport("focusDialogWindow", "av-webview")] + public static partial void FocusDialogWindow(JSObject popup); + [JSImport("resizeDialogWindow", "av-webview")] public static partial bool ResizeDialogWindow(JSObject popup, int width, int height); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs index 7d778cd..b8cf7fc 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs @@ -215,6 +215,12 @@ public bool Show(IPlatformHandle owner) return true; } + public void Focus() => RunOnGlibThreadAsync(() => + { + gtk_window_present(_windowHandle); + _nativeWebView?.Focus(); + }); + public void Close() { // Closing is invoked in two places for the GTK webview: diff --git a/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs index 200e33c..6dc5ce7 100644 --- a/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs @@ -178,6 +178,11 @@ internal interface INativeWebViewDialog : IDisposable /// void Close(); + /// + /// Activates the dialog and moves keyboard focus to the hosted web content. + /// + void Focus(); + /// /// Resizes the WebView dialog. /// diff --git a/src/Avalonia.Controls.WebView.Core/staticwebassets/av-webview.mjs b/src/Avalonia.Controls.WebView.Core/staticwebassets/av-webview.mjs index 79db00a..624f3db 100644 --- a/src/Avalonia.Controls.WebView.Core/staticwebassets/av-webview.mjs +++ b/src/Avalonia.Controls.WebView.Core/staticwebassets/av-webview.mjs @@ -170,6 +170,12 @@ export function closeDialogWindow(popup) { } catch { } } +export function focusDialogWindow(popup) { + try { + popup.focus(); + } catch { } +} + export function resizeDialogWindow(popup, width, height) { try { popup.resizeTo(width, height); From 6b657fb3e230488d64219fb4193adec5061dc754 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 13 Aug 2026 05:08:53 -0700 Subject: [PATCH 2/3] Add ShowFocused and Focus new APIs --- .../EmptyNativeWebViewControlImpl.cs | 2 + .../INativeWebViewControlImpl.cs | 5 ++ .../NativeWebDialog.cs | 49 ++++++++++++++++++- .../NativeWebViewCompositorHost.cs | 3 ++ .../NativeWebViewControlHost.cs | 3 ++ .../WindowNativeWebViewDialog.cs | 18 +++++++ 6 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.WebView/EmptyNativeWebViewControlImpl.cs b/src/Avalonia.Controls.WebView/EmptyNativeWebViewControlImpl.cs index 5d986e7..6717341 100644 --- a/src/Avalonia.Controls.WebView/EmptyNativeWebViewControlImpl.cs +++ b/src/Avalonia.Controls.WebView/EmptyNativeWebViewControlImpl.cs @@ -27,6 +27,8 @@ internal class EmptyNativeWebViewControlImpl : Control, INativeWebViewControlImp public Task GetAdapterAsync() => Task.FromResult(null); + public void FocusWebView() { } + public IDisposable BeginReparenting(bool yieldOnLayoutBeforeExiting) => EmptyDisposable.Instance; public IAsyncDisposable BeginReparentingAsync() => EmptyDisposable.Instance; diff --git a/src/Avalonia.Controls.WebView/INativeWebViewControlImpl.cs b/src/Avalonia.Controls.WebView/INativeWebViewControlImpl.cs index 52ac6cf..cde3d65 100644 --- a/src/Avalonia.Controls.WebView/INativeWebViewControlImpl.cs +++ b/src/Avalonia.Controls.WebView/INativeWebViewControlImpl.cs @@ -27,6 +27,11 @@ internal interface INativeWebViewControlImpl /// Task GetAdapterAsync(); + /// + /// Moves keyboard focus to the hosted web content, if it's already initialized. + /// + void FocusWebView(); + /// . IDisposable BeginReparenting(bool yieldOnLayoutBeforeExiting); diff --git a/src/Avalonia.Controls.WebView/NativeWebDialog.cs b/src/Avalonia.Controls.WebView/NativeWebDialog.cs index f895158..9f04ff6 100644 --- a/src/Avalonia.Controls.WebView/NativeWebDialog.cs +++ b/src/Avalonia.Controls.WebView/NativeWebDialog.cs @@ -45,6 +45,8 @@ public class NativeWebDialog : Core.IWebView, Core.IWebViewHolder, IDisposable private Color? _initialDefaultBackground; private bool _disposed; private bool _dialogInitialized; + private bool _shown; + private bool _focusRequested; static NativeWebDialog() { @@ -299,10 +301,19 @@ public Color? DefaultBackground } } + /// + /// Gets or sets if the dialog moves keyboard focus to its web content when shown. Default is true. + /// + public bool ShowFocused { get; set; } = true; + /// public event EventHandler? Closing; /// - public async void Show() => (await GetOrInitialize()).Show(); + public async void Show() + { + (await GetOrInitialize()).Show(); + OnShown(); + } #if WPF /// @@ -345,6 +356,39 @@ public async void Show(TopLevel owner) { impl.Show(); } + + OnShown(); + } + + /// + /// Activates the dialog and moves keyboard focus to the web content hosted inside of it. + /// + public void Focus() + { + _focusRequested = true; + TryApplyFocus(); + } + + private void OnShown() + { + _shown = true; + _focusRequested |= ShowFocused; + TryApplyFocus(); + } + + private void TryApplyFocus() + { + // The adapter is typically created only after the dialog window was shown, + // and native focus can't be moved before that. + if (!_focusRequested || !_shown + || TryGetImpl() is not { } impl + || impl.TryGetAdapter() is null) + { + return; + } + + _focusRequested = false; + impl.Focus(); } #if WPF @@ -501,6 +545,7 @@ private void DialogImplOnAdapterDestroyed(object? sender, Core.WebViewAdapterEve adapter.WebResourceRequested -= WebViewAdapterOnWebResourceRequested; adapter.NewWindowRequested -= WebViewAdapterOnNewWindowRequested; _dialogInitialized = false; + _shown = false; AdapterDestroyed?.Invoke(this, e); } @@ -532,6 +577,8 @@ private void DialogImplOnAdapterCreated(object? sender, Core.WebViewAdapterEvent else if (_lastSource is ValueTuple pair) adapter.NavigateToString(pair.Item1, pair.Item2); AdapterCreated?.Invoke(this, e); + + TryApplyFocus(); } private void DialogImplOnClosing(object? sender, EventArgs e) diff --git a/src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs b/src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs index 3a2ce2e..3962886 100644 --- a/src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs +++ b/src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs @@ -44,6 +44,9 @@ static NativeWebViewCompositorHost() _webViewReadyCompletion.Task.Result : null; + /// + public void FocusWebView() => Focus(); + /// public async Task GetAdapterAsync() => _webViewReadyCompletion is null ? null : await _webViewReadyCompletion.Task; diff --git a/src/Avalonia.Controls.WebView/NativeWebViewControlHost.cs b/src/Avalonia.Controls.WebView/NativeWebViewControlHost.cs index e04ca28..3e32d44 100644 --- a/src/Avalonia.Controls.WebView/NativeWebViewControlHost.cs +++ b/src/Avalonia.Controls.WebView/NativeWebViewControlHost.cs @@ -72,6 +72,9 @@ async void CompleteAdapter(WebViewAdapter.AdapterWrapper wrapper) _webViewReadyCompletion.Task.Result : null; + /// + public void FocusWebView() => (TryGetAdapter() as Core.IWebViewAdapterWithFocus)?.Focus(); + protected override void DestroyNativeControlCore(IPlatformHandle control) { var adapter = TryGetAdapter(); diff --git a/src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs b/src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs index d6c44ab..9cc4f0f 100644 --- a/src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs +++ b/src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs @@ -113,6 +113,24 @@ event EventHandler? Core.INativeWebViewDialog.Closing bool Core.INativeWebViewDialog.Show(IPlatformHandle _) => false; + void Core.INativeWebViewDialog.Focus() + { + Activate(); + _controlHostImpl?.FocusWebView(); + + if (!IsActive) + { + Activated -= OnActivatedFocusWebView; + Activated += OnActivatedFocusWebView; + } + } + + private void OnActivatedFocusWebView(object? sender, EventArgs e) + { + Activated -= OnActivatedFocusWebView; + _controlHostImpl?.FocusWebView(); + } + public bool Resize(int width, int height) { Width = width; From d31d7c1deef6ceeff3b5cdb3105a65aa60b52e19 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 13 Aug 2026 05:09:43 -0700 Subject: [PATCH 3/3] Add headless focus tests --- .../NativeWebDialogTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs b/tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs index cf0f062..62cd59a 100644 --- a/tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs +++ b/tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs @@ -164,6 +164,56 @@ public void Should_Raise_Closing_Event() Assert.True(closingRaised); } + [AvaloniaFact] + public async Task Should_Focus_WebView_On_Show_By_Default() + { + var dialog = new NativeWebDialog(); + dialog.Show(); + + await WaitForAdapterCreation(dialog); + + var window = dialog.TryGetWindow()!; + Assert.Same(window.Content, window.FocusManager?.GetFocusedElement()); + } + + [AvaloniaFact] + public async Task Should_Not_Focus_WebView_When_ShowFocused_Is_False() + { + var dialog = new NativeWebDialog { ShowFocused = false }; + dialog.Show(); + + await WaitForAdapterCreation(dialog); + + var window = dialog.TryGetWindow()!; + Assert.Null(window.FocusManager?.GetFocusedElement()); + } + + [AvaloniaFact] + public async Task Should_Focus_WebView_On_Demand() + { + var dialog = new NativeWebDialog { ShowFocused = false }; + dialog.Show(); + + await WaitForAdapterCreation(dialog); + dialog.Focus(); + + var window = dialog.TryGetWindow()!; + Assert.Same(window.Content, window.FocusManager?.GetFocusedElement()); + } + + [AvaloniaFact] + public async Task Should_Apply_Focus_Requested_Before_Adapter_Creation() + { + var dialog = new NativeWebDialog { ShowFocused = false }; + dialog.Show(); + dialog.Focus(); + + await WaitForAdapterCreation(dialog); + + var window = dialog.TryGetWindow()!; + Assert.Same(window.Content, window.FocusManager?.GetFocusedElement()); + } + [AvaloniaFact] public async Task Should_Raise_AdapterCreated_And_AdapterDestroyed() {