dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.21k stars 1.75k forks source link

Tab key behaves differently if Entry or Editor is last interactable control on a page on Windows #7746

Open scurvycapn opened 2 years ago

scurvycapn commented 2 years ago

Description

Typically pressing the tab key switches focus to the next available control on the page. However, if an Entry or Editor is the last interactable (type, click, etc.) control on the page, pressing the tab key will insert a tab character into the Entry or Editor field. I would expect it to either wrap around to the first interactable control on the page or just lose focus.

Steps to Reproduce

  1. Create a File > New .NET MAUI App
  2. Add a few Entry or Editor elements (or others as long as an Entry or Editor is the final one) to the page. For example:
    <Entry  Placeholder="Empty" />
    <Button Text="Tab past me" />
    <Editor Placeholder="Empty" />
    <Entry  Placeholder="Empty" />
  3. Start Debugging on Windows
  4. Press the tab key to make your way through the controls, including attempting to tab past the final Entry

Example screenshot. The Placeholder text is not shown as a tab character has been inserted (also evident based on the caret position) image

Version with bug

6.0 (current)

Last version that worked well

Unknown/Other

Affected platforms

Windows, I was not able test on other platforms

Affected platform versions

Windows SDK 10.0.19041.0

Did you find any workaround?

No.

Relevant log output

No response

deepx commented 2 years ago

Verified with latest Win 11, VS 2022 Preview and .NET with latest MAUI-Workload.

kristinx0211 commented 2 years ago

verified repro when debugging on windows. same as described above. repro project: MauiApp8.zip

homeyf commented 1 year ago

Verified this issue with Visual Studio Enterprise 17.6.0 Preview 7.0. Can repro on windows platform with above project. repro project:MauiApp8.zip image

Zaho92 commented 9 months ago

Had the same problem. For anyone stumbling about this like me, I made a little MAUI behavior that solves this more or less elegantly. Here is the code. Maybe you have to adjust it for special use cases, but it should work for the most cases out of the box.

    /// <summary>
    /// This behavior is used to focus the next <see cref="IView"/> control when the <see cref="InputView"/> got a Tab (\t) input.
    /// <para>Deprecated as soon as GitHub Issue #7746 is fixed.</para>
    /// <para><see href="https://github.com/dotnet/maui/issues/7746">To the GitHub Issue #7746</see></para>
    /// </summary>
    public class LastInputViewFocusTargetBehavior : Behavior<InputView>
    { 
        /// <summary>
        /// The next <see cref="IView"/> control that can be <see cref="IView.IsEnabled"/>=<see langword="false"/>
        /// to produce this error, even if the <see cref="Entry"/> or <see cref="Editor"/> is not the last control.
        /// </summary>
        public IView NextDisabledTarget
        {
            get => (IView)GetValue(NextDisabledTargetProperty);
            set => SetValue(NextDisabledTargetProperty, value);
        }

        public static readonly BindableProperty NextDisabledTargetProperty =
            BindableProperty.CreateAttached("NextDisabledTarget", typeof(IView), typeof(LastInputViewFocusTargetBehavior), null);

        /// <summary>
        /// The <see cref="IView"/> control that should be focused when the <see cref="InputView"/> got a Tab (\t) input.
        /// </summary>
        public IView AlternateFocusTarget
        {
            get => (IView)GetValue(AlternateFocusTargetProperty);
            set => SetValue(AlternateFocusTargetProperty, value);
        }

        public static readonly BindableProperty AlternateFocusTargetProperty =
        BindableProperty.CreateAttached("AlternateFocusTarget", typeof(IView), typeof(LastInputViewFocusTargetBehavior), null);

        protected override void OnAttachedTo(InputView inputView)
        {
#if WINDOWS
            inputView.TextChanged += OnInputViewTextChanged;
            base.OnAttachedTo(inputView);
#endif
        }

        protected override void OnDetachingFrom(InputView inputView)
        {
#if WINDOWS
            inputView.TextChanged -= OnInputViewTextChanged;
            base.OnDetachingFrom(inputView);
#endif
        }

        private void OnInputViewTextChanged(object sender, TextChangedEventArgs e)
        {
            var isLastControl = NextDisabledTarget is null || !NextDisabledTarget.IsEnabled;
            if (isLastControl && (e.NewTextValue?.EndsWith('\t') ?? false))
            {
                AlternateFocusTarget?.Focus();
                ((InputView)sender).Text = e.OldTextValue;
            }
        }
    }

I made it only work for Windows (#if WINDOWS) because I only tested it there. To use it in an Entry, for example, you need to copy the class and import the namespace you created it in into XAML.

xmlns:behaviors="clr-namespace:YOURCOOLAPP.Helpers.Behaviors"

Then, you can apply this behavior to any 'last Entry' like this:

<Entry
    x:Name="EntryUsername"       
    Text="{Binding Username}" />
<Entry
    x:Name="EntryPassword"        
    Text="{Binding Password}">
    <Entry.Behaviors>
        <behaviors:LastInputViewFocusTargetBehavior 
            AlternateFocusTarget="{x:Reference EntryUsername}" 
            NextDisabledTarget="{x:Reference ButtonLogin}" />
    </Entry.Behaviors>
</Entry>
<Button
    x:Name="ButtonLogin"
    Command="{Binding LoginCommand}" />

I hope this helps someone. :)