diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs index 1efd44b9f3..f75256bbe1 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs @@ -1,7 +1,5 @@ using CommunityToolkit.Maui.Core; using CommunityToolkit.Maui.Extensions; -using CommunityToolkit.Maui.UnitTests.Extensions; -using CommunityToolkit.Maui.UnitTests.Services; using CommunityToolkit.Maui.Views; using FluentAssertions; using Microsoft.Maui.Controls.PlatformConfiguration; @@ -219,6 +217,50 @@ public void PopupPageT_Close_ShouldThrowOperationCanceledException_WhenTokenIsCa act.Should().ThrowAsync(); } + [Fact] + public void TapGestureRecognizer_VerifyCanBeDismissedByTappingOutsideOfPopup_ShouldNotExecuteWhenEitherFalse() + { + // Arrange + var view = new Popup(); + var popupOptions = new PopupOptions(); + + // Act + var popupPage = new PopupPage(view, popupOptions); + var tapGestureRecognizer = popupPage.Content.Children.OfType().Single().GestureRecognizers.OfType().Single(); + + // Assert + Assert.True(tapGestureRecognizer.Command?.CanExecute(null)); + + // Act + view.CanBeDismissedByTappingOutsideOfPopup = false; + popupOptions.CanBeDismissedByTappingOutsideOfPopup = false; + + // Assert + Assert.False(tapGestureRecognizer.Command?.CanExecute(null)); + + // Act + view.CanBeDismissedByTappingOutsideOfPopup = true; + popupOptions.CanBeDismissedByTappingOutsideOfPopup = false; + + // Assert + Assert.False(tapGestureRecognizer.Command?.CanExecute(null)); + + // Act + view.CanBeDismissedByTappingOutsideOfPopup = false; + popupOptions.CanBeDismissedByTappingOutsideOfPopup = true; + + // Assert + Assert.False(tapGestureRecognizer.Command?.CanExecute(null)); + + // Act + view.CanBeDismissedByTappingOutsideOfPopup = true; + popupOptions.CanBeDismissedByTappingOutsideOfPopup = true; + + // Assert + Assert.True(tapGestureRecognizer.Command?.CanExecute(null)); + + } + [Fact] public void Constructor_WithViewAndPopupOptions_SetsCorrectProperties() { diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs index e3b594e094..7d989d9898 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs @@ -14,6 +14,13 @@ public void PopupBackgroundColor_DefaultValue_ShouldBeWhite() { Assert.Equal(PopupDefaults.BackgroundColor, Colors.White); } + + [Fact] + public void CanBeDismissedByTappingOutsideOfPopup_DefaultValue_ShouldBeTrue() + { + var popup = new Popup(); + Assert.Equal(PopupDefaults.CanBeDismissedByTappingOutsideOfPopup, popup.CanBeDismissedByTappingOutsideOfPopup); + } [Fact] public void Margin_DefaultValue_ShouldBeDefaultThickness() diff --git a/src/CommunityToolkit.Maui/Primitives/Defaults/PopupDefaults.shared.cs b/src/CommunityToolkit.Maui/Primitives/Defaults/PopupDefaults.shared.cs index d4710f8c9b..31ebddac61 100644 --- a/src/CommunityToolkit.Maui/Primitives/Defaults/PopupDefaults.shared.cs +++ b/src/CommunityToolkit.Maui/Primitives/Defaults/PopupDefaults.shared.cs @@ -31,4 +31,9 @@ static class PopupDefaults /// Default value for BackgroundColor /// public static Color BackgroundColor { get; } = Colors.White; + + /// + /// Default value for + /// + public const bool CanBeDismissedByTappingOutsideOfPopup = PopupOptionsDefaults.CanBeDismissedByTappingOutsideOfPopup; } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs index a8a742b3e0..5d35a7bf10 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs @@ -26,6 +26,11 @@ public partial class Popup : ContentView /// Bindable property to set the vertical position of the when displayed on screen /// public static new readonly BindableProperty VerticalOptionsProperty = View.VerticalOptionsProperty; + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty CanBeDismissedByTappingOutsideOfPopupProperty = BindableProperty.Create(nameof(CanBeDismissedByTappingOutsideOfPopup), typeof(bool), typeof(Popup), PopupDefaults.CanBeDismissedByTappingOutsideOfPopup); /// /// Initializes Popup @@ -83,6 +88,17 @@ public Popup() get => base.VerticalOptions; set => base.VerticalOptions = value; } + + /// /> + /// + /// When true and the user taps outside the popup, it will dismiss. + /// On Android - when false the hardware back button is disabled. + /// + public bool CanBeDismissedByTappingOutsideOfPopup + { + get => (bool)GetValue(CanBeDismissedByTappingOutsideOfPopupProperty); + set => SetValue(CanBeDismissedByTappingOutsideOfPopupProperty, value); + } /// /// Close the Popup. diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 13cacf49b0..2fcea86ccd 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs @@ -46,14 +46,15 @@ public PopupPage(Popup popup, IPopupOptions popupOptions) { popupOptions.OnTappingOutsideOfPopup?.Invoke(); await CloseAsync(new PopupResult(true)); - }, () => popupOptions.CanBeDismissedByTappingOutsideOfPopup); + }, () => GetCanBeDismissedByTappingOutsideOfPopup(popup, popupOptions)); // Only set the content if the parent constructor hasn't set the content already; don't override content if it already exists base.Content = new PopupPageLayout(popup, popupOptions, tapOutsideOfPopupCommand); + popup.PropertyChanged += HandlePopupPropertyChanged; if (popupOptions is BindableObject bindablePopupOptions) { - bindablePopupOptions.PropertyChanged += HandlePopupPropertyChanged; + bindablePopupOptions.PropertyChanged += HandlePopupOptionsPropertyChanged; } this.SetBinding(BindingContextProperty, static (Popup x) => x.BindingContext, source: popup, mode: BindingMode.OneWay); @@ -106,8 +107,8 @@ public async Task CloseAsync(PopupResult result, CancellationToken token = defau protected override bool OnBackButtonPressed() { - // Only close the Popup if PopupOptions.CanBeDismissedByTappingOutsideOfPopup is true - if (popupOptions.CanBeDismissedByTappingOutsideOfPopup) + // Only close the Popup if CanBeDismissedByTappingOutsideOfPopup is true + if (GetCanBeDismissedByTappingOutsideOfPopup(popup, popupOptions)) { CloseAsync(new PopupResult(true), CancellationToken.None).SafeFireAndForget(); } @@ -152,13 +153,25 @@ protected override void OnNavigatedTo(NavigatedToEventArgs args) return popup; } - void HandlePopupPropertyChanged(object? sender, PropertyChangedEventArgs e) + // Only dismiss when a user taps outside Popup when **both** Popup.CanBeDismissedByTappingOutsideOfPopup and PopupOptions.CanBeDismissedByTappingOutsideOfPopup are true + // If either value is false, do not dismiss Popup + static bool GetCanBeDismissedByTappingOutsideOfPopup(in Popup popup, in IPopupOptions popupOptions) => popup.CanBeDismissedByTappingOutsideOfPopup & popupOptions.CanBeDismissedByTappingOutsideOfPopup; + + void HandlePopupOptionsPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(IPopupOptions.CanBeDismissedByTappingOutsideOfPopup)) { tapOutsideOfPopupCommand.ChangeCanExecute(); } } + + void HandlePopupPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Popup.CanBeDismissedByTappingOutsideOfPopupProperty.PropertyName) + { + tapOutsideOfPopupCommand.ChangeCanExecute(); + } + } void IQueryAttributable.ApplyQueryAttributes(IDictionary query) {