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

Entry/Editor `Unfocused` event is not firing #23901

Open bzd3y opened 3 months ago

bzd3y commented 3 months ago

Description

I understand there have been issues that addressed this previously, but I still don't think things are working.

Setting HideSoftInputKeyboard to true does not fix the problem.

That may be because I am not clicking outside of the Entry/Editor on the page itself, but another control, like a button.

So what I am seeing is that I can change the focus on the page to an Entry/Editor and enter some text. Then if I click on a button below that, I would expect the Editor's/Entry's Unfocused event to fire because the control has lost focus when it switched to the button. From what I gather there is a limitation in Android about at least one control on a page having focus, but it seems that that shouldn't apply here since the button getting focus when it is clicked should satisfy that condition. But I think that even if there wasn't another focusable control then the Entry/Editor Unfocused event should still fire.

As others have said, there are a lot of use cases/UI practices/standards that are broken by not firing the event.

It is also a development or discoverability issue for those views to have an Unfocused event that does nothing. Shouldn't they at least be marked as obsolete or deprecated?

At the very least the documentation for the event properties should indicate that the event is not implemented at all and won't work.

But, ideally, I think the event should just be fired when the user tries to focus on another control. Note that I said "tries". I think it is perfectly fine if the control can't lose focus that it doesn't actually lose focus. If needed, a developer can check for that within the event. But the event should still fire to indicate that the user tried to change the focus to something else.

Steps to Reproduce

  1. Create a new .NET MAUI app project
  2. Add an Entry or Editor control
  3. Add something to handle the Unfocused event
  4. Run the app and make sure the Entry or Editor has focus
  5. Click the "Count" button from the new project template
  6. Observe that the method attached to the Unfocused event does not get called

Link to public reproduction project repository

No response

Version with bug

8.0.70 SR7

Is this a regression from previous behavior?

Yes, this used to work in Xamarin.Forms

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

No response

Did you find any workaround?

No, but it would be nice to know if this behavior could be added through a view handler.

Relevant log output

No response

github-actions[bot] commented 3 months ago

Hi I'm an AI powered bot that finds similar issues based off the issue title.

Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one and thumbs upping the other issue to help us prioritize it. Thank you!

Open similar issues:

Closed similar issues:

Note: You can give me feedback by thumbs upping or thumbs downing this comment.

bzd3y commented 3 months ago

Thanks, but those issues don't help. I had already seen most of them before submitting my issue (most/all of them are not exactly the same anyway). And #21053 isn't open, it is closed, so I'm not sure what is going on there.

ninachen03 commented 3 months ago

I cannot repro this issue on the latest 17.11.0 Preview 5.0 (8.0.70& 8.0.61). I provided the screen recording and repro project. If it does not match what you described, please provide your sample project and I will verify it again. Focused

MauiApp24.zip

bzd3y commented 3 months ago

Thanks @ninachen03. I tried on 8.0.70, but that was in VS 2022 17.10.4. But I wouldn't think the VS version would matter, right?

Either way, this is reassuring. Let me see if there is something going on with my packages.

Are you seeing the same thing with an Editor? Because that is the actual view I was using.

MisterAcoustic commented 3 months ago

@ninachen03 I noticed that it looks like you may have tested on Windows, rather than Android, which might explain the difference in behavior. I could also be mistaken, so apologies if I'm incorrect.

mattleibow commented 3 months ago

@PureWeen thoughts?

ninachen03 commented 3 months ago

Thank you for your detailed information. I reproduced it at Android platform using the same sample on 17.11.0 Preview 5.0 AndroidF

bzd3y commented 3 months ago

@MisterAcoustic Good catch! I wasn't paying attention and just thought that was the Android emulator.

@ninachen03 Thanks for the reproduction video.

@mattleibow Just to be clear, this happens on the Editor as well. I don't know if that matters for the labels.

Choza-rajan commented 2 months ago

Same issue reproduced in MAC Os also

reid-kirkpatrick commented 1 month ago

@mattleibow @PureWeen what is the status here? This is still happening in SR9 and seems like a pretty major issue! Did you find any workarounds @bzd3y?

cmpalmer66 commented 1 month ago

Agreed that this is causing weird problems in our application. If I'm focused in a text Entry and exit the page, the keyboard is still showing on the new page (iOS). It is also happening using the SearchBar control (not dismissing keyboard)

bzd3y commented 1 month ago

@reid-kirkpatrick I haven't had the time to work on a workaround until now. But now I'm at the point where I can and need to try to figure something out.

bzd3y commented 1 month ago

I finally got around to looking at this. As a small update, I have figured out that the issue isn't with the Entry/Editor/other view. The issue is with the Button and Android not handling focus/clicking very well. I would argue incorrectly.

When the button has FocusableInTouchMode set to true then the Entry.Unfocused works correctly. But then the problem is that because the button is now "focusable in touch mode" it is no longer clickable when it gets focus. I.e., touching/clicking it once gives it focus and doesn't register the click.

My current workaround involves firing the Clicked() manually when the button gets focus, but that seems less than ideal because some apps might not expect the button to be clicked when it gets focus, like if is done programmaticaly, and it also changes the button's appearance so that it stays focused. I'm looking at trying to change that to fire it on Touch, but it appears that Focus, Touch and Click all interfere with each other, at least on Android (that probably being the larger problem here).

For example, it looks like setting a Touched event causes FocusableInTouchMode to be ignored.

And clearing the focus on the element after Clicked() is called causes Clicked() to be called twice. I guess because now that the focus is cleared the original click event fires. But the strange thing is that if I don't call Clicked() manually, then a clicked event isn't fired at all.

For anybody (@reid-kirkpatrick) interested in the current workaround, here is the code:

ButtonHandler.Mapper.AppendToMapping("UnfocusFix", (h, b) =>
{
    h.PlatformView.FocusableInTouchMode = true;
});
ButtonHandler.Mapper.PrependToMapping(nameof(Button.IsFocused), (h, b) =>
{
    if (b.IsFocused)
    {
        b.Clicked();
    }
});

Like I said, this isn't an ideal workaround, but it might get people past this for the time being. In the meantime, I'll see if I can find something better.

bzd3y commented 1 month ago

Okay, I think I have something better:

    ViewHandler.ViewMapper.AppendToMapping("UnfocusFix", (h, v) =>
    {
        if (v is not ScrollView && h.PlatformView is Android.Views.View view && view.Focusable && (view.FocusableInTouchMode == false))
        {
            view.Touch += (s, e) =>
            {
                if (e.Event?.Action == MotionEventActions.Down)
                {
                    view.Context?.GetActivity()?.CurrentFocus?.ClearFocus();
                }

                e.Handled = false;
            };
        }
    });

This adds a Touch event to everything that is Focusable (except for ScrollViews) and not FocusableInTouchMode that clears the focus of the currently focused view.

The ScrollView exemption is there because they are Focusable, so something like an Entry inside of a ScrollView would now lose focus if the user clicks anywhere else in the ScrollView. Although, that could be a desired behavior. In that case you probably want to change the if to just if (h.PlatformView is Android.Views.View view) so that touching any view outside of the current view will cause it to lose focus, though I haven't tested that. Or you may want a handler that sets any "container" view or page to Focusable.

There may also be views other than ScrollView that should be excluded, but I can't think of any.

Finally, this is obviously specific to Android. I haven't seen this issue in iOS, but if it does happen then there probably needs to be a similar handler. At some point I will be checking/testing that.

stewartsims commented 2 weeks ago

Just adding support to this issue and the fact that it appears to be creating problems for us migrating an app from Xamarin Forms. The responses on other tickets from Microsoft PMs (which are quickly subsequently closed after claiming to be open to learning more about developers' needs) seem inadequate and dismissive of developer concerns.

HideSoftInputOnTapped only appears to be available on ContentPages which is not helpful if you are using Popups. This is on top of the problem if you are pressing on buttons it also doesn't work as noted by @bzd3y 's original post here.

I will have to look at implementing some form of workaround for our app based on contributed suggestions by other external developers in this issue and others (e.g. https://github.com/dotnet/maui/issues/21053#issuecomment-1997163629). Thanks to those of you who are working on solutions / workarounds to this and I really hope Microsoft staff can look at how this might be resolved within MAUI in the longer term.

stewartsims commented 2 weeks ago

As a suggestion - could the concept of 'Unfocused' not be bifurcated into two possible contexts:

  1. The user's focus when navigating an interface
  2. The developer's ability to keep track of when controls lose focus

As long as the developer doesn't / cannot manipulate the focus themselves and is only using the 'lost focus' as a hook for background functionality (such as an auto-save or validation) could the accessibility goals then be preserved?

stewartsims commented 2 weeks ago

For anybody (@reid-kirkpatrick) interested in the current workaround, here is the code:

ButtonHandler.Mapper.AppendToMapping("UnfocusFix", (h, b) =>
{
  h.PlatformView.FocusableInTouchMode = true;
});
ButtonHandler.Mapper.PrependToMapping(nameof(Button.IsFocused), (h, b) =>
{
  if (b.IsFocused)
  {
      b.Clicked();
  }
});

Like I said, this isn't an ideal workaround, but it might get people past this for the time being. In the meantime, I'll see if I can find something better.

Just wanted to confirm @bzd3y this fix seems to be the only workaround for me in my app and it does work so thank you very much for posting these fixes I really appreciate it. I am not sure why the other fixes don't work but I suspect it may have something to do with the fact the controls I am using are displayed within a 'Popup'.

bzd3y commented 2 weeks ago

@stewartsims You're welcome, I'm glad one of them worked for you.

I wouldn't be surprised if it has to do with them being in the popup. I don't have the time to look at this right now, but when I get a chance I will try to figure something out. In the meantime, a couple of thoughts that might help you get started if you want to try something else.

  1. I think Android also restricts focus changes to ensure that there is always one focusable view with focus at any given time. So depending on how your popups are designed, that last workaround might not be working because it is trying to clear focus from the only thing that can have it. So one thing you could try is to add something like an empty ContentView with FocusableInTouchMode set to true on it and then move focus to that instead of clearing it.
  2. The buttons in your popup may already have FocusableInTouchMode set to true or something else in that if condition might not be met in the popup, so you could try removing the if completely to see if clearing the focus works at all and if it does, maybe build the condition back to something that works in both situations.
  3. You could also try setting FocusableInTouchMode to true on whatever views your popup is using. By doing that, your popup will "interfere" with focus like a ScrollView would if it wasn't being checked for in the if, but in this case it might be desirable or at least preferrable over focusable buttons. I think what you'd get is that if the user clicks in the popup anywhere outside of a focused view then that view would lose focus.
  4. Focusable buttons in something like a popup might arguably be correct/make sense. On Windows and macOS buttons are generally focusable for things like dialog/alerts/popups so that they can be dismissed without the mouse/touch. On a mobile platform like Android, that same idea might not apply, but Button focus is also handled differently. So what you'd get here is that it could help prevent the popup from being dismissed too early by making the user focus to the button first and then click it. So in that case, you could consider removing the mapping to IsFocused for buttons from the workaround that did work for you entirely and just let them be focusable and "ignore" the first click.

Again, when I have more time, I'll try to figure something out. It might help if you give some details on how your popups are working, like what kind of view you are using, etc.

bzd3y commented 1 day ago

Adding another slightly different solution that I have come up with:

ViewHandler.ViewMapper.AppendToMapping("UnfocusOnTouch", (h, v) =>
{
    if (v is not ScrollView && h.PlatformView is AndroidView view && view.Focusable && (view.FocusableInTouchMode == false))
    {
        view.Touch += (s, a) =>
        {
            if (a.Event?.Action == MotionEventActions.Down)
            {
                view.RequestFocusFromTouch();
            }

            a.Handled = false;
        };
    }
});

The difference between this code and the last solution is that instead of clearing the focus from the previous view, the view that was touched requests the focus "from touch" and if it gets it, then the focus is taken away from the previous view as would be expected. It is also obviously shorter/simpler code.

So this can be used instead of the other solution if the desired behavior is for the touched view to actually gain focus to take it away from the previous view.

@stewartsims You might want to see if this works for you. The last code might not have worked because focus was being cleared and Android was enforcing the requirement to always have a view with focus. This solution satisfies that requirement, like the solution that worked for you does, but this allows the button to behave normally as far as appearance and clicking is concerned.