Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS Keyboard Issue: 'Next' Button Missing on Entry #18399

Closed
NirmalKumarYuvaraj opened this issue Oct 27, 2023 · 6 comments
Closed

iOS Keyboard Issue: 'Next' Button Missing on Entry #18399

NirmalKumarYuvaraj opened this issue Oct 27, 2023 · 6 comments
Labels
area-controls-entry Entry area-keyboard Keyboard, soft keyboard platform/iOS 🍎 s/needs-info Issue needs more info from the author t/bug Something isn't working

Comments

@NirmalKumarYuvaraj
Copy link
Contributor

NirmalKumarYuvaraj commented Oct 27, 2023

Description

On iOS, when utilizing an Entry control with a numeric keyboard type and a return key type set to 'Next,' the expected 'Next' button is not displayed on the keyboard. This behavior works correctly on the Android platform. For your reference, I have attached the code snippet and a screenshot.

** Code snippet**

<StackLayout>
        <Entry ReturnType="Next" Text="ABC"/>
        <Entry Keyboard="Numeric" ReturnType="Next" Text="123"/>
</StackLayout>
Android iOS
Android IOS

Steps to Reproduce

  1. Execute the sample on an iOS device.
  2. Tap on the second entry.
  3. Observe that the "Next" key is not visible.

Link to public reproduction project repository

https://github.com/NirmalKumarYuvaraj/IOSEntryNextKeyIssue/tree/main

Version with bug

7.0.96

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

8.0.0-rc.2.9373

Affected platforms

iOS

Affected platform versions

No response

Did you find any workaround?

No response

Relevant log output

No response

@NirmalKumarYuvaraj NirmalKumarYuvaraj added the t/bug Something isn't working label Oct 27, 2023
@jsuarezruiz jsuarezruiz added platform/iOS 🍎 area-keyboard Keyboard, soft keyboard labels Oct 27, 2023
@Zack-G-I-T
Copy link

Has anyone found a workaround for this?

@NathanielJS1541
Copy link

Has anyone found a workaround for this?

Assuming you're using .NET 8, the easiest option is to add HideSoftInputOnTapped="True" onto the content page that your entry is on. Then, tapping somewhere off the keyboard should close it.

If you're writing a custom control, however, this might not be ideal. In which case the following may work for you. This will create a custom Handler for the Entry, which takes the concept of adding a small "Done" button above the keyboard from the Editor.

Basically you can take a section from the EditorHandler to add the MauiDoneAccessoryView to the Entry. Note that none of this is my code, I have just added comments so I could make sense of what was going on. If you want to see where it came from, look at the MAUI implementations for the EditorHandler.iOS.cs and MauiDoneAccessoryView.cs. Here's what you'll need.

Create your own MauiDoneAccessoryView

This is what actually adds the "Done" button above the keyboard. It is already implemented in MAUI, but is internal. So you will need to make your own.
MauiDoneAccessoryView.cs:

using CoreGraphics;
using UIKit;

namespace Your.App.Platforms.iOS.Handlers
{
    /// <summary>
    /// A <see cref="UIToolbar"/> to add a "Done" button above the keyboard.
    /// </summary>
    internal sealed class MauiDoneAccessoryView : UIToolbar
    {
        /// <summary>
        /// A class only used in the <see cref="MauiDoneAccessoryView"/> to act as a proxy for the
        /// actions associated with the "Done" button on the <see cref="UIBarButtonItem"/>.
        /// </summary>
        private class BarButtonItemProxy
        {
            #region Fields

            #region Private

            /// <summary>
            /// A <see cref="WeakReference"/> to the <see cref="object"/> that is used as the
            /// sender in the <see cref="_doneWithDataClicked"/> <see cref="Action"/>.
            /// </summary>
            private WeakReference<object>? _data;

            /// <summary>
            /// An <see cref="Action"/> to be performed when the "Done" button is clicked.
            /// </summary>
            private readonly Action? _doneClicked;

            /// <summary>
            /// An <see cref="Action"/> to be performed when the "Done" button is clicked, which
            /// sends the <see cref="_data"/> as an argument to the <see cref="Action"/>.
            /// </summary>
            private Action<object>? _doneWithDataClicked;

            #endregion

            #endregion

            #region Construction

            /// <summary>
            /// Creates a <see cref="BarButtonItemProxy"/> without any variables initialised. You
            /// should use <see cref="SetDataContext"/> and <see cref="SetDoneClicked"/> to
            /// set up the action when using this constructor.
            /// </summary>
            public BarButtonItemProxy()
            {
            }

            /// <summary>
            /// Creates a <see cref="BarButtonItemProxy"/> with the <see cref="_doneClicked"/>
            /// action set.
            /// </summary>
            /// <param name="doneClicked"></param>
            public BarButtonItemProxy(Action doneClicked)
            {
                _doneClicked = doneClicked;
            }

            #endregion

            #region Methods

            #region Event Handlers

            /// <summary>
            /// An event handler for the "Done" button being clicked. This invokes the
            /// <see cref="_doneClicked"/> action.
            /// </summary>
            /// <remarks>
            /// The sender and <see cref="EventArgs"/> are ignored.
            /// </remarks>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            public void OnClicked(object? sender, EventArgs e)
            {
                _doneClicked?.Invoke();
            }

            /// <summary>
            /// An event handler for the "Done" button being clicked. This invokes the
            /// <see cref="_doneWithDataClicked"/> action, using the <see cref="_data"/> as an
            /// argument.
            /// </summary>
            /// <remarks>
            /// The sender and <see cref="EventArgs"/> are ignored.
            /// <para>
            /// If <see cref="_data"/> is null, the action will not be performed!
            /// </para>
            /// </remarks>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            public void OnDataClicked(object? sender, EventArgs e)
            {
                if (_data is not null && _data.TryGetTarget(out var data))
                {
                    _doneWithDataClicked?.Invoke(data);
                }
            }

            #endregion

            #region Public

            /// <summary>
            /// Set the <see cref="_data"/> variable to a <see cref="WeakReference"/> to the
            /// provided <see cref="object"/>.
            /// </summary>
            /// <remarks>
            /// If the provided <see cref="object"/> is null, <see cref="_data"/> will also be
            /// set to null.
            /// </remarks>
            /// <param name="dataContext">
            /// The <see cref="object"/> to get a <see cref="WeakReference"/> of.
            /// </param>
            public void SetDataContext(object? dataContext)
            {
                _data = dataContext is null ? null : new WeakReference<object>(dataContext);
            }

            /// <summary>
            /// Set the <see cref="_doneWithDataClicked"/> action.
            /// </summary>
            /// <param name="value"></param>
            public void SetDoneClicked(Action<object>? value)
            {
                _doneWithDataClicked = value;
            }

            #endregion

            #endregion
        }

        #region Fields

        #region Private

        /// <summary>
        /// A <see cref="BarButtonItemProxy"/> to act as a proxy for the data context and actions
        /// associated with the <see cref="UIBarButtonItem"/> that acts as the "Done" button.
        /// </summary>
        private readonly BarButtonItemProxy _proxy;

        #endregion

        #endregion

        #region Construction

        /// <summary>
        /// Creates a <see cref="MauiDoneAccessoryView"/> without any of the actions initialised.
        /// You should use <see cref="SetDataContext"/> and <see cref="SetDoneClicked"/> to
        /// properly initialise this object.
        /// </summary>
        public MauiDoneAccessoryView() : base(new CGRect(0, 0, UIScreen.MainScreen.Bounds.Width, 44))
        {
            _proxy = new BarButtonItemProxy();
            BarStyle = UIBarStyle.Default;
            Translucent = true;

            var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace);
            var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, _proxy.OnDataClicked);

            SetItems(new[] { spacer, doneButton }, false);
        }

        /// <summary>
        /// Creates a <see cref="MauiDoneAccessoryView"/> with an <see cref="Action"/> set for when
        /// the "Done" button is clicked. No further setup is needed.
        /// </summary>
        /// <param name="doneClicked">
        /// The <see cref="Action"/> to be invoked when the "Done" button is pressed.
        /// </param>
        public MauiDoneAccessoryView(Action doneClicked) : base(new CGRect(0, 0, UIScreen.MainScreen.Bounds.Width, 44))
        {
            _proxy = new BarButtonItemProxy(doneClicked);
            BarStyle = UIBarStyle.Default;
            Translucent = true;

            var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace);
            var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, _proxy.OnClicked);

            SetItems(new[] { spacer, doneButton }, false);
        }

        #endregion

        #region Methods

        #region Internal

        /// <summary>
        /// Sets the <see cref="object"/> that will be used as the sender argument for the
        /// <see cref="Action"/> that is set using <see cref="SetDoneClicked"/>.
        /// </summary>
        /// <param name="dataContext"></param>
        internal void SetDataContext(object? dataContext)
        {
            _proxy.SetDataContext(dataContext);
        }

        /// <summary>
        /// Sets the <see cref="Action"/> that will be called when the "Done" button is clicked.
        /// The <see cref="object"/> that will be used as an argument for this action is provided
        /// by <see cref="SetDataContext"/>.
        /// </summary>
        /// <param name="value"></param>
        internal void SetDoneClicked(Action<object>? value)
        {
            _proxy.SetDoneClicked(value);
        }

        #endregion

        #endregion
    }
}

Create your own EntryHandler

You can call this whatever you'd like. I've used CustomEntryHandler because I'm creative like that.
CustomEntryHandler.cs:

using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;

namespace Your.App.Platforms.iOS.Handlers
{
    /// <summary>
    /// This <see cref="EntryHandler"/> is a work around to add a "Done" button above the
    /// keyboard on iOS for the Entry. This allows a user to press
    /// "Done" to dismiss the keyboard, rather than having to add HideSoftInputTapped="True" to
    /// every <see cref="ContentPage"/> that the entry is used on.
    /// </summary>
    internal class CustomEntryHandler : EntryHandler
    {
        #region Methods

        #region Protected

        /// <summary>
        /// Overrides the <see cref="EntryHandler.CreatePlatformView"/> method to add a
        /// <see cref="MauiDoneAccessoryView"/> to the entry.
        /// </summary>
        /// <returns>
        /// A <see cref="MauiTextField"/> with a <see cref="MauiDoneAccessoryView"/> attached to
        /// the soft keyboard.
        /// </returns>
        protected override MauiTextField CreatePlatformView()
        {
            // Get the platform view for an entry from the base function.
            var platformEntry = base.CreatePlatformView();

            // Create an accessoryView which will display a "done" button on top of the iOS
            // keyboard.
            var accessoryView = new MauiDoneAccessoryView();

            // Set the data context to this, so that when OnDoneClicked is called the sender object
            // will be the handler that created the platform entry.
            accessoryView.SetDataContext(this);

            // Set the Action for when the "Done" button is clicked to be the OnDoneClicked method.
            accessoryView.SetDoneClicked(OnDoneClicked);

            // Set the InputAccessoryView for the platformEntry to the accessoryView, so that
            // whenever an entry keyboard is used on iOS it has a "Done" button above it.
            platformEntry.InputAccessoryView = accessoryView;

            return platformEntry;
        }

        #endregion

        #region Private Static

        /// <summary>
        /// A method that handles the "Done" button being clicked on the
        /// <see cref="MauiDoneAccessoryView"/>, and closes the soft keyboard for the entry.
        /// </summary>
        /// <remarks>
        /// This relies on the <see cref="MauiDoneAccessoryView.SetDataContext"/> function being
        /// used in the handler to set itself as the data context for the
        /// <see cref="MauiDoneAccessoryView"/>.
        /// </remarks>
        /// <param name="sender">The data context for the <see cref="MauiDoneAccessoryView"/>.</param>
        private static void OnDoneClicked(object sender)
        {
            // If the sender is an entry handler, we can use it to close the view.
            if (sender is IEntryHandler handler)
            {
                // Notify the platform view of the handler that it has been asked to relinquish its
                // status as first responder in its window.
                handler.PlatformView.ResignFirstResponder();

                // Signal to the IEntry that the text has been finalised.
                handler.VirtualView.Completed();
            }
        }

        #endregion

        #endregion
    }
}

Register your EntryHandler

NOTE: If you are writing a custom control that derives from Entry, then you may wish to replace Entry in this section with your custom control so that it only applies there, rather than to all entries. This can be useful as keyboards other than Keyboard.Numeric seem to have a "done" button already. If your custom control defines Keyboard = Keyboard.Numeric, this may be the best approach.

In your MauiProgram.cs, in the CreateMauiApp method, add this on to your builder:

                .ConfigureMauiHandlers(handlers =>
                {
#if IOS
                    // Register a custom handler for the Entry, which adds a "Done"
                    // button above the keyboard which allows the keyboard to be closed. This is
                    // only needed on iOS.
                    handlers.AddHandler<Entry, CustomEntryHandler>();
#endif
                })

You will need to ensure you have the usings in your MauiApp.cs for the CustomEntryHandler and Entry.

I hope this can help you until there is a proper solution in MAUI, that doesn't involve adding HideSoftInputOnTapped="True" to every content page you use the Entry on... Let me know if you have any issues.

@mattleibow mattleibow added this to the Backlog milestone Dec 6, 2023
@ghost
Copy link

ghost commented Dec 6, 2023

We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.

@ghost ghost added the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label Jan 31, 2024
@samhouts samhouts removed the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label Jan 31, 2024
@bloiseleo
Copy link

Has anyone found a workaround for this?

I don't know if you still need this, but I was facing the same problem. After some days, I've made an workaround based on this post.

My problem was only with the numeric keyboard. So, I've implemented this:

public static class MauiAppDone
{
    public static MauiAppBuilder UseAppDone(this MauiAppBuilder builder)
    {
        Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("Done", (handler, view) =>
        {
            var toolbar = new UIToolbar(new RectangleF(0.0f, 0.0f, 50.0f, 44.0f));
            toolbar.BackgroundColor = UIColor.LightGray; // Set the color you prefer
            var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, delegate
            {
                handler.PlatformView.ResignFirstResponder();
            });
            toolbar.Items = new UIBarButtonItem[] {
                new UIBarButtonItem (UIBarButtonSystemItem.FlexibleSpace),
                doneButton
            };
            // You can remove this section
            if (!(view is Entry))
            {
                return;
            }
            var entry = (Entry) view;
            if(entry.Keyboard != Keyboard.Numeric)
            {
                return;
            }
            // You can remove this section
            handler.PlatformView.InputAccessoryView = toolbar;
        });
        return builder;
    }
}

Basically, I create an extension method to MauiAppBuilder that "looks" like the others we use in MauiProgram.cs. Then, I validated if the View was an entry and, if it was, I validate if the keyboard is numeric. If it's so, I add the Done button above the iOS Keyboard.

image

@jfversluis
Copy link
Member

Reading through this issue again, I think there is a misconception here. The ReturnType="Next" refers to the return/enter key on the keyboard and you can influence that behavior, see the documentation. However, that button is not shown for the numeric keyboard it seems.

Can you please confirm what you mean exactly with the "next" button that you like to see?

@jfversluis jfversluis added the s/needs-info Issue needs more info from the author label Dec 17, 2024
@dotnet-policy-service dotnet-policy-service bot added the s/no-recent-activity Issue has had no recent activity label Dec 23, 2024
@dotnet-policy-service dotnet-policy-service bot removed this from the Backlog milestone Dec 26, 2024
@NathanielJS1541
Copy link

Sorry, as I didn't originally open the issue I didn't get notified of the replies.

Can you please confirm what you mean exactly with the "next" button that you like to see?

I can't speak for the original author, but the issue we were having (that I wrote the posted workaround for) was that I was unable to dismiss the keyboard. As I noted in my workaround, you can add HideSoftInputOnTapped="True" to the content page to tap off the keybaord and dismiss it, but this caused a wealth of other issues for our app so we couldn't use it. I think the expected behaviour is that specifying ReturnType="Next" have consistent behaviour with the Android platform, and add a button to the keyboard that can be used to submit the entry.

Apologies if I've mistemembered anything, it's been over a year since I encountered this issue.

@dotnet-policy-service dotnet-policy-service bot removed the s/no-recent-activity Issue has had no recent activity label Dec 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-controls-entry Entry area-keyboard Keyboard, soft keyboard platform/iOS 🍎 s/needs-info Issue needs more info from the author t/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

8 participants