Search code examples
javascriptxamlblazormaui

Moving between .xaml files ruins javascript .focus() function in MAUI (mobile android)


I'm having an issue where calling a javascript function to focus on a specific element doesn't function correctly right after moving from one .xaml file to another.

The flow begins when one of the scanning types is selected. If type 1 is selected, it goes to ScanningPage (another .xaml page) and waits for input before moving back to MainPage. ViewPopup() is then called, and TempInputValue first equals False False before switching to True True textInput textInput. However, the element with id "textInput" is not actually focused on!

When type 2 is selected, it goes straight to ViewPopup(), and TempInputValue is set to the same values as listed above, but this time, when it is set to True True textInput textInput, it actually brings up the mobile keyboard.

Why is this happening? Any help is appreciated.

Blazor Code:

@page "/"
@inject IJSRuntime JSR

@if (CanSeePopup)
{
    <dialog open="true" class="center-div quantity-dialog">
        <h5>Enter Coil Number:</h5>
        <input @bind:event="oninput" @bind:get="TempInputValue" @bind:set="InputReceivedInput" id="textInput" />
        <div class="center-div add-bottom-margin">
            <button @onclick="ClosePopup">Finish</button>
            <button @onclick="ClosePopup">Cancel</button>
        </div>
    </dialog>
}

<button @onclick="TogglePage">Start Scanning Type 1</button>
<button @onclick="ManualEntryCycle">Start Scanning Type 2</button>

<script>
    async function focusOnElement(elementId) {
        var element = document.getElementById(elementId);
        if (!!element) {
            element.focus();
        }
        return elementId + " " + document.activeElement.id;
    }
</script>

@code {
    private bool CanSeePopup = false;
    private bool NeedsToRender = false;
    private string TempInputValue = string.Empty;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        TempInputValue = NeedsToRender.ToString() + " " + CanSeePopup.ToString();
        await Task.Delay(600);
        if (NeedsToRender)
        {
            if (CanSeePopup) TempInputValue += await JSR.InvokeAsync<string>(identifier:"focusOnElement", args:"textInput");
            if (EnterRadiusForCoil) await JSR.InvokeVoidAsync("focusOnElement", "radiusInput");
            NeedsToRender = false;
            this.StateHasChanged();
        }
    }

    private void InputReceivedInput(string InputValue)
    {
        TempInputValue = InputValue.ToUpper();
    }

    private async void ClosePopup()
    {
        CanSeePopup = false;
        TempInputValue = string.Empty;
    }

    protected async Task TogglePage()
    {
        string ReturnValue = string.Empty;
        NavigationPage NavMain = Microsoft.Maui.Controls.Application.Current.MainPage as NavigationPage;
        MainPage MainPageRoot = NavMain.RootPage as MainPage;
        if (MainPageRoot == null) { }
        else
        {
            do
            {
                await Task.Delay(500);
                ReturnValue = await MainPageRoot.BeginScan();
            } while (!ReturnValue.Equals(string.Empty));
            if (ScanningPage.UserDesiresManualEntry)
            {
                ViewPopup();
                ScanningPage.UserDesiresManualEntry = false;
            }
        }
    }

    protected void ManualEntryCycle()
    {
        TempInputValue = string.Empty;
        ViewPopup();
    }

    private async void ViewPopup()
    {
        CanSeePopup = true;
        NeedsToRender = true;
    }

MainPage.xaml.cs:

public async Task<string> BeginScan()
{
    await Navigation.PushAsync(new ScanningPage());
    while (((NavigationPage)App.Current.MainPage).CurrentPage != this)
    {
        await Task.Delay(50);
    }
    if (ScanningPage.UserDesiresManualEntry)
    {
        return string.Empty;
    }
    return "null";
}

ScanningPage.xaml:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:TestApp"
             x:Class="TestApp.ScanningPage"
             Title="Scan Page"
             BackgroundColor="{DynamicResource PageBackgroundColor}"
             NavigationPage.HasBackButton="False">

    <ContentPage.Content>
        <Grid VerticalOptions="Fill" HorizontalOptions="Fill">
            <Grid.RowDefinitions>
                <RowDefinition Height="70
                               " />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Button Text="Enter Value Manually" Clicked="TempReturn" VerticalOptions="Fill" HorizontalOptions="Start" HeightRequest="50"></Button>
        </Grid>
    </ContentPage.Content>
</ContentPage.Content>

ScanningPage.xaml.cs:

namespace TestApp
{
    public partial class ScanningPage
    {
        public static bool UserDesiresManualEntry = false;

        public ScanningPage()
        { }

        public async void TempReturn(object sender, EventArgs E)
        {
            UserDesiresManualEntry = true;
            await Navigation.PopAsync();
        }
    }
}

App.xaml.cs:

namespace TestApp
{
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            MainPage = new NavigationPage(new MainPage());
        }
    }
}

A lot of code was removed to keep this as lightweight and readable as possible while still retaining its reproduceability, so that is why some parts seem redundant or useless.


Solution

  • Add the following code to your MainPage.xaml.cs:

            protected override void OnAppearing()
            {
                base.OnAppearing();
     
                if (blazorWebView.Handler != null)
                {
    #if ANDROID
                    var androidweb = blazorWebView.Handler.PlatformView as Android.Webkit.WebView;
                    androidweb.Focusable = true;
                    androidweb.RequestFocus(Android.Views.FocusSearchDirection.Down);
                    androidweb.PerformClick();
    #endif
                }
            }
    

    RequestFocus() can give focus to a specific view or to one of its descendants it a hint about what direction focus is heading.