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 @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/Avalonia.Controls.WebView.Core/Browser/WebViewInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ internal interface INativeWebViewDialog : IDisposable
/// </summary>
void Close();

/// <summary>
/// Activates the dialog and moves keyboard focus to the hosted web content.
/// </summary>
void Focus();

/// <summary>
/// Resizes the WebView dialog.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ internal class EmptyNativeWebViewControlImpl : Control, INativeWebViewControlImp

public Task<IWebViewAdapter?> GetAdapterAsync() => Task.FromResult<IWebViewAdapter?>(null);

public void FocusWebView() { }

public IDisposable BeginReparenting(bool yieldOnLayoutBeforeExiting) => EmptyDisposable.Instance;
public IAsyncDisposable BeginReparentingAsync() => EmptyDisposable.Instance;

Expand Down
5 changes: 5 additions & 0 deletions src/Avalonia.Controls.WebView/INativeWebViewControlImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ internal interface INativeWebViewControlImpl
/// </summary>
Task<IWebViewAdapter?> GetAdapterAsync();

/// <summary>
/// Moves keyboard focus to the hosted web content, if it's already initialized.
/// </summary>
void FocusWebView();

/// <inheritdoc cref="NativeWebView.BeginReparenting"/>.
IDisposable BeginReparenting(bool yieldOnLayoutBeforeExiting);

Expand Down
49 changes: 48 additions & 1 deletion src/Avalonia.Controls.WebView/NativeWebDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -299,10 +301,19 @@ public Color? DefaultBackground
}
}

/// <summary>
/// Gets or sets if the dialog moves keyboard focus to its web content when shown. Default is true.
/// </summary>
public bool ShowFocused { get; set; } = true;

/// <inheritdoc cref="Core.INativeWebViewDialog.Closing"/>
public event EventHandler? Closing;
/// <inheritdoc cref="Core.INativeWebViewDialog.Show()"/>
public async void Show() => (await GetOrInitialize()).Show();
public async void Show()
{
(await GetOrInitialize()).Show();
OnShown();
}

#if WPF
/// <summary>
Expand Down Expand Up @@ -345,6 +356,39 @@ public async void Show(TopLevel owner)
{
impl.Show();
}

OnShown();
}

/// <summary>
/// Activates the dialog and moves keyboard focus to the web content hosted inside of it.
/// </summary>
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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -532,6 +577,8 @@ private void DialogImplOnAdapterCreated(object? sender, Core.WebViewAdapterEvent
else if (_lastSource is ValueTuple<string, Uri?> pair)
adapter.NavigateToString(pair.Item1, pair.Item2);
AdapterCreated?.Invoke(this, e);

TryApplyFocus();
}

private void DialogImplOnClosing(object? sender, EventArgs e)
Expand Down
3 changes: 3 additions & 0 deletions src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ static NativeWebViewCompositorHost()
_webViewReadyCompletion.Task.Result :
null;

/// <inheritdoc />
public void FocusWebView() => Focus();

/// <inheritdoc />
public async Task<IWebViewAdapter?> GetAdapterAsync() =>
_webViewReadyCompletion is null ? null : await _webViewReadyCompletion.Task;
Expand Down
3 changes: 3 additions & 0 deletions src/Avalonia.Controls.WebView/NativeWebViewControlHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ async void CompleteAdapter(WebViewAdapter.AdapterWrapper wrapper)
_webViewReadyCompletion.Task.Result :
null;

/// <inheritdoc />
public void FocusWebView() => (TryGetAdapter() as Core.IWebViewAdapterWithFocus)?.Focus();

protected override void DestroyNativeControlCore(IPlatformHandle control)
{
var adapter = TryGetAdapter();
Expand Down
18 changes: 18 additions & 0 deletions src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
50 changes: 50 additions & 0 deletions tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading