uno
uno copied to clipboard
[Wasm] Focus event handling is not working properly
Current behavior
Let's assume a simple 2-page app with login/password boxes and navigation view.
When application is loaded the LoginPage is displayed. The TextBox with username is drawn with a blue underscore as if it has a focus. But in fact this is not the case - text cursor is not displayed, the placeholder text is displayed (Enter username) overlapped by web browser's autofill value (123):
After pressing Tab button on a keyboard the placeholder text disappears, but text cursor is still not displayed, and typing does not work:
So the TextBox is not in edit mode. The second press of Tab button works correctly - focus is moved to the PasswordBox which enters into edit mode.
Another press on Tab button to move focus to Login button, then press on Enter to navigate to MainPage which contains NavigationView. NavigationView has PaneFooter which contains HyperlinkButton. HyperlinkButton gets focus, but web browser draws black border in the wrong position:
Note, that Login button performs HTTP GET request to https://demo.duendesoftware.com/.well-known/openid-configuration to trigger web browser's autofill. TextBox and PasswordBox may contain any non-null values.
Expected behavior
WinUI works correctly (but obviously there is no autofill):
Typical web login forms (like https://github.com/login) work correctly.
Uno Playground (https://playground.platform.uno/#wasm-start) works correctly - XAML editor has text cursor, etc.
How to reproduce it (as minimally and precisely as possible)
See Current behavior above.
Attachments:
Workaround
N/A
Works on UWP/WinUI
Yes
Environment
Uno.WinUI / Uno.WinUI.WebAssembly / Uno.WinUI.Skia
NuGet package version(s)
Wasm head:
WinUI head:
Affected platforms
WebAssembly
IDE
Visual Studio 2022
IDE version
17.2.5
Relevant plugins
N/A
Anything else we need to know?
N/A
Addition: it seems that both Tab when on LastFocusableElement and Shift+Tab when on FirstFocusableElement do not move the focus on WinUI but move it on Wasm. Ex., try pressing Shift+Tab when the focus is on Login TextBox.
This look like a duplicate of https://github.com/unoplatform/uno/issues/7466, for which we still do not have a workaround for.
This look like a duplicate of #7466, for which we still do not have a workaround for.
Thanks, I didn't know about #7466 and #5737. I'm aware of autofill issue in web browsers.
But still, autofill apart:
- TextBox should be actually focused and should be in edit mode when highlighted with an accent color.
- The black border on second page should be around focused element (i.e. HyperlinkButton).
I think I found an easy way to reproduce this issue.
1. I created a minimum possible wasm app using Uno VS extension. 2. Added git support, added a button to show ContentDialog, and committed my changes.
Download UnoFocusApp.zip
Steps to reproduce.
1. Run the app using wasm head.
2. Click on an empty space on the page and press Tab to see the focus mark on the button.
3. Press Enter to see ContentDialog. The focus mark will be in the wrong place:
4. Now expand the browser window to full screen and return to the non-expanded size again. The focus mark will get to the right place when the browser window is not expanded, and will shift again when the window is expanded.
5. Another interesting thing. If you click on the empty space on the page when ContentDialog is displayed, and then press Tab, the button on the main page will be focused, not the dialog button:
- TextBox should be actually focused and should be in edit mode when highlighted with an accent color.
Regarding this point, perhaps the focus() method should be explicitly called by Uno for the "should-be-focused" element after rendering the DOM.
- TextBox should be actually focused and should be in edit mode when highlighted with an accent color.
Regarding this point, perhaps the focus() method should be explicitly called by Uno for the "should-be-focused" element after rendering the DOM.
I came up with the following workaround for this (no future-proof, since it relies on Uno internals). The code below assumes the use of MVVM pattern.
1. First, we need to define the LoadedCommand for LoginPage and assign a name to UserNameBox:
LoginPage.xaml (click to expand)
<Page x:Class="UnoApplication.Views.LoginPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Loaded">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.LoadedCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<...>
<TextBox x:Name="UserNameBox"
...
/>
<...>
</Page>
2. Second, we need to pass UserNameBox to ViewModel:
LoginPage.xaml.cs (click to expand)
using Microsoft.UI.Xaml.Controls;
using UnoApplication.ViewModels;
namespace UnoApplication.Views;
internal sealed partial class LoginPage : Page
{
public LoginPage()
{
InitializeComponent();
ViewModel.BoxToFocus = UserNameBox;
}
public LoginViewModel ViewModel { get; } = App.GetService<LoginViewModel>();
}
3. And finally, the workaround:
LoginViewModel.cs (click to expand)
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
namespace UnoApplication.ViewModels;
internal sealed partial class LoginViewModel : ObservableRecipient
{
public TextBox? BoxToFocus { get; set; }
[RelayCommand]
private async Task Loaded(RoutedEventArgs args)
{
/* Manual focusing is a workaround due to the issue with initial focus,
See https://github.com/unoplatform/uno/issues/9246 */
if (BoxToFocus is null)
return;
var focusResult = await FocusManager.TryFocusAsync(BoxToFocus, FocusState.Pointer);
if (!focusResult.Succeeded)
return;
static FrameworkElement? findInternalTextBoxView(DependencyObject element)
{
var count = VisualTreeHelper.GetChildrenCount(element);
for (var index = 0; index < count; ++index)
{
var child = VisualTreeHelper.GetChild(element, index);
if (child is null)
continue;
if (child.GetType().ToString().EndsWith(".TextBoxView", StringComparison.Ordinal))
return child as FrameworkElement;
var subCount = VisualTreeHelper.GetChildrenCount(child);
if (subCount <= 0)
continue;
var subChild = findInternalTextBoxView(child);
if (subChild is not null)
return subChild;
}
return default;
}
var viewToFocus = findInternalTextBoxView(BoxToFocus);
viewToFocus?.ExecuteJavascript("element.focus();");
}
}