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);
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;
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()
{