diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/PickerShouldDismissWhenClickOnOutside.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/PickerShouldDismissWhenClickOnOutside.png new file mode 100644 index 000000000000..acc02cab00d9 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/PickerShouldDismissWhenClickOnOutside.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue19168.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue19168.cs new file mode 100644 index 000000000000..fc60ac284f3a --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue19168.cs @@ -0,0 +1,45 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 19168, "iOS Picker dismiss does not work when clicking outside of the Picker", PlatformAffected.iOS | PlatformAffected.macOS)] +public class Issue19168 : ContentPage +{ + Picker picker; + public Issue19168() + { + picker = new Picker + { + ItemsSource = new List + { + "Baboon", + "Capuchin Monkey", + "Blue Monkey", + "Squirrel Monkey", + "Golden Lion Tamarin", + "Howler Monkey", + "Japanese Macaque" + }, + AutomationId = "Picker" + }; + + var counterBtn = new Button + { + Text = "Click me", + AutomationId = "Button", + VerticalOptions = LayoutOptions.End, + HorizontalOptions = LayoutOptions.Fill + }; + Content = new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(30, 0), + Spacing = 25, + Children = + { + picker, + counterBtn, + } + } + }; + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/PickerShouldDismissWhenClickOnOutside.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/PickerShouldDismissWhenClickOnOutside.png new file mode 100644 index 000000000000..381a7858e642 Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/PickerShouldDismissWhenClickOnOutside.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19168.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19168.cs new file mode 100644 index 000000000000..c324b15daa94 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19168.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue19168 : _IssuesUITest +{ + public Issue19168(TestDevice testDevice) : base(testDevice) + { + } + public override string Issue => "iOS Picker dismiss does not work when clicking outside of the Picker"; + + [Test] + [Category(UITestCategories.Picker)] + public void PickerShouldDismissWhenClickOnOutside() + { + App.WaitForElement("Picker"); + App.Tap("Picker"); +#if MACCATALYST + App.TapCoordinates(600, 200); + App.WaitForElement("Button"); +#elif ANDROID + App.TapCoordinates(0, 100); +#elif WINDOWS + App.TapCoordinates(60, 600); +#elif IOS + App.Tap("Button"); +#endif + VerifyScreenshot(); + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/PickerShouldDismissWhenClickOnOutside.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/PickerShouldDismissWhenClickOnOutside.png new file mode 100644 index 000000000000..07b5915f91a0 Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/PickerShouldDismissWhenClickOnOutside.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerShouldDismissWhenClickOnOutside.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerShouldDismissWhenClickOnOutside.png new file mode 100644 index 000000000000..b744b233a859 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerShouldDismissWhenClickOnOutside.png differ diff --git a/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs b/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs index 6d903fcb3bed..563d840825d7 100644 --- a/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs +++ b/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs @@ -10,6 +10,7 @@ public partial class PickerHandler : ViewHandler { readonly MauiPickerProxy _proxy = new(); UIPickerView? _pickerView; + UITapGestureRecognizer? _tapGestureRecognizer; #if MACCATALYST UIAlertController? _pickerController; @@ -136,6 +137,7 @@ protected override void ConnectHandler(MauiPicker platformView) protected override void DisconnectHandler(MauiPicker platformView) { _proxy.Disconnect(platformView); + RemoveTouchDismissGesture(); #if MACCATALYST if (_pickerController != null) @@ -269,6 +271,60 @@ void FinishSelectItem(UIPickerView? pickerView, MauiPicker textField) textField.ResignFirstResponder(); } + void SetupTouchDismissGesture() + { + // Don't setup if window isn't available yet + if (PlatformView?.Window is null) + { + return; + } + + // Don't setup if already configured + if (_tapGestureRecognizer is not null) + { + return; + } + var weakHandler = new WeakReference(this); + _tapGestureRecognizer = new UITapGestureRecognizer(() => + { + if (weakHandler.TryGetTarget(out var handler)) + { + handler.DismissPicker(); + } + }); + _tapGestureRecognizer.CancelsTouchesInView = false; + PlatformView.Window.AddGestureRecognizer(_tapGestureRecognizer); + } + + void DismissPicker() + { +#if MACCATALYST + // On MacCatalyst, dismiss the UIAlertController and clean up the tap gesture directly, + // since ResignFirstResponder is not called here and OnEnded will not fire. + if (_pickerController is not null) + { + _pickerController.DismissViewController(true, null); + if (VirtualView is IPicker virtualView) + virtualView.IsFocused = virtualView.IsOpen = false; + } + RemoveTouchDismissGesture(); +#else + // On iOS, dismiss by ending editing + PlatformView?.EndEditing(true); +#endif + } + + + void RemoveTouchDismissGesture() + { + if (_tapGestureRecognizer is not null) + { + _tapGestureRecognizer.View?.RemoveGestureRecognizer(_tapGestureRecognizer); + _tapGestureRecognizer.Dispose(); + _tapGestureRecognizer = null; + } + } + class MauiPickerProxy { WeakReference? _handler; @@ -290,6 +346,7 @@ public void Connect(PickerHandler handler, IPicker virtualView, MauiPicker platf public void Disconnect(MauiPicker platformView) { + Handler?.RemoveTouchDismissGesture(); platformView.EditingDidBegin -= OnStarted; platformView.EditingDidEnd -= OnEnded; platformView.EditingChanged -= OnEditing; @@ -322,10 +379,12 @@ void OnStarted(object? sender, EventArgs eventArgs) int selectedIndex = handler.VirtualView?.SelectedIndex ?? 0; handler.DisplayAlert(handler.PlatformView, selectedIndex); #endif + Handler?.SetupTouchDismissGesture(); } void OnEnded(object? sender, EventArgs eventArgs) { + Handler?.RemoveTouchDismissGesture(); if (Handler is not PickerHandler handler || handler._pickerView is not UIPickerView pickerView) return;