Skip to content

Commit 8291f1f

Browse files
authored
Merge pull request #70 from AvaloniaUI/features/dialog-focus
Add API to focus NativeWebDialog
2 parents 77326b1 + d31d7c1 commit 8291f1f

13 files changed

Lines changed: 158 additions & 1 deletion

src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ private async void ShowCore(IPlatformHandle? owner)
8080
_isOpen = true;
8181
}
8282

83+
public void Focus() => (TryGetAdapter() as IWebViewAdapterWithFocus)?.Focus();
84+
8385
public void Close()
8486
{
8587
if (!_isOpen || _context is null) return;

src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ public bool Show(IPlatformHandle owner)
9494
return true;
9595
}
9696

97+
public void Focus()
98+
{
99+
if (_popup is { } popup)
100+
WebViewInterop.FocusDialogWindow(popup);
101+
(_adapter as IWebViewAdapterWithFocus)?.Focus();
102+
}
103+
97104
public void Close()
98105
{
99106
if (_popup is { } popup)

src/Avalonia.Controls.WebView.Core/Browser/WebViewInterop.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ public static partial Action SubscribeMessages(
9696
[JSImport("closeDialogWindow", "av-webview")]
9797
public static partial void CloseDialogWindow(JSObject popup);
9898

99+
[JSImport("focusDialogWindow", "av-webview")]
100+
public static partial void FocusDialogWindow(JSObject popup);
101+
99102
[JSImport("resizeDialogWindow", "av-webview")]
100103
public static partial bool ResizeDialogWindow(JSObject popup, int width, int height);
101104

src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ public bool Show(IPlatformHandle owner)
215215
return true;
216216
}
217217

218+
public void Focus() => RunOnGlibThreadAsync(() =>
219+
{
220+
gtk_window_present(_windowHandle);
221+
_nativeWebView?.Focus();
222+
});
223+
218224
public void Close()
219225
{
220226
// Closing is invoked in two places for the GTK webview:

src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ internal interface INativeWebViewDialog : IDisposable
178178
/// </summary>
179179
void Close();
180180

181+
/// <summary>
182+
/// Activates the dialog and moves keyboard focus to the hosted web content.
183+
/// </summary>
184+
void Focus();
185+
181186
/// <summary>
182187
/// Resizes the WebView dialog.
183188
/// </summary>

src/Avalonia.Controls.WebView.Core/staticwebassets/av-webview.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ export function closeDialogWindow(popup) {
170170
} catch { }
171171
}
172172

173+
export function focusDialogWindow(popup) {
174+
try {
175+
popup.focus();
176+
} catch { }
177+
}
178+
173179
export function resizeDialogWindow(popup, width, height) {
174180
try {
175181
popup.resizeTo(width, height);

src/Avalonia.Controls.WebView/EmptyNativeWebViewControlImpl.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ internal class EmptyNativeWebViewControlImpl : Control, INativeWebViewControlImp
2727

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

30+
public void FocusWebView() { }
31+
3032
public IDisposable BeginReparenting(bool yieldOnLayoutBeforeExiting) => EmptyDisposable.Instance;
3133
public IAsyncDisposable BeginReparentingAsync() => EmptyDisposable.Instance;
3234

src/Avalonia.Controls.WebView/INativeWebViewControlImpl.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ internal interface INativeWebViewControlImpl
2727
/// </summary>
2828
Task<IWebViewAdapter?> GetAdapterAsync();
2929

30+
/// <summary>
31+
/// Moves keyboard focus to the hosted web content, if it's already initialized.
32+
/// </summary>
33+
void FocusWebView();
34+
3035
/// <inheritdoc cref="NativeWebView.BeginReparenting"/>.
3136
IDisposable BeginReparenting(bool yieldOnLayoutBeforeExiting);
3237

src/Avalonia.Controls.WebView/NativeWebDialog.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public class NativeWebDialog : Core.IWebView, Core.IWebViewHolder, IDisposable
4545
private Color? _initialDefaultBackground;
4646
private bool _disposed;
4747
private bool _dialogInitialized;
48+
private bool _shown;
49+
private bool _focusRequested;
4850

4951
static NativeWebDialog()
5052
{
@@ -299,10 +301,19 @@ public Color? DefaultBackground
299301
}
300302
}
301303

304+
/// <summary>
305+
/// Gets or sets if the dialog moves keyboard focus to its web content when shown. Default is true.
306+
/// </summary>
307+
public bool ShowFocused { get; set; } = true;
308+
302309
/// <inheritdoc cref="Core.INativeWebViewDialog.Closing"/>
303310
public event EventHandler? Closing;
304311
/// <inheritdoc cref="Core.INativeWebViewDialog.Show()"/>
305-
public async void Show() => (await GetOrInitialize()).Show();
312+
public async void Show()
313+
{
314+
(await GetOrInitialize()).Show();
315+
OnShown();
316+
}
306317

307318
#if WPF
308319
/// <summary>
@@ -345,6 +356,39 @@ public async void Show(TopLevel owner)
345356
{
346357
impl.Show();
347358
}
359+
360+
OnShown();
361+
}
362+
363+
/// <summary>
364+
/// Activates the dialog and moves keyboard focus to the web content hosted inside of it.
365+
/// </summary>
366+
public void Focus()
367+
{
368+
_focusRequested = true;
369+
TryApplyFocus();
370+
}
371+
372+
private void OnShown()
373+
{
374+
_shown = true;
375+
_focusRequested |= ShowFocused;
376+
TryApplyFocus();
377+
}
378+
379+
private void TryApplyFocus()
380+
{
381+
// The adapter is typically created only after the dialog window was shown,
382+
// and native focus can't be moved before that.
383+
if (!_focusRequested || !_shown
384+
|| TryGetImpl() is not { } impl
385+
|| impl.TryGetAdapter() is null)
386+
{
387+
return;
388+
}
389+
390+
_focusRequested = false;
391+
impl.Focus();
348392
}
349393

350394
#if WPF
@@ -501,6 +545,7 @@ private void DialogImplOnAdapterDestroyed(object? sender, Core.WebViewAdapterEve
501545
adapter.WebResourceRequested -= WebViewAdapterOnWebResourceRequested;
502546
adapter.NewWindowRequested -= WebViewAdapterOnNewWindowRequested;
503547
_dialogInitialized = false;
548+
_shown = false;
504549
AdapterDestroyed?.Invoke(this, e);
505550
}
506551

@@ -532,6 +577,8 @@ private void DialogImplOnAdapterCreated(object? sender, Core.WebViewAdapterEvent
532577
else if (_lastSource is ValueTuple<string, Uri?> pair)
533578
adapter.NavigateToString(pair.Item1, pair.Item2);
534579
AdapterCreated?.Invoke(this, e);
580+
581+
TryApplyFocus();
535582
}
536583

537584
private void DialogImplOnClosing(object? sender, EventArgs e)

src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ static NativeWebViewCompositorHost()
4444
_webViewReadyCompletion.Task.Result :
4545
null;
4646

47+
/// <inheritdoc />
48+
public void FocusWebView() => Focus();
49+
4750
/// <inheritdoc />
4851
public async Task<IWebViewAdapter?> GetAdapterAsync() =>
4952
_webViewReadyCompletion is null ? null : await _webViewReadyCompletion.Task;

0 commit comments

Comments
 (0)